Zookeeper 技术内幕
1.系统模型
1.1 数据模型
树
Zookeeper的数据节点称为ZNode,ZNode是Zookeeper中数据的最小单元,每个ZNode都可以保存数据,同时还可以挂载子节点,因此构成了一个层次化的命名空间,称为树.
事务ID
在Zookeeper中,事务是指能够改变Zookeeper服务器状态的操作,一般包括
- 节点创建与删除
- 数据节点内容更新
- 客户端会话创建与失效
对于每个事务请求,Zookeeper都会为其分配一个全局唯一的事务ID,用ZXID表示,通常是64位的数字,每个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出Zookeeper处理这些更新操作请求的全局顺序.
1.2 节点特性
Zookeeper的节点类型
- 持久节点(PERSISTENT)
- 临时节点(EPHEMERAL)
- 顺序节点(SEQUENTIAL)
Zookeeper组合型节点类型
- 持久节点(PERSISTENT):该数据节点被创建后,便一直存在于Zookeeper服务器上,直到有删除操作来主动清楚该节点
- 持久顺序节点(PERSISTENT_SEQUENTIAL):相比持久节点,其新增了顺序特性,每个父节点都会为它的第一级子节点维护一份顺序,用于记录每个子节点创建的先后顺序.在创建节点时,会自动添加一个数字后缀,作为新的节点名,该数字后缀的上限是整形的最大值
- 临时节点(EPEMERAL).临时节点的生命周期与客户端会话绑定,客户端失效,节点会被自动清理.同时,Zookeeper规定不能基于临时节点来创建子节点,即临时节点只能作为叶子节点
- 临时顺序节点(EPEMERAL_SEQUENTIAL).在临时节点的基础添加了顺序特性
1.3 版本 —— 保证分布式数据原子性操作
数据节点版本说明 | 版本类型 | 说明 | | --- | --- | | version | 当前数据节点数据内容的版本号 | | cversion | 当前数据子节点的版本号 | | aversion | 当前数据节点ACL变更版本号 |
Zookeeper中的版本表示的是对数据节点数据内容,子节点列表,节点ACL信息的修改次数. 如version为1表示对数据节点的内容变更了一次.即使前后两次变更并没有改变数据内容,version的值仍然会改变.version可以用于写入验证,类似于CAS.
1.4 Watcher —— 数据变更的通知
Zookeeper使用Watcher机制实现分布式数据的发布/订阅功能
Zookeeper的Watcher机制
- 客户端线程
- 客户端WatcherManager
- Zookeeper服务器三部分
客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中.当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatcherManager中取出对应的Watcher对象来执行回调逻辑.
Watcher通知状态与事件类型一览
| KeeperState | EventType | 触发条件 | 说明 |
| --- | --- | --- | --- |
| SyncConnected(3) | None(-1) | 客户端与服务端成功建立连接 | 此时客户端和服务器处于连接状态 |
| 同上 | NodeCreated(1)| Watcher监听的对应数据节点被创建 | 同上 |
| 同上 | NodeDeleted(2)| Watcher监听的对应数据节点被删除 | 同上 |
| 同上 | NodeDataChanged(3)| Watcher监听的对应数据节点的数据内容发生变更 | 同上 |
| 同上 | NodeChildChanged(4)| Wather监听的对应数据节点的子节点列表发生变更 | 同上 |
| Disconnected(0)| None(-1)| 客户端与ZooKeeper服务器断开连接 | 此时客户端和服务器处于断开连接状态 |
| Expired(-112)| Node(-1)| 会话超时 | 此时客户端会话失效,通常同时也会受到SessionExpiredException异常 |
| AuthFailed(4)| None(-1)| 通常有两种情况,1:使用错误的schema进行权限检查 2:SASL权限检查失败 | 通常同时也会收到AuthFailedException异常 |
工作机制

Watcher特性总结
- 一次性
- 无论是服务端还是客户端,一旦一个 Watcher 被触发,ZooKeeper 都会将其从相应的存储中移除.
- 因此,在 Watcher 的使用上,需要反复注册.这样的设计有效地减轻了服务端的压力.
- 客户端串行执行
- 客户端 Watcher 回调的过程是一个串行同步的过程,这为我们保证了顺序,同时,需要注意的一点是,一定不能因为一个 Watcher 的处理逻辑影响了整个客户端的 Watcher 回调,所以,我觉得客户端 Watcher 的实现类要另开一个线程进行处理业务逻辑,以便给其他的 Watcher 调用让出时间.
- 轻量
- WatcherEvent 是 ZooKeeper 整个 Watcher 通知机制的最小通知单元,这个数据结构中只包含三部分内容:通知状态、事件类型和节点路径.也就是说,Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容.例如针对 NodeDataChanged 事件,ZooKeeper 的Watcher 只会通知客户端指定数据节点的数据内容发生了变更,而对于原始数据以及变更后的新数据都无法从这个事件中直接获取到,而是需要客户端主动重新去获取数据——这也是 ZooKeeper 的 Watcher 机制的一个非常重要的特性.
1.5 ACL——保障数据的安全
Zookeeper内部存储了分布式系统运行时状态的元数据,这些元数据会直接影响基于Zookeeper进行构造的分布式系统的运行状态,如何保障系统中数据的安全,从而避免因误操作而带来的数据随意变更而导致的数据库异常十分重要,Zookeeper提供了一套完善的ACL权限控制机制来保障数据的安全. 我们可以从三个方面来理解ACL机制:
- 权限模式(Scheme)
- 授权对象(ID)
- 权限(Permission)
通常使用"scheme:id:permission"来标识一个有效的ACL信息
权限模式用来确定权限验证过程中使用的检验策略
- IP
- 通过IP地址粒度来进行权限控制,如"ip:192.168.0.110"表示权限控制针对该IP地址,同时IP模式可以支持按照网段方式进行配置,如"ip:192.168.0.1/24"表示针对192.168.0.*这个网段进行权限控制.
- Digest
- 使用"username:password"形式的权限标识来进行权限配置,便于区分不同应用来进行权限控制.Zookeeper会对其进行SHA-1加密和BASE64编码.
- World
- 最为开放的权限控制模式,数据节点的访问权限对所有用户开放.
- Super
- 超级用户,是一种特殊的Digest模式,超级用户可以对任意Zookeeper上的数据节点进行任何操作.
授权对象ID
授权对象是指权限赋予的用户或一个指定实体,如IP地址或机器等.不同的权限模式通常有不同的授权对象.
权限:Permission
权限是指通过权限检查可以被允许执行的操作,Zookeeper对所有数据的操作权限分为
- CREATE(节点创建权限)
- DELETE(节点删除权限)
- READ(节点读取权限)
- WRITE(节点更新权限)
- ADMIN(节点管理权限)
自定义权限控制
权限控制器需要实现AuthenticationProvider接口,注册自定义权限控制器通过在zoo.cfg配置文件中配置如下配置项:
authProvider.1=com.zkbook.CustomAuthenticationProvider
2 序列化与协议
Zookeeper的客户端与服务端之间会进行一系列的网络通信来实现数据传输,Zookeeper使用Jute组件来完成数据的序列化和反序列化操作.
2.1 Jute介绍
Jute是Zookeeper底层序列化组件,其用于Zookeeper进行网络数据传输和本地磁盘数据存储的序列化和反序列化工作.
在Zookeeper的src文件夹下有zookeeper.jute文件,定义了所有的实体类的所属包名、类名及类的所有成员变量和类型,该文件会在源代码编译时,Jute会使用不同的代码生成器为这些类定义生成实际编程语言的类文件,如java语言生成的类文件保存在src/java/generated目录下,每个类都会实现Record接口.
public interface Record {
public void serialize(OutputArchive archive, String tag) throws IOException;
public void deserialize(InputArchive archive, String tag) throws IOException;
}
2.2 通信协议
基于TCP/IP协议,Zookeeper实现了自己的通信协议来玩按成客户端与服务端、服务端与服务端之间的网络通信,对于请求,主要包含请求头和请求体,对于响应,主要包含响应头和响应体.
2.2.1 请求协议
对于请求协议而言,如下为获取节点数据请求的完整协议定义

class RequestHeader {
int xid;
int type;
}
从zookeeper.jute中可知RequestHeader包含了xid和type,xid用于记录客户端请求发起的先后序号,用来确保单个客户端请求的响应顺序,type代表请求的操作类型,如创建节点(OpCode.create)、删除节点(OpCode.delete)、获取节点数据(OpCode.getData).
协议的请求主体内容部分,包含了请求的所有操作内容,不同的请求类型请求体不同.
Zookeeper客户端和服务器在创建会话时,会发送ConnectRequest请求,其请求体如下
class ConnectRequest {
// 协议版本号
int protocolVersion;
// 最近一次接收到服务器
long lastZxidSeen;
// 会话超时时间
int timeOut;
// 会话标识
long sessionId;
// 会话密码
buffer passwd;
}
Zookeeper客户端在向服务器发送节点数据请求时,会发送GetDataRequest请求,其请求体如下
class GetDataRequest {
// 数据节点路径
ustring path;
// 是否注册Watcher的标识
boolean watch;
}
Zookeeper客户端在向服务器发送更新节点数据请求时,会发送SetDataRequest请求,其请求体如下
class SetDataRequest {
// 数据节点路径
ustring path;
数据内容
buffer data;
// 节点数据的期望版本号
int version;
}
2.2.2 响应协议
对于响应协议而言,如下为获取节点数据响应的完整协议定义
响应头中包含了每个响应最基本的信息
class ReplyHeader {
// 与请求头中的xid一致
int xid;
// Zookeeper服务器上当前最新的事务ID
long zxid;
// err则是一个错误码,表示当请求处理过程出现异常情况时,就会在错误码中标识出来,常见的包括处理成功(Code.OK)、节点不存在(Code.NONODE)、没有权限(Code.NOAUTH)
int err;
}
协议的响应主体内容部分,包含了响应的所有数据,不同的响应类型请求体不同.
针对客户端的会话创建请求,服务端会返回客户端一个ConnectResponse响应,其响应体如下
class ConnectResponse {
// 版本号
int protocolVersion;
// 会话的超时时间
int timeOut;
// 会话标识
long sessionId;
// 会话密码
buffer passwd;
}
针对客户端的获取节点数据请求,服务端会返回客户端一个GetDataResponse响应,其响应体如下
class GetDataResponse {
// 数据节点内容
buffer data;
// 节点状态
org.apache.zookeeper.data.Stat stat;
}
针对客户端的更新节点数据请求,服务端会返回客户端一个SetDataResponse响应,其响应体如下
class SetDataResponse {
// 最新的节点状态
org.apache.zookeeper.data.Stat stat;
}
2.3 stat对象状态属性说明
| 状态属性 | 说明 |
|---|---|
czxid |
即Created zxid,表示该数据节点被创建时的事务id |
mzxid |
即Modified zxid,表示该节点最后一次被更新时的事务id |
ctime |
即Created time,表示节点被创建的时间 |
mtime |
即Modified time,表示该节点最后一次被更新的时间 |
version |
数据节点的版本号 |
cversion |
子节点的版本号 |
aversion |
节点的acl版本号 |
ephemeralOwner |
创建该临时节点的会话的sessionID,如果该节点是持久节点,那么这个属性值为0 |
dataLength |
数据内容的长度 |
numChildren |
当前节点的子节点个数 |
pzxid |
表示该节点的子节点列表最后一次被修改时的事务id,注意,只有子节点列表变更了才会变更pzxid,子节点内容变更不会影响pzxid |
节点的version
- 在一个数据节点/zk-book被创建完毕后,节点的version值是0,表示的含义是"当前节点自从创建之后,被更新过0次"
- 如果现在对该节点的数据内容进行更新操作,那么随后,version值就会变成1
- 需要注意的是,其表示的是对数据节点数据内容的变更次数,强调的是变更次数,因此即使前后两次变更并没有使得数据内容的值发生变化,version的值任然会变更
3.客户端
客户端是开发人员使用Zookeeper最主要的途径,很有必要弄懂客户端是如何与服务端通信的.
Zookeeper客户端主要由如下核心部件构成
Zookeeper实例: 客户端入口ClientWatchManager: 客户端Watcher管理器HostProvider: 客户端地址列表管理器ClientCnxn: 客户端核心线程,内部包含了SendThread和EventThread两个线程SendThread为I/O线程,主要负责Zookeeper客户端和服务器之间的网络I/O通信EventThread为事件线程,主要负责对服务端事件进行处理
Zookeeper客户端初始化与启动环节,就是Zookeeper对象的实例化过程.
客户端在初始化和启动过程中大体可以分为如下3个步骤
- 设置默认Watcher
- 设置Zookeeper服务器地址列表
- 创建ClientCnxn
若在Zookeeper构造方法中传入Watcher对象时,那么Zookeeper就会将该Watcher对象保存在ZKWatcherManager的defaultWatcher中,并作为整个客户端会话期间的默认Watcher.
3.1 一次会话的创建过程

