在ZooKeeper集群服务器启动的过程中,整个集群完成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服务器提议缓存队列committedLog中的最小ZXID。
- maxCommittedLog:Leader服务器提议缓存队列committedLog中的最大ZXID。
ZooKeeper集群数据同步通常分为四类,分别是直接差异化同步(DIFF同步)、先回滚再差异化同步(TRUNC+DIFF同步)、仅回滚同步(TRUNC同步)和全量同步(SNAP同步)。在初始化阶段,Leader服务器会优先初始化以全量同步方式来同步数据——当然,这并非最终的数据同步方式,在以下步骤中,会根据Leader和Learner服务器之间的数据差异情况来决定最终的数据同步方式。
直接差异化同步(DIFF同步)
场景:peerLastZxid介于minCommittedLog和maxCommittedLog之间。
对于这个场景,就使用直接差异化同步(DIFF同步)方式即可。Leader服务器会首先向这个Learner发送一个DIFF指令,用于通知Learner“进入差异化数据同步阶段,Leader服务器即将把一些Proposal同步给自己”。在实际Proposal同步过程中,针对每个Proposal,Leader服务器都会通过发送两个数据包来完成,分别是PROPOSAL内容数据包和COMMIT指令数据包——这和ZooKeeper运行时Leader和Follower之间的事务请求的提交过程是一致的。
举个例子来说,假如某个时刻Leader服务器的提议缓存队列对应的ZXID依次是:
而Learner服务器最后处理的ZXID为0x500000003,于是Leader服务器就会依次将0x500000004和0x500000005两个提议同步给Learner服务器,同步过程汇总的数据包发送顺序如下表所示。
发送顺序 数据包类型 对应的ZXID 1 PROPOSAL 0x500000004 2 COMMIT 0x500000004 3 PROPOSAL 0x500000005 4 COMMIT 0x500000005
通过以上四个数据包的发送,Learner服务器就可以接收到自己和Leader服务器的所有差异数据。Leader服务器在发送完差异数据之后,就会将该Learner加入到forwardingFollowers或observingLearners队列中,这两个队列在ZooKeeper运行期间的事务请求处理过程中都会使用到。随后Leader还会立即发送一个NEWLEADER指令,用于通知Learner,已经将提议缓存队列中的Proposal都同步给自己了。
下面我们再来看Learner对Leader发送过来的数据包的出了力。根据上面讲解的Leader服务器的数据包发送顺序,Leader会首先接收到一个DIFF指令,于是便确定了接下来进入DIFF同步阶段。然后依次收到上表中的四个数据包,Learner会依次将其应用到内存数据库中。紧接着,Learner还会接收到来自Leader的NEWLEADER指令,此时Learner就会反馈给Leader一个ACK消息,表明自己也确实完成了对提议缓存队列中Proposal的同步。
Leader在接收到来自Learner的这个ACK消息以后,就认为当前Learner已经完成了数据同步,同时进入“过半策略”等待阶段——Leader会和其他Learner服务器进行上述同样的数据同步流程,直到集群中有过半的Learner机器响应了Leader这个ACK消息。一旦满足“过半策略”后,Leader服务器就会向所有已经完成数据同步的Learner发送一个UPTODATE指令,用来通知Learner已经完成了数据同步,同时集群中已经有过半机器完成了数据同步,集群已经具备了对外服务的能力了。
Learner在接收到这个来自Leader的UPTODATE指令后,会终止数据同步流程,然后向Leader再次反馈一个ACK消息。
整个直接差异化同步过程中涉及的Leader和Learner之间的数据包通信如下图所示。
先回滚再差异化同步(TRUNC+DIFF同步)
场景:针对上卖弄的场景,我们已经介绍了直接差异化同步的详细过程。但是在这种场景中,会有一个罕见但是确实存在的特殊场景:设有A、B、C三台机器,假如某一时刻B是Leader服务器,此时的Leader_Epoch为5,同时当前已经被集群中绝大部分机器都提交的ZXID包括:0x500000001和0x500000002。此时,Leader正要处理ZXID:0x500000003,并且已经将该事务系如到了Leader本地的事务日志中去——就在Leader恰好要将该Proposal发送给其他Follower机器进行投票的时候,Leader服务器挂了,Proposal没有被同步出去。此时ZooKeeper集群会进行新一轮的Leader选举,假设此次选举产生的新的Leader是A,同时Leader_Epoch变更为6,之后A和C两台服务器继续对外进行服务,又提交了0x600000001和0x600000002两个事务。此时,服务器B再次启动,并开始数据同步。
简单的讲,上面这个场景就是Leader服务器在已经将事务记录到了本地事务日志中,但是没有成功发起Proposal流程的时候就挂了。在这个特殊场景中,我们看到,peerLastZxid、minCommittedLog和maxCommittedLog的值分别是0x500000003、0x500000001和0x600000002,显然,peerLastZxid介于minCommittedLog和maxCommittedLog之间。
对于这个特殊场景,就使用先回滚再差优化同步(TRUNC+DIFF同步)的方式。当Leader服务器发现某个Learner包含了一条自己没有的事务记录,那么就需要让该Learner进行事务回滚——回滚到Leader服务器上存在的,同时也是最接近于peerLastZxid的ZXID。在上面这个例子中,Leader会需要Learner回滚到ZXID为0x500000002的事务记录。
先回滚再差异化同步的数据同步方式在具体实现上和差异话同步是一样的,都是会将差异化的Proposal发送给Learner。同步过程中的数据包发送顺序如下表所示。
发送顺序 数据包类型 对应的ZXID 1 TRUNC 0x500000002 2 PROPOSAL 0x600000001 3 COMMIT 0x600000001 4 PROPOSAL 0x600000002 5 COMMIT 0x600000002
仅回滚同步(TRUNC同步)
场景:peerLastZxid大于maxCommittedLog。
这种场景其实就是上述先回滚再差异化同步的简化模式,Leader会要求Learner回滚到ZXID值为maxCommitedLog对应的事务操作。
全量同步(SNAP同步)
- 场景1:peerLastZxid小于minCommittedLog。
- 场景2:Leader服务器上没有提议缓存队列,peerLastZxid不等于lastProcessedZxid(Leader服务器数据恢复后得到的最大ZXID)。
上述这两个场景非常类似,在这两种场景下,Leader服务器都无法直接使用提议缓存队列和Learner进行数据同步,因此只能进行全量同步(SNAP同步)。
所谓全量同步就是Leader服务器将本机上的全量内存数据同步给Learner。Leader服务器首先向Learner发送一个SNAP指令,通知Leader即将进行全量数据同步。随后,Leader会从内存数据库中获取到全量的数据节点和会话超时时间记录器,将他们序列化后传输给Learner。Learner服务器接收到该全量数据后,会对其反序列化后载入到内存数据库中。
以上就是ZooKeeper集群间机器的数据同步流程。整个数据同步流程的代码实现主要在LearnerHandler和Learner两个类中。