本文为书籍《从Paxos到Zookeeper 分布式一致性原理与实践》倪超著_北京:电子工业出版社的读书笔记,这本书还是蛮值得推荐的。
zookeeper 服务端对于会话创建的处理,大体可以分为请求接收、会话创建、预处理、事务处理、事务应用和会话响应6个大环节。
大体流程图如图:
一、请求接收
1. I/O层接收来自客户端的请求
在ZooKeeper中,NIOServerCnxn实例维护每一个客户端连接,客户端与服务端的所有通信都是由NIOServerCnxn负责的----其负责统一接收来自客户端的所有请求,并将请求内容从底层网络I/O中完整地读取出来。
2.判断是否是客户端“会话创建”请求
NIOServerCnxn在负责网络通信的同时,也承担了客户端会话的载体------每个会话都会对应一个NIOServerCnxn实体。
对于每一次请求ZooKeeper都会检查当前NIOServerCnxn实体是否已经被初始化。
如果没有初始化,那么就认为该客户端请求一定是“会话创建”请求。
3.反序列化ConnectRequest实体
一旦确定当前客户端请求是“会话创建”请求,那么服务端就可以对其进行反序列化,并生成一个ConnectRequest请求实体。
4.判断是否是ReadOnly客户端
如果当前ZooKeeper服务器是以readOnly模式启动的,那么所有来自非readOnly型客户端的请求将无法被处理。
5.检查客户端ZXID
正常情况下,同一个ZooKeeper集群中,服务端的ZXID必定大于客户端的ZXID,因此如果发现客户端的ZXID值大于服务端的ZXID值,那么服务端将不接受该客户端的“会话创建”请求。
6.协商SessionTimeout
客户端在构建ZooKeeper实例的时候,会有一个SessionTimeout参数用于指定会话的超时时间。
客户端向服务器发送这个超时时间后,服务器会根据自己的超时时间限制最终确定该会话的超时时间------这个过程就是sessionTimeout协商过程。
默认情况下,ZooKeeper服务端对超时时间的限制介于2个tickTime到20个tickTime之间。
即如果我们设置tickTime值为2000毫秒的话,那服务端就是限制客户端的超时时间,使之介于4秒到40秒之间。
我们可以通过配置服务端的配置文件zoo.cfg中的相关配置来修改这个值。
7.判断是否需要重新创建会话
服务端会根据客户端请求中是否包含了sessionID来判断客户端是否需要重新创建会话。
如果客户端请求中已经包含了sessionID,那么就认为该客户端正在进行会话重连。在这种情况下,服务端只需要重新打开这个会话,否则需要重新创建。
二、会话创建
8. 为客户端生成sessionID
每个ZooKeeper服务器在启动的时候,都会初始化一个会话管理器(SessionTracker),同时初始化sessionID,我们将其称为“基准SessionID”。因此针对每个客户端,只需要在这个“基准sessionID”的基础上进行逐个递增就可以了。
9.注册会话
向SessionTracker中注册会话。
SessionTracker维护了两个重要的数据结构:sessionsWithTimeout和sessionsById
前者根据SessionID保存了所有会话的超时时间,后者根据SessionID保存了所有会话实体
10. 激活会话
向SessionTracker注册完会话后,会通过分桶策略激活会话。
分桶策略请看https://blog.csdn.net/jy02268879/article/details/107495971 中的3.1
11. 生成会话密码
服务端在创建一个客户端会话的时候,会同时为客户端生成一个会话密码,连同sessionID一起发给客户端,作为会话在集群中不同机器间转移的凭证。
三、预处理
12. 将请求交给ZooKeeper的PrepRequestProcessor处理器进行处理
ZooKeeper对于每个客户端请求的处理模型采用了典型的责任链模式。
在提交给第一个处理器之前,ZooKeeper还会根据该请求所属的会话,进行一次激活会话的操作,以确保当前会话处于激活状态。
13.创建请求事务头
对于事务请求,ZooKeeper首先会为其创建请求事务头,服务端后续的请求处理器都是基于该请求头来识别当前请求是否是事务请求。
14. 创建请求事务体
15. 注册与激活会话
和上面第九步中说的是一致的。
虽然重复,但不会引起额外的问题。
此次进行会话注册和激活的目的是处理由非Leader服务器转发过来的会话创建请求。在这种情况下,其实尚未在Leader的SessionTracker中进行会话的注册,因此需要在这里进行一次注册和激活。
四、事务处理
16. 将请求交给ProposalRequestProcessor处理器
完成对请求的预处理后,PreRequestProcessor处理器会将请求交付给自己的下一级处理器:ProposalRequestProcessor。
ProposalRequestProcessor处理器是一个与提案相关的处理器。所谓的提案,是ZooKeeper中针对事务请求所展开的一个投票流程中对事务操作的包装。
从ProposalRequestProcessor处理器开始,请求的处理将会进入三个子处理流程:sync流程、Proposal流程、Commit流程
sync流程
核心就是使用SyncRequestProcessor处理器记录日志的过程。
Leader服务器和Follower服务器的请求处理链中都会有这个处理器,两者在事务日志的记录功能上是完全一致的。
完成事务日志记录后,每个Follower服务器都会向Leader服务器发送ACK消息,表明自身完成了事务日志的记录,以便Leader服务器统计每个事务请求的投票情况。
Proposal流程
在ZooKeeper的实现中,每一个事务请求都需要集群中过半机器投票认可才能被真正应用到ZooKeeper的内存数据库中去,这个投票与统计过程被称为“Proposal流程”。
1.发起投票
如果当前请求是事务请求,那么Leader服务器就会发起一轮事务投票。在发起事务投票之前,首先会检查当前服务端的ZXID是否可用。如果不可用,那么抛出异常XidRolloverException
2.生成提议Proposal
如果当前服务器的ZXID可用,那么就开始事务投票了。ZooKeeper会将之前创建的请求头和事务体,以及ZXID和请求本身序列化到Proposal对象中
3.广播提议
生成Proposal对象后,Leader服务器会以ZXID作为标识,将该提议放入投票箱outstandingProposal中,同时会将该提议广播给所有的Follower服务器
4.收集投票
Follower服务器在接收到Leader服务器发来的这个提以后,会进入Sync流程来进行事务和日志的记录,日志记录完成后就会返送ACK消息给Leader服务器,Leader服务器根据这些ACK消息来统计每个提议的投票情况。
当一个提议获得了集群中过半机器的投票,那么就认为该提议通过,接下去就可用进入提议的Commit阶段了。
5.将请求放入toBeApplied队列
在该提议被提交之前,ZooKeeper首先会将其放入toBeApplied队列中。
6.广播Commit消息
一旦ZooKeeper确认一个提案可用被提交了,那么Leader服务器就会向Follower和Observer服务器发起Commit消息,以便所有服务器都能提交该提议。
由于Observer服务器并不参与投票,所以它尚未保存关于该提议的信息,所以在广播的时候要区别对待,Leader向Observer发送一种被称为”INFORM“的消息,该消息体中包含了当前提议的内容。
而Leader向Follower只需要发送ZXID即可。
Commit流程
1.将请求交付给CommitProcessor处理器
CommitProcessor处理器在收到请求后并不会立即处理,而是将其放到queuedRequests队列中去。
2.处理queueRequests队列请求
CommitProcessor处理器有一个单独的线程来处理上一级处理器流转下来的请求。
当检查到queuedRequests队列中有新的请求进来,就会逐个从队列中取出请求进行处理。
3.标记nextPending
如果从queuedRequests队列中取出的请求是个事务请求,那么就需要进行集群中各服务器之间的投票处理,同时需要将nextPending标记为当前请求。标识nextPending的作用,一方面是为了确保事务请求的顺序性,一方面也便于CommitProcessor处理器检查当前集群中是否正在进行事务请求的投票
4.等待Proposal投票
在Commit流程处理的同时,Leader已经根据当前事务请求生成了一个提议Proposal,并广播给了所有的Follower服务器。
因此在这个时候,Commit流程需要等待,直到投票结束。
5.投票通过
如果一个提议已经获得了过半机器的认可,那么将会进入请求提交阶段。
ZooKeeper会将该请求放入committedRequests队列中,同时唤醒Commit流程。
6.提交请求
一旦发现committedRequests队列中已经有可以提交的请求了,那么commit流程就会开始提交请求。
在提交之前为了保障事务请求的顺序执行,commit流程还会对比之前标记的nextPending和commitedRequests队列中第一个请求是否一致。如果检查通过,那么commit流程就会将该请求放入toProcess队列中,然后交付给下一个请求处理器FinalRequestProcessor
五、事务应用
17. 交付给FinalRequestProcessor处理器
请求流转到FinalRequestProcessor处理器后,也就接近请求处理的尾声了。
FinalRequestProcessor处理器会首先检查outstandingChanges队列中请求的有效性,如果发现这些请求已经落后于当前正在处理的请求,那么直接从outstandingChanges队列中移除。
18. 事务应用
在之前的请求处理逻辑中,我们仅仅是将该事务请求记录到了事务日志中去,而内存数据库中的状态尚未变更。
在这步骤就是要将事务变更应用到内存数据库中。
注意:对于“会话创建”这类事务请求,ZooKeeper做了特殊处理,会话管理都是由SessionTracker负责的,而会话创建在第九步ZooKeeper就已经将会话信息注册到了SessionTracker中,因此在此处无须对内存数据库做任何处理,只需要再次向SessionTracker进行会话注册即可。
19. 将事务请求放入队列:commitProposal
一旦完成事务请求的内存数据库应用,就可以将该请求放入commitProposal队列中。
commitProposal列队是用来保存最近被提交的事务请求,以便集群间机器进行数据的快速同步。
六、会话响应
20. 统计处理
至此,客户端“会话创建”请求已经从ZooKeeper请求处理链路上的所有请求处理器间完成了流转。
到这一步,ZooKeeper会记录请求在服务器处理所花费的时间,统计客户端连接的一些基本信息(最新的ZXID:lastZxid、最后一次和服务端的操作:lastOp、最后一次请求处理所花费的时间:lastLatency等)
21. 创建响应ConnectResponse
ConnectResponse就是一个会话创建成功后的响应,包含了当前客户端与服务端之间的通信协议版本号protocolVersion、会话超时时间、sessionID和会话密码。
22. 序列化Connectresponse
23. I/O层发送响应给客户端