客户端与服务端会话建立的整个过程
- 始化阶段(第一阶段)
- 初始化Zookeeper对象
- 调用Zookeeper的构造方法来实例化一个Zookeeper,在初始化过程中,会创建一个客户端的Watcher管理器:ClientWatchManager
- 设置会话默认Watcher
- 如果在构造方法中传入一个Watcher对象,那么客户端会将这个对象作为默认Watcher保存在ClientWatchManager
- 构造Zookeeper服务器地址列表管理器:HostProvider
- 在构造方法中传入的服务器地址,客户端会将其存放在服务器地址列表管理器HostProvider中
- 创建并初始化客户端网络连接器:ClientCnxn
- Zookeeper客户端首先会创建一个网络连接器ClientCnxn.用来管理客户端与服务器的网络交互
- 另外,客户端在创建ClientCnxn的同时,还会初始化客户端两个核心队列outgoingQueue和pendingQueue,分别作为客户端的请求发送队列和服务器端响应的等待队列
- 初始化SendThread和EventThread
- 客户端会创建两个核心网络线程SendThread和EventThread,前者用于管理客户端和服务端之间的所有网络I/O,后者则用于进行客户端的事件处理.
- 同时,客户端还会将ClientCnxnSocket分配给SendThread作为底层网络I/O处理器,并初始化EventThread的待处理事件队列waitingEvents,用于存放所有等待被客户端处理的事情
- 初始化Zookeeper对象
- 会话创建阶段(第二阶段)
- 启动SendThread和EventThread
- SendThread首先会判断当前客户端的状态,进行一系列请理性工作,为客户端发送"会话创建"请求做准备
- 获取一个服务器地址
- 在开始创建TCP之前,SendThread首先需要获取一个Zookeeper服务器的目标地址,这通常是从HostProvider中随机获取出一个地址,然后委托给ClientCnxnSocket去创建与Zookeeper服务器之间的TCP连接
- 创建TCP连接
- 获取一个服务器地址后,ClientCnxnSocket负责和服务器创建一个TCP长连接
- ClientCnxnSocketNetty实现了ClientCnxnSocket的抽象方法,它负责连接到server,读取/写入网络流量,并作为网络数据层和更高packet层的中间层
- 其生命周期如下
loop: try: !isConnected() connect() doTransport() catch: cleanup() close()
- 构造ConnectRequest请求
- 在TCP连接创建完毕后,可能有的读者会认为,这样是否就说明已经和Zookeeper服务器完成连接了呢?其实不然,上面的步骤只是纯粹地从网络TCP层完成了客户端与服务端之间的Socket连接,但远未完成Zookeeper客户端的会话创建
- SendThread会负责根据当前客户端的实际设置,构造出一个ConnectRequest请求,该请求代表了客户端试图与服务端创建一个会话.同时,Zookeeper客户端还会进一步将该请求包装成网络I/O层的Packet对象,放入发送队列outgoingQueue中去
- 发送请求
- 当客户端请求准备完毕后,就可以开始向服务端发送请求了
- ClientCnxnSocket负责从outgoingQueue中取出一个待发送的Packet对象,将其序列化成ByteBuffer后,向服务端进行发送
- 启动SendThread和EventThread
- 响应处理阶段(第三阶段)三个阶段
- 接受服务器端响应
- ClientCnxnSocket接受到服务端响应后,会首先判断当前的客户端状态是否是"已初始化",如果尚未完成初始化,那么就认为该响应一定是会话创建请求的响应,直接交由readConnectResult方法来处理该响应
- 处理Response
- ClientCnxnSocket会对接受到的服务端响应进行反序列化,得到ConnectResponse对象,并从中获取到Zookeeper服务端分配的会话SessionId
- 连接成功
- 连接成功后,一方面需要通知SendThread线程,进一步对客户端进行会话参数的设置,包括readTimeout和connectTimeout等,并更新客户端状态
- 另一方面,需要通知地址管理器HostProvider当前成功连接的服务器地址
- 生成事件: SyncConnected-None
- 为了能够让上层应用感知到会话的成功创建,SendThread会生成一个事件SyncConnected-None,代表客户端与服务器会话创建成功,并将该事件传递给EventThread线程
- 查询Watcher
- EventThread线程收到事件后,会从ClientWatchManager管理器中查询出对应的Watcher,针对SyncConnected-None事件,那么就直接找出存储的默认Watcher,然后将其放到EventThread的watingEvents队列中去
- 处理事件
- EventThread不断的从watingEvents队列中取出待处理的Watcher对象,然后直接调用该对象的process接口方法,以达到触发Watcher的目的.
3.2 服务器地址列表
Zookeeper收到服务器地址列表后,会解析出chrootPath和保存服务器地址列表.
- EventThread不断的从watingEvents队列中取出待处理的Watcher对象,然后直接调用该对象的process接口方法,以达到触发Watcher的目的.
- 接受服务器端响应
Chroot,每个客户端可以设置自己的命名空间,若客户端设置了Chroot,此时,该客户端对服务器的任何操作都将被限制在自己的命名空间下,如设置Choot为/app/X,那么该客户端的所有节点路径都是以/app/X为根节点.- 地址列表管理,Zookeeper使用
StaticHostProvider打散服务器地址(shuffle),并将服务器地址形成一个环形循环队列,然后再依次取出服务器地址
3.3 ClientCnxn:网络I/O
ClientCnxn是Zookeeper客户端中负责维护客户端与服务端之间的网络连接并进行一系列网络通信的核心工作类,Packet是ClientCnxn内部定义的一个堆协议层的封装,用作Zookeeper中请求和响应的载体.
Packet
- 请求头(requestHeader)
- 响应头(replyHeader)
- 请求体(request)
- 响应体(response)
- 节点路径(clientPath/serverPath)
- 注册的Watcher(watchRegistration)等信息
Packet在客户端与服务端之间进行网络传输,只会将requestHeader、request、readOnly三个属性序列化,并生成可用于底层网络传输的ByteBuffer,其他属性都保存在客户端的上下文中,不会进行与服务端之间的网络传输.
ClientCnxn维护着outgoingQueue(客户端的请求发送队列)和pendingQueue(服务端响应的等待队列)
outgoingQueue专门用于存储那些需要发送到服务端的Packet集合pendingQueue用于存储那些已经从客户端发送到服务端的,但是需要等待服务端响应的Packet集合
请求发送 在正常情况下,会从outgoingQueue中取出一个可发送的Packet对象,同时生成一个客户端请求序号XID并将其设置到Packet请求头中去,然后序列化后再发送,请求发送完毕后,会立即将该Packet保存到pendingQueue中,以便等待服务端响应返回后进行相应的处理.

响应接收
客户端获取到来自服务端的完整响应数据后,根据不同的客户端请求类型,会进行不同的处理
- 若检测到此时客户端尚未进行初始化,那么说明当前客户端与服务端之间正在进行会话创建,直接将接收的ByteBuffer序列化成ConnectResponse对象
- 若当前客户端已经处于正常会话周期,并且接收到服务端响应是一个事件,那么将接收的ByteBuffer序列化成WatcherEvent对象,并将该事件放入待处理队列中
- 若是一个常规请求(Create、GetData、Exist等),那么从pendingQueue队列中取出一个Packet来进行相应处理.首先会检验响应中的XID来确保请求处理的顺序性,然后再将接收到的ByteBuffer序列化成Response对象
SendThread
SendThread是客户端ClientCnxn内部的一个核心I/O调度线程,用于管理客户端与服务端之间的所有网络I/O操作,在Zookeeper客户端实际运行中,SendThread的作用如下
- 维护了客户端与服务端之间的会话生命周期(通过一定周期频率内向服务端发送PING包检测心跳),如果会话周期内客户端与服务端出现TCP连接断开,那么就会自动且透明地完成重连操作
- 管理了客户端所有的请求发送和响应接收操作,其将上层客户端API操作转换成相应的请求协议并发送到服务端,并完成对同步调用的返回和异步调用的回调
- 将来自服务端的事件传递给EventThread去处理
EventThread
EventThread是客户端ClientCnxn内部的一个事件处理线程,负责客户端的事件处理,并触发客户端注册的Watcher监听.EventThread中的watingEvents队列用于临时存放那些需要被触发的Object,包括客户端注册的Watcher和异步接口中注册的回调器AsyncCallback.同时,EventThread会不断地从watingEvents中取出Object,识别具体类型(Watcher或AsyncCallback),并分别调用process和processResult接口方法来实现对事件的触发和回调.
4.会话
客户端与服务端之间任何交互操作都与会话息息相关,如临时节点的生命周期、客户端请求的顺序执行、Watcher通知机制等.Zookeeper的连接与会话就是客户端通过实例化Zookeeper对象来实现客户端与服务端创建并保持TCP连接的过程.
4.1 会话状态
在Zookeeper客户端与服务端成功完成连接创建后,就创建了一个会话,Zookeeper会话在整个运行期间的生命周期中,会在不同的会话状态中之间进行切换,这些状态可以分为CONNECTING、CONNECTED、RECONNECTING、RECONNECTED、CLOSE等.
一旦客户端开始创建Zookeeper对象,那么客户端状态就会变成CONNECTING状态,同时客户端开始尝试连接服务端,连接成功后,客户端状态变为CONNECTED,通常情况下,由于断网或其他原因,客户端与服务端之间会出现断开情况,一旦碰到这种情况,Zookeeper客户端会自动进行重连服务,同时客户端状态再次变成CONNCTING,直到重新连上服务端后,状态又变为CONNECTED,在通常情况下,客户端的状态总是介于CONNECTING和CONNECTED之间.但是,如果出现诸如会话超时、权限检查或是客户端主动退出程序等情况,客户端的状态就会直接变更为CLOSE状态.

