暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

漫谈ZAB协议——数据恢复与同步

糖爸的架构师之路 2021-06-24
1355

写在前面

今天是年三十,也是农历2020年的最后一天。先给小伙伴们拜个年,祝大家新年健康顺遂,牛气冲天,代码永无Bug。相信现在每个人的家里一定都是忙忙碌碌,一片新年的气氛了。但是对于我这种什么家务都不会的人来说,就有点无聊。如果你也和我一样,不妨来看看我的文章,提点宝贵意见。年三十拿出时间来学习,也是一件挺酷的事情
在ZAB协议的第一篇文章漫谈ZAB协议——选举中,我们着重的介绍了一下ZAB协议的基本内容和ZAB协议中崩溃恢复阶段的选过程。今天我们来重点介绍下崩溃恢复阶段的另一个非常重要的内容,即数据恢复与同步。

数据同步策略
正常情况下的数据同步流程
完成Leader选举之后,在正式开始工作(即接收客户端的事务请求,然后提出新的Proposal)之前,Leader服务器会首先确认事务日志中的所有Proposal是否都已经被集群中过半的机器提交了,即是否完成数据同步。

所有正常运行的服务器,要么成为Leader,要么成为Follower并和Leader保持同步。Leader服务器需要确保所有的Follower服务器能够接收到每一条事务Proposal,并且能够正确地将所有已经提交了的事务Proposal在内存中执行。具体点说,Leader服务器会为每一个 Follower 服务器都准备一个队列,并将那些没有被各Follower服务器同步的事务以Proposal消息的形式逐个发送给 Follower服务器,并在每一个Proposal消息后面紧接着再发送一个Commit 消息,以表示该事务已经被提交。等到Follower服务器将所有其尚未同步的事务 Proposal 都从 Leader 服务器上同步过来并成功保存到本地后,Leader服务器就会将该 Follower 服务器加入到真正的可用Follower列表中,并开始之后的其他流程。如下是正常情况下的数据同步流程图:


异常数据的处理
上面讲述的是正常情况下,即一个完整的事务Proposal被集群中的所有节点都提交后Leader出现异常的情况。但是在实际场景中,往往并不会这么理想。我们假设两种场景:
  1. 一个事务在Leader上提交了,并且过半的Follower都响应Ack了,但是Leader在Commit消息发出之前宕机了。

  2. 假设一个事务在 Leader 提出之后,Leader 宕机了。

要确保如果发生上述两种情况,数据还能保持一致性,那么 Zab 协议选举算法必须满足以下要求:
  • 确保已经被 Leader 提交的 Proposal 必须最终被所有的 Follower 服务器提交

  • 确保丢弃已经被 Leader 提出的但是没有被提交的 Proposal

所以,根据上述要求Zab协议需要保证选举出来的Leader需要满足以下条件

  • 新选举出来的 Leader 不能包含未提交的 Proposal 。即新选举的 Leader 必须都是已经提交了 Proposal 的 Follower 服务器节点

  • 新选举的 Leader 节点中含有最大的 zxid 

为了保证上述两个条件可以高效的被实现,ZAB协议引入了zxid。关于zxid在漫谈ZAB协议——选举中已经进行了详细的介绍,这里我们在简单回顾一下。

zxid是一个64位的数字,其中低32 位可以看作是一个简单的单调递增的计数器,针对客户端的每一个事务请求,Leader服务器在产生一个新的事务Proposal的时候,都会对该计数器进行加1操作。而高32位则代表了Leader 周期epoch的编号。每当选举产生一个新的Leader服务器,就会从这个Leader服务器上取出其本地日志中最大事务 Proposal的zxid,并从该zxid中解析出对应的epoch 值,然后再对其进行加1操作,之后就会以此编号作为新的 epoch,并将低32位置0来开始生成新的zxidZAB协议中的这一通过epoch编号来区分 Leader 周期变化的策略,能够有效地避免不同的Leader 服务器错误地使用相同的zxid编号提出不一样的事务 Proposal的异常情况,这对于识别在Leader 崩溃恢复前后生成的 Proposal非常有帮助,大大简化和提升了数据恢复流程。

基于这样的策略,当一个包含了上一个 Leader 周期中尚未提交过的事务 Proposal 的服务器启动时,其肯定无法成为 Leader,原因很简单,因为当前集群中一定包含一个 Quorum集合,该集合中的机器一定包含了更高 epoch的事务 Proposal,因此这台机器的事务 Proposal 肯定不是最高,也就无法成为 Leader 了。当这台机器加入到集群中,以Follower 角色连接上Leader服务器之后,Leader 服务器会根据自己服务器上最后被提交的 Proposal来和 Follower 服务器的 Proposal进行比对,比对的结果当然是 Leader 会要求 Follower 进行一个回退操作——回退到一个确实已经被集群中过半机器提交的最新的事务 Proposal。举个例子来说,如下图,集群正常运行过程中的某一个时刻, Server1 是 Leader 服务器,其先后广播了消息 P1、P2、C1、P3 和 C2。假设初始的 Leader 服务器 Server1 在提出了一个事务 P3 之后就崩溃退出了,从而导致集群中的其他服务器都没有收到这个事务 Proposal。于是,当 Server1 恢复过来再次加入到集群中的时候,ZAB 协议需要确保丢弃 P3 这个事务。


数据同步方式

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

  • peerLastZxid:该 Follower服务器最后处理的 zxid。

  • minCommittedLog:Leader 服务器Proposal缓存队列committedLog中的最小zxid。

  • maxCommittedLog:Leader服务器Proposal缓存队列committedLog中的最大zxid。

ZooKeeper集群数据同步通常分为四类

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

场景:minCommittedLog < peerLastZxid < maxCommittedLog

  • 先回滚再差异化同步(TRUNC+DIFF 同步)

场景:Leader 服务器在已经将事务记录到了本地事务日志中,但是没有成功发起 Proposal流程的时候就挂了。即上面针对异常数据处理举例的那种情况。

  • 仅回滚同步(TRUNC同步)

场景:peerLastZxid > maxCommittedLog

  • 全量(快照)同步(SNAP同步)

场景:peerLastZxid < maxCommittedLog或Leader 服务器上没有提议缓存队列,peerLastZxid != lastProcessedZxid(Leader 服务器数据恢复后得到的最大 ZXID)。

在初始化阶段,Leader 服务器会优先初始化以全量同步方式来同步数据,之后会根据 Leader和Follower 服务器之间的数据差异情况来决定最终的数据同步方式。


总结

如果是正常流程,会会按照Proposal-ack-commit这种三段式的提交进行数据的同步,并且针对每一个Follower的每一次数据同步都在一个FIFO的队列里进行。
如果提交过程中出现异常,即Leader宕机等极端情况,会首先根据一定的规则(epcho、zxid、mysid)进行选举,为了保证集群节点数据的一致性,Leader的产生必须满足zxid在进群节点中值最大和没有未提交的Proposal两个条件。选举完成后,Leader会向各个Follower进行数据恢复,恢复的策略会根据Follower中的peerLastZxid和Leader中的minCommittedLog、maxCommittedLog的大小来决定采用哪种同步策略。
以上就是本文的全部内容,对ZAB协议的Zookeeper实现感兴趣的同学可以参考相关的源代码。谢谢小伙伴们的阅读~~
文章转载自糖爸的架构师之路,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论