Luckylau's Blog

Zookeeper的原理(4)

​ 本章开始讲解Zookeeper重要的技术实现。上一篇我们讲解了Zookeeper客户端的原理与实现,本篇深入解析Zookeeper服务端的相关原理。

服务端

会话

会话创建流程

上面2个图展示了服务端会话创建处理和事务处理流程图。

初始化阶段

​ 在ZooKeeper中,每一个NIOServerCnxn实例维护一个客户端连接,将请求内容从底层I/O中完整的读取出来。对于每个请求ZooKeeper都会检查当前NIOServerCnxn实体是否已经被初始化。如果尚未被初始化,那么就可以确定该客户单请求一定是“会话创建”请求。很显然,在会话创建初期,NIOServerCnxn尚未得到初始化,因此此时的第一个请求必定是“会话创建”请求。 一旦确定当前客户端请求是“会话创建”请求,那么服务端就可以对其进行反序列化,并生成一个ConnectRequest请求实体。

​ 在ZooKeeper的设计实现中,如果当前ZooKeeper服务器是以ReadOnly模式启动的,那么所有来自非ReadOnly型客户端的请求将无法被处理。因此,针对ConnectRequest,服务器会首先检查其是否是ReadOnly客户端,并以此来决定是否接收该“会话创建”请求。

​ 在正常情况下,同一个ZooKeeper集群中,服务端的ZXID必定大于客户端的ZXID,因此如果发现客户端的ZXID值大于服务端的ZXID值,那么服务端将不接受该客户端的“会话创建”请求。

​ 客户端在构造ZooKeeper实例的时候,会有一个sessionTimeout参数用于指定会话的超时时间。客户端向服务器发送这个超时时间后,服务器会根据自己的超时时间限制最终确定该会话的超时时间——这个过程就是sessionTimeout协商过程。默认情况下,ZooKeeper服务端对超时时间的限制介于2个tickTime到20个tickTime之间。即如果我们设置tickTime值为2000(单位:毫秒)的话,那么服务端就会限制客户端的超时时间,使之介于4秒到40秒之间。可以通过zoo.cfg中的tickTime配置来调整这个超时时间的限制。

​ 服务端根据客户端请求中是否包含sessionID来判断该客户端是否需要重新创建会话。如果客户端请求中已经包含了sessionID,那么就认为该客户端正在进行会话重连。在这种情况下,服务端只需要重新打开这个会话,否则需要重新创建。在创建新的会话时候,服务端首先给每个客户端都分配一个sessionID,通过如下算法保证全局唯一性:

1
2
3
4
5
6
public static long initializeNextSession(long id) {
long nextSid;
nextSid = (Time.currentElapsedTime() << 24) >>> 8;
nextSid = nextSid | (id <<56);
return nextSid;
}

创建会话重要工作就是向SessionTracker中注册会话。SessionTracker中维护了两个比较重要的数据结构,分别是sessionsWithTimeout和sessionById。前者根据sessionID保存了所有会话的超时时间,而后者则是根据sessionID保存了所有会话实体。在会话创建初期,就应该将该客户端会话的相关信息保存到这两个数据结构中,方便后续会话管理器进行管理。向SessionTracker注册完会话后,接下来还需要对会话进行激活操作。激活会话过程涉及ZooKeeper会话管理的分桶策略。激活会话的核心是为会话安排一个区块,以便会话清理程序能够快速高效地进行会话清理。

1
2
3
//过期时间设置规则
ExpirationTime_ = CurrentTime + SessionTimeout
ExpirationTime = (ExpirationTime_/ExpirationInterval + 1)* ExpirationInterval;

服务端在创建一个客户端会话的时候,会同时为客户端生成一个会话密码,连同sessionID一起发送给客户端,作为会话在集群中不同机器间转移的凭证。

预处理阶段

​ 将请求交给PrepRequestProcessor处理器进行处理。对于事务请求,ZooKeeper首先会为其创建请求事务头,它包含了sessionID、ZXID、CXID和请求类型,除此之外还会创建事务体CreateSessionTxn。

