在服务器启动阶段,会进行磁盘数据的恢复,完成数据恢复后就会进行Leader选举。一旦选举产生Leader服务器后,就立即开始进行集群间的数据同步,在整个过程中,Zookeeper都处于不可用状态,直到数据同步完毕,Zookeeper才可以对外提供正常服务。

一、数据初始化

在Zookeeper服务器启动期间,首先会进行数据初始化工作,用于将存储在磁盘上的数据文件加载到Zookeeper服务器内存中。完整的数据初始化流程如下:

图片

数据的初始化从磁盘中加载数据的过程,主要包括了从快照文件中加载快照数据和根据事务日志进行数据订正两个过程。

1. 初始化FileTxnSnapLog

FileTxnSnapLog是Zookeeper事务日志和快照数据访问层,用于衔接上层业务与底层数据存储,底层数据包含了事务日志和快照数据两部分,因此FileTxnSnapLog内部又分为FileTxnLog和FileSnap的初始化,分别代表事务日志管理器和快照数据管理器的初始化。

2. 初始化ZKDatabase

完成FileTxnSnapLog的初始化后,就要开始构建内存数据库ZKDatabase了,在初始化过程中,首先会构建一个初始化DataTree,同时会将步骤1中初始化的FileTxnSnapLog交给ZKDatabase,以便内存数据库能够对事务日志和快照数据进行访问。

3. 创建PlayBackListener监听器

PlayBackListener监听器主要用来接收事务应用过程中的回调,在Zookeeper数据恢复后期,会有一个事务订正的过程,在这个过程中,会回调PlayBackListener监听器来进行对应的数据订正。PlayBackListener会将这些刚刚应用到内存数据库中的事务转存到ZKDatabase.committedLog中,以便集群中服务器间进行快速的数据同步。

4. 处理快照文件

完成内存数据库的初始化后,java培训Zookeeper就可以开始从磁盘中恢复数据了,每一个快照数据文件中都保存了Zookeeper服务器几乎全量的数据,因此首先会从这些快照文件开始加载。

5. 获取最新的100个快照文件

Zookeeper会加载最新的至多100个快照文件。

6. 解析快照文件

如果正确性校验通过的话,那么通常只会解析最新的那个快照文件。只有当最新的快照文件不可用时,才会逐个解析,直到将这100个文件全部解析完。

7. 获取最新的ZXID

完成步骤6后,就已经基于快照文件构建了一个完整的DataTree实例和sessionsWithTimeouts集合汇总,此时根据这个快照文件的文件名就可以解析出一个最新的ZXID:zxid_for_snap,代表了Zookeeper开始进行数据快照的时刻。

8. 处理事务日志

经过前面7步流程的处理,此时Zookeeper服务器内存中已经有了一份近似全量的数据了,现在开始就要通过事务日志来更新增量数据了。

9. 获取所有zxid_for_snap之后提交的事务

Zookeeper中数据的快照机制决定了快照文件中并非包含了所有的事务操作,但是未被包含在快照文件中的那部分事务操作可以通过数据订正来实现,因此,我们这里只需要从事务日志中获取所有ZXID比步骤7得到的zxid_for_snap大的事务操作即可。

10. 事务应用

获取到所有ZXID大于zxid_for_snap的事务后,将其逐个应用到之前基于快照文件恢复出来的

DataTree和sessionsWithTimeouts中。

每当有一个事务被应用到内存数据库汇总,Zookeeper同时会回调PlayBackListener监听器,将这一事务操作记录转换成Proposal,并保存到ZKDatabase.committedLog中,以便Follower进行快速同步。

11. 获取最新ZXID

待所有的事务都被完整地应用到内存数据库中之后,基本上也就完成了数据的初始化过程,此时再获取一个ZXID,用来表示上次服务器正常运行时提交的最大事务ID。

12. 校验epoch

每次选举产生一个新的Leader服务器之后,就会生成一个新的epoch,在完成数据加载后,Zookeeper从步骤11中确定的ZXID中解析出事务处理的Leader周期,同时也会从磁盘文件中读取出上次记录的最新epoch值,进行校验。

接下来,我们就来看看数据同步的流程是怎样的。

二、数据同步

在介绍数据同步流程中,我们使用Learner代表所有非Leader服务器。

在集群服务器启动过程中,当整个集群完成Leader选举之后,Learner服务器会向Leader服务器进行注册,当所有服务器都注册完之后,就进入到了数据同步环节。数据同步过程就是Leader服务器将那些没有在Learner服务器上提交过的事务请求同步给Learner服务器。

大体过程如下:

图片

获取Learner状态

在注册Learner的最后阶段,Learner服务器会发送给Leader服务器一个ACKEPOCH数据包,Leader会从这个数据包中解析出该LearnerDE currentEpoch和lastZxid。

数据同步初始化

