前面我们已经介绍过了Zookeeper的集群启动、Leader选举、以及客户端一次会话请求的过程,接下来,我们就来看看针对客户端的一次请求,Zookeeper是如何进行处理的。
一、会话创建请求
Zookeeper服务端对于会话创建的处理,大体可以分为请求接收、会话创建、预处理、事务处理、事务应用和会话响应6大环节,其完整流程图如下:
请求接收
1. I/O层接收来自客户端的请求
在Zookeeper中,客户端与服务端的所有通信都是由NIOServerCnxn负责统一接收来自客户端的所有请求,并将请求内容从底层网络I/O中完整读取出来。
2. 判断是否是客户端会话创建请求
对于每个请求,Zookeeper都会检查当前NIOServerCnxn实体是否已经被初始化,如果尚未被初始化,那么就可以确定该客户端请求一定是会话创建请求。
3. 反序列化ConnectRequest请求
一旦确定当前客户端请求是会话创建请求,服务端就可以进行反序列化,并生成一个ConnectRequest请求实体。
4. 判断是否是ReadOnly客户端
如果当前Zookeeper服务器是以ReadOnly模式启动的,那么所有来自非ReadOnly型客户端的请求将无法被处理。
5. 检查客户端ZXID
在正常情况下,同一个Zookeeper集群中,服务端的ZXID必定大于客户端的ZXID,因此如果发现客户端的ZXID值大于服务端的ZXID值,那么服务端将不接受该客户端的会话创建请求。
6. 协商sessionTimeOut
客户端向服务器发送超时时间后,服务器会根据自己的超时时间限制最终确定该会话的超时时间。
7. 判断是否需要重新创建会话
服务端根据客户端请求中是否包含sessionID来判断客户端是否需要重新创建会话,如果客户端请求中包含了sessionID,就认为该客户端正在进行会话重连,在这种情况下,服务端只需要重新打开这个会话,否则需要重新创建。
会话创建
8. 为客户端生成sessionID
在为客户端创建会话之前,服务端首先会为每个客户端都分配一个sessionID,这个sessionID的分配方式是:在每个Zookeeper服务器启动的时候,都会初始化一个会话管理器SessionTracker,同时初始化基准sessionID,针对每个客户端,只需要在基准sessionID基础上逐个递增就可以了。
9. 注册会话
创建会话最重要的工作就是向SessionTracker中注册会话,SessionTracker中维护了两个比较重要的数据结构,分别是sessionWithTimeout和sessionsById,前者根据sessionID保存了所有会话的超时时间,后者根据sessionID保存了所有会话实体。
10. 激活会话
激活会话过程涉及到了我们之前介绍过的分桶策略。
11. 生成会话密码
服务端在创建一个客户端会话的时候,会同时为客户端生成一个会话密码。连同sessionID一起发送给客户端,作为会话在集群中不同机器间转移的凭证,会话密码的生成算法如下:
static final private long superSecret = 0XB3415C00L;Random r = new Random(sessionId ^ superSecret);r.nextBytes(passwd);
预处理
12. 将请求交给PrepRequestProcessor处理器
在提交给第一个请求处理器之前,Zookeeper会根据该请求所属的会话,进行一个激活会话操
作,以确保当前会话处于激活状态,完成激活之后,Zookeeper就会将请求提交给第一个请求处理器PrepRequestProcessor。
13. 创建请求事务头
服务端后续的请求处理器都是基于该请求头来识别当前请求是否是事务请求,请求事务头包含了一个事务请求最基本的一些信息,包括sessionID、ZXID、CXID和请求类型等。
14. 创建请求事务体
对于事务请求,Zookeeper还会为其创建请求事务体,会话创建请求因此会创建事务体CreateSessionTxn。
15. 注册与激活会话
此处进行会话注册与激活的目的是处理所有非Leader服务器转发过来的会话创建请求。
事务处理
16. 将请求交给ProposalRequestProcessor处理器
完成对请求的预处理后,PrepRequestProcessor处理器会将请求交给自己的下一个处理器ProposalRequestProcessor。从ProposalRequestProcessor处理器开始,请求的处理将会进入三个子处理流程,分别是Sync流程、Proposal流程和Commit流程。
Sync流程
Sync流程,核心就是使用SyncRequestProcessor处理器记录事务日志的过程,ProposalRequestProcessor处理器在接收到一个上级处理器流转过来的请求后,首先会判断该请求是否是事务请求,针对每个事务请求,都会通过事务日志的形式将其记录下来。
完成事务日志记录后,每个Follower服务器都会向Leader服务器发送ACK消息,表明自身完成了事务日志的记录,以便Leader服务器统计每个事务请求的投票情况。
Proposal流程
在Zookeeper的实现中,每一个事务请求都需要集群中过半机器投票认可才能被真正应用到Zookeeper的内存数据库中,这个投票与统计过程被称为Proposal流程。
(1)发起投票
如果当前请求是事务请求,那么Leader服务器就会发起一轮事务投票。
(2)生成提议Proposal
Zookeeper会将之前创建的请求头和事务体,以及ZXID和请求本身序列化到Proposal对象中。
(3)广播提议
生成提议后,Leader服务器会以ZXID作为标识,将该提议放入投票箱outstandingProposals中,同时会将该提议广播给所有的Follower服务器。
(4)收集投票
Follower服务器在接收到Leader发来的这个提议后,会进入Sync流程来进行事务日志的记录,一旦日志记录完成,就会发送ACK消息给Leader服务器,Leader服务器统一当一个提议获得了集群中过半机器的投票,该提议就通过了,接下来,就可以进入提议的Commit阶段了。
(5)将请求放入toBeApplied队列
在该提议被提交之前,Zookeeper首先会将其放入到toBeApplied队列中。
(6)广播COMMIT消息
一旦Zookeeper确认了一个提议可以被提交了,那么Leader服务器就会向Follower和Observer服务器发送COMMIT消息,以便所有服务器都能够提交该提议。
Commit流程
(1)将请求交给CommitProcessor处理器
CommitProcessor处理器在收到请求后,将其放入queuedRequests队列中。
(2)处理queuedRequests队列请求
CommitProcessor处理器会有一个单独的线程来处理从上一级处理器流转下来的请求,当检测到queuedRequests队列中已经有新的请求进来,就会逐个从队列中取出请求进行处理。
(3)标记nextPending
如果从queuedRequests队列中取出的请求是一个事务请求,那么就需要进行集群中各服务器之间的投票处理,同时需要将nextPending标记为当前请求,标记nextPending的作用,一方面是为了确保事务请求的顺序性,另一方面也是便于CommitProcessor处理器检测当前集群中是否正在进行事务请求的投票。
(4)等待Proposal投票
在Commit流程处理的同时,Leader已经根据当前事务请求生成了一个提议Proposal,并广播给了所有Follower服务器,因此这时,Commit流程需要等待,直到投票结束。
(5)投票通过
如果一个提议已经获得了过半机器的投票认可,将会进入请求提交阶段,Zookeeper会将该请求放入committedRequests队列中,同时唤醒Commit流程。
(6)提交请求
一旦发现committedRequests队列中已经有可以提交的请求了,Commit流程就会开始提交请求,在提交之前,为了保证事务请求的顺序执行,Commit流程还会对比之前标记的nextPending和committedRequests队列中的第一个请求是否一致。
如果检查通过,那么Commit流程就会将该请求放入toProcess队列中,然后交付给下一个请求处理器FinalRequestProcessor。
事务应用
17. 交付给FinalRequestProcessor处理器
FinalRequestProcessor处理器首先检查outstandingProposals队列中请求的有效性,如果发现这些请求已经落后于当前正在处理的请求,那么直接从outstandingProposals队列中移除。
18. 事务应用
在之前的请求处理逻辑中,我们仅仅是将该事务请求记录到了事务日志中,而内存数据库中的状态尚未变更,因此,在这个环节,需要将事务变更应用到内存数据库中。
19. 将事务请求放入队列commitProposal
commitProposal队列用来保存最近被提交的事务请求,以便集群间集群进行数据的快速同步。
会话响应
客户端请求在经过Zookeeper服务端处理链路的所有请求处理器的处理后,就进入最后的会话响应阶段了。
20. 统计处理
Zookeeper会计算请求在服务端处理所花费的时间,同时还会统计客户端连接的一些基本信息,包括最新的ZXID、lastOp(最后一次和服务端的操作)和lastLatency(最后一次请求处理所花费的时间)等。
21. 创建响应ConnectResponse
ConnectResponse是一个会话创建成功后的响应,包含了当前客户端与服务端之间的通信协议版本号protocolVersion、会话超时时间、sessionID和会话密码。
22. 序列化ConnectResponse
23. I/O层发送响应给客户端
二、setData请求
Zookeeper服务端对于setData请求的处理,大体可以分为请求预处理、事务处理、事务应用和请求响应4大步骤,其完整流程图如下:
预处理
1. I/O层接收来自客户端的请求
2. 判断是否是客户端会话创建请求
对于setData请求,因为此时已经完成了会话创建,因此按照正常的事务请求进行处理。
3. 将请求交给PrepRequestProcessor处理器
4. 创建请求事务头
5. 会话检查
客户端会话检查是指检查会话是否已经超时,如果该会话已经超时,那么服务端就会向客户端抛出SessionExpiredException异常。
6. 反序列化请求,并创建ChangeRecord记录
对于客户端请求,Zookeeper首先会将其进行反序列化并生成特定的setDataRequest请求 ,setDataRequest请求中通常包含了数据节点路径path、更新的数据内容data和期望的数据版本version,同时,根据请求中对应的path,Zookeeper会生成一个ChangeRecord记录,并放入到outstandingChanges队列中。
7. ACL检查
由于当前请求是数据更新请求,因此Zookeeper需要检查该客户端是否具有数据更新的权限。如果没有权限,那么会抛出NoAuthException异常。
8. 数据版本检查
如果Zookeeper服务端发现当前数据内容版本号与客户端预期的版本不匹配的话,会抛出异常。
9. 创建请求事务体SetDataTxn
10. 保存事务操作到outstandingChanges队列中
事务处理
事务处理流程都是由ProposalRequestProcessor处理器发起,通过Sync、Proposal和Commit三个子流程相互协作完成的。
事务应用
11. 交付给FinalRequestProcessor处理器
12. 事务应用
Zookeeper会将请求事务头和事务体直接交给内存数据库ZKDatabase进行事务应用,同时返回ProcessTxnResult对象,包含了数据节点内容更新后的stat。
13. 将事务请求放入队列commitProposal
请求响应
14. 统计处理
15. 创建响应体SetDataResponse
SetDataResponse是一个数据更新成功后的响应,主要包含了当前数据节点的最新状态stat。
16. 创建响应头
响应头是每个请求响应的基本信息,方便客户端对响应进行快速解析,包括当前响应对应的事务ZXID和请求处理是否成功的表示err。
17. 序列化响应
18. I/O层发送响应给客户端
三、事务请求转发
在事务请求的过程中,为了保证事务请求被顺序执行,从而确保Zookeeper集群的数据一致性,所有的事务请求必须由Leader服务器来处理,所有非Leader服务器如果接受到了客户端的事务请求,就将其转发给Leader服务器来处理。
在Follower或是Observer服务器中,第一个请求处理器分别是FollowerRequestProcessor和ObserverRequestProcessor,无论是哪个处理器,都会检查当前请求是否是事务请求,如果是事务请求,就将该客户端的请求以REQUEST消息的形式转发给Leader服务器,Leader服务器在接收到这个消息后,会解析出客户端的原始请求,然后提交到自己的请求处理链中开始进行事务请求处理。
四、getData请求
前面已经介绍了setData的请求处理流程,接下来,我们就来看看getData的请求处理流程,大体可以分为3大步骤,预处理、非事务处理和请求响应,完整流程图如下:
预处理
1. I/O层接收来自客户端的请求
2. 判断是否是客户端会话创建请求
3. 将请求交给PrepRequestProcessor处理器
4. 会话检查
由于getData请求是非事务请求,因此省去了很多事务预处理逻辑,包括创建请求事务头、ChangeRecord和事务体,以及对数据节点版本的检查。
非事务处理
5. 反序列化getDataRequest请求
6. 获取数据节点
根据步骤5反序列化出的getDataRequest对象,包括了数据节点的path和Watcher注册情况,Zookeeper会从内存数据库中获取到该节点及其ACL信息。
7. ACL检查
8. 获取数据内容和stat,注册Watcher
与客户端注册Watcher过程一致。
请求响应
9. 创建响应体getDataResponse
getDataResponse是一个数据获取成功后的响应,主要包含了当前数据节点的内容和状态stat。
10. 创建响应头
11. 统计处理
12. 序列化响应
13. I/O层发送响应给客户端
推荐阅读
Zookeeper开篇介绍
Zookeeper启动流程
Zookeeper的Leader选举机制详解
Zookeeper客户端会话连接流程
看完本文有收获?请转发分享给更多人
关注「并发编程之美」,一起交流Java学习心得