属性 说明
clientId 客户端ID,用来唯一标识该请求所属的客户端
cxid 客户端的操作序列号
zxid 该事务请求对应的事务ZXID
time 服务器开始处理该事务请求的时间
type 事务请求的类型,例如create、delete、setData和createSession等,这些事务类型都被定义在org.apache.zookeeper.ZooDefs.OpCode类中

然后还会再进行一次注册和激活会话,这里的会话注册与激活的目的是处理由非Leader服务器转发过来的会话创建请求。在这种情况下,其实尚未在Leader的SessionTracker中进行会话的注册,因此需要在此处进行一次注册与激活。

事务处理阶段

完成请求的预处理后,会将请求交给ProposalRequestProcessor处理器,包括三个子处理流程,分别是Sync流程、Proposal流程和Commit流程。

Sync流程:使用SyncRequestProcessor处理器记录事务日志的过程。Leader服务器和Follower服务器的请求处理链路中都会有这个处理器,两者在事务日志的记录功能上是完全一致的。完成事务日志记录后,每个Follower服务器都会向Leader服务器发送ACK消息,表明自身完成了事务日志的记录,以便Leader服务器统计每个事务请求的投票情况。

Proposal流程:每一个事务请求都需要集群中过半机器投票认可才能被真正应用到ZooKeeper的内存数据库中去的投票与统计过程。

Commit流程:将请求交付给CommitProcessor处理器。

最后请求会交付给FinalRequestProcessor处理器,完成事务请求的内存数据库应用,并将该请求放入commitProposal队列中。

响应处理阶段

会话响应阶段非常简单,大体分为以下4个步骤:

统计处理:ZooKeeper会计算请求在服务端处理所花费的时间,同时还会统计客户端连接的一些基本信息,包括lastZxid(最新的ZXID)、lastOp(最后一次和服务端的操作)和lastLatency(最后一次请求处理所花费的时间)等。

创建响应ConnectResponse:ConnectResponse就是一个会话创建成功后的响应,包含了当前客户端与服务端之间的通信协议版本号protocolVersion、会话超时时间、sessionID和会话密码。

序列化ConnectResponse。

I/O层发送响应给客户端。

事务请求处理示例

例如SetData请求,大体分为4步骤:请求的预处理、事务处理、事务应用和请求响应。

非事务请求处理示例

例如GetData请求

启动

单机版启动

ZooKeeper服务器的启动,大体可以分为以下五个主要步骤:配置文件解析、初始化数据管理器、初始化网络I/O管理器、数据恢复和对外服务。

集群版启动

Leader选举机制

​ ZooKeeper中,提供了三种Leader选举的算法,分别是LeaderElectionUDP版本的FastLeaderElectionTCP版本的FastLeaderElection。 从3.4.0版本开始只保留了TCP版本的FastLeaderElection选举算法。

进入Leader选举的情况包括:服务器初始化启动,服务器运行期间无法和Leader保持连接。

服务器状态:

LOOKING:寻找Leader状态。当服务器处于该状态时,他会认为当前集群中没有Leader,因此需要进入Leader选举流程。

FOLLOWING:跟随者状态,表明当前服务器角色是Follower。

LEADING:领导者状态,表明当前服务器角色是Leader。

OBSERVING:观察者状态,表明当前服务器角色是Observer。

投票数据结构:

属性 说明
id 被推举的Leader的SID值
zxid 被推举的Leader的事务ID
electionEpoch 逻辑时钟,用来判断多个投票是否在同一轮选举周期中。该值在服务端是一个自增序列。每次进入新一轮的投票后,都会对该值进行加1操作
peerEpoch 被推举的Leader的epoch
state 当前服务器的状态

自增选举轮次:在FastLeaderElection实现中,有一个logicalclock属性,用于标识当前Leader的选举轮次,ZooKeeper规定了所有有效地投票都必须在同一轮次中。ZooKeeper在开始新一轮的投票时,会首先对logicalclock进行自增操作。

初始化选票:在开始进行新一轮的投票之前,每个服务器都会首先初始化自己的选票。初始化选票也就是对Vote属性的初始化。

发送选票:放入sendqueue队列中,由发送器WorkerSender负责发送出去。