在开始数据同步之前,Leader服务器会进行数据同步初始化,首先会从Zookeeper的内存数据库中提取出事务请求对应的提议缓存队列proposals,同时完成以下三个ZXID值的初始化:

  • peerLastZxid:该Learner服务器最后处理的ZXID

  • minCommittedLog:Leader服务器提议缓存队列committedLog中的最小ZXID

  • maxCommittedLog:Leader服务器提议缓存队列committedLog中的最大ZXID

Zookeeper集群数据同步分四类:直接差异化同步(DIFF同步)、先回滚再差异化同步(TRUNC+DIFF同步)、仅回滚同步(TRUNC同步)和全量同步(SNAP同步)。java培训在初始化节点,Leader服务器会优先初始化以全量同步方式来同步数据,但不是最终的同步方式,最终的同步方式会根据Leader和Learner服务器之间的数据差异情况来决定。

直接差异化同步(DIFF同步)

场景:peerLastZxid介于minCommittedLog和maxCommittedLog之间。

对于这种场景,直接使用直接差异化同步方式即可。Leader服务器会首先向这个Learner发送一个DIFF指令,用于通知Learner进入差异化数据同步阶段。在实际Proposal同步过程中,针对每个Proposal,Leader服务器都会通过发送两个数据包来完成,分别是PROPOSAL内容数据包和COMMIT指令数据包。

举例说明:假如某个时刻Leader服务器的提议缓存队列对应的ZXID依次是:

0x500000001、0x500000002、0x500000003、0x500000004、0x500000005

而Learner服务器最后处理的ZXID为0x500000003,于是Leader服务器就会依次将0x500000004和0x500000005两个提议同步给Learner服务器,同步过程中的数据包发送顺序如下:

发送顺序数据包类型对应的ZXID
1PROPOSAL0x500000004
2COMMIT0x500000004
3PROPOSAL0x500000005
4COMMIT0x500000005

通过以上四个数据包的发送,Learner服务器就可以接收到自己和Leader服务器的所有差异数据,Leader服务器在发送完差异数据之后,就会将该Learner加入到forwardingFollowers或observingLearners队列中。随后Leader还会立即发送一个NEWLEADER指令,用于通知Learner已经将提议缓存队列中的Proposal都同步给你自己了。

接下来,我们看看Learner对Leader发送过来的数据包是如何处理的?

Learner首先会接收到一个DIFF指令,于是便确定了进入DIFF同步阶段。然后依次收到上述四个数据包,Learner会依次将其应用到内存数据库中,然后在接收到Leader的NEWLEADER指令后,Learner会反馈给Leader一个ACK消息,表明自己也完成了对提议缓存队列中Proposal的同步。

Leader在接收到来自Learner的这个ACK消息后,就认为当前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正要处理0x500000003,并且已经将该事务写入到了Leader本地的事务日志中,就在Leader要将该Proposal发送给其他Follower机器进行投票的时候,Leader服务器挂了,开始新一轮选举,同时Leader_epoch变更为6,之后A和C继续对外进行服务,又提交了0x600000001和0x600000002两个事务,此时B再次启动,并开始数据同步。

简单来说,上面这个场景就是Leader服务器已经将事务记录到了本地事务日志中,但是没有成功发起Proposal流程的时候就挂了,在这个特殊场景汇总,我们看到peerLastZxid和minCommittedLog和maxCommittedLog的值分别是0x500000003、0x500000001和0x600000002,显然,peerLastZxid介于minCommittedLog和maxCommittedLog之间。

对于这种特殊场景,就是用先回滚再差异化同步的方式。当Leader服务器发现某个Learner包含了一条自己没有的事务记录,那么就需要让该Learner进行事务回滚,回滚到Leader服务器上存在的,同时也是最接近于peerLastZxid的ZXID。在上述示例中,Leader需要Learner回滚到ZXID为0x500000002的事务记录。

先回滚再差异化同步的数据同步方式在具体实现上和差异化同步是一样的,都会将差异化的Proposal发送给Learner,同步过程中的数据包发送顺序如下:

发送顺序数据包类型对应的ZXID
1TRUNC0x500000002
2PROPOSAL0x600000001
3COMMIT0x600000001
4PROPOSAL0x600000002
5COMMIT0x600000002

仅回滚同步(TRUNC同步)

场景:peerLastZxid大于maxCommittedLog。

这种场景其实就是上述先回滚再差异化同步的简化模式,Leader会要求Learner回滚到ZXID值为maxCommitedLog对应的事务操作。

全量同步(SNAP同步)

场景1:peerLastZxid小于maxCommittedLog。

场景2:Leader上没有提议缓存队列,peerLastZxid不等于maxCommittedLog。

在这两种场景下,Leader服务器都无法直接使用提议缓存队列和Learner进行数据同步,因此只能进行全量同步。

全量同步就是Leader服务器将本机上的全量内存数据都同步给Learner。Leader服务器首先向Learner发送一个SNAP指令,通知Learner即将进行全量数据同步,随后,Leader会从内存数据库中获取到全量的数据节点和会话超时时间记录器,将它们序列化后传输给Learner。Learner服务器接收到该全量数据后,会对器反序列化后载入到内存数据库中。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