4.2 会话创建
Session是Zookeeper中的会话实体,代表了一个客户端会话,Session的属性
sessionID: 会话ID,唯一标识一个会话,每次客户端创建新的会话时,Zookeeper都会为其分配一个全局唯一的sessionIDTimeOut: 会话超时时间,客户端在构造Zookeeper实例时,会配置sessionTimeout参数用于指定会话的超时时间,Zookeeper客户端向服务端发送这个超时时间后,服务端会根据自己的超时时间限制最终确定会话的超时时间TickTime: 下次会话超时时间点,为了便于Zookeeper对会话实行"分桶策略"管理,同时为了高效低耗地实现会话的超时检查与清理,Zookeeper会为每个会话标记一个下次会话超时时间点,其值大致等于当前时间加上TimeOutisClosing: 标记一个会话是否已经被关闭,当服务端检测到会话已经超时失效时,会将该会话的isClosing标记为"已关闭",这样就能确保不再处理来自该会话的心情求了
Zookeeper为了保证请求会话的全局唯一性,在SessionTracker初始化时,调用initializeNextSession方法生成一个sessionID,之后在Zookeeper运行过程中,会在该sessionID的基础上为每个会话进行分配,初始化算法如下
public static long initializeNextSession(long id) {
long nextSid = 0;
// 无符号右移8位使为了避免左移24后,再右移8位出现负数而无法通过高8位确定sid值
nextSid = (System.currentTimeMillis() << 24) >>> 8;
nextSid = nextSid | (id << 56);
return nextSid;
}
SessionTracker是Zookeeper服务端的会话管理器,负责会话的创建、管理和清理等工作.
HashMap<Long, SessionImpl> sessionsById: 用于根据sessionId来管理Session实体HashMap<Long, SessionSet> sessionSets: 用于根据下次会话超时时间点来归档会话,便于进行会话管理和超时管理ConcurrentHashMap<Long, Integer> sessionsWithTimeout: 用于根据sessionID来管理会话的超时时间.该数据结构和Zookeeper内部数据库相连通,会被定期持久化到快照文件中去
4.3 会话管理
分桶策略
Zookeeper的会话管理主要是通过SessionTracker来负责,其采用了分桶策略(将类似的会话放在同一区块中进行管理)进行管理,以便Zookeeper对会话进行不同区块的隔离处理以及同一区块的统一处理.
Zookeeper将所有的会话都分配在不同的区块一种,分配的原则是每个会话的下次超时时间点(ExpirationTime).ExpirationTime指该会话最近一次可能超时的时间点.同时,Zookeeper Leader服务器在运行过程中会定时地进行会话超时检查,时间间隔是ExpirationInterval,默认为tickTime的值,ExpirationTime的计算时间如下
ExpirationTime = ((CurrentTime + SessionTimeOut) / ExpirationInterval + 1) * ExpirationInterval
会话激活
为了保持客户端会话的有效性,客户端会在会话超时时间过期范围内向服务端发送PING请求来保持会话的有效性(心跳检测).同时,服务端需要不断地接收来自客户端的心跳检测,并且需要重新激活对应的客户端会话,这个重新激活过程称为TouchSession.会话激活不仅能够使服务端检测到对应客户端的存货性,同时也能让客户端自己保持连接状态.

整个流程分为四步
- 检查该会话是否已经被关闭.若已经被关闭,则直接返回即可
- 计算该会话新的超时时间ExpirationTime_New.使用上面提到的公式计算下一次超时时间点
- 获取该会话上次超时时间ExpirationTime_Old.计算该值是为了定位其所在的区块
- 迁移会话.将该会话从老的区块中取出,放入ExpirationTime_New对应的新区块中
在上面会话激活过程中,只要客户端发送心跳检测,服务端就会进行一次会话激活,心跳检测由客户端主动发起,以PING请求形式向服务端发送,在Zookeeper的实际设计中,只要客户端有请求发送到服务端,那么就会触发一次会话激活,以下两种情况都会触发会话激活.
- 客户端向服务端发送请求,包括读写请求,就会触发会话激活
- 客户端发现在sessionTimeout/3时间内尚未和服务端进行任何通信,那么就会主动发起PING请求,服务端收到该请求后,就会触发会话激活
会话超时检查
Zookeeper中,会话的超时检查是由SessionTracker来负责. SessionTracker使用单独的线程(超时检查线程)专门进行会话超时检查,即逐个一次地对会话桶中剩下的会话进行清理.如果一个会话被激活,那么Zookeeper就会将其从上一个会话桶迁移到下一个会话桶中,如ExpirationTime 1 的session n 迁移到ExpirationTime n 中,此时ExpirationTime 1中留下的所有会话都是尚未被激活的,超时检查线程就定时检查这个会话桶中所有剩下的未被迁移的会话,超时检查线程只需要在这些指定时间点(ExpirationTime 1、ExpirationTime 2…)上进行检查即可,这样提高了检查的效率,性能也非常好.
4.4 会话清理
当SessionTracker的会话超时线程检查出已经过期的会话后,就开始进行会话清理工作,大致可以分为如下七步
- 标记会话状态为已关闭
- 由于会话清理过程需要一段时间,为了保证在此期间不再处理来自该客户端的请求,SessionTracker会首先将该会话的isClosing标记为true,这样在会话清理期间接收到该客户端的心情求也无法继续处理了
- 发起会话关闭请求
- 为了使对该会话的关闭操作在整个服务端集群都生效,Zookeeper使用了提交会话关闭请求的方式,并立即交付给PreRequestProcessor进行处理
- 收集需要清理的临时节点
- 一旦某个会话失效后,那么和该会话相关的临时节点都需要被清理,因此,在清理之前,首先需要将服务器上所有和该会话相关的临时节点都整理出来.Zookeeper在内存数据库中会为每个会话都单独保存了一份由该会话维护的所有临时节点集合,在Zookeeper处理会话关闭请求之前,若正好有以下两类请求到达了服务端并正在处理中(这两类请求其共同点都是事务尚未处理完成).
- 节点删除请求,删除的目标节点正好是上述临时节点中的一个.
- 临时节点创建请求,创建的目标节点正好是上述临时节点中的一个.
- 对于第一类请求,需要将所有请求对应的数据节点路径从当前临时节点列表中移出,以避免重复删除,对于第二类请求,需要将所有这些请求对应的数据节点路径添加到当前临时节点列表中,以删除这些即将被创建但是尚未保存到内存数据库中的临时节点.
- 一旦某个会话失效后,那么和该会话相关的临时节点都需要被清理,因此,在清理之前,首先需要将服务器上所有和该会话相关的临时节点都整理出来.Zookeeper在内存数据库中会为每个会话都单独保存了一份由该会话维护的所有临时节点集合,在Zookeeper处理会话关闭请求之前,若正好有以下两类请求到达了服务端并正在处理中(这两类请求其共同点都是事务尚未处理完成).
- 添加节点删除事务变更
- 完成该会话相关的临时节点收集后,Zookeeper会逐个将这些临时节点转换成"节点删除"请求,并放入事务变更队列outstandingChanges中
- 删除临时节点
- FinalRequestProcessor会触发内存数据库,删除该会话对应的所有临时节点
- 移除会话
- 完成节点删除后,需要将会话从SessionTracker中删除
- 关闭NIOServerCnxn
- 最后,从NIOServerCnxnFactory找到该会话对应的NIOServerCnxn,将其关闭
4.5 重连
4.5.1 重连状态(CONNECTED & EXPIRED)
当客户端与服务端之间的网络连接断开时,Zookeeper客户端会自动进行反复的重连,直到最终成功连接上Zookeeper集群中的一台机器.此时,再次连接上服务端的客户端有可能处于以下两种状态之一
- 最后,从NIOServerCnxnFactory找到该会话对应的NIOServerCnxn,将其关闭
CONNECTED: 如果在会话超时时间内重新连接上集群中一台服务器EXPIRED: 如果在会话超时时间以外重新连接上,那么服务端其实已经对该会话进行了会话清理操作,此时会话被视为非法会话
在客户端与服务端之间维持的是一个长连接,在sessionTimeout时间内,服务端会不断地检测该客户端是否还处于正常连接,服务端会将客户端的每次操作视为一次有效的心跳检测来反复地进行会话激活.因此,在正常情况下,客户端会话时一直有效的.然而,当客户端与服务端之间的连接断开后,用户在客户端可能主要看到两类异常:CONNECTION_LOSS(连接断开)和SESSION_EXPIRED(会话过期).
4.5.2 重连异常: CONNECTION_LOSS(连接断开)和SESSION_EXPIRED(会话过期)
连接断开connection_loss
有时因为网络闪断导致客户端与服务器断开连接,或是因为客户端当前连接的服务器出现问题导致连接断开,我么称"客户端与服务器断开连接"现象,即connection_loss.在这种情况下,zookeeper客户端会自动从地址列表中重新获取新的地址并尝试进行重新连接,直到最终成功连接上服务器.
举个例子:某应用在使用zookeeper客户端进行setData操作时,正好出现了connection_loss现象,那么客户端会记录接收到事件:none-disconnected通知,同时会抛出异常ConnectionLossException.这时,我们的应用需要做的事情是捕获异常,然后等待zookeeper的客户端自动完成重连,一旦客户端成功连上一台zookeeper机器后,那么客户端就会收到事件none-syncconnnected通知,之后就可以重试刚才的setData操作.
会话失效session_expired
通常发生在connection_loss期间,客户端和服务器连接断开后,由于重连期间耗时过长,超过了会话超时时间限制后还没有成功连接上服务器,那么服务器认为这个会话已经结束了,就会开始进行会话清理,但是另一方面,该客户端本身不知道会话已经失效了,并且其客户端状态还是disconnected.之后,如果客户端重新连接上了服务器,服务器会告知客户端会话已经失效,在这时,用户就需要重新实例化一个zookeeper对象,并看应用的复杂程度,重新恢复临时数据.
会话转移session_moved
是指客户端会话从一台服务器转移到另一台服务器上,假设客户端和服务器s1之间的连接断开后,如果通过尝试重连后,成功连接上了新的服务器s2并且延续了有效会话,那么就可以说会话从s1转移到了s2上.
5.服务器启动
服务端整体架构如下
Zookeeper服务器的启动,大致可以分为以下五个步骤
- 配置文件解析
- 初始化数据管理器
- 初始化网络I/O管理器
- 数据恢复
- 对外服务
5.1 单机版服务器启动
单机版服务器的启动其流程图
5.1.1 单机版服务器启动 - 预启动
- 统一由
QuorumPeerMain作为启动类- 无论单机或集群,在zkServer.cmd和zkServer.sh中都配置了QuorumPeerMain作为启动入口类
- 解析配置文件zoo.cfg
- zoo.cfg配置运行时的基本参数,如tickTime、dataDir、clientPort等参数
- 创建并启动历史文件清理器DatadirCleanupManager
- 对事务日志和快照数据文件进行定时清理
- 判断当前是集群模式还是单机模式启动
- 若是单机模式,则委托给
ZooKeeperServerMain进行启动
- 若是单机模式,则委托给
- 再次进行配置文件zoo.cfg的解析
- 创建服务器实例
ZooKeeperServer- Zookeeper服务器首先会进行服务器实例的创建,然后对该服务器实例进行初始化,包括
- 连接器
- 内存数据库
- 请求处理器等
- Zookeeper服务器首先会进行服务器实例的创建,然后对该服务器实例进行初始化,包括
5.1.2 单机版服务器启动 - 初始化
- 创建服务器统计器ServerStats
- ServerStats是Zookeeper服务器运行时的统计器,包含了最基本的运行时信息
- packetsSent: 从Zookeeper启动开始,或是最近一次重置服务端统计信息之后,服务端向客户端发送的响应包次数
- packetsReceived: 从Zookeeper启动开始,或是最近一次重置服务端统计信息之后,服务端接收到的来自客户端的请求包次数
- maxLatency/minlatency/totalLatency: 从Zookeeper启动开始,或是最近一次重置服务端统计信息之后,服务端请求处理的最大延迟、最小延迟以及总延迟
- count: 从Zookeeper启动开始,或是最近一次重置服务端统计信息之后,服务端处理的客户端请求总次数
- ServerStats是Zookeeper服务器运行时的统计器,包含了最基本的运行时信息
- 创建Zookeeper数据管理器FileTxnSnapLog
- FileTxnSnapLog是Zookeeper上层服务器和底层数据存储之间的对接层,提供了一系列操作数据文件的接口,如事务日志文件和快照数据文件.Zookeeper根据zoo.cfg文件中解析出的快照数据目录dataDir和事务日志目录dataLogDir来创建FileTxnSnapLog
- 设置服务器tickTime和会话超时时间限制
- 创建ServerCnxnFactory
- 通过配置系统属性zookeper.serverCnxnFactory来指定使用Zookeeper自己实现的NIO还是使用Netty框架作为Zookeeper服务端网络连接工厂
- 初始化ServerCnxnFactory
- Zookeeper会初始化Thread作为ServerCnxnFactory的主线程,然后再初始化NIO服务器
- 启动ServerCnxnFactory主线程
- 进入Thread的run方法,此时服务端还不能处理客户端请求
- 恢复本地数据
- 启动时,需要从本地快照数据文件和事务日志文件进行数据恢复
- 创建并启动会话管理器
- Zookeeper会创建会话管理器SessionTracker进行会话管理
- 初始化Zookeeper的请求处理链
- Zookeeper请求处理方式为责任链模式的实现.会有多个请求处理器依次处理一个客户端请求,在服务器启动时,会将这些请求处理器串联成一个请求处理链
- 注册JMX服务
- Zookeeper会将服务器运行时的一些信息以JMX的方式暴露给外部
- 注册Zookeeper服务器实例
- 将Zookeeper服务器实例注册给ServerCnxnFactory,之后Zookeeper就可以对外提供服务
至此,单机版的Zookeeper服务器启动完毕.
5.2 集群版服务器启动
集群版服务器的启动流程图
集群版服务器启动流程
- 预启动
- 初始化
- Leader选举
- Leader与Follower启动期交互过程
- Leader与Follower启动
5.2.1 集群版服务器启动 - 预启动
- 统一由
QuorumPeerMain作为启动类 - 解析配置文件zoo.cfg
- 创建并启动历史文件清理器DatadirCleanupFactory
- 判断当前是集群模式还是单机模式的启动
- 在集群模式中,在zoo.cfg文件中配置了多个服务器地址,可以选择集群启动
5.2.2 集群版服务器启动 - 初始化
- 创建ServerCnxnFactory
- 初始化ServerCnxnFactory
- 创建Zookeeper数据管理器FileTxnSnapLog
- 创建
QuorumPeer实例- Quorum是集群模式下特有的对象,是Zookeeper服务器实例(ZooKeeperServer)的托管者,QuorumPeer代表了集群中的一台机器,在运行期间,QuorumPeer会不断检测当前服务器实例的运行状态,同时根据情况发起Leader选举
- 创建内存数据库ZKDatabase
- ZKDatabase负责管理ZooKeeper的所有会话记录以及DataTree和事务日志的存储
- 初始化
QuorumPeer- 将核心组件如FileTxnSnapLog、ServerCnxnFactory、ZKDatabase注册到QuorumPeer中
- 同时配置QuorumPeer的参数,如服务器列表地址、Leader选举算法和会话超时时间限制等
- 将核心组件如FileTxnSnapLog、ServerCnxnFactory、ZKDatabase注册到QuorumPeer中
- 恢复本地数据
- 启动ServerCnxnFactory主线程
5.2.3 Leader选举
- 初始化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选举端口的监听,等待集群中其他服务器创建连接
- 集群模式特有,Zookeeper首先会根据自身的服务器ID(SID)、最新的ZXID(lastLoggedZxid)和当前的服务器epoch(currentEpoch)来生成一个初始化投票,在初始化过程中,每个服务器都会给自己投票.然后,根据zoo.cfg的配置,创建相应Leader选举算法实现
- 注册JMX服务.
- 检测当前服务器状态
- 运行期间,QuorumPeer会不断检测当前服务器状态
- 在正常情况下,Zookeeper服务器的状态在LOOKING、LEADING、FOLLOWING/OBSERVING之间进行切换
- 在启动阶段,QuorumPeer的初始状态是LOOKING,因此开始进行Leader选举
- 运行期间,QuorumPeer会不断检测当前服务器状态
- Leader选举
- 通过投票确定Leader,其余机器称为Follower和Observer
5.2.4 Leader和Follower启动期交互过程