接收外部投票:从recvqueue队列中获取外部投票。如果服务器发现无法获取到任何的外部投票,那么就会立即确认自己是否和集群中其他服务器保持着有效连接。如果发现没有建立连接,那么就会马上建立连接。如果已经建立了连接,那么就再次发送自己当前的内部投票。

判断选举轮次:当外部投票的选举轮次大于内部投票时候,立即更新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票,最终再将内部投票发送出去;外部投票的选举轮次小于内部投票时候,ZooKeeper就会直接忽略该外部投票,不做任何处理,继续处于LOOKING状态;外部投票的选举轮次和内部投票一致,开始进行选票PK。

选票PK:如果外部投票中被推举的Leader服务器的选举轮次大于内部投票,那么就需要进行投票变更;如果选举轮次一致的话,那么就对比两者的ZXID。如果外部投票的ZXID大于内部投票,那么就需要进行投票变更;如果两者的ZXID一致,那么就对比两者的SID。如果外部投票的SID大于内部投票,那么就需要进行投票变更。

变更投票:通过选票PK后,如果确定了外部投票优于内部投票(所谓“优于”,是指外部投票所推举的服务器更适合成为Leader),那么就进行投票变更——使用外部投票的选票信息来覆盖内部投票。变更完成后,再次将这个变更后的内部投票发送出去。

选票归档:无论是否进行了投票变更,都会将刚刚收到的那份外部投票放入“选票集合”recvset中进行归档。recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票。

统计投票:完成了选票归档之后,就可以开始统计投票了。统计投票的过程就是为了统计集群中是否已经有过半的服务器认可了当前的内部投票。如果确定已经有过半的服务器认可了该内部投票,则终止投票,否则就回到LOOKING状态。

更新服务器状态: 统计投票后,如果已经确定可以终止投票,那么就开始更新服务器状态。服务器会首先判断当前被过半服务器认可的投票状态更新为LEADING。如果自己不是被选举产生的Leader的话,那么就会根据具体情况来确定自己是FOLLOWING或是OBSERVING。

​ Leader是事务请求的唯一调度和处理者,保证集群事务处理的顺序性,同时也是集群内部各服务器的调度者。Follower主要处理客户端的非事务请求,转发事务请求给Leader服务器,同时参与事务请求Proposal的投票,参与Leader选举投票。Observer是3.3.0版本之后的角色,可以处理非事务请求,转发事务请求给Leader服务器,和Follower的区别是不参与任何形式的投票(事务请求Proposal和Leader选举投票)。

集群间的消息通信

数据同步型消息

在Learner和Leader服务器进行数据同步的时候,网络通信所用到的消息,通常有DIFFTRUNC、SNAP和UPTODATE四种:

消息类型 发送方→接收方 说明
DIFF, 13 Leader→Learner 用于通知Learner服务器、Leader即将与其进行“DIFF”方式的数据同步
TRUNC,14 Leader→Learner 用于触发Learner服务器进行内存数据库的回滚操作
SNAP,15 Leader→Learner 用于通知Learner服务器,Learner即将与其进行“全量”方式的数据同步
UPTODATE,12 Leader→Learner 用来告诉Learner服务器,已经完成了数据同步,可以开始对外提供服务了

服务器初始化型消息

在整个集群或是某些新机器初始化的时候,Leader和Learner之间相互通信所使用的消息类型,常见的有OBSERVERINFOFOLLOWERINFOLEADERINFOACKEPOCHNEWLEADER五种:

消息类型 发送方→接收方 说明
OBSERVERINFO, 16 Observer→Leader 该信息通常是由Observer服务器在启动的时候发送给Leader的,用于向Leader服务器注册自己,同时向Leader服务器表明当前Learner服务器的角色是Observer。消息中包含了当前Observer服务器的SID和已经处理的最新ZXID。
FOLLOWERINFO, 11 Follower→Leader 该信息通常是由Follower服务器在启动的时候发送给Leader的,用于向Leader服务器注册自己,同时向Leader服务器表明当前Learner服务器的角色是Follower。消息中包含了当前Follower服务器的SID和已经处理的最新ZXID
LEADERINFO,17 Leader→Learner 在Learner连接上Leader后,会向Leader发送LearnerInfo消息(包含了OBSERVERINFO和FOLLOWERINFO两类消息),Leader服务器在接收到该消息后,也会将Leader服务器的基本信息发送给这些Learner,这个消息就是LEADERINFO,通常包含了当前Leader服务器的最新EPOCH值
ACKEPOCH,18 Learner→Leader Learner在接收到Leader发来的LEADERINFO消息后,会将自己最新的ZXID和EPOCH以ACKEPOCH消息的形式发送给Leader
NEWLEADER,10 Leader→Learner 该消息通常用于Leader服务器向Learner发送一个阶段性的标识消息——Leader会在和Learner完成一个交互流程后,向Learner发送NEWLEADER消息,同时带上当前Leader服务器处理的最新ZXID。这一系统交互流程包括:足够多的Follower服务器连接上Leader或是完成数据同步

