Zookeeper ZAB协议
这篇博客是从源码的角度了解Zookeeper 从接收客户端请求开始,到返回数据为止,有很多涉及到的对象创建因为在前几篇文章已经说明过了,这里就不再重复的说明了,如果不是很明白的的,可以先看前几篇博文了解一下,先了解一下整体架构,对整个架构图有清晰的认识后,再带着线程流转模型去看源码感觉效率会有比较大的提升
另外因为zk 中大量的使用了线程和队列,代码相对来说比较绕,所以我这里画了一张整体的线程队列的流程图,便于理清思路,建议看博文或者源码的时候基于这张图看,便于理清线程队列之间的关系
在QuorumPeer
启动的时候,ZK 会启动一个socket 来接收客户端的信息,这个socket 会有两种,ZK 默认的是NIO, 但是可以在启动的时候配置参数-Dzookeeper.serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory
的方式指定使用netty
Leader节点
QuorumPeer#start
QuorumPeer#startServerCnxnFactory
因为Client 源码分析我们使用的是NIO 的方式,这里我们就用Netty 进行解析,这里看一下构造函数和start 方法,见start
NettyServerCnxnFactory
这个代码是比较清晰的了,NettyServerCnxnFactory
的构造函数中很经典的使用了netty
, 并且在pipline
中放入了一个名称为servercnxnfactory
的Handler, 这个Handler 是CnxnChannelHandler
对象,在接收客户端的数据都会经过这个handler,然后在start 方法中启动netty
CnxnChannelHandler
是数据接收的入口,我们重点分析一下这个,详情见CnxnChannelHandler
CnxnChannelHandler
CnxnChannelHandler 中包含了多个方式,包括读,写,心跳事件,连接事件,断开连接等,这里我们暂时只分析
读事件channlRead
,见channelRead
写事件write
CnxnChannelHandler#channelRead
在netty
中处理每一个socket 数据都会走pipline
中Handler的channelRead
方法,在channelRead
通过processMessage
进行处理,见processMessage
NettyServerCnxn#processMessage
NettyServerCnxn#receiveMessage
因为只关注主流程,所以折叠了一些代码,在分析客户端的源码的时候,了解到zk 客户端是将数据封装成Packet,然后序列化后发送给服务端,processPacket
方法就是开始正式解析数据了,见processPacket
ZookeeperServer#processPacket
在执行submitRequest
方法的时候,数据开始进入Processor 调用链,这个调用链在上一篇文章中讲过,他的创建就不进行具体描述,leader 和follower 都有自己调用链,下面来看一下submitRequest
,见submitRequest
ZookeeperServer#submitRequest
submitRequest
在上一篇博客中已经分析过了processor 生成时机和排列顺序,这里就不再进行重复说明
LeaderRequestProcessor#processRequest
根据调用链,这里的nextPricessor
是PrepRequestProcessor
,详情见PrepRequestProcessor#processRequest
PrepRequestProcessor#processRequest
PrepRequestProcessor#processRequest
这里很简单,会将request 请求数据放入到阻塞队列中,这里添加到队列中的数据会在什么地方被消费的,根据processor
调用链或者博客开始的流转图,可以很容易的看出来同样是被PrepRequestProcessor
消费了,PrepRequestProcessor
是个线程,在LeaderZookeeperServer
初始化已经启动,可见submitRequest的setupRequestProcessor
方法,所以我们来看一下PreRequestProcessor
的run 方法执行了哪些逻辑,见PrepRequestProcessor#processRequest
PrepRequestProcessor#run
可以看到PrepRequestProcessor
的run方法中从submittedRequests
中获取了数据,然后执行了pRequest
方法进行处理,见PrepRequestProcessor#pRequest
PrepRequestProcessor#pRequest
客户端每种类型,在zk服务端都有一种类型与之匹配,这里我们就直接分析create 类型,最终我们会调用到pRequest2TxnCreate
方法,见PrepRequestProcessor#pRequest2TxnCreate
在执行完创建事务以后,会执行nextProcessor
,这个Processor 是ProposalRequestProcessor
,见ProposalRequestProcessor#processRequest
PrepRequestProcessor#pRequest2TxnCreate
PrepRequestProcessor#pRequest2TxnCreate
ProposalRequestProcessor#processRequest
ProposalRequestProcessor#processRequest
总结一下
1:执行下一个Processor 的方法,将request 放入到一个队列中,下一个Processor 是CommitProcessor
, 是一个线程,在run方法中处理数据CommitProcessor#processRequest
2: 找到所有和leader 连接的follower, 然后将数据放入到每一个follower 对应的队列中,由队列对应的线程进行处理Leader#propose
3: 执行syncprocessor
的方法,将请求数据放入到队列中,由syncprocessor
是一个线程,在syncprocessor
的run 方法中处理数据,见SyncRequestProcessor#processRequest
CommitProcessor#processRequest
CommitProcessor#processRequest
这里将请求数据放入到队列,在CommitProcessor#run
方法中消费这个数据,然后wekeup
唤醒CommitProcessor
线程,见CommitProcessor#run
CommitProcessor#run
总结一下:
1: 在CommitProcessor
线程刚启动的时候,队列中不存在数据,因此线程被阻塞
2: 当zk 服务端接收到数据,并且最终把请求数据放入到queueRequests
,并且唤醒了CommitProcessor
线程,线程从queueRequests
中获得数据,并且放入到等待commit 的队列中,这时候processCommitted
方法中if 判断暂时还不满足条件,直接结束
3: 这时候因为queueRequests
队列中数据被消费重新为空,因此当前线程重新处于wait 的状态
Leader#propose
propose 方法主要是构造了Proposal 数据,然后准备发送给各个Follower,见Leader#sendPacket
Leader#sendPacket
从LeaderZookeeperServer
启动的时候,就创建了一个socket 和Follower,Observer 连接,并且根据连接每个节点都创建了一个LearnerHandler
线程进行处理,每个线程里面都维护了一个阻塞队列,见LearnerHandler#queuePacket
LearnerHandler#queuePacket
同样的LearnerHandler
中的队列,由LearnerHandler
线程自己消费,来看一下他的run 方法,见LearnerHandler#queuePacket
LearnerHandler#run
因为run 方法中代码较多,只粘贴部分关键代码startSendingPackets
,这是直接发送packet 数据直接进入startSendingPackets
,见startSendingPackets
LearnerHandler#startSendingPackets
这里我们发现,每次发送数据的时候都新建了一个线程用于数据发送,整体的线程架构图,还是建议看博客顶部的图作为对照,线程中调用sendPacket
方法发送数据,见LearnerHandler#sendPacket
LearnerHandler#sendPacket
LearnerHandler#sendPacket
这里发送的数据就是ProposalRequestProcessor#processRequest中执行zks.getLeader().propose(request)
添加到队列中的数据,经过jute 序列化后发送
SyncRequestProcessor#processRequest
SyncRequestProcessor#processRequest
SyncRequestProcessor
对象在上一篇博文中介绍过,在LeaderZookeeperServer
初始化的时候设置了ProposalRequestProcessor
,在ProposalRequestProcessor
构造函数中创建了SyncRequestProcessor
对象, 这里又出现了一个queueRequests
队列,这个队列专属于SyncRequestProcessor
,就是架构图中的队列3, 看一下这个类的run 方法,见SyncRequestProcessor#run
SyncRequestProcessor#run
SyncRequestProcessor#run
在run 方法中会将队列中的数据写入到log 日志文件中, 见SyncRequestProcessor#flush
SyncRequestProcessor#flush
SyncRequestProcessor#flush
这里会执行两点
1: 将当前请求写入到log 日志文件中ZKDatabase#commit
2: 执行下一个processor, 这里leader 和 follower 是不一样的,因为现在是在分析leader 源码,所以现在只分析leader 部分,follower 在follower 解析中进行分析,见AckRequestProcessor#processRequest
ZKDatabase#commit
ZKDatabase#commit
因为代码比较简单,就根据输出流写入到日志文件中,不再进行解析
AckRequestProcessor#processRequest
AckRequestProcessor#processRequest
主节点的数据写入到log 日志以后,也和follower 节点一样参与ack 确认,只有超过半数以上的ack 确认以后,后续才会执行修改内存数据库的数据,看一下具体的执行流程Leader#processAck
Leader#processAck
Leader#processAck
方法中执行了2点
1: 将ack 数据添加到set 列表中
2: leader 节点根据set 中接收到的ack 数据判断是否满足提交要求,如果满足,那么将更改内存数据库中的数据Leader#tryToCommit
Leader#tryToCommit
总结一下:
1: 判断接收ack 的set 队列中接收到的ack 是否已经满足半数,如果不满足直接返回继续等待,如果满足执行后续流程,SyncedLearnerTracker#hasAllQuorums
2: 在ack 满足条件的情况下, 通知所有的follower节点提交,更改节点中的内存数据,Leader#commit
3: 将消息同步给所有的observer 节点Leader#inform
4: 唤醒处于wait的commitProcessor
线程,CommitProcessor#commit
SyncedLearnerTracker#hasAllQuorums
SyncedLearnerTracker#hasAllQuorums
Leader#commit
Leader#inform
CommitProcessor#commit
CommitProcessor#commit
在唤醒commitProcessor
线程以后,实际上就是在上文中的CommitProcessor#run 的run 方法中,执行processCommitted
方法,但是在之前没有进行解析,现在我们解析一下,再把图片拉下来看一下
processCommitted
见processCommitted
CommitProcessor#processCommitted
CommitProcessor#sendToNextProcessor
CommitProcessor#sendToNextProcessor
这里用了线程池的方式进行了一次处理,但是如果不存在线程池,那么直接调用当前线程进行处理, 最后会调用到CommitWorkRequest
的doWork
方法,这里流程比较简单就不过多介绍,来看doWork
,CommitWorkRequest#doWork
CommitWorkRequest#doWork
在doWork
中调用了ToBeAppliedRequestProcessor
的processRequest
方法
ToBeAppliedRequestProcessor#processRequest
在FinalRequestProcessor
中真正的执行了内存数据更改以及构造返回的数据FinalRequestProcessor#processRequest
FinalRequestProcessor#processRequest
FinalRequestProcessor#processRequest
总结一下:
1: 执行Zookeeper
内存数据库的修改Zookeeper#processTxn
2: 根据不同的请求类型构建返回参数
3: 还有getData
等请求的一些监听器的处理,监听器的源码分析等有空的时候补充一下,暂时不在这里解析
4: 给客户端返回数据sendResponse
ZooKeeperServer#processTxn
NettyServerCnxn#sendResponse
在初始化的时候,我们确定了server 服务端是利用了netty
进行通信的,所以这里的连接是NettyServerCnxn
,最后将resp 结果返回给Zookeeper客户端,至此Leader 节点的流程已经完成
Follower 节点
上述Leader 节点已经解析完成了,下面解析Follower 节点,在上一篇博文中解析了Follower 节点初始化,并且创建一个socket 和leader连接,我们从Follower的followLeader
方法开始,readPacket
读取packet 数据,然后processPacket
方法进行处理packet
Follower#processPacket
总结一下:这里根据不同的数据类型走不同的逻辑,我们这里暂时只分析PROPOSAL, COMMIT, 一个是将请求数据写入到log日志中,一个是更新内存数据库数据,在这执行的步骤中又是调用了Processor 调用链, Commit 走上面一条, Proposal 走下面一条,下面我们来进行分析
Proposal 见 logRequest
Commit 见 FollowerZookeeperServer#commit
Proposal
FollowerZookeeperServer#logRequest
这里SyncProcessor
是在FollowerZookeeperServer
初始化设置的SyncRequestProcessor
, 这里的逻辑和Leader节点中的SyncRequestProcessor#processRequest 是一致的,都是将request 添加到一个队列中,然后在SyncRequestProcessor
的run 方法进行消费SyncRequestProcessor#run, 这里就不进行重复解析了,也是将数据写入到log 日志中,然后执行next.processRequest
,可以参照上文的代码,但是在执行了next.processRequest
代码的时候,next 指代和Leader 中的不一样,是指SendAckRequestProcessor
SendAckRequestProcessor#processRequest
Commit
FollowerZookeeperServer#commit
FollowerZookeeperServer#commit
这里commitProcessor
和Leader中的CommitProcessor
是一样的,所以后续执行的逻辑和Leader 节点也是一样的的,建议参考CommitProcessor#run
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南