- 创建Leader服务器和Follower服务器
- 完成Leader选举后,每个服务器会根据自己服务器的角色创建相应的服务器实例,并进入各自角色的主流程
- Leader服务器启动Follower接收器
LearnerCnxAcceptor- 运行期间,Leader服务器需要和所有其余的服务器(统称为Learner)保持连接以确定集群的机器存活情况,LearnerCnxAcceptor接收器负责接收所有非Leader服务器的连接请求
- Learner服务器开始和Leader建立连接
- 所有Learner服务器在启动完毕后,会从Leader选举的投票结果中找到当前集群中的Leader服务器,然后与其建立连接
- Leader服务器创建LearnerHandler
- Leader接收到来自其他机器连接创建请求后,会创建一个LearnerHandler实例,每个LearnerHandler实例都对应一个Leader与Learner服务器之间的连接,其负责Leader和Learner服务器之间几乎所有的消息通信和数据同步
- 向Leader注册
- Learner完成和Leader的连接后,会向Leader进行注册,即将Learner服务器的基本信息
LearnerInfo,包括SID和ZXID,发送给Leader服务器
- Learner完成和Leader的连接后,会向Leader进行注册,即将Learner服务器的基本信息
- Leader解析Learner信息计算新的epoch
- Leader接收到Learner服务器基本信息后,会解析出该Learner的SID和ZXID,然后根据ZXID解析出对应的epoch_of_learner,并和当前Leader服务器的epoch_of_leader进行比较,如果该Learner的epoch_of_learner更大,则更新Leader的epoch:
epoch_of_leader = epoch_of_learner + 1- 然后LearnHandler进行等待,直到过半Learner已经向Leader进行了注册,同时更新了epoch_of_leader后,Leader就可以确定当前集群的epoch了
- Leader接收到Learner服务器基本信息后,会解析出该Learner的SID和ZXID,然后根据ZXID解析出对应的epoch_of_learner,并和当前Leader服务器的epoch_of_leader进行比较,如果该Learner的epoch_of_learner更大,则更新Leader的epoch:
- 发送Leader状态
- 计算出新的epoch后,Leader会将该信息以一个LEADERINFO消息的形式发送给Learner,并等待Learner的响应
- Learner发送ACK消息
- Learner接收到LEADERINFO后,会解析出epoch和ZXID,然后向Leader反馈一个ACKEPOCH响应
- 数据同步
- Leader收到Learner的ACKEPOCH后,即可进行数据同步
- 启动Leader和Learner服务器
- 当有过半Learner已经完成了数据同步,那么Leader和Learner服务器实例就可以启动了
5.2.5. Leader和Follower启动
- 创建启动会话管理器
- 初始化Zookeeper请求处理链
- 集群模式的每个处理器也会在启动阶段串联请求处理链,只是根据服务器角色的不同,会有不同的请求处理链路
- 注册JMX服务
至此,集群版的Zookeeper服务器启动完毕.
6.Leader选举
Leader选举是Zookeeper中最重要的技术之一,也是保证分布式数据一致性的关键所在.
6.1 Leader选举概述
6.1.1 服务器启动时期的Leader选举
- 每个server会发出一个投票
- 由于是初始情况,因此对于server1和server2来说,都会将自己作为leader服务器来投票,每次投票包含的最基本的元素为:所推举的服务器的myid和zxid,我们以(myid, zxid)的形式来表示.因为是初始化阶段,因此无论是server1和是server2都会投给自己,即server1的投票为(1, 0),server2的投票为(2, 0),然后各自将这个投票发给集群中其它所有机器.
- 接收来自各个服务器的投票
- 每个服务器都会接收来自其它服务器的投票,接收到后会判断该投票的有效性,包括检查是否是本轮投票,是否来自looking状态的服务器
- 处理投票
- 在接收到来自其它服务器的投票后,针对每一个投票,服务器都需要将别人的投票和自己的投票进行pk,pk的规则如下:
- 优先检查zxid,zxid大的服务器优先作为leader
- 如果zxid相同,那么比较myid,myid大的服务器作为leader服务器
- 现在我们来看server1和server2实际是如何进行投票的
- 对于server1来说,他自己的投票是(1, 0),而接收到的投票为(2, 0).首先会对比两者的zxid,因为都是0,所以接下来会比较两者的myid,server1发现接收到的投票中的myid是2,大于自己,于是就会更新自己的投票为(2, 0),然后重新将投票发出去
- 而对于server2,不需要更新自己的投票信息,只是再一次向集群中的所有机器发出上一次投票信息即可
- 在接收到来自其它服务器的投票后,针对每一个投票,服务器都需要将别人的投票和自己的投票进行pk,pk的规则如下:
- 统计投票
- 每次投票后,服务器都会统计所有投票,判断是否已经有过半的机器接收到相同的投票信息
- 对于server1和server2来说,都统计出集群中已经有两台机器接受了(2, 0)这个投票信息.这里过半的概念是指大于集群机器数量的一半,即大于或等于(n/2+1).对于这里由3台机器构成的集群大于等于2台即为达到过半要求
- 每次投票后,服务器都会统计所有投票,判断是否已经有过半的机器接收到相同的投票信息
- 改变服务器状态
- 一旦确定了leader,每个服务器就会更新自己的状态,如果是follower,那么就变更为following,如果是leader,就变更为leading
6.1.2 服务器运行时期的Leader选举
- 变更状态
- 当leader挂了之后,余下的非observer服务器都会将自己的状态变为looking,然后开始进行leader选举流程
- 每个server会发出一个投票
- 在这个过程中,需要生成投票信息(myid, zxid),因为是运行期间,因此每个服务器上的zxid可能不同
- 我们假定server1的zxid为123,而server3的zxid为122.
- 在第一轮投票中,server1和server3都会投给自己,即分别产生投票(1, 123)和(3, 122),然后各自将这个投票发给集群中的所有机器.
- 我们假定server1的zxid为123,而server3的zxid为122.
- 在这个过程中,需要生成投票信息(myid, zxid),因为是运行期间,因此每个服务器上的zxid可能不同
- 接收来自各个服务器的投票
- 处理投票
- 投票的处理
- 和上面提到的服务器启动期间的处理规则是一致的,在这个例子中,由于server1的zxid是123,server3的zxid是122,显然server1会成为leader
- 统计投票
- 改变服务器状态
6.2 Leader选举的算法分析
6.2.1 术语解释
SID(服务器ID): 在zoo.cfg文件中,对集群中的每一个server都赋予一个id,标识着集群中的一台server.每台机器不能重复,和myid值一致epoch: 代表一个Leader周期.每当一个新的leader产生,该leader便会从服务器本地日志中最大事务Proposal的zxid解析出epoch值,然后对其进行+1操作,作为新的epoch.zxid(事务ID): 标识这对一次服务器状态的变更.- 是一个64bit的long数值,高32位标识着当前epoch,低32位是计数器.Leader在产生一个新的事务Proposal的时候,都会对该计数器进行+1操作.
- 新的Leader产生的时候,epoch+1的同时,低32会被置为0,在此基础上开始生成新的ZXID
- 是一个64bit的long数值,高32位标识着当前epoch,低32位是计数器.Leader在产生一个新的事务Proposal的时候,都会对该计数器进行+1操作.
Vote(投票): Leader选举,顾名思义必须通过投票来实现Quorum: 过半机器数
6.2.2 进入leader选举
当zookeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入leader选举:
- 服务器初始化启动
- 服务器运行期间无法和leader保持连接
而当一台机器进入leader选举流程时,当前集群也可能会处于以下两种状态:
- 集群中本来就已经存在一个leader
- 集群中确实不存在leader
已经存在leader的情况
这种情况通常是集群中的某一台机器启动比较晚,在它启动之前,集群已经可以正常工作,即已经存在一台leader服务器.针对这种情况,当该机器试图去选举leader的时候,会被告知当前服务器的leader信息,对于该机器来说,仅仅需要和leader机器建立连接,并进行状态同步即可.
leader不存在
- 第一次投票
- 无论哪种导致进行Leader选举,集群的所有机器都处于试图选举出一个Leader的状态,即LOOKING状态,LOOKING机器会向所有其他机器发送消息,该消息称为投票.投票中包含了SID(服务器的唯一标识)和ZXID(事务ID),(SID, ZXID)形式来标识一次投票信息.
- 变更投票
- 每台机器发出投票后,也会收到其他机器的投票,每台机器会根据一定规则来处理收到的其他机器的投票,并以此来决定是否需要变更自己的投票,这个规则也是整个Leader选举算法的核心所在
- 定义术语
vote_sid:接收到的投票中所推举Leader服务器的SIDvote_zxid:接收到的投票中所推举Leader服务器的ZXIDself_sid:当前服务器自己的SIDself_zxid:当前服务器自己的ZXID
- 每次对收到的投票的处理,都是对(vote_sid, vote_zxid)和(self_sid, self_zxid)对比的过程.
- 规则一:如果vote_zxid大于self_zxid,就认可当前收到的投票,并再次将该投票发送出去
- 规则二:如果vote_zxid小于self_zxid,那么坚持自己的投票,不做任何变更
- 规则三:如果vote_zxid等于self_zxid,那么就对比两者的SID,如果vote_sid大于self_sid,那么就认可当前收到的投票,并再次将该投票发送出去
- 规则四:如果vote_zxid等于self_zxid,并且vote_sid小于self_sid,那么坚持自己的投票,不做任何变更
- 确定Leader
- 经过第二轮投票后,集群中的每台机器都会再次接收到其他机器的投票,然后开始统计投票,如果一台机器收到了超过半数的相同投票,那么这个投票对应的SID机器即为Leader
小结
由上面规则可知,通常那台服务器上的数据越新(ZXID会越大),其成为Leader的可能性越大,也就越能够保证数据的恢复.如果ZXID相同,则SID越大机会越大.