请求处理型消息

在进行请求处理的过程中,Leader和Learner服务器之间互相通信所使用的消息,常见的有REQUESTPROPOSALACKCOMMITINFORMSYNC六种:

消息类型 发送方→接收方 说明
REQUEST,1 Learner→Leader 该消息是ZooKeeper的请求转发消息。在ZooKeeper中,所有的事务请i去必须由Leader服务器来处理。当Learner服务器接收到客户端的事务请求后,就会将请求以REQUEST消息的形式转发给Leader服务器来处理。
PROPOSAL,2 Leader→Follower 该消息是ZooKeeper实现ZAB算法的核心所在,即ZAB协议中的提议。在处理事务请求的时候,Leader服务器会将事务请求以PROPOSAL消息的形式创建投票发送给集群中所有的Follower服务器来进行事务日志的记录。
ACK,3 Follower→Leader Follower服务器在接收到来自Leader的PROPOSAL消息后,会进行事务日志的记录。如果完成了事务日志的记录,那么就会以ACK消息的反馈给Leader。
COMMIT,4 Leader→Follower 该消息用于通知集群中所有的Follower服务器,可以进行事务请求的提交了。Leader服务器在接收到过半的Follower服务器发来的ACK消息后,就进入事务请求的最终提交流程——生成COMMIT消息,告知所有的Follower服务器进行事务请求的提交。
INFORM,8 Leader→Observer 在事务请求提交阶段,针对Follower服务器,Leader仅仅只需要发送一个COMMIT消息,Follower服务器就可以完成事务请求的提交了,因为在这之前的事务请求投票阶段,Follower已经接受过PROPOSAL消息,该消息中包含了事务请求的内存,因此Follower有从之前的Proposal缓存中再次获取到事务请求。而对于Observer来说,由于之前没有参与事务请求的投票,因此没有该事务请求的上下文,显然,如果Leader同样对其发送一个简单地COMMIT消息,Observer服务器是无法完成事务请求的提交的。为了解决这个问题,ZooKeeper特别设计了INFORM消息,该消息不仅能够通知Observer已经可以提交事务请求,同时还会在消息中携带事务请求的内容。
SYNC,7 Leader→Learner 该消息用于通知Learner服务器已经完成了Sync操作。

会话管理型消息

ZooKeeper在进行会话管理的过程中,和Learner服务器之间互相通信所使用的消息,常见的有PINGREVALIDATE两种:

消息类型 发送方→接收方 说明
PING,5 Leader→Learner 该消息用于Leader同步Learner服务器上的客户端心跳检测,用以激活存活的客户端。ZooKeeper的客户端往往会随机地和任意一个ZooKeeper服务器保持连接,因此Leader服务器无法直接收到所有客户端的心跳检测,需要委托给Learner来保存这些客户端的心跳检测的客户端列表,同样以PING消息的形式反馈给Leader服务器,由Leader服务器来负责逐个对这些客户端进行会话激活。
REVALIDATE,6 Learner→leader 该消息用于Learner校验会话是否有效,同时也会激活会话。这通常发生在客户端重连的过程中,新的服务器需要向Leader发送REVALIDATE消息以确定该会话是否已经超时。

参考

https://blog.csdn.net/cnh294141800/article/details/52959028

《从Paoxs到zookeeper: 分布式一致性原理与实践》

Luckylau wechat
如果对您有价值,看官可以打赏的!