Zookeeper
ZooKeeper最为主要的使⽤场景,是作为分布式系统的分布式协同服务。
我们将分布式系统定义为:分布式系统是同时跨越多个物理主机,独⽴运⾏的多个软件所组成系统。
分布式系统的协调⼯作就是通过某种⽅式,让每个节点的信息能够同步和共享。这依赖于服务进程之间的通信。通信⽅式有两种:
1、通过⽹络进⾏信息共享
2、通过共享存储
ZooKeeper对分布式系统的协调,使⽤的是第⼆种⽅式,共享存储。其实共享存储,分布式应⽤也需要和存储进⾏⽹络通信
当主节点(组leader),对某个从节点的分⼯信息作出改变时,相关订阅的从节点
得到zookeeper的通知,取得⾃⼰最新的任务分配。完成⼯作后,把完成情况存储到zookeeper。主节
点订阅了该任务的完成情况信息,所以将得到zookeeper的完⼯的通知。参考下图,是不是和前⾯项⽬
组通过svn分配⼯作的例⼦⼀模⼀样?仅仅是把svn和邮件系统合⼆为⼀,以ZooKeeper代替
当主节点(组leader),对某个从节点的分⼯信息作出改变时,相关订阅的从节点
得到zookeeper的通知,取得⾃⼰最新的任务分配。完成⼯作后,把完成情况存储到zookeeper。主节点订阅了该任务的完成情况信息,所以将得到zookeeper的完⼯的通知。
Slave节点要想获取ZooKeeper的更新通知,需事先在关⼼的数据节点上设置观察点
ZooKeeper解决协同问题的关键,就是在于保证分布式系统信息的⼀致性。
zookeeper的基本概念
① 集群⻆⾊: 了Leader、Follower、Observer三种⻆⾊
。Zookeeper集群中的所有机器通过Leader选举来选定⼀台被称为
Leader的机器,Leader服务器为客户端提供读和写服务,
唯⼀的区别在于Observer不参与Leader选举过程,不参与写操作的过半写成功策略,因此Observer可以在不影响写性能的情况下提升集群的性能。
② 会话(session)
Session指客户端会话,⼀个客户端连接是指客户端和服务端之间的⼀个TCP⻓连接,Zookeeper对外的服务端⼝默认为2181,客户端启动的时候,⾸先会与服务器建⽴⼀个TCP连接,从第⼀次连接建⽴开始,客户端会话的⽣命周期也开始了,通过这个连接,客户端能够⼼跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来⾃服务器的Watch事件
通知。
③ 数据节点(Znode)
在ZooKeeper中,“节 点”分为两类,第⼀类同样是指构成集群的机器,我们称之为机器节点;第⼆类则是指数据模型中的数据
单元,我们称之为数据节点——ZNode。ZooKeeper将所有数据存储在内存中,数据模型是⼀棵树(ZNode Tree),由斜杠(/)进⾏分割的路径,就是⼀个Znode,例如/app/path1。每个ZNode上都会保存⾃⼰的数据内容,同时还会保存⼀系列属性信息。
④ 版本
对于每个ZNode,Zookeeper都会为其维护
⼀个叫作Stat的数据结构,Stat记录了这个ZNode的三个数据版本,分别是version(当前ZNode的版本)、cversion(当前ZNode⼦节点的版本)、aversion(当前ZNode的ACL版本)。
⑤ Watcher(事件监听器)
,Zookeeper允许⽤户在指定节点上注册
⼀些Watcher,并且在⼀些特定事件触发的时候,Zookeeper服务端会将事件通知到感兴趣的客户端,该机制是Zookeeper实现分布式协调服务的重要特性
⑥ ACL
Zookeeper采⽤ACL(Access Control Lists)策略来进⾏权限控制,其定义了如下五种权限:
· CREATE:创建⼦节点的权限。
· READ:获取节点数据和⼦节点列表的权限。
· WRITE:更新节点数据的权限。
· DELETE:删除⼦节点的权限。
· ADMIN:设置节点ACL的权限。
其中需要注意的是,CREATE和DELETE这两种权限都是针对⼦节点的权限控制
2. Zookeeper环境搭建
Zookeeper安装⽅式有三种,单机模式和集群模式以及伪集群模式。
■ 单机模式:Zookeeper只运⾏在⼀台服务器上,适合测试环境;
■ 集群模式:Zookeeper运⾏于⼀个集群上,适合⽣产环境,这个计算机集群被称为⼀个“集合体”
■ 伪集群模式:就是在⼀台服务器上运⾏多个Zookeeper 实例;
⼀台机器上部署了3个server,也就是说单台机器及上运⾏多个Zookeeper实例。这种情况下,必须保证每个配置⽂档的各个端⼝号不能冲突,除clientPort不同之外,dataDir也不同。另外还要在dataDir所对应的⽬录中创建myid⽂件来指定对应的Zookeeper服务器实例
■ clientPort端⼝:
如果在1台机器上部署多个server,那么每台机器都要不同的 clientPort,⽐如 server1是2181,server2
是2182,server3是2183
■ dataDir和dataLogDir:
dataDir和dataLogDir也需要区分下,将数据⽂件和⽇志⽂件分开存放,同时每个server的这两变量所
对应的路径都是不同的
■ server.X和myid:
server.X 这个数字就是对应,data/myid中的数字。在3个server的myid⽂件中分别写⼊了1,2,3,那么每个server中的zoo.cfg都配 server.1 server.2,server.3就⾏了。因为在同⼀台机器上,后⾯连着的2个端⼝,3个server都不要⼀样,否则端⼝冲突
配置每⼀个Zookeeper 的
dataDir(zoo.cfg)
dataLogDir(zoo.cfg)
clientPort 分别为2181 2182 2183
配置集群
(1)在每个zookeeper的 data ⽬录下创建⼀个 myid ⽂件,内容分别是1、2、3 。这个⽂件就是记录
每个服务器的ID
touch myid
(2)在每⼀个zookeeper 的 zoo.cfg配置客户端访问端⼝(clientPort)和集群服务器IP列表。
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890
#server.服务器ID=服务器IP地址:服务器之间通信端⼝:服务器之间投票选举端⼝
注意:
服务器端口
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890
clientPort 分别为2181 2182 2183
服务器端口 和 zookper clientPort 分别为2181 2182 2183 不能一样啊
启动集群
依次启动三个zk实例
注意查看状态和角色
3、Zookeeper基本使⽤
3-1 ZooKeeper系统模型
在ZooKeeper中,数据信息被保存在⼀个个数据节点上,这些节点被称为znode。ZNode 是
Zookeeper 中最⼩数据单位
ZNode 的类型
Zookeeper 节点类型可以分为三⼤类:
持久性节点(Persistent)
临时性节点(Ephemeral)
顺序性节点(Sequential)
持久节点:
是Zookeeper中最常⻅的⼀种节点类型,所谓持久节点,就是指节点被创建后会⼀直存在服务器,直到删除操作主动清除
持久顺序节点:
就是有顺序的持久节点,节点特性和持久节点是⼀样的,只是额外特性表现在顺序上。顺序特性实质是在创建节点的时候,会在节点名后⾯加上⼀个数字后缀,来表示其顺序。
临时节点:
就是会被⾃动清理掉的节点,它的⽣命周期和客户端会话绑在⼀起,客户端会话结束,节点会被删除掉。与持久性节点不同的是,临时节点不能创建⼦节点。
临时顺序节点:
就是有顺序的临时节点,和持久顺序节点相同,在其创建的时候会在名字后⾯加上数字
后缀。
事务ID
在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操
作,⼀般包括数据节点创建与删除、数据节点内容更新等操作。对于每⼀个事务请求,ZooKeeper都会为其分配⼀个全局唯⼀的事务ID,⽤ ZXID 来表示,通常是⼀个 64 位的数字。每⼀个 ZXID 对应⼀次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序
ZNode 的状态信息
整个 ZNode 节点内容包括两部分:节点数据内容和节点状态信息。图中quota 是数据内容,其他的属
于状态信息
cZxid 就是 Create ZXID,表示节点被创建时的事务ID。
ctime 就是 Create Time,表示节点创建时间。
mZxid 就是 Modified ZXID,表示节点最后⼀次被修改时的事务ID。
mtime 就是 Modified Time,表示节点最后⼀次被修改的时间。
pZxid 表示该节点的⼦节点列表最后⼀次被修改时的事务 ID。只有⼦节点列表变更才会更新 pZxid,
⼦节点内容变更不会更新。
cversion 表示⼦节点的版本号。
dataVersion 表示内容版本号。
aclVersion 标识acl版本
ephemeralOwner 表示创建该临时节点时的会话 sessionID,如果是持久性节点那么值为 0
dataLength 表示数据⻓度。
numChildren 表示直系⼦节点数。
Watcher–数据变更通知
Zookeeper使⽤Watcher机制实现分布式数据的发布/订阅功能
ZooKeeper 允许客户端向服务端注册⼀个 Watcher 监听,当服务端的⼀些指定事件触发了这个 Watcher,那么就会向指定客户端发送⼀个事件通知来实现分布式的通知功能。
Zookeeper的Watcher机制主要包括客户端线程、客户端WatcherManager、Zookeeper服务器三部
分。
具体⼯作流程为:
客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatcherManager中取出对应的Watcher对象来执⾏回调逻辑。
ACL–保障数据的安全
权限模式(Scheme)、
授权对象(ID)、
权限(Permission),通常使⽤"scheme: id : permission"来标识⼀个有效的ACL信息。
权限模式:Scheme
权限模式⽤来确定权限验证过程中使⽤的检验策略,有如下四种模式:
1. IP
IP模式就是通过IP地址粒度来进⾏权限控制,如"ip:192.168.0.110"表示权限控制针对该IP地址,
同时IP模式可以⽀持按照⽹段⽅式进⾏配置,如"ip:192.168.0.1/24"表示针对192.168.0.*这个⽹段
进⾏权限控制。
2. Digest
Digest是最常⽤的权限控制模式,要更符合我们对权限控制的认识,其使
⽤"username:password"形式的权限标识来进⾏权限配置,便于区分不同应⽤来进⾏权限控制。
当我们通过“username:password”形式配置了权限标识后,Zookeeper会先后对其进⾏SHA-1加密
和BASE64编码。
3. World
World是⼀种最开放的权限控制模式,这种权限控制⽅式⼏乎没有任何作⽤,数据节点的访问权限
对所有⽤户开放,即所有⽤户都可以在不进⾏任何权限校验的情况下操作ZooKeeper上的数据。
另外,World模式也可以看作是⼀种特殊的Digest模式,它只有⼀个权限标识,即“world:
anyone”。
4. Super
Super模式,顾名思义就是超级⽤户的意思,也是⼀种特殊的Digest模式。在Super模式下,超级
⽤户可以对任意ZooKeeper上的数据节点进⾏任何操作。
授权对象:ID
授权对象指的是权限赋予的⽤户或⼀个指定实体,例如 IP 地址或是机器等。在不同的权限模式下,授
权对象是不同的,表中列出了各个权限模式和授权对象之间的对应关系。
权限模式 | 授权对象 |
---|---|
IP | 通常是⼀个IP地址或IP段:例如:192.168.10.110 或192.168.10.1/24 |
Digest | ⾃定义,通常是username:BASE64(SHA-1(username:password))例如:zm:sdfndsllndlksfn7c= |
World | 只有⼀个ID :anyone |
Super | 超级用户 |
权限
权限就是指那些通过权限检查后可以被允许执⾏的操作。在ZooKeeper中,所有对数据的操作权限分为
以下五⼤类:
CREATE(C):数据节点的创建权限,允许授权对象在该数据节点下创建⼦节点。 ·
DELETE(D):⼦节点的删除权限,允许授权对象删除该数据节点的⼦节点。
READ(R):数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或⼦节点列表等。 WRITE(W):数据节点的更新权限,允许授权对象对该数据节点进⾏更新操作。 ·
ADMIN(A):数据节点的管理权限,允许授权对象对该数据节点进⾏ ACL 相关的设置操作。
3.2 ZooKeeper命令⾏操作
通过zkClient进⼊zookeeper客户端命令⾏
./zkcli.sh 连接本地的zookeeper服务器
./zkCli.sh -server ip:port 连接指定的服务器
连接成功之后,系统会输出Zookeeper的相关环境及配置信息等信息。输⼊help之后,屏幕会输出可⽤
的Zookeeper命令,如下图所示
创建节点
使用客户端操作
使⽤create命令,可以创建⼀个Zookeeper节点, 如
create [-s][-e] path data acl
其中,-s或-e分别指定节点特性,顺序或临时节点,若不指定,则创建持久节点;acl⽤来进⾏权限控制。
① 创建顺序节点
使⽤ create -s /zk-test 123 命令创建zk-test顺序节点
执⾏完后,就在根节点下创建了⼀个叫做/zk-test的节点,该节点内容就是123,同时可以看到创建的
zk-test节点后⾯添加了⼀串数字以示区别
创建临时节点
使⽤ create -e /zk-temp 123 命令创建zk-temp临时节
临时节点在客户端会话结束后,就会⾃动删除,下⾯使⽤quit命令退出客户端
再次使⽤客户端连接服务端,并使⽤ls / 命令查看根⽬录下的节点
创建永久节点
使⽤ create /zk-permanent 123 命令创建zk-permanent永久节点
可以看到永久节点不同于顺序节点,不会⾃动在后⾯添加⼀串数字
读取节点
ls命令可以列出Zookeeper指定节点下的所有⼦节点,但只能查看指定节点下的第⼀级的所有⼦节点;
ls path
其中,path表示的是指定数据节点的节点路径
get命令可以获取Zookeeper指定节点的数据内容和属性信息
get path
若获取根节点下⾯的所有⼦节点,使⽤ls / 命令即可
若想获取/zk-permanent的数据内容和属性,可使⽤如下命令:get /zk-permanent
从上⾯的输出信息中,我们可以看到,第⼀⾏是节点/zk-permanent 的数据内容,其他⼏⾏则是创建该
节点的事务ID(cZxid)、最后⼀次更新该节点的事务ID(mZxid)和最后⼀次更新该节点的时间
(mtime)等属性信息
更新节点
使⽤set命令,可以更新指定节点的数据内容,⽤法如下
set path data [version]
data就是要更新的新内容,version表示数据版本,在zookeeper中,节点的数据是有版本概
念的,这个参数⽤于指定本次更新操作是基于Znode的哪⼀个数据版本进⾏的,
如将/zk-permanent节点的数据更新为456,可以使⽤如下命令:set /zk-permanent 456
现在dataVersion已经变为1了,表示进⾏了更新
删除节点
使⽤delete命令可以删除Zookeeper上的指定节点,⽤法如下
delete path [version]
其中version也是表示数据版本,使⽤delete /zk-permanent 命令即可删除/zk-permanent节点
若删除节点存在⼦节点,那么⽆法删除该节点,必须先删除⼦节点,再删除⽗节点
3-3、Zookeeper的api使⽤
Zookeeper API共包含五个包,分别为:
(1)org.apache.zookeeper
(2)org.apache.zookeeper.data
(3)org.apache.zookeeper.server
(4)org.apache.zookeeper.server.quorum
(5)org.apache.zookeeper.server.upgrade
其中org.apache.zookeeper,包含Zookeeper类,他是我们编程时最常⽤的类⽂件。这个类是
Zookeeper客户端的主要类⽂件。如果要使⽤Zookeeper服务,应⽤程序⾸先必须创建⼀个Zookeeper
实例,这时就需要使⽤此类。⼀旦客户端和Zookeeper服务端建⽴起了连接,Zookeeper系统将会给本
次连接会话分配⼀个ID值,并且客户端将会周期性的向服务器端发送⼼跳来维持会话连接。只要连接有
效,客户端就可以使⽤Zookeeper API来做相应处理了。
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
</dependency>
建⽴会话
-------CountDownLatch 让main方法阻塞中 等待线程结束 ------------------
public class CreateSession implements Watcher {
//countDownLatch这个类使⼀个线程等待,主要不让main⽅法结束
private static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) throws InterruptedException,
IOException {
/*
客户端可以通过创建⼀个zk实例来连接zk服务器
new Zookeeper(connectString,sesssionTimeOut,Wather)
connectString: 连接地址:IP:端⼝
sesssionTimeOut:会话超时时间:单位毫秒
Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端)
*/
ZooKeeper zooKeeper = new ZooKeeper("10.211.55.4:2181", 5000, new
CreateSession());
System.out.println(zooKeeper.getState());
countDownLatch.await();
//表示会话真正建⽴
System.out.println("=========Client Connected to
zookeeper==========");
}
// 当前类实现了Watcher接⼝,重写了process⽅法,该⽅法负责处理来⾃Zookeeper服务端的
watcher通知,在收到服务端发送过来的SyncConnected事件之后,解除主程序在CountDownLatch上
的等待阻塞,⾄此,会话创建完毕
public void process(WatchedEvent watchedEvent) {
//当连接创建了,服务端发送给客户端SyncConnected事件
if(watchedEvent.getState() == Event.KeeperState.SyncConnected){
countDownLatch.countDown();
}
}
}
```java
**注意,**
ZooKeeper 客户端和服务端会话的建⽴是⼀个异步的过程,也就是说在程序中,构造⽅法会在处
理完客户端初始化⼯作后⽴即返回,在⼤多数情况下,此时并没有真正建⽴好⼀个可⽤的会话,在会话
的⽣命周期中处于“CONNECTING”的状态。 当该会话真正创建完毕后ZooKeeper服务端会向会话对应
的客户端发送⼀个事件通知,以告知客户端,客户端只有在获取这个通知之后,才算真正建⽴了会话。
**创建节点**
```java
public class CreateNote implements Watcher {
//countDownLatch这个类使⼀个线程等待,主要不让main⽅法结束
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper("10.211.55.4:2181", 5000, new CreateNote());
countDownLatch.await();
}
public void process(WatchedEvent watchedEvent) {
//当连接创建了,服务端发送给客户端SyncConnected事件
if(watchedEvent.getState() == Event.KeeperState.SyncConnected){
countDownLatch.countDown();
}
//调⽤创建节点⽅法
try {
createNodeSync();
} catch (Exception e) {
e.printStackTrace();
}
}
private void createNodeSync() throws Exception {
/**
* path :节点创建的路径
* data[] :节点创建要保存的数据,是个byte类型的
* acl :节点创建的权限信息(4种类型)
* ANYONE_ID_UNSAFE : 表示任何⼈
* AUTH_IDS :此ID仅可⽤于设置ACL。它将被客户机验证的ID替
换。
* OPEN_ACL_UNSAFE :这是⼀个完全开放的ACL(常⽤)-->
world:anyone
* CREATOR_ALL_ACL :此ACL授予创建者身份验证ID的所有权限
* createMode :创建节点的类型(4种类型)
* PERSISTENT:持久节点
* PERSISTENT_SEQUENTIAL:持久顺序节点
* EPHEMERAL:临时节点
* EPHEMERAL_SEQUENTIAL:临时顺序节点
String node = zookeeper.create(path,data,acl,createMode);
*/
String node_PERSISTENT = zooKeeper.create("/lg_persistent", "持久节点内
容".getBytes("utf-8"), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
String node_PERSISTENT_SEQUENTIAL =
zooKeeper.create("/lg_persistent_sequential", "持久节点内容".getBytes("utf-8"),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
String node_EPERSISTENT = zooKeeper.create("/lg_ephemeral", "临时节点内
容".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
System.out.println("创建的持久节点是:"+node_PERSISTENT);
System.out.println("创建的持久顺序节点是:"+node_PERSISTENT_SEQUENTIAL);
System.out.println("创建的临时节点是:"+node_EPERSISTENT);
}
}
`
**获取节点数据**
```java
public class GetNoteData implements Watcher {
//countDownLatch这个类使⼀个线程等待,主要不让main⽅法结束
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper("10.211.55.4:2181", 10000, new
GetNoteDate());
Thread.sleep(Integer.MAX_VALUE);
}
public void process(WatchedEvent watchedEvent) {
//⼦节点列表发⽣变化时,服务器会发出NodeChildrenChanged通知,但不会把变化情况告
诉给客户端
// 需要客户端⾃⾏获取,且通知是⼀次性的,需反复注册监听
if(watchedEvent.getType() ==Event.EventType.NodeChildrenChanged){
//再次获取节点数据
try {
List<String> children =
zooKeeper.getChildren(watchedEvent.getPath(), true);
System.out.println(children);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//当连接创建了,服务端发送给客户端SyncConnected事件
if(watchedEvent.getState() == Event.KeeperState.SyncConnected){
try {
//调⽤获取单个节点数据⽅法
getNoteDate();
getChildrens();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static void getNoteData() throws Exception {
/**
* path : 获取数据的路径
* watch : 是否开启监听
* stat : 节点状态信息
* null: 表示获取最新版本的数据
* zk.getData(path, watch, stat);
*/
byte[] data = zooKeeper.getData("/lg_persistent/lg-children", true,
null);
System.out.println(new String(data,"utf-8"));
}
private static void getChildrens() throws KeeperException,
InterruptedException {
/*
path:路径
watch:是否要启动监听,当⼦节点列表发⽣变化,会触发监听
zooKeeper.getChildren(path, watch);
*/
List<String> children = zooKeeper.getChildren("/lg_persistent", true);
System.out.println(children);
}
}
修改节点数据
public class updateNote implements Watcher {
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper("10.211.55.4:2181", 5000, new updateNote());
Thread.sleep(Integer.MAX_VALUE);
}
public void process(WatchedEvent watchedEvent) {
//当连接创建了,服务端发送给客户端SyncConnected事件
try {
updateNodeSync();
} catch (Exception e) {
e.printStackTrace();
}
}
private void updateNodeSync() throws Exception {
/*
path:路径
data:要修改的内容 byte[]
version:为-1,表示对最新版本的数据进⾏修改
zooKeeper.setData(path, data,version);
*/
byte[] data = zooKeeper.getData("/lg_persistent", false, null);
System.out.println("修改前的值:"+new String(data));
//修改 stat:状态信息对象 -1:最新版本
Stat stat = zooKeeper.setData("/lg_persistent", "客户端修改内
容".getBytes(), -1);
byte[] data2 = zooKeeper.getData("/lg_persistent", false, null);
System.out.println("修改后的值:"+new String(data2));
}
}
删除节点
public class DeleteNote implements Watcher {
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper("10.211.55.4:2181", 5000, new DeleteNote());
Thread.sleep(Integer.MAX_VALUE);
}
public void process(WatchedEvent watchedEvent) {
//当连接创建了,服务端发送给客户端SyncConnected事件
try {
deleteNodeSync();
} catch (Exception e) {
e.printStackTrace();
}
}
private void deleteNodeSync() throws KeeperException, InterruptedException
{
/*
zooKeeper.exists(path,watch) :判断节点是否存在
zookeeper.delete(path,version) : 删除节点
*/
Stat exists = zooKeeper.exists("/lg_persistent/lg-children", false);
System.out.println(exists == null ? "该节点不存在":"该节点存在");
zooKeeper.delete("/lg_persistent/lg-children",-1);
Stat exists2 = zooKeeper.exists("/lg_persistent/lg-children", false);
System.out.println(exists2 == null ? "该节点不存在":"该节点存在");
}
}
3.4 Zookeeper-开源客户端
ZkClient
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.2</version>
</dependency>
创建会话:
package com.hust.grid.leesf.zkclient.examples;
import java.io.IOException;
import org.I0Itec.zkclient.ZkClient;
public class CreateSession {
/*
创建⼀个zkClient实例来进⾏连接
注意:zkClient通过对zookeeperAPI内部包装,将这个异步的会话创建过程同步化了
*/
public static void main(String[] args) {
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
System.out.println("ZooKeeper session established.");
}
}
运⾏结果:ZooKeeper session established.
结果表明已经成功创建会话
创建节点
ZkClient提供了递归创建节点的接⼝,即其帮助开发者先完成⽗节点的创建,再创建⼦节点
package com.hust.grid.leesf.zkclient.examples;
import org.I0Itec.zkclient.ZkClient;
public class Create_Node_Sample {
public static void main(String[] args) {
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
System.out.println("ZooKeeper session established.");
//createParents的值设置为true,可以递归创建节点
zkClient.createPersistent("/lg-zkClient/lg-c1",true);
System.out.println("success create znode.");
}
}
运⾏结果:success create znode.
结果表明已经成功创建了节点,值得注意的是,在原⽣态接⼝中是⽆法创建成功的(⽗节点不存在),
但是通过ZkClient通过设置createParents参数为true可以递归的先创建⽗节点,再创建⼦节点
删除节点
ZkClient提供了递归删除节点的接⼝,即其帮助开发者先删除所有⼦节点(存在),再删除⽗节点。
package com.hust.grid.leesf.zkclient.examples;
import org.I0Itec.zkclient.ZkClient;
public class Del_Data_Sample {
public static void main(String[] args) throws Exception {
String path = "/lg-zkClient/lg-c1";
ZkClient zkClient = new ZkClient("127.0.0.1:2181", 5000);
zkClient.deleteRecursive(path);
System.out.println("success delete znode.");
}
}
运⾏结果: success delete znode.
结果表明ZkClient可直接删除带⼦节点的⽗节点,因为其底层先删除其所有⼦节点,然后再删除⽗节点
获取⼦节点
package com.hust.grid.leesf.zkclient.examples;
import java.util.List;
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.ZkClient;
public class Get_Children_Sample {
public static void main(String[] args) throws Exception {
ZkClient zkClient = new ZkClient("127.0.0.1:2181", 5000);
List<String> children = zkClient.getChildren("/lg-zkClient");
System.out.println(children);
//注册监听事件
zkClient.subscribeChildChanges(path, new IZkChildListener() {
public void handleChildChange(String parentPath, List<String>
currentChilds) throws Exception {
System.out.println(parentPath + " 's child changed,
currentChilds:" + currentChilds);
}
});
zkClient.createPersistent("/lg-zkClient");
Thread.sleep(1000);
zkClient.createPersistent("/lg-zkClient/c1");
Thread.sleep(1000);
zkClient.delete("/lg-zkClient/c1");
Thread.sleep(1000);
zkClient.delete(path);
Thread.sleep(Integer.MAX_VALUE);
}
}
运⾏结果:
/zk-book 's child changed, currentChilds:[]
/zk-book 's child changed, currentChilds:[c1]
/zk-book 's child changed, currentChilds:[]
/zk-book 's child changed, currentChilds:null
结果表明:
客户端可以对⼀个不存在的节点进⾏⼦节点变更的监听。
⼀旦客户端对⼀个节点注册了⼦节点列表变更监听之后,那么当该节点的⼦节点列表发⽣变更时,服务
端都会通知客户端,并将最新的⼦节点列表发送给客户端
该节点本身的创建或删除也会通知到客户端。
获取数据(节点是否存在、更新、删除)
public class Get_Data_Sample {
public static void main(String[] args) throws InterruptedException {
String path = "/lg-zkClient-Ep";
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
//判断节点是否存在
boolean exists = zkClient.exists(path);
if (!exists){
zkClient.createEphemeral(path, "123");
}
//注册监听
zkClient.subscribeDataChanges(path, new IZkDataListener() {
public void handleDataChange(String path, Object data) throws
Exception {
System.out.println(path+"该节点内容被更新,更新后的内容"+data);
}
public void handleDataDeleted(String s) throws Exception {
System.out.println(s+" 该节点被删除");
}
});
//获取节点内容
Object o = zkClient.readData(path);
System.out.println(o);
//更新
zkClient.writeData(path,"4567");
Thread.sleep(1000);
//删除
zkClient.delete(path);
Thread.sleep(1000);
}
}
运⾏结果:
123
/lg-zkClient-Ep该节点内容被更新,更新后的内容4567
/lg-zkClient-Ep 该节点被删除
结果表明可以成功监听节点数据变化或删除事件。
Curator客户端
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
创建会话
Curator的创建会话⽅式与原⽣的API和ZkClient的创建⽅式区别很⼤。Curator创建客户端是通过
CuratorFrameworkFactory⼯⼚类来实现的。具体如下:
1.使⽤CuratorFramework这个⼯⼚类的两个静态⽅法来创建⼀个客户端
public static CuratorFramework newClient(String connectString, RetryPolicy
retryPolicy)
public static CuratorFramework newClient(String connectString, int
sessionTimeoutMs, int connectionTimeoutMs, RetryPolicy retryPolicy)
其中参数RetryPolicy提供重试策略的接⼝,可以让⽤户实现⾃定义的重试策略,默认提供了以下实现,
分别为ExponentialBackoffRetry(基于backoff的重连策略)、RetryNTimes(重连N次策略)、
RetryForever(永远重试策略)、
2.通过调⽤CuratorFramework中的start()⽅法来启动会话
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
CuratorFramework client =
CuratorFrameworkFactory.newClient("127.0.0.1:2181",retryPolicy);
client.start();
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181",
5000,1000,retryPolicy);
client.start();
其实进⼀步查看源代码可以得知,其实这两种⽅法内部实现⼀样,只是对外包装成不同的⽅法。它们的
底层都是通过第三个⽅法builder来实现的
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
private static CuratorFramework Client = CuratorFrameworkFactory.builder()
.connectString("server1:2181,server2:2181,server3:2181")
.sessionTimeoutMs(50000)
.connectionTimeoutMs(30000)
.retryPolicy(retryPolicy)
.build();
client.start();
参数:
connectString:zk的server地址,多个server之间使⽤英⽂逗号分隔开
connectionTimeoutMs:连接超时时间,如上是30s,默认是15s
sessionTimeoutMs:会话超时时间,如上是50s,默认是60s
retryPolicy:失败重试策略
ExponentialBackoffRetry:构造器含有三个参数 ExponentialBackoffRetry(intbaseSleepTimeMs, int maxRetries, int maxSleepMs)
baseSleepTimeMs:初始的sleep时间,⽤于计算之后的每次重试的sleep时间,
计算公式:当前sleep时间=baseSleepTimeMs*Math.max(1,random.nextInt(1<<(retryCount+1)))
maxRetries:最⼤重试次数
maxSleepMs:最⼤sleep时间,如果上述的当前sleep计算出来⽐这个⼤,那么sleep⽤
这个时间,默认的最⼤时间是Integer.MAX_VALUE毫秒。
其他,查看org.apache.curator.RetryPolicy接⼝的实现类
start():完成会话的创建
package com.hust.grid.leesf.curator.examples;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
public class Create_Session_Sample {
public static void main(String[] args) throws Exception {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client =
CuratorFrameworkFactory.newClient("127.0.0.1:2181", 5000, 3000, retryPolicy);
client.start();
System.out.println("Zookeeper session1 established. ");
CuratorFramework client1 = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181") //server地址
.sessionTimeoutMs(5000) // 会话超时时间
.connectionTimeoutMs(3000) // 连接超时时间
.retryPolicy(retryPolicy) // 重试策略
.namespace("base") // ᇿ⽴命名空间/base
.build(); //
client1.start();
System.out.println("Zookeeper session2 established. ");
}
}
运⾏结果:Zookeeper session1 established. Zookeeper session2 established
需要注意的是session2会话含有隔离命名空间,即客户端对Zookeeper上数据节点的任何操作都是相
对/base⽬录进⾏的,这有利于实现不同的Zookeeper的业务之间的隔离
创建节点
curator提供了⼀系列Fluent⻛格的接⼝,通过使⽤Fluent编程⻛格的接⼝,开发⼈员可以进⾏⾃由组合
来完成各种类型节点的创建。
下⾯简单介绍⼀下常⽤的⼏个节点创建场景。
(1)创建⼀个初始内容为空的节点
client.create().forPath(path);
Curator默认创建的是持久节点,内容为空。
(2)创建⼀个包含内容的节点
client.create().forPath(path,"我是内容".getBytes());
Curator和ZkClient不同的是依旧采⽤Zookeeper原⽣API的⻛格,内容使⽤byte[]作为⽅法参数。
(3)递归创建⽗节点,并选择节点类型
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPa
th(path);
creatingParentsIfNeeded这个接⼝⾮常有⽤,在使⽤ZooKeeper 的过程中,开发⼈员经常会碰到
NoNodeException 异常,其中⼀个可能的原因就是试图对⼀个不存在的⽗节点创建⼦节点。因此,开
发⼈员不得不在每次创建节点之前,都判断⼀下该⽗节点是否存在——这个处理通常⽐较麻烦。在使⽤
Curator 之后,通过调⽤creatingParentsIfNeeded 接⼝,Curator 就能够⾃动地递归创建所有需要的
⽗节点。
package com.hust.grid.leesf.curator.examples;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
public static void main(String[] args) throws Exception {
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181") //server地址
.sessionTimeoutMs(5000) // 会话超时时间
.connectionTimeoutMs(3000) // 连接超时时间
.retryPolicy(new ExponentialBackoffRetry(1000,5)) //
重试策略
.build(); //
client.start();
System.out.println("Zookeeper session established. ");
//添加节点
String path = "/lg-curator/c1";
client.create().creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT).forPath(path,"init".getBytes());
Thread.sleep(1000);
System.out.println("success create znode"+path);
}
}
运⾏结果:Zookeeper session established. success create znode/lg-curator/c1
其中,也创建了lg-curator/c1的⽗节点lg-curator节点。
删除节点
删除节点的⽅法也是基于Fluent⽅式来进⾏操作,不同类型的操作调⽤ 新增不同的⽅法调⽤即可。
(1)删除⼀个⼦节点
client.delete().forPath(path);
(2)删除节点并递归删除其⼦节点
client.delete().deletingChildrenIfNeeded().forPath(path);
(3)指定版本进⾏删除
client.delete().withVersion(1).forPath(path);
如果此版本已经不存在,则删除异常,异常信息如下。
org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode =
BadVersion for
(4)强制保证删除⼀个节点
client.delete().guaranteed().forPath(path);
只要客户端会话有效,那么Curator会在后台持续进⾏删除操作,直到节点删除成功。⽐如遇到⼀些⽹
络异常的情况,此guaranteed的强制删除就会很有效果。
演示:
package com.hust.grid.leesf.curator.examples;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
public static void main(String[] args) throws Exception {
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181") //server地址
.sessionTimeoutMs(5000) // 会话超时时间
.connectionTimeoutMs(3000) // 连接超时时间
.retryPolicy(new ExponentialBackoffRetry(1000,5)) //
重试策略
.build(); //
client.start();
System.out.println("Zookeeper session established. ");
//删除节点
String path = "/lg-curator";
client.delete().deletingChildrenIfNeeded().withVersion(-1).forPath(path);
System.out.println("success create znode"+path);
}
}
运⾏结果:Zookeeper session established. success create znode/lg-curator
结果表明成功删除/lg-curator节点
获取数据
获取节点数据内容API相当简单,同时Curator提供了传⼊⼀个Stat变量的⽅式来存储服务器端返回的最
新的节点状态信息
// 普通查询
client.getData().forPath(path);
// 包含状态查询
Stat stat = new Stat();
client.getData().storingStatIn(stat).forPath(path);
演示:
package com.hust.grid.leesf.curator.examples;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
public class Get_Node_Sample {
public static void main(String[] args) throws Exception {
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181") //server地址
.sessionTimeoutMs(5000) // 会话超时时间
.connectionTimeoutMs(3000) // 连接超时时间
.retryPolicy(new ExponentialBackoffRetry(1000,5)) //
重试策略
.build(); //
client.start();
System.out.println("Zookeeper session established. ");
//添加节点
String path = "/lg-curator/c1";
client.create().creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT).forPath(path,"init".getBytes());
System.out.println("success create znode"+path);
//获取节点数据
Stat stat = new Stat();
byte[] bytes = client.getData().storingStatIn(stat).forPath(path);
System.out.println(new String(bytes));
}
}
运⾏结果:Zookeeper session established. success create znode/lg-curator/c1 init
结果表明成功获取了节点的数据
更新数据
更新数据,如果未传⼊version参数,那么更新当前最新版本,如果传⼊version则更新指定version,如
果version已经变更,则抛出异常。
// 普通更新
client.setData().forPath(path,"新内容".getBytes());
// 指定版本更新
client.setData().withVersion(1).forPath(path);
版本不一致异常:
org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode =
KeeperErrorCode = Unimplemented for (重点)
BadVersion for
案例演示:
package com.hust.grid.leesf.curator.examples;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
public class Set_Node_Sample {
public static void main(String[] args) throws Exception {
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181") //server地址
.sessionTimeoutMs(5000) // 会话超时时间
.connectionTimeoutMs(3000) // 连接超时时间
.retryPolicy(new ExponentialBackoffRetry(1000,5)) //
重试策略
.build(); //
client.start();
System.out.println("Zookeeper session established. ");
String path = "/lg-curator/c1";
//获取节点数据
Stat stat = new Stat();
byte[] bytes = client.getData().storingStatIn(stat).forPath(path);
System.out.println(new String(bytes));
//更新节点数据
int version =
client.setData().withVersion(stat.getVersion()).forPath(path).getVersion();
System.out.println("Success set node for : " + path + ", new
version: "+version);
client.setData().withVersion(stat.getVersion()).forPath(path).getVersion();
}
}
运⾏结果:
Zookeeper session established.
init
Success set node for : /lg-curator/c1, new version: 1
Exception in thread “main”
org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode =
BadVersion for /lg-curator/c1
结果表明当携带数据版本不⼀致时,⽆法完成更新操作。
4. Zookeeper应⽤场景
4-1、数据发布/订阅
分别是推(Push)模式和拉(Pull)模式
顾名思义就是发布者将数据发布到
ZooKeeper的⼀个或⼀系列节点上,供订阅者进⾏数据订阅,进⽽达到动态获取数据的⽬的,实现配置信息的集中式管理和数据的动态更新
4-2、命名服务
是分布式系统最基本的公共服务之
⼀。在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等
布式全局唯⼀ID的分配机制
UUID 1、 最⼤的问题就在于⽣成的字符串过⻓ 2 含义不明
zookeeper全局唯⼀ID⽣成的ZooKeeper节点示意图
对于⼀个任务列表的主键,使⽤ZooKeeper⽣成唯⼀ID的基本步骤:
1. 所有客户端都会根据⾃⼰的任务类型,在指定类型的任务下⾯通过调⽤create()接⼝来创建⼀个顺序节点,例如创建“job-”节点。
2. 节点创建完毕后,create()接⼝会返回⼀个完整的节点名,例如“job-0000000003”。
3. 客户端拿到这个返回值后,拼接上 type 类型,例如“type2-job-0000000003”,这就可以作为⼀个全局唯⼀的ID了。
4-3、集群管理
所谓集群管理,包括集群监控与集群控制两⼤块,前者侧重对集群运⾏时状态的收集,后者则是对集群进⾏操作与控制。
Zookeeper的两⼤特性:
1.客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其⼦节点列表发⽣变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。
2.对在Zookeeper上创建的临时节点,⼀旦客户端与服务器之间的会话失效,那么临时节点也会被⾃动删除
分布式⽇志收集系统
在⼀个典型的⽇志系统的架构设计中,整个⽇志系统会把所有需要收集的⽇志机器(我们以“⽇志源机器”代表此类机器)分为多个组别,每个组别对应⼀个收集器,这个收集器其实就是⼀个后台机器(我们以“收集器机器”代表此类机器),⽤于收集⽇志对于⼤规模的分布式⽇志收集系统场景,通常需要解决两个问题:
· 变化的⽇志源机器
在⽣产环境中,伴随着机器的变动,每个应⽤的机器⼏乎每天都是在变化的(机器硬件问题、扩容、机房迁移或是⽹络问题等都会导致⼀个应⽤的机器变化),也就是说每个组别中的⽇志源机器通常是在不断变化的
· 变化的收集器机器
⽇志收集系统⾃身也会有机器的变更或扩容,于是会出现新的收集器机器加⼊或是⽼的收集器机器退出的情况。
使⽤Zookeeper的场景步骤如下
① 注册收集器机器
使⽤ZooKeeper来进⾏⽇志系统收集器的注册,典型做法是在ZooKeeper上创建⼀个节点作为收集器的根节点,例如/logs/collector(下⽂我们以“收集器节点”代表该数据节点),每个收集器机器在启动的时候,都会在收集器节点下创建⾃⼰的节点,例如/logs/collector/[Hostname]
② 任务分发
待所有收集器机器都创建好⾃⼰对应的节点后,系统根据收集器节点下⼦节点的个数,将所有⽇志源机
器分成对应的若⼲组,然后将分组后的机器列表分别写到这些收集器机器创建的⼦节点(例如/logs/collector/host1)上去。这样⼀来,每个收集器机器都能够从⾃⼰对应的收集器节点上获取⽇志源机器列表,进⽽开始进⾏⽇志收集⼯作。
③ 状态汇报
完成收集器机器的注册以及任务分发后,我们还要考虑到这些机器随时都有挂掉的可能。因此,针对这个问题,我们需要有⼀个收集器的状态汇报机制:每个收集器机器在创建完⾃⼰的专属节点后,还需要在对应的⼦节点上创建⼀个状态⼦节点,例如/logs/collector/host1/status每个收集器机器都需要定期向该节点写⼊⾃⼰的状态信息。我们可以把这种策略看作是⼀种⼼跳检测机制,通常收集器机器都会在这个节点中写⼊⽇志收集进度信息。⽇志系统根据该状态⼦节点的最后更新时间来判断对应的收集器机器是否存活。
④ 动态分配
如果收集器机器挂掉或是扩容了,就需要动态地进⾏收集任务的分配。在运⾏过程中,⽇志系统始终关注着/logs/collector这个节点下所有⼦节点的变更,⼀旦检测到有收集器机器停⽌汇报或是有新的收集器机器加⼊,就要开始进⾏任务的重新分配。⽆论是针对收集器机器停⽌汇报还是新机器加⼊的情况,⽇志系统都需要将之前分配给该收集器的所有任务进⾏转移。为了解决这个问题,通常有两种做法:
· 全局动态分配
这是⼀种简单粗暴的做法,在出现收集器机器挂掉或是新机器加⼊的时候,⽇志系统需要根据新的收集
器机器列表,⽴即对所有的⽇志源机器重新进⾏⼀次分组,然后将其分配给剩下的收集器机器。
· 局部动态分配
全局动态分配⽅式虽然策略简单,但是存在⼀个问题:⼀个或部分收集器机器的变更,就会导致全局动态任务的分配,影响⾯⽐较⼤,因此⻛险也就⽐较⼤。所谓局部动态分配,顾名思义就是在⼩范围内进⾏任务的动态分配。在这种策略中,每个收集器机器在汇报⾃⼰⽇志收集态的同时,也会把⾃⼰的负载汇报上去。请注意,这⾥提到的负载并不仅仅只是简单地指机器CPU负载(Load),⽽是⼀个对当前收集器任务执⾏的综合评估,这个评估算法和ZooKeeper本身并没有太⼤的关系,这⾥不再赘述。
在这种策略中,如果⼀个收集器机器挂了,那么⽇志系统就会把之前分配给这个机器的任务重新分配到那些负载较低的机器上去。同样,如果有新的收集器机器加⼊,会从那些负载⾼的机器上转移部分任务给这个新加⼊的机器。
上述步骤已经完整的说明了整个⽇志收集系统的⼯作流程,其中有两点注意事项:
①节点类型
在/logs/collector节点下创建临时节点可以很好的判断机器是否存活,但是,若机器挂了,其节点会被
删除,记录在节点上的⽇志源机器列表也被清除,所以需要选择持久节点来标识每⼀台机器,同时在节点下分别创建/logs/collector/[Hostname]/status节点来表征每⼀个收集器机器的状态,这样,既能实现对所有机器的监控,同时机器挂掉后,依然能够将分配任务还原。
② ⽇志系统节点监听
若采⽤Watcher机制,那么通知的消息量的⽹络开销⾮常⼤,需要采⽤⽇志系统主动轮询收集器节点的策略,这样可以节省⽹络流量,但是存在⼀定的延时
4-4Master选举
整个系统⼤体上可以分成客户端集群、分布式缓存系统、海量数据处理总线和 ZooKeeper四个部分
。图中的Client集群每天定时会通过ZooKeeper来实现Master选举。
选举产⽣Master客户端之后,这个Master就会负责进⾏⼀系列的海量数据处理,最终计算得到⼀个数据结果,并将其放置在⼀个内存/数据库中。同时,Master还需要通知集群中其他所有的客户端从这个内存/数据库中共享计算结果。
⾸先会在 ZooKeeper 上创建⼀个⽇期节点,例如“2020-11-11
客户端集群每天都会定时往ZooKeeper 上创建⼀个临时节点,例如/master_election/2020-11-11/binding。在这个过程中,只有⼀个客户端能够成功创建这个节点,那么这个客户端所在的机器就成为了Master。同时,其他没有在ZooKeeper上成功创建节点的客户端,都会在节点/master_election/2020-11-11 上注册⼀个⼦节点变更的 Watcher,⽤于监控当前的 Master 机器是否存活,⼀旦发现当前的 Master 挂了,那么其余的客户端将会重新进⾏Master选举。从上⾯的讲解中,我们可以看到,如果仅仅只是想实现Master选举的话,那么其实只需要有⼀个能够保证数据唯⼀性的组件即可,例如关系型数据库的主键模型就是⾮常不错的选择。但是,如果希望能够快速地进⾏集群 Master 动态选举,那么就可以基于 ZooKeeper来实现
4-5、分布式锁
排他锁
排他锁(Exclusive Locks,简称 X 锁),⼜称为写锁或ᇿ占锁,是⼀种基本的锁类型。如果事务 T1对数据对象 O1加上了排他锁,那么在整个加锁期间,只允许事务 T1对 O1进⾏读取和更新操作,其他任何事务都不能再对这个数据对象进⾏任何类型的操作——直到T1释放了排他锁
① 定义锁
在通常的Java开发编程中,有两种常⻅的⽅式可以⽤来定义锁,分别是synchronized机制和JDK5提供的ReentrantLock。然⽽,在ZooKeeper中,没有类似于这样的API可以直接使⽤,⽽是通过 ZooKeeper上的数据节点来表示⼀个锁,例如/exclusive_lock/lock节点就可以被定义为⼀个锁
② 获取锁
在需要获取排他锁时,所有的客户端都会试图通过调⽤ create()接⼝,在/exclusive_lock节点下创建临时⼦节点/exclusive_lock/lock。在前⾯,我们也介绍了,ZooKeeper 会保证在所有的客户端中,最终只有⼀个客户端能够创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock 节点上注册⼀个⼦节点变更的Watcher监听,以便实时监听到lock节点的变更情况
③释放锁
在“定义锁”部分,我们已经提到,/exclusive_lock/lock 是⼀个临时节点,因此在以下两种情况下,都有可能释放锁。 · 当前获取锁的客户端机器发⽣宕机,那么ZooKeeper上的这个临时节点就会被移除。 ·正常执⾏完业务逻辑后,客户端就会主动将⾃⼰创建的临时节点删除。 ⽆论在什么情况下移除了lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了⼦节点变更Watcher监听的客户端。这些
客户端在接收到通知后,再次重新发起分布式锁获取,即重复“获取锁”过程。整个排他锁的获取和释放
流程,如下图:
共享锁
共享锁(Shared Locks,简称S锁),⼜称为读锁,同样是⼀种基本的锁类型。
如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进⾏读取操作,其他事务也只能对这
个数据对象加共享锁——直到该数据对象上的所有共享锁都被释放。
共享锁和排他锁最根本的区别在于,加上排他锁后,数据对象只对⼀个事务可⻅,⽽加上共享锁后,数
据对所有事务都可⻅。
① 定义锁 和排他锁⼀样,同样是通过 ZooKeeper 上的数据节点来表示⼀个锁,是⼀个类似于“/shared_lock/[Hostname]-请求类型-序号”的临时顺序节点,例如/shared_lock/host1-R-0000000001,那么,这个节点就代表了⼀个共享锁,如图所示:
② 获取锁
在需要获取共享锁时,所有客户端都会到/shared_lock 这个节点下⾯创建⼀个临时顺序节点,如果当前是读请求,那么就创建例如/shared_lock/host1-R-0000000001的节点;如果是写请求,那么就创建例如/shared_lock/host2-W-0000000002的节点。
判断读写顺序
通过Zookeeper来确定分布式读写顺序,⼤致分为四步
1. 创建完节点后,获取/shared_lock节点下所有⼦节点,并对该节点变更注册监听。
2. 确定⾃⼰的节点序号在所有⼦节点中的顺序。
3. 对于读请求:若没有⽐⾃⼰序号⼩的⼦节点或所有⽐⾃⼰序号⼩的⼦节点都是读请求,那么表
明⾃⼰已经成功获取到共享锁,同时开始执⾏读取逻辑,若有写请求,则需要等待。对于写请求:若⾃⼰不
是序号最⼩的⼦节点,那么需要等待。
4. 接收到Watcher通知后,重复步骤1
③ 释放锁,其释放锁的流程与ᇿ占锁⼀致。
⽺群效应
针对如上图所示的情况进⾏分析
- host1⾸先进⾏读操作,完成后将节点/shared_lock/host1-R-00000001删除。
- 余下4台机器均收到这个节点移除的通知,然后重新从/shared_lock节点上获取⼀份新的⼦节点列表。
- 每台机器判断⾃⼰的读写顺序,其中host2检测到⾃⼰序号最⼩,于是进⾏写操作,余下的机器则继续等待。
4.继续…
可以看到,host1客户端在移除⾃⼰的共享锁后,Zookeeper发送了⼦节点更变Watcher通知给所有机器,然⽽除了给host2产⽣影响外,对其他机器没有任何作⽤。⼤量的Watcher通知和⼦节点列表获取
两个操作会重复运⾏,这样不仅会对zookeeper服务器造成巨⼤的性能影响影响和⽹络开销,更为严重的是,如果同⼀时间有多个节点对应的客户端完成事务或是事务中断引起节点消失,ZooKeeper服务器
就会在短时间内向其余客户端发送⼤量的事件通知,这就是所谓的⽺群效应。上⾯这个ZooKeeper分布式共享锁实现中出现⽺群效应的根源在于,没有找准客户端真正的关注点。我
们再来回顾⼀下上⾯的分布式锁竞争过程,它的核⼼逻辑在于:判断⾃⼰是否是所有⼦节点中序号最⼩
的。于是,很容易可以联想到,每个节点对应的客户端只需要关注⽐⾃⼰序号⼩的那个相关节点的变更
情况就可以了——⽽不需要关注全局的⼦列表变更情况。
来避免⽺群效应。
改动
在于:每个锁竞争者,只需要关注/shared_lock节点下序号⽐⾃⼰⼩的那个节点是否存在即可,具体实
现如下。
4. 客户端调⽤create接⼝常⻅类似于/shared_lock/[Hostname]-请求类型-序号的临时顺序节点。
5. 客户端调⽤getChildren接⼝获取所有已经创建的⼦节点列表(不注册任何Watcher)。
6. 3. 如果⽆法获取共享锁,就调⽤exist接⼝来对⽐⾃⼰⼩的节点注册Watcher。对于读请求:向⽐⾃⼰
序号⼩的最后⼀个写请求节点注册Watcher监听。对于写请求:向⽐⾃⼰序号⼩的最后⼀个节点注
册Watcher监听。
4. 等待Watcher通知,继续进⼊步骤2
此⽅案改动主要在于:每个锁竞争者,只需要关注/shared_lock节点下序号⽐⾃⼰⼩的那个节点是否存在即可
4-6、分布式队列
分布式队列可以简单分为两⼤类:⼀种是常规的FIFO先⼊先出队列模型,还有⼀种是 等待队列元素聚
集后统⼀安排处理执⾏的Barrier模型。
① FIFO先⼊先出
创建完节点后,根据如下4个步骤来确定执⾏顺序。
1. 通过调⽤getChildren接⼝来获取/queue_fifo节点的所有⼦节点,即获取队列中所有的元素。
2. 确定⾃⼰的节点序号在所有⼦节点中的顺序。
3. 如果⾃⼰的序号不是最⼩,那么需要等待,同时向⽐⾃⼰序号⼩的最后⼀个节点注册Watcher监
听。
4. 接收到Watcher通知后,重复步骤1。
② Barrier:分布式屏障
Barrier原意是指障碍物、屏障,⽽在分布式系统中,特指系统之间的⼀个协调条件,规定了⼀个队列的元素必须都集聚后才能统⼀进⾏安排,否则⼀直等待.
⼤致的设计思想如下:开始时,/queue_barrier 节点是⼀个已经存在的默认节点,并且将其节点的数据内容赋值为⼀个数字n来代表Barrier值,例如n=10表示只有当/queue_barrier节点下的⼦节点个数达到10后,才会打开Barrier。之后,所有的客户端都会到/queue_barrie节点下创建⼀个临
时节点,例如/queue_barrier/host1,如图所示。
创建完节点后,按照如下步骤执⾏。
1. 通过调⽤getData接⼝获取/queue_barrier节点的数据内容:10。
2. 通过调⽤getChildren接⼝获取/queue_barrier节点下的所有⼦节点,同时注册对⼦节点变更的
Watcher监听。
3. 统计⼦节点的个数。
4. 如果⼦节点个数还不⾜10个,那么需要等待。
5. 接受到Wacher通知后,重复步骤2
5、Zookeeper深⼊进阶
5-1、ZAB协议
zookeeper并没有完全采⽤paxos算法,⽽是使⽤了⼀种称为Zookeeper Atomic Broadcast(ZAB,Zookeeper原⼦消息⼴播协议)的协议作为其数据⼀致性的核⼼算法。
ZAB协议并不像Paxos算法那样 是⼀种通⽤的分布式⼀致性算法,它是⼀种特别为zookeeper专⻔设计的⼀种⽀持崩溃恢复的原⼦⼴播协议
在zookeeper中,主要就是依赖ZAB协议来实现分布式数据的⼀致性,基于该协议,Zookeeper实现了⼀种主备模式的系统架构来保持集群中各副本之间的数据的⼀致性,表现形式就是 使⽤⼀个单⼀的主进程来接收并处理客户端的所有事务请求,并采⽤ZAB的原⼦⼴播协议,将服务器数据的状态变更以事务Proposal的形式⼴播到所有的副本进程中,ZAB协议的主备模型架构保证了同⼀时刻集群中只能够有⼀个主进程来⼴播服务器的状态变更,因此能够很好地处理客户端⼤量的并发请求。但是,也要考虑到主进程在任何时候都有可能出现崩溃退出或重启现象,因此,ZAB协议还需要做到当前主进程当出现上述异常情况的时候,依旧能正常⼯作。
ZAB核⼼
ZAB协议的核⼼是定义了对于那些会改变Zookeeper服务器数据状态的事务请求的处理⽅式
即:所有事务请求必须由⼀个全局唯⼀的服务器来协调处理,这样的服务器被称为Leader服务器,余下的服务器则称为Follower服务器,Leader服务器负责将⼀个客户端事务请求转化成⼀个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器,之后Leader服务器需要等待所有Follower服务器的反馈,⼀旦超过半数的Follower服务器进⾏了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前⼀个Proposal进⾏提交
ZAB协议包括两种基本的模式**:崩溃恢复和消息⼴播**
进⼊崩溃恢复模式:
当整个服务框架启动过程中,或者是Leader服务器出现⽹络中断、崩溃退出或重启等异常情况时,ZAB协议就会进⼊崩溃恢复模式,同时选举产⽣新的Leader服务器。当选举产⽣了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式,其中,所谓的状态同步 就是指数据同步,⽤来保证集群中过半的机器能够和Leader服务器的数据状态保持⼀致
进⼊消息⼴播模式:
当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进⼊消息⼴播模式,当⼀台同样遵守ZAB协议的服务器启动后加⼊到集群中,如果此时集群中已经存在⼀个Leader服务器在负责进⾏消息⼴播,那么加⼊的服务器就会⾃觉地进⼊数据恢复模式:找到Leader所在的服务器,并与其进⾏数据同步,然后⼀起参与到消息⼴播流程中去。Zookeeper只允许唯⼀的⼀即:所有事务请求必须由⼀个全局唯⼀的服务器来协调处理,这样的服务器被称为Leader服务器,余下的服务器则称为Follower服务器,Leader服务器负责将⼀个客户端事务请求转化成⼀个事务Proposal(提
议),并将该Proposal分发给集群中所有的Follower服务器,之后Leader服务器需要等待所有Follower服务器的反馈,⼀旦超过半数的Follower服务器进⾏了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前⼀个Proposal进⾏提交个Leader服务器来进⾏事务请求的处理,Leader服务器在接收到客户端的事务请求后,会⽣成对应的事务提议并发起⼀轮⼴播协议,⽽如果集群中的其他机器收到客户端的事务请求后,那么这些⾮Leader服务器会⾸先将这个事务请求转发给Leader服务器。
① 消息⼴播
ZAB协议的消息⼴播过程使⽤原⼦⼴播协议,类似于⼀个⼆阶段提交过程,针对客户端的事务请求,
Leader服务器会为其⽣成对应的事务Proposal,并将其发送给集群中其余所有的机器,然后再分别收集各⾃的选票,最后进⾏事务提交。
在ZAB的⼆阶段提交过程中,移除了中断逻辑,所有的Follower服务器要么正常反馈Leader提出的事务
Proposal,要么就抛弃Leader服务器,同时,ZAB协议将⼆阶段提交中的中断逻辑移除意味着我们可以在过半的Follower服务器已经反馈Ack之后就开始提交事务Proposal了,⽽不需要等待集群中所有的Follower服务器都反馈响应,但是,在这种简化的⼆阶段提交模型下,⽆法处理因Leader服务器崩溃退出⽽带来的数据不⼀致问题,因此ZAB采⽤了崩溃恢复模式来解决此问题,另外,整个消息⼴播协议是基于具有FIFO特性的TCP协议来进⾏⽹络通信的,因此能够很容易保证消息⼴播过程中消息接受与发送的顺序性。
在整个消息⼴播过程中,Leader服务器会为每个事务请求⽣成对应的Proposal来进⾏⼴播,并且在⼴播事务Proposal之前,Leader服务器会⾸先为这个事务Proposal分配⼀个全局单调递增的唯⼀ID,称之为事务ID(ZXID),由于ZAB协议需要保证每个消息严格的因果关系,因此必须将每个事务Proposal按照其ZXID的先后顺序来进⾏排序和处理。
具体的过程:在消息⼴播过程中,Leader服务器会为每⼀个Follower服务器都各⾃分配⼀个单ᇿ的队列,然后将需要⼴播的事务 Proposal 依次放⼊这些队列中去,并且根据 FIFO策略进⾏消息发送。每⼀个Follower服务器在接收到这个事务Proposal之后,都会⾸先将其以事务⽇志的形式写⼊到本地磁盘中去,并且在成功写⼊后反馈给Leader服务器⼀个Ack响应。当Leader服务器接收到超过半数Follower的Ack响应后,就会⼴播⼀个Commit消息给所有的Follower服务器以通知其进⾏事务提交,同时Leader⾃身也会完成对事务的提交,⽽每⼀个Follower服务器在接收到Commit消息后,也会完成对事务的提交。
② 崩溃恢复
ZAB协议的这个基于原⼦⼴播协议的消息⼴播过程,在正常情况下运⾏⾮常良好,但是⼀旦在Leader服务器出现崩溃,或者由于⽹络原因导致Leader服务器失去了与过半Follower的联系,那么就会进⼊崩溃恢复模式。在ZAB协议中,为了保证程序的正确运⾏,整个恢复过程结束后需要选举出⼀个新的Leader服务器,因此,ZAB协议需要⼀个⾼效且可靠的Leader选举算法,从⽽保证能够快速地选举出新的Leader,同时,Leader选举算法不仅仅需要让Leader⾃身知道已经被选举为Leader,同时还需要让集群中的所有其他机器也能够快速地感知到选举产⽣出来的新Leader服务器。
基本特性
根据上⾯的内容,我们了解到,ZAB协议规定了如果⼀个事务Proposal在⼀台机器上被处理成功,那么应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。接下来我们看看在崩溃恢复过程中,可能会出现的两个数据不⼀致性的隐患及针对这些情况ZAB协议所需要保证的特性
ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交
假设⼀个事务在 Leader 服务器上被提交了,并且已经得到过半 Folower 服务器的Ack反馈,但是在它
将Commit消息发送给所有Follower机器之前,Leader服务器挂了,如图所示
图中的消息C2就是⼀个典型的例⼦:在集群正常运⾏过程中的某⼀个时刻,Server1 是 Leader 服务器,其先后⼴播了消息 P1、P2、C1、P3 和 C2,其中,当Leader服务器将消息C2(C2是Commit Of
Proposal2的缩写,即提交事务Proposal2)发出后就⽴即崩溃退出了。针对这种情况,ZAB协议就需要确保事务Proposal2最终能够在所有的服务器上都被提交成功,否则将出现不⼀致。
ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务
如果在崩溃恢复过程中出现⼀个需要被丢弃的提案,那么在崩溃恢复结束后需要跳过该事务Proposal,
如图所示。
在图所示的集群中,假设初始的 Leader 服务器 Server1 在提出了⼀个事务Proposal3 之后就崩溃退出了,从⽽导致集群中的其他服务器都没有收到这个事务Proposal3。于是,当 Server1 恢复过来再次加⼊到集群中的时候,ZAB 协议需要确保丢弃Proposal3这个事务。
结合上⾯提到的这两个崩溃恢复过程中需要处理的特殊情况,就决定了 ZAB 协议必须设计这样⼀个Leader 选举算法:能够确保提交已经被 Leader 提交的事务 Proposal,同时丢弃已经被跳过的事务Proposal。针对这个要求,如果让Leader选举算法能够保证新选举出来的Leader服务器拥有集群中所
有机器最⾼编号(即ZXID最⼤)的事务Proposal,那么就可以保证这个新选举出来的Leader⼀定具有所有已经提交的提案。更为重要的是,如果让具有最⾼编号事务Proposal 的机器来成为 Leader,就可以省去 Leader 服务器检查Proposal的提交和丢弃⼯作的这⼀步操作了。
数据同步
完成Leader选举之后,在正式开始⼯作(即接收客户端的事务请求,然后提出新的提案)之前,Leader服务器会⾸先确认事务⽇志中的所有Proposal是否都已经被集群中过半的机器提交了,即是否完
成数据同步。下⾯我们就来看看ZAB协议的数据同步过程。
所有正常运⾏的服务器,要么成为 Leader,要么成为 Follower 并和 Leader 保持同步。Leader服务器需要确保所有的Follower服务器能够接收到每⼀条事务Proposal,并且能够正确地将所有已经提交了的事务Proposal应⽤到内存数据库中去。具体的,Leader服务器会为每⼀个Follower服务器都准备⼀个队列,并将那些没有被各Follower服务器同步的事务以Proposal消息的形式逐个发送给Follower服务器,在每⼀个Proposal消息后⾯紧接着再发送⼀个Commit消息,以表示该事务已经被提交。等到Follower服务器将所有其尚未同步的事务 Proposal 都从 Leader 服务器上同步过来并成功应⽤到本地数据库中后,Leader服务器就会将该Follower服务器加⼊到真正的可⽤Follower列表中,并开始之后的其
他流程。
运⾏时状态分析
在ZAB协议的设计中,每个进程都有可能处于如下三种状态之⼀
· LOOKING:Leader选举阶段。
· FOLLOWING:Follower服务器和Leader服务器保持同步状态。
· LEADING:Leader服务器作为主进程领导状态
所有进程初始状态都是LOOKING状态,此时不存在Leader,接下来,进程会试图选举出⼀个新的Leader,之后,如果进程发现已经选举出新的Leader了,那么它就会切换到FOLLOWING状态,并开始和Leader保持同步,处于FOLLOWING状态的进程称为Follower,LEADING状态的进程称为Leader,当Leader崩溃或放弃领导地位时,其余的Follower进程就会转换到LOOKING状态开始新⼀轮的Leader选
举。
⼀个Follower只能和⼀个Leader保持同步,Leader进程和所有的Follower进程之间都通过⼼跳检测
机制来感知彼此的情况。若Leader能够在超时时间内正常收到⼼跳检测,那么Follower就会⼀直与该Leader保持连接,⽽如果在指定时间内Leader⽆法从过半的Follower进程那⾥接收到⼼跳检测,或者TCP连接断开,那么Leader会放弃当前周期的领导,并转换到LOOKING状态,其他的Follower也会选择放弃这个Leader,同时转换到LOOKING状态,之后会进⾏新⼀轮的Leader选举
ZAB与Paxos的联系和区别
联系:
① 都存在⼀个类似于Leader进程的⻆⾊,由其负责协调多个Follower进程的运⾏。
② Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将⼀个提议进⾏提交。
③ 在ZAB协议中,每个Proposal中都包含了⼀个epoch值,⽤来代表当前的Leader周期,在Paxos
算法中,同样存在这样的⼀个标识,名字为Ballot。
区别:
Paxos算法中,新选举产⽣的主进程会进⾏两个阶段的⼯作,第⼀阶段称为读阶段,新的主进程和
其他进程通信来收集主进程提出的提议,并将它们提交。第⼆阶段称为写阶段,当前主进程开始提出⾃
⼰的提议。
ZAB协议在Paxos基础上添加了同步阶段,此时,新的Leader会确保 存在过半的Follower已经提交
了之前的Leader周期中的所有事务Proposal。这⼀同步阶段的引⼊,能够有效地保证Leader在新的周
期中提出事务Proposal之前,所有的进程都已经完成了对之前所有事务Proposal的提交。
总的来说,ZAB协议和Paxos算法的本质区别在于,两者的设计⽬标不太⼀样,ZAB协议主要⽤于
构建⼀个⾼可⽤的分布式数据主备系统,⽽Paxos算法则⽤于构建⼀个分布式的⼀致性状态机系统
5.2 服务器⻆⾊
Leader
Leader服务器是Zookeeper集群⼯作的核⼼,其主要⼯作有以下两个:
(1) 事务请求的唯⼀调度和处理者,保证集群事务处理的顺序性。
(2) 集群内部各服务器的调度者。
从prepRequestProcessor到FinalRequestProcessor前后⼀共7个请求处理器组成了leader
服务器的请求处理链
(1) PrepRequestProcessor。请求预处理器,也是leader服务器中的第⼀个请求处理器。在Zookeeper
中,那些会改变服务器状态的请求称为事务请求(创建节点、更新数据、删除节点、创建会话等),
PrepRequestProcessor能够识别出当前客户端请求是否是事务请求。对于事务请求,
PrepRequestProcessor处理器会对其进⾏⼀系列预处理,如创建请求事务头、事务体、会话检查、ACL
检查和版本检查等。
(2) ProposalRequestProcessor。事务投票处理器。也是Leader服务器事务处理流程的发起者,对
于⾮事务性请求,ProposalRequestProcessor会直接将请求转发到CommitProcessor处理器,不再做
任何处理,⽽对于事务性请求,处理将请求转发到CommitProcessor外,还会根据请求类型创建对应的
Proposal提议,并发送给所有的Follower服务器来发起⼀次集群内的事务投票。同时,
ProposalRequestProcessor还会将事务请求交付给SyncRequestProcessor进⾏事务⽇志的记录。
(3) SyncRequestProcessor。事务⽇志记录处理器。⽤来将事务请求记录到事务⽇志⽂件中,同时
会触发Zookeeper进⾏数据快照。
(4) AckRequestProcessor。负责在SyncRequestProcessor完成事务⽇志记录后,向Proposal的投
票收集器发送ACK反馈,以通知投票收集器当前服务器已经完成了对该Proposal的事务⽇志记录。
(5) CommitProcessor。事务提交处理器。对于⾮事务请求,该处理器会直接将其交付给下⼀级处
理器处理;对于事务请求,其会等待集群内 针对Proposal的投票直到该Proposal可被提交,利⽤
CommitProcessor,每个服务器都可以很好地控制对事务请求的顺序处理。
(6) ToBeCommitProcessor。该处理器有⼀个toBeApplied队列,⽤来存储那些已经被
CommitProcessor处理过的可被提交的Proposal。其会将这些请求交付给FinalRequestProcessor处理
器处理,待其处理完后,再将其从toBeApplied队列中移除。
(7) FinalRequestProcessor。⽤来进⾏客户端请求返回之前的操作,包括创建客户端请求的响应,
针对事务请求,该处理器还会负责将事务应⽤到内存数据库中。
Follower
Follower服务器是Zookeeper集群状态中的跟随者,其主要⼯作有以下三个:
(1) 处理客户端⾮事务性请求(读取数据),转发事务请求给Leader服务器。
(2) 参与事务请求Proposal的投票。
(3) 参与Leader选举投
和 Leader 服务器的请求处理链最⼤的不同点在于,Follower 服务器的第⼀个处理器换成了
FollowerRequestProcessor处理器,同时由于不需要处理事务请求的投票,因此也没有了
ProposalRequestProcessor处理器。
(1) FollowerRequestProcessor
其⽤作识别当前请求是否是事务请求,若是,那么Follower就会将该请求转发给Leader服务器,
Leader服务器在接收到这个事务请求后,就会将其提交到请求处理链,按照正常事务请求进⾏处理。
(2) SendAckRequestProcessor
其承担了事务⽇志记录反馈的⻆⾊,在完成事务⽇志记录后,会向Leader服务器发送ACK消息以表明⾃
身完成了事务⽇志的记录⼯作
Observer
Observer服务器在⼯作原理上和Follower基本是⼀致的,对于⾮事务请求,都可以进⾏ᇿ⽴的处理,⽽
对于事务请求,则会转发给Leader服务器进⾏处理。和Follower唯⼀的区别在于,Observer不参与任
何形式的投票,包括事务请求Proposal的投票和Leader选举投票。简单地讲,Observer服务器只提供
⾮事务服务,通常⽤于在不影响集群事务处理能⼒的前提下提升集群的⾮事务处理能⼒。
Observer 服务器在初始化阶段会将SyncRequestProcessor处理器也组装上去,但是在实际运⾏过程中,Leader服务器不会将事务请求的投票发送给Observer服务器。
5.3 服务器启动
Zookeeper服务器的启动,⼤致可以分为以下五个步骤
1. 配置⽂件解析
2. 初始化数据管理器
3. 初始化⽹络I/O管理器
4. 数据恢复
5. 对外服务
单机版
1. 预启动
1. 统⼀由QuorumPeerMain作为启动类。⽆论单机或集群,在zkServer.cmd和zkServer.sh
中都配置了QuorumPeerMain作为启动⼊⼝类。
2. 解析配置⽂件zoo.cfg。zoo.cfg配置运⾏时的基本参数,如tickTime、dataDir、
clientPort等参数。
3. 创建并启动历史⽂件清理器DatadirCleanupManager。对事务⽇志和快照数据⽂件进⾏定
时清理。
4. 判断当前是集群模式还是单机模式启动。若是单机模式,则委托给ZooKeeperServerMain进
⾏启动。
5. 再次进⾏配置⽂件zoo.cfg的解析。
6. 创建服务器实例ZooKeeperServer。Zookeeper服务器⾸先会进⾏服务器实例的创建,然后
对该服务器实例进⾏初始化,包括连接器、内存数据库、请求处理器等组件的初始化。
2. 初始化
1. 创建服务器统计器ServerStats。ServerStats是Zookeeper服务器运⾏时的统计器。
2. 创建Zookeeper数据管理器FileTxnSnapLog。FileTxnSnapLog是Zookeeper上层服务
器和底层数据存储之间的对接层,提供了⼀系列操作数据⽂件的接⼝,如事务⽇志⽂件和快照数据⽂件。
Zookeeper根据zoo.cfg⽂件中解析出的快照数据⽬录dataDir和事务⽇志⽬录dataLogDir来创建
FileTxnSnapLog。
3. 设置服务器tickTime和会话超时时间限制。
4. 创建ServerCnxnFactory。通过配置系统属性zookeper.serverCnxnFactory来指定使
⽤Zookeeper⾃⼰实现的NIO还是使⽤Netty框架作为Zookeeper服务端⽹络连接⼯⼚。
5. 初始化ServerCnxnFactory。Zookeeper会初始化Thread作为ServerCnxnFactory的主
线程,然后再初始化NIO服务器。
6. 启动ServerCnxnFactory主线程。进⼊Thread的run⽅法,此时服务端还不能处理客户端
请求。
7. 恢复本地数据。启动时,需要从本地快照数据⽂件和事务⽇志⽂件进⾏数据恢复。
8. 创建并启动会话管理器。Zookeeper会创建会话管理器SessionTracker进⾏会话管理。
9. 初始化Zookeeper的请求处理链。Zookeeper请求处理⽅式为责任链模式的实现。会有多个
请求处理器依次处理⼀个客户端请求,在服务器启动时,会将这些请求处理器串联成⼀个请求处理链。
10. 注册JMX服务。Zookeeper会将服务器运⾏时的⼀些信息以JMX的⽅式暴露给外部。
11. 注册Zookeeper服务器实例。将Zookeeper服务器实例注册给ServerCnxnFactory,之
后Zookeeper就可以对外提供服务。
集群服务器启动
预启动、初始化、Leader选举、Leader与Follower启动期交互、Leader与
Follower启动等过程
1. 预启动
1. 统⼀由QuorumPeerMain作为启动类。
2. 解析配置⽂件zoo.cfg。
3. 创建并启动历史⽂件清理器DatadirCleanupFactory。
4. 判断当前是集群模式还是单机模式的启动。在集群模式中,在zoo.cfg⽂件中配置了多个服务器
地址,可以选择集群启动。
2. 初始化
5. 创建ServerCnxnFactory。
6. 初始化ServerCnxnFactory。
7. 创建Zookeeper数据管理器FileTxnSnapLog。
`4. 创建QuorumPeer实例。Quorum是集群模式下特有的对象,是Zookeeper服务器实例
(ZooKeeperServer)的托管者,QuorumPeer代表了集群中的⼀台机器,在运⾏期间,
QuorumPeer会不断检测当前服务器实例的运⾏状态,同时根据情况发起Leader选举。
8. 创建内存数据库ZKDatabase。ZKDatabase负责管理ZooKeeper的所有会话记录以及
DataTree和事务⽇志的存储。
9. 初始化QuorumPeer。将核⼼组件如FileTxnSnapLog、ServerCnxnFactory、ZKDatabase
注册到QuorumPeer中,同时配置QuorumPeer的参数,如服务器列表地址、Leader选举算法和会话
超时时间限制等。
10. 恢复本地数据。
11. 启动ServerCnxnFactory主线程
3. Leader选举
1. 初始化Leader选举。
集群模式特有,Zookeeper⾸先会根据⾃身的服务器ID(SID)、最新的
ZXID(lastLoggedZxid)和当前的服务器epoch(currentEpoch)来⽣成⼀个初始化投票,在初始化过程中,每个服务器都会给⾃⼰投票。然后,根据zoo.cfg的配置,创建相应Leader选举算法实现,Zookeeper提供了三种默认算法(LeaderElection、AuthFastLeaderElection、FastLeaderElection),可通过zoo.cfg中的electionAlg属性来指定,但现只⽀持FastLeaderElection选举算法。在初始化阶段,Zookeeper会创建Leader选举所需的⽹络I/O层QuorumCnxManager,同时启动对Leader选举端⼝的监听,等待集群中其他服务器创建连接。
2. 注册JMX服务。
3. 检测当前服务器状态
运⾏期间,QuorumPeer会不断检测当前服务器状态。在正常情况下,Zookeeper服务器的状态
在LOOKING、LEADING、FOLLOWING/OBSERVING之间进⾏切换。在启动阶段,QuorumPeer的初始状态是LOOKING,因此开始进⾏Leader选举。
4. Leader选举
ZooKeeper的Leader选举过程,简单地讲,就是⼀个集群中所有的机器相互之间进⾏⼀系列投票,选举产⽣最合适的机器成为Leader,同时其余机器成为Follower或是Observer的集群机器⻆⾊初始化过程。关于Leader选举算法,简⽽⾔之,就是集群中哪个机器处理的数据越新(通常我们根据每个服务器处理过的最⼤ZXID来⽐较确定其数据是否更新),其越有可能成为Leader。当然,如果集群中的所有机器处理的ZXID⼀致的话,那么SID最⼤的服务器成为Leader,其余机器称为
Follower和Observer
4. Leader和Follower启动期交互过程
1. 创建Leader服务器和Follower服务器。完成Leader选举后,每个服务器会根据⾃⼰服务器的⻆⾊创建相应的服务器实例,并进⼊各⾃⻆⾊的主流程。
2. Leader服务器启动Follower接收器LearnerCnxAcceptor。运⾏期间,Leader服务器需要和所有其余的服务器(统称为Learner)保持连接以确集群的机器存活情况,LearnerCnxAcceptor负责接收所有⾮Leader服务器的连接请求。
3. Learner服务器开始和Leader建⽴连接。所有Learner会找到Leader服务器,并与其建⽴连接。
4. Leader服务器创建LearnerHandler。Leader接收到来⾃其他机器连接创建请求后,会创建⼀个LearnerHandler实例,每个LearnerHandler实例都对应⼀个Leader与Learner服务器之间的连接,其负责Leader和Learner服务器之间⼏乎所有的消息通信和数据同步。
5. 向Leader注册。Learner完成和Leader的连接后,会向Leader进⾏注册,即将Learner服务器的基
本信息(LearnerInfo),包括SID和ZXID,发送给Leader服务器。
6. Leader解析Learner信息,计算新的epoch。Leader接收到Learner服务器基本信息后,会解析出该Learner的SID和ZXID,然后根据ZXID解析出对应的epoch_of_learner,并和当前Leader服务器的epoch_of_leader进⾏⽐较,如果该Learner的epoch_of_learner更⼤,则更新Leader的epoch_of_leader = epoch_of_learner + 1。然后LearnHandler进⾏等待,直到过半Learner已经向Leader进⾏了注册,同时更新了epoch_of_leader后,Leader就可以确定当前集群的epoch了。
7. 发送Leader状态。计算出新的epoch后,Leader会将该信息以⼀个LEADERINFO消息的形式发送给Learner,并等待Learner的响应。
8. Learner发送ACK消息。Learner接收到LEADERINFO后,会解析出epoch和ZXID,然后向Leader反馈⼀个ACKEPOCH响应。
9. 数据同步。Leader收到Learner的ACKEPOCH后,即可进⾏数据同步。
10. 启动Leader和Learner服务器。当有过半Learner已经完成了数据同步,那么Leader和Learner服
务器实例就可以启动
5. Leader和Follower启动
1. 创建启动会话管理器。
2. 初始化Zookeeper请求处理链,集群模式的每个处理器也会在启动阶段串联请求处理链。
3. 注册JMX服务。
5.4 leader选举
当Zookeeper集群中的⼀台服务器出现以下两种情况之⼀时,需要进⼊Leader选举。
(1) 服务器初始化启动。
(2) 服务器运⾏期间⽆法和Leader保持连接。
服务器启动时期的Leader选举
若进⾏Leader选举,则⾄少需要两台机器,这⾥选取3台机器组成的服务器集群为例。在集群初始化阶段,当有⼀台服务器Server1启动时,其单ᇿ⽆法进⾏和完成Leader选举,当第⼆台服务器Server2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进⼊Leader选举过程。选举过程
如下
(1) 每个Server发出⼀个投票
由于是初始情况,Server1(假设myid为1)和Server2假设myid为2)都会将⾃⼰作为Leader服务器来进⾏投票,每次投票会包含所推举的服务器的myid和ZXID,使⽤(myid, ZXID)来表示,此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各⾃将这个投票发给集群中其他机器
(2) 接受来⾃各个服务器的投票
集群的每个服务器收到投票后,⾸先判断该投票的有效性,如检查是否是本轮投票、是否来⾃LOOKING状态的服务器。
(3) 处理投票
针对每⼀个投票,服务器都需要将别⼈的投票和⾃⼰的投票进⾏PK,PK规则如下
· 优先检查ZXID。ZXID⽐较⼤的服务器优先作为Leader。
· 如果ZXID相同,那么就⽐较myid。myid较⼤的服务器作为Leader服务器。
现在我们来看Server1和Server2实际是如何进⾏投票处理的。对于Server1来说,它⾃⼰的投票是
(1,0),⽽接收到的投票为(2,0)。⾸先会对⽐两者的ZXID,因为都是0,所以⽆法决定谁是Leader。接下来会对⽐两者的myid,很显然,Server1发现接收到的投票中的myid是2,⼤于⾃⼰,于是就会更新⾃⼰的投票为(2,0),然后重新将投票发出去。⽽对于Server2来说,不需要更新⾃⼰的投票
(4) 统计投票
每次投票后,服务器都会统计所有投票,判断是否已经有过半的机器接收到相同的投票信息。对于Server1和Server2服务器来说,都统计出集群中已经有两台机器接受了(2,0)这个投票信息。这⾥我们需要对“过半”的概念做⼀个简单的介绍。所谓“过半”就是指⼤于集群机器数量的⼀半,即⼤于或等于(n/2+1)。对于这⾥由3台机器构成的集群,⼤于等于2台即为达到“过半”要求。
(5) 改变服务器状态
⼀旦确定了 Leader,每个服务器就会更新⾃⼰的状态:如果是 Follower,那么就变更为
FOLLOWING,如果是Leader,那么就变更为LEADING。
服务器运⾏时期的Leader选举
在ZooKeeper集群正常运⾏过程中,⼀旦选出⼀个Leader,那么所有服务器的集群⻆⾊⼀般不会再发⽣变化——也就是说,Leader服务器将⼀直作为集群的Leader,即使集群中有⾮Leader机器挂了或是有新机器加⼊集群也不会影响Leader。但是⼀旦Leader所在的机器挂了,那么整个集群将暂时⽆法对外服务,⽽是进⼊新⼀轮的Leader选举。服务器运⾏期间的Leader选举和启动时期的Leader选举基本过程是⼀致的
(1) 变更状态
Leader挂后,余下的⾮Observer服务器都会将⾃⼰的服务器状态变更为LOOKING,然后开始进⼊Leader选举过程。
(2) 每个Server会发出⼀个投票
在运⾏期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为123,Server3的ZXID为122;
在第⼀轮投票中,Server1和Server3都会投⾃⼰,产⽣投票(1, 123),(3, 122),然后各⾃将投票发送给
集群中所有机器。
(3) 接收来⾃各个服务器的投票,与启动时过程相同
(4) 处理投票。与启动时过程相同,此时,Server1将会成为Leader
(5) 统计投票。与启动时过程相同
(6) 改变服务器的状态。与启动时过程相同
6、源码
zk单机模式启动主要流程:
1、注册jmx
2、解析ServerConfig配置对象
3、根据配置对象,运⾏单机zk服务
4、创建管理事务⽇志和快照FileTxnSnapLog对象,zookeeperServer对象,并设置zkServer的统计对
象5、设置zk服务钩⼦,原理是通过设置CountDownLatch,调⽤ZooKeeperServerShutdownHandler的
handle⽅法,可以将触发shutdownLatch.await⽅法继续执⾏,即调⽤shutdown关闭单机服务
6、基于jetty创建zk的admin服务
7、创建连接对象cnxnFactory和secureCnxnFactory(安全连接才创建该对象),⽤于处理客户端的请
求8、创建定时清除容器节点管理器,⽤于处理容器节点下不存在⼦节点的清理容器节点⼯作等
在这里插入代码片public static void main(String[] args) {
ZooKeeperServerMain main = new ZooKeeperServerMain();
main.initializeAndRun(args);
}
protected void initializeAndRun(String[] args)
throws ConfigException, IOException, AdminServerException
{
ServerConfig config = new ServerConfig();
//如果⼊参只有⼀个,则认为是配置⽂件的路径
if (args.length == 1) {
config.parse(args[0]);
} else {
//否则是各个参数
config.parse(args);
}
runFromConfig(config);
}
//省略部分代码,只保留了核⼼逻辑
public void runFromConfig(ServerConfig config) throws
IOException,AdminServerException {
FileTxnSnapLog txnLog = null;
try {
//初始化⽇志⽂件
txnLog = new FileTxnSnapLog(config.dataLogDir, config.dataDir);
//初始化ZkServer对象
final ZooKeeperServer zkServer = new ZooKeeperServer(txnLog,
config.tickTime, config.minSessionTimeout, config.maxSessionTimeout, null);
txnLog.setServerStats(zkServer.serverStats());
if (config.getClientPortAddress() != null) {
//初始化server端IO对象,默认是NIOServerCnxnFactory
cnxnFactory = ServerCnxnFactory.createFactory();
//初始化配置信息
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns(), false);
//启动服务
cnxnFactory.startup(zkServer);
}
//container ZNodes是3.6版本之后新增的节点类型,Container类型的节点会在它没有⼦
节点时
// 被删除(新创建的Container节点除外),该类就是⽤来周期性的进⾏检查清理⼯作
containerManager = new ContainerManager(zkServer.getZKDatabase(),
zkServer.firstProcessor,
Integer.getInteger("znode.container.checkIntervalMs", (int)
TimeUnit.MINUTES.toMillis(1)),
Integer.getInteger("znode.container.maxPerMinute", 10000)
);
containerManager.start();
//省略关闭逻辑
} catch (InterruptedException e) {
LOG.warn("Server interrupted", e);
} finally {
if (txnLog != null) {
txnLog.close();
}
}
}
可以看到关键点在于解析配置跟启动两个⽅法,先来看下解析配置逻辑,对应上⾯的configure⽅法:
//依旧省略掉了部分逻辑
public void configure(InetSocketAddress addr, int maxcc, boolean secure)
throws IOException {
maxClientCnxns = maxcc;
//会话超时时间
sessionlessCnxnTimeout =
Integer.getInteger(ZOOKEEPER_NIO_SESSIONLESS_CNXN_TIMEOUT, 10000);
//过期队列
cnxnExpiryQueue = new ExpiryQueue<NIOServerCnxn>(sessionlessCnxnTimeout);
//过期线程,从cnxnExpiryQueue中读取数据,如果已经过期则关闭
expirerThread = new ConnectionExpirerThread();
//根据CPU个数计算selector线程的数量
int numCores = Runtime.getRuntime().availableProcessors();
numSelectorThreads =
Integer.getInteger(ZOOKEEPER_NIO_NUM_SELECTOR_THREADS, Math.max((int)
Math.sqrt((float) numCores/2), 1));
if (numSelectorThreads < 1) {
throw new IOException("numSelectorThreads must be at least 1");
}
//计算woker线程的数量
numWorkerThreads = Integer.getInteger(ZOOKEEPER_NIO_NUM_WORKER_THREADS, 2 * numCores);
//worker线程关闭时间
workerShutdownTimeoutMS = Long.getLong(ZOOKEEPER_NIO_SHUTDOWN_TIMEOUT,
5000);
//初始化selector线程
for(int i=0; i<numSelectorThreads; ++i) {
selectorThreads.add(new SelectorThread(i));
}
this.ss = ServerSocketChannel.open();
ss.socket().setReuseAddress(true);
ss.socket().bind(addr);
ss.configureBlocking(false);
//初始化accept线程,这⾥看出accept线程只有⼀个,⾥⾯会注册监听ACCEPT事件
acceptThread = new AcceptThread(ss, addr, selectorThreads);
}
启动
public void startup(ZooKeeperServer zkServer) throws IOException,
InterruptedException {
startup(zkServer, true);
}
//启动分了好⼏块,⼀个⼀个看
public void startup(ZooKeeperServer zks, boolean startServer)
throws IOException, InterruptedException {
start();
setZooKeeperServer(zks);
if (startServer) {
zks.startdata();
zks.startup();
}
}
//⾸先是start⽅法
public void start() {
stopped = false;
//初始化worker线程池
if (workerPool == null) {
workerPool = new WorkerService("NIOWorker", numWorkerThreads, false);
}
//挨个启动select线程
for(SelectorThread thread : selectorThreads) {
if (thread.getState() == Thread.State.NEW) {
thread.start();
}
}
//启动acceptThread线程
if (acceptThread.getState() == Thread.State.NEW) {
acceptThread.start();
}
//启动expirerThread线程
if (expirerThread.getState() == Thread.State.NEW) {
expirerThread.start();
}
}
//初始化数据结构
public void startdata() throws IOException, InterruptedException {
//初始化ZKDatabase,该数据结构⽤来保存ZK上⾯存储的所有数据
源码分析之Leader选举(⼀)
分析Zookeeper中⼀个核⼼的模块,Leader选举。
总体框架图
对于Leader选举,其总体框架图如下图所示
if (zkDb == null) {
//初始化数据数据,这⾥会加⼊⼀些原始节点,例如/zookeeper
zkDb = new ZKDatabase(this.txnLogFactory);
}
//加载磁盘上已经存储的数据,如果有的话
if (!zkDb.isInitialized()) {
loadData();
}
}
//启动剩余项⽬
public synchronized void startup() {
//初始化session追踪器
if (sessionTracker == null) {
createSessionTracker();
}
//启动session追踪器
startSessionTracker();
//建⽴请求处理链路
setupRequestProcessors();
registerJMX();
setState(State.RUNNING);
notifyAll();
}
//这⾥可以看出,单机模式下请求的处理链路为:
//PrepRequestProcessor -> SyncRequestProcessor -> FinalRequestProcessor
protected void setupRequestProcessors() {
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
RequestProcessor syncProcessor = new SyncRequestProcessor(this,
finalProcessor);
((SyncRequestProcessor)syncProcessor).start();
firstProcessor = new PrepRequestProcessor(this, syncProcessor);
((PrepRequestProcessor)firstProcessor).start();
}
源码分析之Leader选举(⼀)
Election源码分析
public interface Election {
public Vote lookForLeader() throws InterruptedException;
public void shutdown();
}
源码分析之Leader选举(⼆)之FastLeaderElection
FastLeaderElection源码分析
类的继承关系
public class FastLeaderElection implements Election {}
外部投票:特指其他服务器发来的投票。
内部投票:服务器⾃身当前的投票。
选举轮次:ZooKeeper服务器Leader选举的轮次,即logical clock(逻辑时钟)。
PK:指对内部投票和外部投票进⾏⼀个对⽐来确定是否需要变更内部投票。选票管理
sendqueue:选票发送队列,⽤于保存待发送的选票。
recvqueue:选票接收队列,⽤于保存接收到的外部投票。
lookForLeader函数
当 ZooKeeper 服务器检测到当前服务器状态变成 LOOKING 时,就会触发 Leader选举,即调⽤
lookForLeader⽅法来进⾏Leader选举。
public Vote lookForLeader() throws InterruptedException {
synchronized(this){
// ⾸先会将逻辑时钟⾃增,每进⾏⼀轮新的leader选举,都需要更新逻辑时钟
logicalclock++;
// 更新选票(初始化选票)
updateProposal(getInitId(), getInitLastLoggedZxid(),
getPeerEpoch());
}
LOG.info("New election. My id = " + self.getId() +
", proposed zxid=0x" + Long.toHexString(proposedZxid));
// 向其他服务器发送⾃⼰的选票(已更新的选票)
sendNotifications();
之后每台服务器会不断地从recvqueue队列中获取外部选票。如果服务器发现⽆法获取到任何外部投
票,就⽴即确认⾃⼰是否和集群中其他服务器保持着有效的连接,如果没有连接,则⻢上建⽴连接,如
果已经建⽴了连接,则再次发送⾃⼰当前的内部投票,其流程如下
// 从recvqueue接收队列中取出投票
Notification n = recvqueue.poll(notTimeout,
TimeUnit.MILLISECONDS);
/*
* Sends more notifications if haven't received enough.
* Otherwise processes new notification.
*/
if(n == null){ // ⽆法获取选票
if(manager.haveDelivered()){ // manager已经发送了所有选票消息
(表示有连接)
// 向所有其他服务器发送消息
sendNotifications();
} else { // 还未发送所有消息(表示⽆连接)
// 连接其他每个服务器
manager.connectAll();
}
/*
* Exponential backoff
*/
int tmpTimeOut = notTimeout*2;
notTimeout = (tmpTimeOut < maxNotificationInterval?
tmpTimeOut : maxNotificationInterval);
LOG.info("Notification time out: " + notTimeout);
}
在发送完初始化选票之后,接着开始处理外部投票。在处理外部投票时,会根据选举轮次来进⾏不同的
处理。
· 外部投票的选举轮次⼤于内部投票。若服务器⾃身的选举轮次落后于该外部投票对应服务器
的选举轮次,那么就会⽴即更新⾃⼰的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使
⽤初始化的投票来进⾏PK以确定是否变更内部投票。最终再将内部投票发送出去。
· 外部投票的选举轮次⼩于内部投票。若服务器接收的外选票的选举轮次落后于⾃身的选举轮
次,那么Zookeeper就会直接忽略该外部投票,不做任何处理。
· 外部投票的选举轮次等于内部投票。此时可以开始进⾏选票PK,如果消息中的选票更优,则
需要更新本服务器内部选票,再发送给其他服务器。
之后再对选票进⾏归档操作,⽆论是否变更了投票,都会将刚刚收到的那份外部投票放⼊选票集合
recvset中进⾏归档,其中recvset⽤于记录当前服务器在本轮次的Leader选举中收到的所有外部投票,
然后开始统计投票,统计投票是为了统计集群中是否已经有过半的服务器认可了当前的内部投票,如果
确定已经有过半服务器认可了该投票,然后再进⾏最后⼀次确认,判断是否⼜有更优的选票产⽣,若
⽆,则终⽌投票,然后最终的选票,其流程如下
if (n.electionEpoch > logicalclock) { // 其选举周期⼤于逻辑时钟
// 重新赋值逻辑时钟
logicalclock = n.electionEpoch;
// 清空所有接收到的所有选票
recvset.clear();
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
getInitId(), getInitLastLoggedZxid(),
getPeerEpoch())) { // 进⾏PK,选出较优的服务器
// 更新选票
updateProposal(n.leader, n.zxid, n.peerEpoch);
} else { // ⽆法选出较优的服务器
// 更新选票
updateProposal(getInitId(),
getInitLastLoggedZxid(),
getPeerEpoch());
}
// 发送本服务器的内部选票消息
sendNotifications();
} else if (n.electionEpoch < logicalclock) { // 选举周期
⼩于逻辑时钟,不做处理,直接忽略
if(LOG.isDebugEnabled()){
LOG.debug("Notification election epoch is
smaller than logicalclock. n.electionEpoch = 0x"
+ Long.toHexString(n.electionEpoch)
+ ", logicalclock=0x" +
Long.toHexString(logicalclock));
}
break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch))
{ // PK,选出较优的服务器
// 更新选票
updateProposal(n.leader, n.zxid, n.peerEpoch);
// 发送消息
sendNotifications();
}
if(LOG.isDebugEnabled()){
LOG.debug("Adding vote: from=" + n.sid +
", proposed leader=" + n.leader +
", proposed zxid=0x" +
Long.toHexString(n.zxid) +
", proposed election epoch=0x" +
Long.toHexString(n.electionEpoch));
}
// recvset⽤于记录当前服务器在本轮次的Leader选举中收到的所有外
部投票
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
if (termPredicate(recvset,
new Vote(proposedLeader, proposedZxid,
logicalclock, proposedEpoch))) { // 若
能选出leader
// Verify if there is any change in the proposed
leader
while((n = recvqueue.poll(finalizeWait,
TimeUnit.MILLISECONDS)) != null){ // 遍历已
经接收的投票集合
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid,
proposedEpoch)){ // 选票有变更,⽐之前提议的Leader有更好的选票加⼊
// 将更优的选票放在recvset中
recvqueue.put(n);
break;
}
}
/*
* This predicate is true once we don't read any
new
* relevant message from the reception queue
*/
if (n == null) { // 表示之前提议的Leader已经是最优的
// 设置服务器状态
self.setPeerState((proposedLeader ==
self.getId()) ?
ServerState.LEADING: learningState());
// 最终的选票
Vote endVote = new Vote(proposedLeader,
proposedZxid,
logicalclock,
proposedEpoch);
// 清空recvqueue队列的选票
leaveInstance(endVote);
// 返回选票
return endVote;
}
}
1.⾃增选举轮次。 在 FastLeaderElection 实现中,有⼀个 logicalclock 属性,⽤于标识当前Leader的选举轮次,ZooKeeper规定了所有有效的投票都必须在同⼀轮次中。ZooKeeper在开始新⼀轮的投票时,会⾸先对logicalclock进⾏⾃增操作。
2.初始化选票。 在开始进⾏新⼀轮的投票之前,每个服务器都会⾸先初始化⾃⼰的选票。初始化选票也就是对 Vote 属性的初始化。在初始化阶段,每台服务器都会将⾃⼰推举为Leader
3.发送初始化选票。 在完成选票的初始化后,服务器就会发起第⼀次投票。ZooKeeper 会将刚刚初始
化好的选票放⼊sendqueue队列中,由发送器WorkerSender负
4.接收外部投票。 每台服务器都会不断地从 recvqueue 队列中获取外部投票。如果服务器发现⽆法获
取到任何的外部投票,那么就会⽴即确认⾃⼰是否和集群中其他服务器保持着有效连接。如果发现没有
建⽴连接,那么就会⻢上建⽴连接。如果已经建⽴了连接,那么就再次发送⾃⼰当前的内部投票。
5.判断选举轮次。 当发送完初始化选票之后,接下来就要开始处理外部投票了。在处理外部投票的时
候,会根据选举轮次来进⾏不同的处理。 · 外部投票的选举轮次⼤于内部投票。如果服务器发现⾃⼰的
选举轮次已经落后于该外部投票对应服务器的选举轮次,那么就会⽴即更新⾃⼰的选举轮次
(logicalclock),并且清空所有已经收到的投票,然后使⽤初始化的投票来进⾏PK以确定是否变更内
部投票(关于P K的逻辑会在步骤6中统⼀讲解),最终再将内部投票发送出去。 · 外部投票的选举轮次
⼩于内部投票。 如果接收到的选票的选举轮次落后于服务器⾃身的,那么ZooKeeper就会直接忽略该外
部投票,不做任何处理,并返回步骤4。
· 外部投票的选举轮次和内部投票⼀致。 这也是绝⼤多数投票的场景,如外部投票的选举轮次和内部投
票⼀致的话,那么就开始进⾏选票PK。 总的来说,只有在同⼀个选举轮次的投票才是有效的投票。
6.选票PK。 在步骤5中提到,在收到来⾃其他服务器有效的外部投票后,就要进⾏选票PK了——也就是
FastLeaderElection.totalOrderPredicate⽅法的核⼼逻辑。选票PK的⽬的是为了确定当前服务器是否
需要变更投票,主要从选举轮次、ZXID和 SID 三个因素来考虑,具体条件如下:在选票 PK 的时候依次
判断,符合任意⼀个条件就需要进⾏投票变更。 · 如果外部投票中被推举的Leader服务器的选举轮次⼤
于内部投票,那么就需要进⾏投票变更。 · 如果选举轮次⼀致的话,那么就对⽐两者的ZXID。如果外部
投票的ZXID⼤于内部投票,那么就需要进⾏投票变更。 · 如果两者的 ZXID ⼀致,那么就对⽐两者的
SID。如果外部投票的 SID ⼤于内部投票,那么就需要进⾏投票变更。 7.变更投票。 通过选票PK后,如
果确定了外部投票优于内部投票(所谓的“优于”,是指外部投票所推举的服务器更适合成为Leader),
那么就进⾏投票变更——使⽤外部投票的选票信息来覆盖内部投票。变更完成后,再次将这个变更后的
内部投票发送出去。
8.选票归档。 ⽆论是否进⾏了投票变更,都会将刚刚收到的那份外部投票放⼊“选票集合”recvset中进⾏
归档。recvset⽤于记录当前服务器在本轮次的Leader选举中收到的所有外部投票——按照服务器对应
的SID来区分,例如,{(1,vote1),(2,vote2),…}。 9.统计投票。 完成了选票归档之后,就可
以开始统计投票了。统计投票的过程就是为了统计集群中是否已经有过半的服务器认可了当前的内部投
票。如果确定已经有过半的服务器认可了该内部投票,则终⽌投票。否则返回步骤4。 10.更新服务器状
态。 统计投票后,如果已经确定可以终⽌投票,那么就开始更新服务器状态。服务器会⾸先判断当前被
过半服务器认可的投票所对应的Leader服务器是否是⾃⼰,如果是⾃⼰的话,那么就会将⾃⼰的服务器
状态更新为 LEADING。如果⾃⼰不是被选举产⽣的 Leader 的话,那么就会根据具体情况来确定⾃⼰是
FOLLOWING或是OBSERVING。 以上 10 个步骤,就是 FastLeaderElection 选举算法的核⼼步骤,其
中步骤 4~9 会经过⼏轮循环,直到Leader选举产⽣。另外还有⼀个细节需要注意,就是在完成步骤9
之后,如果统计投票发现已经有过半的服务器认可了当前的选票,这个时候,ZooKeeper 并不会⽴即进
⼊步骤 10 来更新服务器状态,⽽是会等待⼀段时间(默认是 200 毫秒)来确定是否有新的更优的投票
zookeeper源码分析之集群模式服务端
集群模式下启动所有的ZK节点启动⼊⼝都是QuorumPeerMain类的main⽅法。 main⽅法加载配置⽂
件以后,最终会调⽤到QuorumPeer的start⽅法,来看下:
public synchronized void start() {
//校验ServerId是否合法
if (!getView().containsKey(myid)) {
throw new RuntimeException("My id " + myid + " not in the peer list");
}
//载⼊之前持久化的⼀些信息
loadDataBase();
//启动线程监听
startServerCnxnFactory();
try {
adminServer.start();
} catch (AdminServerException e) {
LOG.warn("Problem starting AdminServer", e);
System.out.println(e);
}
//初始化选举投票以及算法
startLeaderElection();
//当前也是⼀个线程,注意run⽅法
super.start();
}
初始化选举的逻辑
synchronized public void startLeaderElection() {
try {
//所有节点启动的初始状态都是LOOKING,因此这⾥都会是创建⼀张投⾃⼰为Leader的票
if (getPeerState() == ServerState.LOOKING) {
currentVote = new Vote(myid, getLastLoggedZxid(),
getCurrentEpoch());
}
} catch(IOException e) {
//异常处理
}
//初始化选举算法,electionType默认为3
this.electionAlg = createElectionAlgorithm(electionType);
}
protected Election createElectionAlgorithm(int electionAlgorithm){
Election le = null;
switch (electionAlgorithm) {
case 1:
//忽略
case 2:
//忽略
case 3:
//electionAlgorithm默认是3,直接⾛到这⾥
qcm = createCnxnManager();
//监听选举事件的listener
QuorumCnxManager.Listener listener = qcm.listener;
if(listener != null){
//开启监听器
listener.start();
//初始化选举算法
FastLeaderElection fle = new FastLeaderElection(this, qcm);
//发起选举
fle.start();
le = fle;
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
//忽略
}
return le; }
super.start(),
public void run() {
try {
while (running) {
//根据当前节点的状态执⾏不同流程
switch (getPeerState()) {
case LOOKING:
try {
//寻找Leader节点
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
setPeerState(ServerState.LOOKING);
}
break;
case OBSERVING:
try {
//当前节点启动模式为Observer
setObserver(makeObserver(logFactory));
//与Leader节点进⾏数据同步
observer.observeLeader();
} catch (Exception e) {
} finally {
}
break;
case FOLLOWING:
try {
//当前节点启动模式为Follower
setFollower(makeFollower(logFactory));
//与Leader节点进⾏数据同步
follower.followLeader();
} catch (Exception e) {
} finally {
}
break;
case LEADING:
try {
//当前节点启动模式为Leader
setLeader(makeLeader(logFactory));
//发送⾃⼰成为Leader的通知
leader.lead();
setLeader(null);
} catch (Exception e) {
} finally {
}
break;
}
}
}}
节点初始化的状态为LOOKING,因此启动时直接会调⽤lookForLeader⽅法发起Leader选举
public Vote lookForLeader() throws InterruptedException {
try {
Map<Long, Vote> recvset = new HashMap<Long, Vote>();
Map<Long, Vote> outofelection = new HashMap<Long, Vote>();
//向所有投票节点发送⾃⼰的投票信息
sendNotifications();
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){
//读取各个节点返回的投票信息
Notification n = recvqueue.poll(notTimeout,
TimeUnit.MILLISECONDS);
//超时重发
if(n == null){
//如果前⾯待发送的消息已经全部发送,则重新发送
if(manager.haveDelivered()){
sendNotifications();
} else {
//否则尝试与各个节点建⽴连接
manager.connectAll();
}
//退避算法修改下次等待时间
int tmpTimeOut = notTimeout*2;
notTimeout = (tmpTimeOut < maxNotificationInterval?
tmpTimeOut : maxNotificationInterval);
}
else if (validVoter(n.sid) && validVoter(n.leader)) {
switch (n.state) {
case LOOKING:
//如果节点的周期⼤于⾃⼰的
if (n.electionEpoch > logicalclock.get()) {
logicalclock.set(n.electionEpoch);
//清除已收到的投票信息
recvset.clear();
//两个节点根据epoch,zxid,serverId来判断新的投票信息
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
updateProposal(n.leader, n.zxid, n.peerEpoch);
} else {
updateProposal(getInitId(),
getInitLastLoggedZxid(), getPeerEpoch());
}
//修改选举周期以及投票信息,发起新⼀轮投票
sendNotifications();
} else if (n.electionEpoch < logicalclock.get()) {
//这⾥的break是跳出switch语句,别跟循环弄混
break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch))
{
//如果对⽅的epoch,zxid,serverId⽐⾃⼰⼤
//则更新⾃⼰的投票给n的投票节点
updateProposal(n.leader, n.zxid, n.peerEpoch);
//重新发送⾃⼰新的投票信息
sendNotifications();
}
//把节点的投票信息记录下
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
//统计投票信息,判断当前选举是否可以结束,也就是收到的票数信息已
经⾜够确认Leader
if (termPredicate(recvset, new Vote(proposedLeader,
proposedZxid,
logicalclock.get(), proposedEpoch))) {
while((n = recvqueue.poll(finalizeWait,
TimeUnit.MILLISECONDS)) != null){
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)){
recvqueue.put(n);
break;
}
}
//如果没有多余的投票信息则可以结束本次选举周期
if (n == null) {
//根据serverId修改当前节点的类型
self.setPeerState((proposedLeader ==
self.getId()) ? ServerState.LEADING: learningState());
Vote endVote = new Vote(proposedLeader,
proposedZxid, proposedEpoch);
//清空接收消息队列
leaveInstance(endVote);
//返回最终的投票信息
return endVote;
}
}
break;
case OBSERVING:
//Observer节点不参与投票,忽略
break;
case FOLLOWING:
case LEADING:
//如果周期相同,说明当前节点参与了这次选举
if(n.electionEpoch == logicalclock.get()){
//保存投票信息
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
//判断当前节点收到的票数是否可以结束选举
if(termPredicate(recvset, new Vote(n.leader,
n.zxid, n.electionEpoch, n.peerEpoch, n.state))
&& checkLeader(outofelection, n.leader, n.electionEpoch)) {
self.setPeerState((n.leader == self.getId()) ?
ServerState.LEADING: learningState());
Vote endVote = new Vote(n.leader, n.zxid, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
}
//把Leader跟Follower的投票信息加⼊outofelection,确认下它们
的信息是否⼀致
outofelection.put(n.sid, new Vote(n.leader,
IGNOREVALUE, IGNOREVALUE, n.peerEpoch, n.state));
if (termPredicate(outofelection, new Vote(n.leader,
IGNOREVALUE, IGNOREVALUE, n.peerEpoch, n.state))
&& checkLeader(outofelection, n.leader,
IGNOREVALUE)) {
synchronized(this){
logicalclock.set(n.electionEpoch);
self.setPeerState((n.leader == self.getId()) ?
ServerState.LEADING: learningState());
}
Vote endVote = new Vote(n.leader, n.zxid, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
break;
default:
break;
}
}
}
return null;
}
节点根据类型的不同会执⾏以下逻辑:
1. 如果是Leader节点,⾸先会想其他节点发送⼀条NEWLEADER信息,确认⾃⼰的身份,等到各个节点的ACK消息以后开始正式对外提供服务,同时开启新的监听器,处理新节点加⼊的逻辑。
2. 如果是Follower节点,⾸先向Leader节点发送⼀条FOLLOWERINFO信息,告诉Leader节点⾃⼰已处理的事务的最⼤Zxid,然后Leader节点会根据⾃⼰的最⼤Zxid与Follower节点进⾏同步,如果Follower节点落后的不多则会收到Leader的DIFF信息通过内存同步,如果Follower节点落后的很多则会收到SNAP通过快照同步,如果Follower节点的Zxid⼤于Leader节点则会收到TRUNC信息忽略多余的事务。
3. 如果是Observer节点,则与Follower节点相同