6.6.3 Leader选举的实现细节
服务器状态
LOOKING:寻找Leader状态.处于该状态的服务器会认为当前集群中不存在Leader,然后发起leader选举FOLLOWING:表明当前服务器角色是FollwerLEADING:表明当前服务器角色是LeaderOBSERVING:表明当前服务器角色是Observer,不参与Leader选举
投票数据结构
Zookeeper中对投票Vote的数据结构的定义
| 属性 | 说明 |
| --- | --- |
| id | 被推举的Leader的SID |
| zxid | 被推举的Leader事务ID |
| electionEpoch | 逻辑时钟,用来判断多个投票是否在同一轮选举周期中,该值在服务端是一个自增序列,每次进入新一轮的投票后,都会对该值进行加1操作 |
| peerEpoch | 被推举的Leader的epoch |
| state | 当前服务器的状态 |
QuorumCnxManager:网络I/O
每台服务器在启动的过程中,会启动一个QuorumPeerManager,负责各台服务器之间的底层Leader选举过程中的网络通信.
- 消息队列
- QuorumCnxManager内部维护了一系列的队列,用来保存接收到的、待发送的消息以及消息的发送器,除接收队列以外,其他队列都按照SID分组形成队列集合,如一个集群中除了自身还有3台机器,那么就会为这3台机器分别创建一个发送队列,互不干扰.
recvQueue:消息接收队列,用于存放那些从其他服务器接收到的消息queueSendMap:消息发送队列,用于保存那些待发送的消息,按照SID进行分组senderWorkerMap:发送器集合,每个SenderWorker消息发送器,都对应一台远程Zookeeper服务器,负责消息的发送,也按照SID进行分组lastMessageSent:最近发送过的消息,为每个SID保留最近发送过的一个消息
- QuorumCnxManager内部维护了一系列的队列,用来保存接收到的、待发送的消息以及消息的发送器,除接收队列以外,其他队列都按照SID分组形成队列集合,如一个集群中除了自身还有3台机器,那么就会为这3台机器分别创建一个发送队列,互不干扰.
- 建立连接
- 为了能够相互投票,Zookeeper集群中的所有机器都需要两两建立起网络连接.QuorumCnxManager在启动时会创建一个ServerSocket来监听Leader选举的通信端口(默认为3888).开启监听后,Zookeeper能够不断地接收到来自其他服务器的创建连接请求,在接收到其他服务器的TCP连接请求时,会进行处理.
- 为了避免两台机器之间重复地创建TCP连接,Zookeeper只允许SID大的服务器主动和其他机器建立连接,否则断开连接.在接收到创建连接请求后,服务器通过对比自己和远程服务器的SID值来判断是否接收连接请求,如果当前服务器发现自己的SID更大,那么会断开当前连接,然后自己主动和远程服务器建立连接.一旦连接建立,就会根据远程服务器的SID来创建相应的消息发送器SendWorker和消息接收器RecvWorker,并启动.
- 消息接收与发送.
消息接收:由消息接收器RecvWorker负责,由于Zookeeper为每个远程服务器都分配一个单独的RecvWorker,因此,每个RecvWorker只需要不断地从这个TCP连接中读取消息,并将其保存到recvQueue队列中.消息发送:由于Zookeeper为每个远程服务器都分配一个单独的SendWorker,因此,每个SendWorker只需要不断地从对应的消息发送队列中获取出一个消息发送即可,同时将这个消息放入lastMessageSent中.在SendWorker中,一旦Zookeeper发现针对当前服务器的消息发送队列为空,那么此时需要从lastMessageSent中取出一个最近发送过的消息来进行再次发送,这是为了解决接收方在消息接收前或者接收到消息后服务器挂了,导致消息尚未被正确处理.同时,Zookeeper能够保证接收方在处理消息时,会对重复消息进行正确的处理.
FastLeaderElection:选举算法核心

