Zookeeper
初识 Zookeeper
Zookeeper概念
- Zookeeper 是 Apache Hadoop 项目下的一个子项目,是一个树形目录服务。
- Zookeeper 翻译过来就是 动物园管理员,他是用来管 Hadoop(大象)、Hive(蜜蜂)、Pig(小 猪)的管理员。简称zk
- Zookeeper 是一个分布式的、开源的分布式应用程序的协调服务。
- Zookeeper 提供的主要功能包括:
- 配置管理
- 分布式锁
- 集群管理
ZooKeeper 安装与配置
详见zookeeper的安装
ZooKeeper 命令操作
节点类型
- ZooKeeper 是一个树形目录服务,其数据模型和Unix的文件系统目录树很类似,拥有一个层次化结构。
- 这里面的每一个节点都被称为: ZNode,每个节点上都会保存自己的数据和节点信息。
- 节点可以拥有子节点,同时也允许少量(1MB)数据存储在该节点之下。
- 节点可以分为四大类:
- PERSISTENT 持久化节点
- EPHEMERAL 临时节点 :-e
- PERSISTENT_SEQUENTIAL 持久化顺序节点 :-s
- EPHEMERAL_SEQUENTIAL 临时顺序节点 :-es
服务端命令
- 启动 ZooKeeper 服务: ./zkServer.sh start
- 查看 ZooKeeper 服务状态: ./zkServer.sh status
- 停止 ZooKeeper 服务: ./zkServer.sh stop
- 重启 ZooKeeper 服务: ./zkServer.sh restart
客户端命令
常用命令
- 连接ZooKeeper服务端:./zkCli.sh –server ip:port
- 断开连接:quit
- 查看命令帮助:help
- 显示指定目录下节点:ls 目录
- 创建节点:create /节点path value
- 获取节点值:get /节点path
- 设置节点值:set /节点path value
- 删除单个节点:delete /节点path
- 删除带有子节点的节点:deleteall /节点path
创建临时有序节点
- 创建临时节点:create -e /节点path value
- 创建顺序节点:create -s /节点path value
- 查询节点详细信息:ls –s /节点path
详细信息介绍
- czxid:节点被创建的事务ID
- ctime: 创建时间
- mzxid: 最后一次被更新的事务ID
- mtime: 修改时间
- pzxid:子节点列表最后一次被更新的事务ID
- cversion:子节点的版本号
- dataversion:数据版本号
- aclversion:权限版本号
- ephemeralOwner:用于临时节点,代表临时节点的事务ID,如果为持久节点则为0
- dataLength:节点存储的数据的长度
- numChildren:当前节点的子节点个数
ZooKeeper JavaAPI 操作
Curator介绍
- Curator 是 Apache ZooKeeper 的Java客户端库。
- 常见的ZooKeeper Java API :
- 原生Java API
- ZkClient
- Curator
- Curator 项目的目标是简化 ZooKeeper 客户端的使用。
- Curator 最初是 Netfix 研发的,后来捐献了 Apache 基金会,目前是 Apache 的顶级项目。
- 官网:curator.apache.org/
JavaAPI操作
建立连接
使用curator连接zookeeper
@Before
public void testConnect() {
//重试策略 3秒重试一次 重试10次
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
/*//zkServer地址,回话超时时间,连接超时时间,重试策略
CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.67.128", 60000, 15000, retryPolicy);*/
client = CuratorFrameworkFactory.builder()
.connectString("192.168.67.128")
.sessionTimeoutMs(60000)
.connectionTimeoutMs(15000)
.retryPolicy(retryPolicy)
.namespace("itheima")
.build();
//开启连接
client.start();
}
创建节点
/**
* 创建节点:create 持久 临时 顺序 数据
* 1. 基本创建 :create().forPath("")
* 2. 创建节点 带有数据:create().forPath("",data)
* 3. 设置节点的类型:create().withMode().forPath("",data)
* 4. 创建多级节点 /app1/p1 :create().creatingParentsIfNeeded().forPath("",data)
*/
@Test
public void testCreate() throws Exception {
//1. 基本创建 :create().forPath("")
//创建节点没有指定数据的话,默认会写入本机的ip地址
String zNode = client.create().forPath("/app1");
System.out.println(zNode);
}
@Test
public void testCreate2() throws Exception {
//创建节点 带有数据:create().forPath("",data)
String zNode = client.create().forPath("/app2","itcast".getBytes(StandardCharsets.UTF_8));
System.out.println(zNode);
}
@Test
public void testCreate3() throws Exception {
//3. 设置节点的类型:create().withMode().forPath("",data)
String zNode = client.create().withMode(CreateMode.EPHEMERAL).forPath("/app3","itcast3".getBytes(StandardCharsets.UTF_8));
TimeUnit.SECONDS.sleep(10);
System.out.println(zNode);
}
@Test
public void testCreate4() throws Exception {
//4. 创建多级节点 /app1/p1 :create().creatingParentsIfNeeded().forPath("",data)
String zNode = client.create().creatingParentsIfNeeded().forPath("/app4/p1","itcast4".getBytes(StandardCharsets.UTF_8));
System.out.println(zNode);
}
查询节点
/**
* 查询节点:
* 1. 查询数据:get: getData().forPath()
* 2. 查询子节点: ls: getChildren().forPath()
* 3. 查询节点状态信息:ls -s:getData().storingStatIn(状态对象).forPath()
*/
@Test
public void testGet() throws Exception {
//1. 查询数据:get: getData().forPath()
byte[] bytes = client.getData().forPath("/app1");
System.out.println(new String(bytes, StandardCharsets.UTF_8));
}
@Test
public void testGet2() throws Exception {
//2. 查询子节点: ls: getChildren().forPath()
List<String> path = client.getChildren().forPath("/");
System.out.println(path);
}
@Test
public void testGet3() throws Exception {
//3. 查询节点状态信息:ls -s:getData().storingStatIn(状态对象).forPath()
Stat stat = new Stat();
client.getData().storingStatIn(stat).forPath("/");
System.out.println(stat);
}
修改节点
/**
* 修改数据
* 1. 基本修改数据:setData().forPath()
* 2. 根据版本修改: setData().withVersion().forPath()
* * version 是通过查询出来的。目的就是为了让其他客户端或者线程不干扰我。
*
* @throws Exception
*/
@Test
public void testSet() throws Exception {
//1. 基本修改数据:setData().forPath()
Stat stat = client.setData().forPath("/app1", "itcast".getBytes(StandardCharsets.UTF_8));
System.out.println(stat.getAversion());
}
@Test
public void testSetForVersion() throws Exception {
//2. 根据版本修改: setData().withVersion().forPath()
Stat beforeStat = new Stat();
client.getData().storingStatIn(beforeStat).forPath("/app1");
int version = beforeStat.getVersion();
System.out.println(version);
client.setData().withVersion(version).forPath("/app1", "haha".getBytes(StandardCharsets.UTF_8));
}
删除节点
/**
* 删除节点: delete deleteall
* 1. 删除单个节点:delete().forPath("/app1");
* 2. 删除带有子节点的节点:delete().deletingChildrenIfNeeded().forPath("/app1");
* 3. 必须成功的删除:为了防止网络抖动。本质就是重试。
client.delete().guaranteed().forPath("/app2");
* 4. 回调:inBackground
* @throws Exception
*/
@Test
public void testDelete() throws Exception {
//1. 删除单个节点:delete().forPath("/app1");
client.delete().forPath("/app1");
}
@Test
public void testDelete2() throws Exception {
//2. 删除带有子节点的节点:delete().deletingChildrenIfNeeded().forPath("/app1");
client.delete().deletingChildrenIfNeeded().forPath("/app4");
}
@Test
public void testDelete3() throws Exception {
//3. 必须成功的删除:为了防止网络抖动。本质就是重试。
client.delete().guaranteed().forPath("/app2");
}
@Test
public void testDelete4() throws Exception {
//4. 回调:inBackground
client.delete().guaranteed().inBackground((clent,event)->{
System.out.println("删除回调事件");
System.out.println(event);
}).forPath("/app1");
}
Watch监听
概述
- ZooKeeper 允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
- ZooKeeper 中引入了Watcher机制来实现了发布/订阅功能能,能够让多个订阅者同时监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者。
- ZooKeeper 原生支持通过注册Watcher来进行事件监听,但是其使用并不是特别方便需要开发人员自己反复注册Watcher,比较繁琐。
- Curator引入了 Cache 来实现对 ZooKeeper 服务端事件的监听。
- ZooKeeper提供了三种Watcher:
- NodeCache : 只是监听某一个特定的节点
- PathChildrenCache : 监控一个ZNode的子节点.
- TreeCache : 可以监控整个树上的所有节点,类似于PathChildrenCache和NodeCache的组合
NodeCache
/**
* 演示 NodeCache:给指定一个节点注册监听器
*/
@Test
public void testNodeCache() throws Exception {
//创建NodeCache对象
NodeCache nodeCache = new NodeCache(client, "/app1");
//注册监听
nodeCache.getListenable().addListener(() -> {
System.out.println("节点变化了");
byte[] data = nodeCache.getCurrentData().getData();
System.out.println(new String(data, StandardCharsets.UTF_8));
});
//开启监听 是否缓存
nodeCache.start(true);
while (true) ;
}
PathChildrenCache
/**
* Watch监听-PathChildrenCache:监听子节点的变化
*/
@Test
public void testPathChildrenCache() throws Exception {
PathChildrenCache pathChildrenCache = new PathChildrenCache(client,"/app2",true);
pathChildrenCache.getListenable().addListener((client,event)->{
System.out.println("子节点变化了");
System.out.println("event:"+event);
PathChildrenCacheEvent.Type type = event.getType();
if (type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){
System.out.println("数据发送变化了");
byte[] data = event.getData().getData();
System.out.println(new String(data,StandardCharsets.UTF_8));
}
});
//启动节点监听
pathChildrenCache.start();
while (true);
}
TreeCache
/**
* Watch监听-TreeCache:监听自己和子节点的变化
*/
@Test
public void testTreeCache() throws Exception {
TreeCache treeCache = new TreeCache(client,"/app2");
treeCache.getListenable().addListener((client,event)->{
System.out.println("节点发生变化了");
System.out.println(event);
});
treeCache.start();
while (true);
}
Zookeeper分布式锁
概念
- 在我们进行单机应用开发,涉及并发同步的时候,我们往往采用synchronized或者Lock的方式来解决多线程间的代码同步问题,这时多线程的运行都是在同一个JVM之下,没有任何问题。
- 但当我们的应用是分布式集群工作的情况下,属于多JVM下的工作环境,跨JVM之间已经无法通过多线程的锁解决同步问题。
- 那么就需要一种更加高级的锁机制,来处理种跨机器的进程之间的数据同步问题——这就是分布式锁。
原理
核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点。
- 客户端获取锁时,在lock节点下创建临时顺序节点。
- 然后获取lock下面的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。使用完锁后,将该节点删除。
- 如果发现自己创建的节点并非lock所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,同时对其注册事件监听器,监听删除事件。
- 如果发现比自己小的那个节点被删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。
锁类型
共享锁
又称读锁。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都被释放
排它锁
又称写锁或独占锁。如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取或更新操作,其他任务事务都不能对这个数据对象进行任何操作,直到T1释放了排他锁
可以将临时有序节点分为读锁节点和写锁节点
- 对于读锁节点而言,其只需要关心前一个写锁节点的释放。如果前一个写锁释放了,则多个读锁节点对应的线程可以并发地读取数据
- 对于写锁节点而言,其只需要关心前一个节点的释放,而不需要关心前一个节点是写锁节点还是读锁节点。因为为了保证有序性,写操作必须要等待前面的读操作或者写操作执行完成
模拟12306售票案例
Curator实现分布式锁API
在Curator中有五种锁方案:
- InterProcessSemaphoreMutex:分布式排它锁(非可重入锁)
- InterProcessMutex:分布式可重入排它锁
- InterProcessReadWriteLock:分布式读写锁
- InterProcessMultiLock:将多个锁作为单个实体管理的容器
- InterProcessSemaphoreV2:共享信号量
创建线程进行加锁设置
public class Ticket12306 implements Runnable {
private int ticks = 10;//10张票
private InterProcessMutex lock;
@Override
public void run() {
//卖票
while (true) {
try {
//获取锁
lock.acquire(3, TimeUnit.SECONDS);
if (ticks > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了" + ticks + "号票");
TimeUnit.SECONDS.sleep(1);
ticks--;
continue;
}
break;
}
catch (Exception e) {
System.out.println("获取锁失败");
e.printStackTrace();
}
finally {
//释放锁
try {
lock.release();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
}
创建连接,并且初始化锁
private InterProcessMutex lock;
Ticket12306() {
//重试策略 3秒重试一次 重试10次
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.67.128")
.sessionTimeoutMs(60000)
.connectionTimeoutMs(15000)
.retryPolicy(retryPolicy)
.build();
//开启连接
client.start();
this.lock = new InterProcessMutex(client, "/lock");
}
运行多个线程进行测试
public static void main(String[] args) {
Ticket12306 ticket12306 = new Ticket12306();
Thread t1 = new Thread(ticket12306,"携程");
Thread t2 = new Thread(ticket12306,"飞猪");
t1.start();
t2.start();
}
ZooKeeper伪集群搭建
详见zookeeper的安装--伪集群压缩包安装
Zookeeper核心理论
Zookeepe集群角色
在ZooKeeper集群服中务中有三个角色:
- Leader 领导者 :
- 处理事务请求
- 集群内部各服务器的调度者
- Follower 跟随者 :
- 处理客户端非事务请求,转发事务请求给Leader服务器
- 参与Leader选举投票
- Observer 观察者:
- 处理客户端非事务请求,转发事务请求给Leader服务器
Zookeeper选举策略
zookeeper 集群
为什么zookeeper节点推荐配奇数?
- 容错率:需要保证集群能够有半数进行投票
- 2台服务器,至少2台正常运行才行(2的半数为1,半数以上最少为2),正常运行1台服务器都不允许挂掉,但是相对于 单节点服务器,2台服务器还有两个单点故障,所以直接排除了。
- 3台服务器,至少2台正常运行才行(3的半数为1.5,半数以上最少为2),正常运行可以允许1台服务器挂掉
- 4台服务器,至少3台正常运行才行(4的半数为2,半数以上最少为3),正常运行可以允许1台服务器挂掉
- 5台服务器,至少3台正常运行才行(5的半数为2.5,半数以上最少为3),正常运行可以允许2台服务器挂掉
- 防脑裂:脑裂集群的脑裂通常是发生在节点之间通信不可达的情况下,集群会分裂成不同的小集群,小集群各自选出自己的leader节点,导致原有的集群出现多个leader节点的情况,这就是脑裂
- 3台服务器,投票选举半数为1.5,一台服务裂开,和另外两台服务器无法通行,这时候2台服务器的集群(2票大于半数1.5票),所以可以选举出leader,而 1 台服务器的集群无法选举。
- 4台服务器,投票选举半数为2,可以分成 1,3两个集群或者2,2两个集群,对于 1,3集群,3集群可以选举;对于2,2集群,则不能选择,造成没有leader节点。
- 5台服务器,投票选举半数为2.5,可以分成1,4两个集群,或者2,3两集群,这两个集群分别都只能选举一个集群,满足zookeeper集群搭建数目。
- 以上分析,我们从容错率以及防止脑裂两方面说明了3台服务器是搭建集群的最少数目,4台发生脑裂时会造成没有leader节点的错误
算法模型
Paxos算法
Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一,其解决的问题就是在分布式系统中如何就某个值(决议)达成一致,paxos是一个分布式选举算法
该算法定义了三种角色
- Proposer:提案(决议)发起者
- Acceptor:提案接收者,可同意或不同意
- Learners:虽然不同意提案,但也只能被动接收学习;或者是后来的,只能被动接受提案遵循少数服从多数的原则,过半原则。
ZAB协议
ZooKeeper使用的是ZAB协议作为数据一致性的算法, ZAB(ZooKeeper Atomic Broadcast ) 全称为:原子消息广播协议。在Paxos算法基础上进行了扩展改造而来的,ZAB协议设计了支持原子广播、崩溃恢复,ZAB协议保证Leader广播的变更序列被顺序的处理。
ZAB协议模式
- 崩溃恢复: 一旦 Leader 服务器出现崩溃或者由于网络原因导致 Leader 服务器失去了与过半 Follower 的联系那么就会进入崩溃恢复模式。在Zab 协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出个新的 Leader服务器,并与过半follower完成数据同步,然后进入到消息广播模式
- 原子广播:这个阶段,Zookeeper集群才能正式对外提供事务服务,并且 Leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。需要注意的是,Zab 提交事务并不像 2PC一样需要全部Follower都Ack,只需要得到 quorum (超过半数的节点)的Ack 就可以。
四种状态
其中三种跟选举有关
- LOOKING:系统刚启动时或者Leader崩溃后正处于选举状态
- FOLLOWING:Follower节点所处的状态,同步leader状态,参与投票
- LEADING:Leader所处状态
- OBSERVING,观察状态,同步leader状态,不参与投票选举时也是半数以上通过才算通过
Zookeeper选举
先介绍几个重要的参数。
- 服务器 ID(myid):编号越大在选举算法中权重越大
- 事务 ID(zxid):值越大说明数据越新,权重越大
- 逻辑时钟(epoch-logicalclock):同一轮投票过程中的逻辑时钟值是相同的,每投完一次值会增加
支持的领导选举算法
可通过ElectionAlg配置项设置ZooKeeper用于领导选举的算法,到3.4.10版本为止,可选项有:
- 0:基于UDP的LeaderElection
- 1:基于UDP的 FastLeaderElection
- 2:基于UDP和认证的 FastLeaderElection
- 3:某于TCP的 FastLeaderElpction
在3.4.10版本中,默认值为3,也即基于TCP的FastLeaderElection。另外三种算法已经被弃用,并且有计划在之后的版本中将它们废除而不再支持。
FastLeaderElection 原理
选举流程
服务器启动时的 leader 选举
每个节点启动的时候都 LOOKING 观望状态,接下来就开始进行选举主流程。这里选取三台机器组成的集群为例。第一台服务器 server1启动时,无法进行 leader 选举,当第二台服务器 server2 启动时,两台机器可以相互通信,进入 leader 选举过程。
- 每台 server 发出一个投票,由于是初始情况,server1 和 server2 都将自己作为 leader 服务器进行投票,每次投票包含所推举的服务器myid、zxid、epoch,使用(myid,zxid)表示,此时 server1 投票为(1,0),server2 投票为(2,0),然后将各自投票发送给集群中其他机器。
- 接收来自各个服务器的投票。集群中的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票(epoch)、是否来自 LOOKING 状态的服务器。
- 分别处理投票。针对每一次投票,服务器都需要将其他服务器的投票和自己的投票进行对比,对比规则如下:
- 优先比较 epoch
- 检查 zxid,zxid 比较大的服务器优先作为 leader
- 如果 zxid 相同,那么就比较 myid,myid 较大的服务器作为 leader 服务器
- 统计投票。每次投票后,服务器统计投票信息,判断是都有过半机器接收到相同的投票信息。server1、server2 都统计出集群中有两台机器接受了(2,0)的投票信息,此时已经选出了 server2 为 leader 节点。
- 改变服务器状态。一旦确定了 leader,每个服务器响应更新自己的状态,如果是 follower,那么就变更为 FOLLOWING,如果是 Leader,变更为 LEADING。此时 server3继续启动,直接加入变更自己为 FOLLOWING。
运行过程中的 leader 选举
当集群中 leader 服务器出现宕机或者不可用情况时,整个集群无法对外提供服务,进入新一轮的 leader 选举。
- 变更状态。leader 挂后,其他非 Oberver服务器将自身服务器状态变更为 LOOKING。
- 每个 server 发出一个投票。在运行期间,每个服务器上 zxid 可能不同。
- 处理投票。规则同启动过程。
- 统计投票。与启动过程相同。
- 改变服务器状态。与启动过程相同。
源码
zookeeper 集群数据读写
读请求
当Client向zookeeper发出读请求时,无论是Leader还是Follower,都直接返回查询结果
写请求
leader
Client向Leader发出写请求,Leader将数据写入到本节点,并将数据发送到所有的Follower节点,等待Follower节点返回,当Leader接收到一半以上节点(包含自己)返回写成功的信息之后,返回写入成功消息给client
follwer
Client向Follower发出写请求,Follower节点将请求转发给Leader,Leader将数据写入到本节点,并将数据发送到所有的Follower节点,等待Follower节点返回,当Leader接收到一半以上节点(包含自己)返回写成功的信息之后,返回写入成功消息给原来的Follower,原来的Follower返回写入成功消息给Client