- 约定概念
- 外部投票:特指其他服务器发来的投票
- 内部投票:服务器自身当前的投票
- 选举轮次:Zookeeper服务器Leader选举的轮次,即logicalclock
- PK:对内部投票和外部投票进行对比来确定是否需要变更内部投票
- 选票管理
sendqueue:选票发送队列,用于保存待发送的选票recvqueue:选票接收队列,用于保存接收到的外部投票WorkerReceiver:选票接收器.其会不断地从QuorumCnxManager中获取其他服务器发来的选举消息,并将其转换成一个选票,然后保存到recvqueue中,在选票接收过程中,如果发现该外部选票的选举轮次小于当前服务器的,那么忽略该外部投票,同时立即发送自己的内部投票- `WorkerSender :选票发送器,不断地从sendqueue中获取待发送的选票,并将其传递到底层QuorumCnxManager中

- Leader选举的基本流程如下
- 1.自增选举轮次
- Zookeeper规定所有有效的投票都必须在同一轮次中,在开始新一轮投票时,会首先对logicalclock进行自增操作
- 2.初始化选票
- 在开始进行新一轮投票之前,每个服务器都会初始化自身的选票,并且在初始化阶段,每台服务器都会将自己推举为Leader
- 3.发送初始化选票
- 完成选票的初始化后,服务器就会发起第一次投票.Zookeeper会将刚刚初始化好的选票放入sendqueue中,由发送器WorkerSender负责发送出去
- 4.接收外部投票
- 每台服务器会不断地从recvqueue队列中获取外部选票.如果服务器发现无法获取到任何外部投票,那么就会立即确认自己是否和集群中其他服务器保持着有效的连接,如果没有连接,则马上建立连接,如果已经建立了连接,则再次发送自己当前的内部投票
- 5.判断选举轮次
- 在发送完初始化选票之后,接着开始处理外部投票.在处理外部投票时,会根据选举轮次来进行不同的处理
- 外部投票的选举轮次大于内部投票
- 若服务器自身的选举轮次落后于该外部投票对应服务器的选举轮次,那么就会立即更新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票.最终再将内部投票发送出去.
- 外部投票的选举轮次小于内部投票
- 若服务器接收的外选票的选举轮次落后于自身的选举轮次,那么Zookeeper就会直接忽略该外部投票,不做任何处理,并返回步骤4.
- 外部投票的选举轮次等于内部投票.此时可以开始进行选票PK.
- 外部投票的选举轮次大于内部投票
- 在发送完初始化选票之后,接着开始处理外部投票.在处理外部投票时,会根据选举轮次来进行不同的处理
- 6.选票PK
- 在进行选票PK时,符合任意一个条件就需要变更投票
- 若外部投票中推举的Leader服务器的选举轮次大于内部投票,那么需要变更投票
- 若选举轮次一致,那么就对比两者的ZXID,若外部投票的ZXID大,那么需要变更投票
- 若两者的ZXID一致,那么就对比两者的SID,若外部投票的SID大,那么就需要变更投票
- 在进行选票PK时,符合任意一个条件就需要变更投票
- 7.变更投票
- 经过PK后,若确定了外部投票优于内部投票,那么就变更投票,即使用外部投票的选票信息来覆盖内部投票,变更完成后,再次将这个变更后的内部投票发送出去
- 8.选票归档
- 无论是否变更了投票,都会将刚刚收到的那份外部投票放入选票集合recvset中进行归档.recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票(按照服务队的SID区别,如{(1, vote1), (2, vote2)...})
- 9.统计投票
- 完成选票归档后,就可以开始统计投票,统计投票是为了统计集群中是否已经有过半的服务器认可了当前的内部投票,如果确定已经有过半服务器认可了该投票,则终止投票.否则返回步骤4.
- 10.更新服务器状态
- 若已经确定可以终止投票,那么就开始更新服务器状态,服务器首选判断当前被过半服务器认可的投票所对应的Leader服务器是否是自己,若是自己,则将自己的服务器状态更新为LEADING,若不是,则根据具体情况来确定自己是FOLLOWING或是OBSERVING.
- 1.自增选举轮次
7.各服务器角色介绍
7.1 Leader
Leader服务器是Zookeeper集群工作的核心,其主要工作如下
- 事务请求的唯一调度和处理者,保证集群事务处理的顺序性
- 集群内部各服务器的调度者
请求处理链

使用责任链来处理每个客户端的请求时Zookeeper的特色,Leader服务器的请求处理链如下
- PrepRequestProcessor
- 请求预处理器.在Zookeeper中,那些会改变服务器状态的请求称为事务请求(创建节点、更新数据、删除节点、创建会话等),PrepRequestProcessor能够识别出当前客户端请求是否是事务请求.对于事务请求,PrepRequestProcessor处理器会对其进行一系列预处理,如创建请求事务头、事务体、会话检查、ACL检查和版本检查等
ProposalRequestProcessor- 事务投票处理器.Leader服务器事务处理流程的发起者,对于非事务性请求,ProposalRequestProcessor会直接将请求转发到CommitProcessor处理器,不再做任何处理,而对于事务性请求,处理将请求转发到CommitProcessor外,还会根据请求类型创建对应的Proposal提议,并发送给所有的Follower服务器来发起一次集群内的事务投票.同时,ProposalRequestProcessor还会将事务请求交付给SyncRequestProcessor进行事务日志的记录
SyncRequestProcessor- 事务日志记录处理器.用来将事务请求记录到事务日志文件中,同时会触发Zookeeper进行数据快照
AckRequestProcessor- 负责在SyncRequestProcessor完成事务日志记录后,向Proposal的投票收集器发送ACK反馈,以通知投票收集器当前服务器已经完成了对该Proposal的事务日志记录
CommitProcessor- 事务提交处理器.对于非事务请求,该处理器会直接将其交付给下一级处理器处理;对于事务请求,其会等待集群内针对Proposal的投票直到该Proposal可被提交,利用CommitProcessor,每个服务器都可以很好地控制对事务请求的顺序处理
ToBeCommitProcessor- 该处理器有一个toBeApplied队列,用来存储那些已经被CommitProcessor处理过的可被提交的Proposal.其会将这些请求交付给FinalRequestProcessor处理器处理,待其处理完后,再将其从toBeApplied队列中移除
FinalRequestProcessor- 用来进行客户端请求返回之前的操作,包括创建客户端请求的响应,针对事务请求,该处理还会负责将事务应用到内存数据库中去
LearnerHandler
为了保证整个集群内部的实时通信,同时为了确保可以控制所有的Follower/Observer服务器,Leader服务器会与每个Follower/Observer服务器建立一个TCP长连接.同时也会为每个Follower/Observer服务器创建一个名为LearnerHandler的实体. LearnerHandler是Learner服务器的管理者,主要负责Follower/Observer服务器和Leader服务器之间的一系列网络通信,包括数据同步、请求转发和Proposal提议的投票等.Leader服务器中保存了所有Follower/Observer对应的LearnerHandler.
7.2 Follower
Follower是Zookeeper集群的跟随者,其主要工作如下
- 处理客户端非事务性请求(读取数据),转发事务请求给Leader服务器
- 参与事务请求Proposal的投票
- 参与Leader选举投票

Follower也采用了责任链模式组装的请求处理链来处理每一个客户端请求,由于不需要对事务请求的投票处理,因此Follower的请求处理链会相对简单,其处理链如下
FollowerRequestProcessor- 其用作识别当前请求是否是事务请求,若是,那么Follower就会将该请求转发给Leader服务器,Leader服务器是在接收到这个事务请求后,就会将其提交到请求处理链,按照正常事务请求进行处理
SendAckRequestProcessor- 其承担了事务日志记录反馈的角色,在完成事务日志记录后,会向Leader服务器发送ACK消息以表明自身完成了事务日志的记录工作
7.3 Observer
Observer充当观察者角色,观察Zookeeper集群的最新状态变化并将这些状态同步过来,其对于非事务请求可以进行独立处理,对于事务请求,则会转发给Leader服务器进行处理. Observer不会参与任何形式的投票,包括事务请求Proposal的投票和Leader选举投票.Observer服务器只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力.

7.4 集群间消息通信
数据同步型
指在Learner和Leader服务器进行数据同步时,网络通信所用到的消息,通常有DIFF、TRUNC、SNAP、UPTODATE.
| 消息类型 | 发送方->接收方 | 说明 |
|---|---|---|
DIFF,13 |
Leader -> Lealner | 说明用于通知Learner服务器,Leader即将与其进行DIFF方式的数据同步 |
TRUNC,14 |
Leader -> Lealner | 用于触发 Learner 服务器进行内存数据库的回滚操作 |
SNAP,15 |
Leader -> Lealner | 用于通知Learner服务器,Leader即将与其进行"全量"方式的数据同步 |
UPTODTE,12 |
Leader -> Lealner | 用于告知Learner服务器已经完成了数据同步,可以开始对外提供服务 |
服务器初始化型
指在整个集群或是某些新机器初始化时,Leader和Learner之间相互通信所使用的消息类型,常见的有OBSERVERINFO、FOLLOWERINFO、LEADERINFO、ACKEPOCH和NEWLEADER五种.
| 消息类型 | 发送方->接收方 | 说明 |
|---|---|---|
OBSERVERINFO,16 |
Obserfer -> Leader | 该信息通常是由Observer服务器在启动的时候发送给Leader的,用于向Leader服务器注册自己,同时向Leader服务器表明当前Learner服务器的角色是Observer ,消息中包含了当前Observer服务器的SID和已经处理的最新的ZXID |
FOLLOWERINFO,11 |
Follower -> Leader | 该信息通常是由Follower服务器在启动时发送给Leader的,用于向Leader服务器注册自己,表明当前Learner服务器的角色是 Follower,消息中包含Follower服务器的SID和已经处理的最新的ZXID |
LEADERINFO,17 |
Leader -> Learner | Learner连接上Leader后,回向Leader发送LearnerInfo信息,Leader服务器在接收到该消息后,会将Leader服务器的基本信息发送给这些Learner.通常包含了当前Leader服务器的最新EPOCH值 |
ACKEPOCH,18 |
Lealner -> Leader | Learner接受到Leader发来的LEADERDINFO后,会将自己最新的ZXID和EPOCH以ACKEPOCH消息的形式发送给Leader |
NEWLEADER,10 |
Leader -> Learner | 通常用于Leader服务器向Leaner发送一个阶段性的标识消息——Leader与Learner完成交互后,向Leaner发送NEWLEADER消息,同时带上当前Leader服务器处理的最新ZXID.这一系列的交互流程包括:足够多的Follower服务器连接上Leader或是完成数据同步 |
请求处理型
指在进行清理时,Leader和Learner服务器之间互相通信所使用的消息,常见的有REQUEST、PROPOSAL、ACK、COMMIT、INFORM和SYNC六种.
| 消息类型 | 发送方->接收方 | 说明 |
|---|---|---|
REQUEST,1 |
Lealner 一 Leader | 是Zookeeper中的请求转发消息. 当Learner收到事务请求后,将该请求以REQUEST消息的形式转发给Leader服务器处理 |
PROPOSAL,2 |
Leader 一 Follower | ZAB协议中的提议.在处理事务请求时,Leader服务器会将事务请求以PROPOSAL消息的形式创建投票发送给集群中的所有 Follower来进行事务日志的记录 |
ACK,3 |
Follower -> Leader | Follower 接收到来自Leader的PROPOSAL消息后,会进行事务日志记录,如果完成事务日志的记录,就会以 ACK 消息的形式反馈给 Leader |
COMMIT,4 |
Leader -> Follower | 用于通知集群中所有Follower可以进行事务请求的提交了,Leader在接收到过半的Folfower服务器发来的ACK消息后,就进入事务请求的最终提交流程——生成COMMIT消息,告知所有的Follower服务器进行事务请求的提交 |
INFORM,8 |
Leader -> Observer | 通知Observer已经可以提交事务请求,同时还会在消息中携带事务请求的内容 |
SYNC,7 |
Leader -> Learner | 通知 Learner服务器已经完成了Sync操作 |
会话管理型
指Zookeeper在进行会话管理时和Learner服务器之间互相通信所使用的消息,常见的有PING和REVALIDATE两种.
| 消息类型 | 发送方->接收方 | 说明 |
|---|---|---|
PING,5 |
Leader -> Learner | 用于Leader同步Learner服务器上的客户端心跳检测,用以激活存活的客户端.Zookeeper的客户端往往会随机地和任意一台服务器保持连接,因此Leader无法直接接收到所有客户端的心跳检测,需要委托给Learner来保存这些客户端的心跳检测记录.Leader会定时地向Learner发送PING消息, Learner在接收到PING消息后,会将这段时间内保持心跳的客户端列表,同样以PING消息的形式反馈给Leader服务器,由Leader负责逐个对这些客户端进行会话激活 |
REVALIDATE,6 |
Learner -> Leader | 用于Leader检验会话是否有效,同时也会激活会话,通常发生在客户端重连的过程中,新的服务器需要向Leader发送REVALIDATE消息以确定该会话是否已经超时 |
8.请求处理
8.1 会话创建请求
Zookeeper服务端对于会话创建的处理,大体可以分为请求接收、会话创建、预处理、事务处理、事务应用和会话响应六大环节.

会话创建请求具体流程
- 请求接收
- 1.I/O层接收来自客户端的请求
- NIOServerCnxn维护每一个客户端连接,客户端与服务器端的所有通信都是由NIOServerCnxn负责,其负责统一接收来自客户端的所有请求,并将请求内容从底层网络I/O中完整地读取出来.
- 2.判断是否是客户端会话创建请求
- 每个会话对应一个NIOServerCnxn实体,对于每个请求,Zookeeper都会检查当前NIOServerCnxn实体是否已经被初始化,如果尚未被初始化,那么就可以确定该客户端一定是会话创建请求.
- 3.反序列化ConnectRequest请求
- 一旦确定客户端请求是否是会话创建请求,那么服务端就可以对其进行反序列化,并生成一个ConnectRequest载体.
- 4.判断是否是ReadOnly客户端
- 如果当前Zookeeper服务器是以ReadOnly模式启动,那么所有来自非ReadOnly型客户端的请求将无法被处理.因此,服务端需要先检查是否是ReadOnly客户端,并以此来决定是否接受该会话创建请求.
- 5.检查客户端ZXID
- 正常情况下,在一个Zookeeper集群中,服务端的ZXID必定大于客户端的ZXID,因此若发现客户端的ZXID大于服务端ZXID,那么服务端不接受该客户端的会话创建请求.
- 6.协商sessionTimeout
- 在客户端向服务器发送超时时间后,服务器会根据自己的超时时间限制最终确定该会话超时时间,这个过程就是sessionTimeout协商过程.
- 7.判断是否需要重新激活创建会话
- 服务端根据客户端请求中是否包含sessionID来判断该客户端是否需要重新创建会话,若客户单请求中包含sessionID,那么就认为该客户端正在进行会话重连,这种情况下,服务端只需要重新打开这个会话,否则需要重新创建.
- 1.I/O层接收来自客户端的请求
- 会话创建
- 8.为客户端生成sessionID
- 在为客户端创建会话之前,服务端首先会为每个客户端分配一个sessionID,服务端为客户端分配的sessionID是全局唯一的.
- 9.注册会话
- 向SessionTracker中注册会话,SessionTracker中维护了sessionsWithTimeout和sessionsById,在会话创建初期,会将客户端会话的相关信息保存到这两个数据结构中.
- 10.激活会话
- 激活会话涉及Zookeeper会话管理的分桶策略,其核心是为会话安排一个区块,以便会话清理程序能够快速高效地进行会话清理.
- 11.生成会话密码
- 服务端在创建一个客户端会话时,会同时为客户端生成一个会话密码,连同sessionID一同发给客户端,作为会话在集群中不同机器间转移的凭证.
- 向SessionTracker中注册会话,SessionTracker中维护了sessionsWithTimeout和sessionsById,在会话创建初期,会将客户端会话的相关信息保存到这两个数据结构中.
- 8.为客户端生成sessionID
- 预处理
- 12.将请求交给PrepRequestProcessor处理器处理
- 在提交给第一个请求处理器之前,Zookeeper会根据该请求所属的会话,进行一次激活会话操作,以确保当前会话处于激活状态,完成会话激活后,则提交请求至处理器.
- 13.创建请求事务头
- 对于事务请求,Zookeeper会为其创建请求事务头,服务端后续的请求处理器都是基于该请求头来识别当前请求是否是事务请求,请求事务头包含了一个事务请求最基本的一些信息,包括sessionID、ZXID(事务请求对应的事务ZXID)、CXID(客户端的操作序列)和请求类型(如create、delete、setData、createSession等)等.
- 14.创建请求事务体
- 由于此时是会话创建请求,其事务体是CreateSessionTxn.
- 15.注册于激活会话
- 处理由非Leader服务器转发过来的会话创建请求.
- 12.将请求交给PrepRequestProcessor处理器处理
- 事务处理
- 16.将请求交给
ProposalRequestProcessor处理器- 与提议相关的处理器,从ProposalRequestProcessor开始,请求的处理将会进入三个子处理流程,分别是Sync流程、Proposal流程、Commit流程.

Sync流程- 使用
SyncRequestProcessor处理器记录事务日志,针对每个事务请求,都会通过事务日志的形式将其记录,完成日志记录后,每个Follower都会向Leader发送ACK消息,表明自身完成了事务日志的记录,以便Leader统计每个事务请求的投票情况.
- 使用
Proposal流程- 每个事务请求都需要集群中过半机器投票认可才能被真正应用到内存数据库中,这个投票与统计过程就是Proposal流程.
- 发起投票: 如果当前请求是事务请求,Leader会发起一轮事务投票,在发起事务投票之前,会检查当前服务端的ZXID是否可用.
- 生成提议Proposal: 如果ZXID可用,Zookeeper会将已创建的请求头和事务体以及ZXID和请求本身序列化到Proposal对象中,此Proposal对象就是一个提议.
- 广播提议: Leader以ZXID作为标识,将该提议放入投票箱outstandingProposals中,同时将该提议广播给所有Follower.
- 收集投票: Follower接收到Leader提议后,进入Sync流程进行日志记录,记录完成后,发送ACK消息至Leader服务器,Leader根据这些ACK消息来统计每个提议的投票情况,当一个提议获得半数以上投票时,就认为该提议通过,进入Commit阶段. - 将请求放入toBeApplied队列中 - 广播Commit消息: Leader向Follower和Observer发送COMMIT消息.向Observer发送INFORM消息,向Leader发送ZXID.
Commit流程- 将请求交付CommitProcessor: CommitProcessor收到请求后,将其放入queuedRequests队列中.
- 处理queuedRequest队列请求: CommitProcessor中单独的线程处理queuedRequests队列中的请求.
- 标记nextPending: 若从queuedRequests中取出的是事务请求,则需要在集群中进行投票处理,同时将nextPending标记位当前请求.
- 等待Proposal投票: 在进行Commit流程的同时,Leader会生成Proposal并广播给所有Follower服务器,此时,Commit流程等待,直到投票结束.
- 投票通过: 若提议获得过半机器认可,则进入请求提交阶段,该请求会被放入commitedRequests队列中,同时唤醒Commit流程.
- 提交请求: 若commitedRequests队列中存在可以提交的请求,那么Commit流程则开始提交请求,将请求放入toProcess队列中,然后交付下一个请求处理器:FinalRequestProcessor.
- 与提议相关的处理器,从ProposalRequestProcessor开始,请求的处理将会进入三个子处理流程,分别是Sync流程、Proposal流程、Commit流程.
- 16.将请求交给
- 事务应用
- 17.交付给
FinalRequestProcessor处理器- FinalRequestProcessor处理器检查outstandingChanges队列中请求的有效性,若发现这些请求已经落后于当前正在处理的请求,那么直接从outstandingChanges队列中移除.
- 18.事务应用
- 之前的请求处理仅仅将事务请求记录到了事务日志中,而内存数据库中的状态尚未改变,因此,需要将事务变更应用到内存数据库.
- 19.将事务请求放入队列commitProposal
- 完成事务应用后,则将该请求放入commitProposal队列中,commitProposal用来保存最近被提交的事务请求,以便集群间机器进行数据的快速同步.
- 17.交付给
- 会话响应
- 20.统计处理
- Zookeeper计算请求在服务端处理所花费的时间,统计客户端连接的基本信息,如lastZxid(最新的ZXID)、lastOp(最后一次和服务端的操作)、lastLatency(最后一次请求处理所花费的时间)等.
- 21.创建响应ConnectResponse
- 会话创建成功后的响应,包含了当前客户端和服务端之间的通信协议版本号、会话超时时间、sessionID和会话密码.
- 22.序列化ConnectResponse
- 23.I/O层发送响应给客户端
- 20.统计处理
8.2 SetData请求
服务端对于SetData请求大致可以分为四步,预处理、事务处理、事务应用、请求响应.
- 预处理
- I/O层接收来自客户端的请求
- 判断是否是客户端"会话创建"请求
- 对于SetData请求,按照正常事务请求进行处理
- 将请求交给PrepRequestProcessor处理器进行处理
- 创建请求事务头
- 会话检查
- 检查该会话是否有效.
- 反序列化请求,并创建ChangeRecord记录
- 反序列化并生成特定的SetDataRequest请求,请求中包含了数据节点路径path、更新的内容data和期望的数据节点版本version.同时根据请求对应的path,Zookeeper生成一个ChangeRecord记录,并放入outstandingChanges队列中
- ACL检查
- 检查客户端是否具有数据更新的权限.
- 数据版本检查
- 通过version属性来实现乐观锁机制的写入校验.
- 创建请求事务体SetDataTxn
- 保存事务操作到outstandingChanges队列中
- 事务处理
- 对于事务请求,服务端都会发起事务处理流程.所有事务请求都是由
ProposalRequestProcessor处理器处理,通过Sync、Proposal、Commit三个子流程相互协作完成
- 对于事务请求,服务端都会发起事务处理流程.所有事务请求都是由
- 事务应用
- 交付给
FinalRequestProcessor处理器 - 事务应用
- 将请求事务头和事务体直接交给内存数据库ZKDatabase进行事务应用,同时返回ProcessTxnResult对象,包含了数据节点内容更新后的stat
- 将事务请求放入commitProposal队列
- 交付给
- 请求响应
- 创建响应体SetDataResponse
- 其包含了当前数据节点的最新状态stat
- 创建响应头
- 包含当前响应对应的事务ZXID和请求处理是否成功的标识.
- 序列化响应
- I/O层发送响应给客户端
- 创建响应体SetDataResponse
8.3 事务请求转发
Zookeeper中的事务请求转发机制:所有非Leader服务器如果接收到了来自客户端的事务请求,那么必须将其转发给Leader服务器处理
第一个请求处理器FollowerRequestProcessor和ObserverRequestProcessor都会检查当前请求是否是事务请求,如果是事务请求,就会将该客户端请求已REQUEST消息的形式转发给Leader服务器
8.4 GetData请求
服务端对于GetData请求的处理,大致分为三步,预处理、非事务处理、请求响应.
- 预处理
- I/O层接收来自客户端的请求
- 判断是否是客户端"会话创建"请求
- 将请求交给PrepRequestProcessor处理器进行处理
- 会话检查
- 非事务处理
- 反序列化GetDataRequest请求
- 获取数据节点
- ACL检查
- 获取数据内容和stat,注册Watcher
- 请求响应
- 创建响应体GetDataResponse
- 响应体包含当前数据节点的内容和状态stat
- 创建响应头
- 统计处理
- 序列化响应
- I/O层发送响应给客户端
- 创建响应体GetDataResponse
9.数据与存储
9.1 内存数据
Zookeeper的数据模型是树结构,在内存数据库中,存储了整棵树的内容,包括所有的节点路径、节点数据、ACL信息,Zookeeper会定时将这个数据存储到磁盘上.
- DataTree
- DataTree是内存数据存储的核心,是一个树结构,代表了内存中一份完整的数据.DataTree不包含任何与网络、客户端连接及请求处理相关的业务逻辑,是一个独立的组件.底层的数据结构其实是一个典型的
ConcurrentHashMap
- DataTree是内存数据存储的核心,是一个树结构,代表了内存中一份完整的数据.DataTree不包含任何与网络、客户端连接及请求处理相关的业务逻辑,是一个独立的组件.底层的数据结构其实是一个典型的
- DataNode
- DataNode是数据存储的最小单元,其内部除了保存了结点的数据内容、ACL列表、节点状态之外,还记录了父节点的引用和子节点列表两个属性,其也提供了对子节点列表进行操作的接口.
- ZKDatabase
- Zookeeper的内存数据库,管理Zookeeper的所有会话、DataTree存储和事务日志.ZKDatabase会定时向磁盘dump快照数据,同时在Zookeeper启动时,会通过磁盘的事务日志和快照文件恢复成一个完整的内存数据库.
9.2 事务日志
文件存储
在配置Zookeeper集群时需要配置dataDir目录,其用来存储事务日志文件.也可以为事务日志单独分配一个文件存储目录:dataLogDir. 若配置dataLogDir为/home/admin/zkData/zk_log,那么Zookeeper在运行过程中会在该目录下建立一个名字为version-2的子目录,该目录确定了当前Zookeeper使用的事务日志格式版本号,当下次某个Zookeeper版本对事务日志格式进行变更时,此目录也会变更,即在version-2子目录下会生成一系列文件大小一致(64MB)的文件.
日志格式
在配置好日志文件目录,启动Zookeeper后,完成如下操作
- 创建/test_log节点,初始值为v1
- 更新/test_log节点的数据为v2
- 创建/test_log/c节点,初始值为v1
- 删除/test_log/c节点
经过四步操作后,会在/log/version-2/目录下生成一个日志文件log.cec.
将Zookeeper下的zookeeper-3.4.6.jar和slf4j-api-1.6.1.jar复制到/log/version-2目录下,使用如下命令打开log.cec文件.
java -classpath ./zookeeper-3.4.6.jar:./slf4j-api-1.6.1.jar org.apache.zookeeper.server.LogFormatter log.cec
ZooKeeper Transactional Log File with dbid 0 txnlog format version 2 .是文件头信息,主要是事务日志的DBID和日志格式版本号.
…session 0x159…0xcec createSession 30000.表示客户端会话创建操作.
…session 0x159…0xced create ‘/test_log,… .表示创建/test_log节点,数据内容为#7631(v1).
…session 0x159…0xcee setData ‘/test_log,….表示设置了/test_log节点数据,内容为#7632(v2).
…session 0x159…0xcef create ’/test_log/c,….表示创建节点/test_log/c.
…session 0x159…0xcf0 delete ‘/test_log/c.表示删除节点/test_log/c.
日志写入
FileTxnLog负责维护事务日志对外的接口,包括事务日志的写入和读取等.Zookeeper的事务日志写入过程大体可以分为如下6个步骤.
- 确定是否有事务日志可写.当Zookeeper服务器启动完成需要进行第一次事务日志的写入,或是上一次事务日志写满时,都会处于与事务日志文件断开的状态,即Zookeeper服务器没有和任意一个日志文件相关联.因此在进行事务日志写入前,Zookeeper首先会判断FileTxnLog组件是否已经关联上一个可写的事务日志文件.若没有,则会使用该事务操作关联的ZXID作为后缀创建一个事务日志文件,同时构建事务日志的文件头信息,并立即写入这个事务日志文件中去,同时将该文件的文件流放入streamToFlush集合,该集合用来记录当前需要强制进行数据落盘的文件流.
- 确定事务日志文件是否需要扩容(预分配).Zookeeper会采用磁盘空间预分配策略.当检测到当前事务日志文件剩余空间不足4096字节时,就会开始进行文件空间扩容,即在现有文件大小上,将文件增加65536KB(64MB),然后使用"0"填充被扩容的文件空间.
- 事务序列化.对事务头和事务体的序列化,其中事务体又可分为会话创建事务、节点创建事务、节点删除事务、节点数据更新事务等.
- 生成Checksum.为保证日志文件的完整性和数据的准确性,Zookeeper在将事务日志写入文件前,会计算生成Checksum.
- 写入事务日志文件流.将序列化后的事务头、事务体和Checksum写入文件流中,此时并为写入到磁盘上.
- 事务日志刷入磁盘.由于步骤5中的缓存原因,无法实时地写入磁盘文件中,因此需要将缓存数据强制刷入磁盘.
日志截断
在Zookeeper运行过程中,可能出现非Leader记录的事务ID比Leader上大,这是非法运行状态.此时,需要保证所有机器必须与该Leader的数据保持同步,即Leader会发送TRUNC命令给该机器,要求进行日志截断,Learner收到该命令后,就会删除所有包含或大于该事务ID的事务日志文件.
9.3 snapshot——数据快照
数据快照是Zookeeper数据存储中非常核心的运行机制,数据快照用来记录Zookeeper服务器上某一时刻的全量内存数据内容,并将其写入指定的磁盘文件中.
文件存储
与事务文件类似,Zookeeper快照文件也可以指定特定磁盘目录,通过dataDir属性来配置.若指定dataDir为/home/admin/zkData/zk_data,则在运行过程中会在该目录下创建version-2的目录,该目录确定了当前Zookeeper使用的快照数据格式版本号.在Zookeeper运行时,会生成一系列文件.
数据快照
FileSnap负责维护快照数据对外的接口,包括快照数据的写入和读取等,将内存数据库写入快照数据文件其实是一个序列化过程.针对客户端的每一次事务操作,Zookeeper都会将他们记录到事务日志中,同时也会将数据变更应用到内存数据库中,Zookeeper在进行若干次事务日志记录后,将内存数据库的全量数据Dump到本地文件中,这就是数据快照.其步骤如下
- 确定是否需要进行数据快照.每进行一次事务日志记录之后,Zookeeper都会检测当前是否需要进行数据快照,考虑到数据快照对于Zookeeper机器的影响,需要尽量避免Zookeeper集群中的所有机器在同一时刻进行数据快照.采用过半随机策略进行数据快照操作.
- 切换事务日志文件.表示当前的事务日志已经写满,需要重新创建一个新的事务日志.
- 创建数据快照异步线程.创建单独的异步线程来进行数据快照以避免影响Zookeeper主流程.
- 获取全量数据和会话信息.从ZKDatabase中获取到DataTree和会话信息.
- 生成快照数据文件名.Zookeeper根据当前已经提交的最大ZXID来生成数据快照文件名.
- 数据序列化.首先序列化文件头信息,然后再对会话信息和DataTree分别进行序列化,同时生成一个Checksum,一并写入快照数据文件中去.
9.4 初始化
在Zookeeper服务器启动期间,首先会进行数据初始化工作,用于将存储在磁盘上的数据文件加载到Zookeeper服务器内存中.
初始化流程

数据的初始化工作是从磁盘上加载数据的过程,主要包括了从快照文件中加载快照数据和根据实物日志进行数据修正两个过程.
- 1.初始化FileTxnSnapLog
- FileTxnSnapLog是Zookeeper事务日志和快照数据访问层,用于衔接上层业务和底层数据存储,底层数据包含了事务日志和快照数据两部分.FileTxnSnapLog中对应FileTxnLog和FileSnap.
- 2.初始化ZKDatabase
- 首先构建DataTree,同时将FileTxnSnapLog交付ZKDatabase,以便内存数据库能够对事务日志和快照数据进行访问.在ZKDatabase初始化时,DataTree也会进行相应的初始化工作,如创建一些默认结点,如/、/zookeeper、/zookeeper/quota三个节点.
- 3.创建PlayBackListener
- 其主要用来接收事务应用过程中的回调,在Zookeeper数据恢复后期,会有事务修正过程,此过程会回调PlayBackListener来进行对应的数据修正.
- 4.处理快照文件
- 此时可以从磁盘中恢复数据了,首先从快照文件开始加载.
- 5.获取最新的100个快照文件
- 更新时间最晚的快照文件包含了最新的全量数据.
- 6.解析快照文件
- 逐个解析快照文件,此时需要进行反序列化,生成DataTree和sessionsWithTimeouts,同时还会校验Checksum及快照文件的正确性.对于100个快找文件,如果正确性校验通过时,通常只会解析最新的那个快照文件.只有最新快照文件不可用时,才会逐个进行解析,直至100个快照文件全部解析完.若将100个快照文件解析完后还是无法成功恢复一个完整的DataTree和sessionWithTimeouts,此时服务器启动失败.
- 7.获取最新的ZXID
- 此时根据快照文件的文件名即可解析出最新的ZXID:zxid_for_snap.该ZXID代表了Zookeeper开始进行数据快照的时刻.
- 8.处理事务日志
- 此时服务器内存中已经有了一份近似全量的数据,现在开始通过事务日志来更新增量数据.
- 9.获取所有zxid_for_snap之后提交的事务
- 此时,已经可以获取快照数据的最新ZXID.只需要从事务日志中获取所有ZXID比步骤7得到的ZXID大的事务操作.
- 10.事务应用
- 获取大于zxid_for_snap的事务后,将其逐个应用到之前基于快照数据文件恢复出来的DataTree和sessionsWithTimeouts.每当有一个事务被应用到内存数据库中后,Zookeeper同时会回调PlayBackListener,将这事务操作记录转换成Proposal,并保存到ZKDatabase的committedLog中,以便Follower进行快速同步.
- 11.获取最新的ZXID
- 待所有的事务都被完整地应用到内存数据库中后,也就基本上完成了数据的初始化过程,此时再次获取ZXID,用来标识上次服务器正常运行时提交的最大事务ID.
- 12.校验epoch
- epoch标识了当前Leader周期,集群机器相互通信时,会带上这个epoch以确保彼此在同一个Leader周期中.完成数据加载后,Zookeeper会从步骤11中确定ZXID中解析出事务处理的Leader周期:epochOfZxid.同时也会从磁盘的currentEpoch和acceptedEpoch文件中读取上次记录的最新的epoch值,进行校验.
9.5 数据同步
整个集群完成Leader选举后,Learner会向Leader进行注册,当Learner向Leader完成注册后,就进入数据同步环节,同步过程就是Leader将那些没有在Learner服务器上提交过的事务请求同步给Learner服务器

获取Learner状态
- 在注册Learner的最后阶段,Learner服务器会发送给Leader服务器一个ACKEPOCH数据包,Leader会从这个数据包中解析出该Learner的currentEpoch和lastZxid.
数据同步初始化
在开始数据同步之前,Leader服务器会进行数据同步初始化.首先从Zookeeper内存数据库中提取出事务请求对应的提议缓存队列proposals,同时完成以下三个ZXID值的初始化
peerLastZxid: 该Learner最后处理的ZXID)minCommittedLog: Leader提议缓存队列commitedLog中最小的ZXID)maxCommittedLog: Leader提议缓存队列commitedLog中的最大ZXID)三个ZXID值的初始化
对于集群数据同步而言,通常分为四类
- 直接差异化同步(DIFF同步)
- 先回滚再差异化同步(TRUNC+DIFF同步)
- 仅回滚同步(TRUNC同步)
- 全量同步(SNAP同步)
在初始化阶段,Leader会优先以全量同步方式来同步数据.同时,会根据Leader和Learner之间的数据差异情况来决定最终的数据同步方式.
- 直接差异化同步
- 场景: DIFF同步,peerLastZxid介于minCommittedLog和maxCommittedLog之间
- Leader首先向这个Learner发送一个DIFF指令,用于通知Learner进入差异化数据同步阶段,Leader即将把一些Proposal同步给自己,针对每个Proposal,Leader都会通过发送PROPOSAL内容数据包和COMMIT指令数据包来完成,
- 先回滚再差异化同步
- 场景: TRUNC+DIFF同步,Leader已经将事务记录到本地事务日志中,但是没有成功发起Proposal流程
- 当Leader发现某个Learner包含了一条自己没有的事务记录,那么就需要该Learner进行事务回滚,回滚到Leader服务器上存在的,同时也是最接近于peerLastZxid的ZXID.
- 仅回滚同步
- 场景: TRUNC同步,peerLastZxid大于maxCommittedLog
- Leader要求Learner回滚到ZXID值为maxCommittedLog对应的事务操作.
- 全量同步
- 场景: SNAP同步,peerLastZxid小于minCommittedLog或peerLastZxid不等于lastProcessedZxid
- Leader无法直接使用提议缓存队列和Learner进行同步,因此只能进行全量同步.Leader将本机的全量内存数据同步给Learner.Leader首先向Learner发送一个SNAP指令,通知Learner即将进行全量同步,随后,Leader会从内存数据库中获取到全量的数据节点和会话超时时间记录器,将他们序列化后传输给Learner.Learner接收到该全量数据后,会对其反序列化后载入到内存数据库中.