极客时间已完结课程限时免费阅读

加餐 | ZAB协议(二):如何从故障中恢复?

加餐 | ZAB协议(二):如何从故障中恢复?-极客时间

加餐 | ZAB协议(二):如何从故障中恢复?

讲述:于航

时长16:11大小14.82M

你好,我是韩健。
我们上一讲提到了 ZAB 的领导者选举,在我看来,它只是选举了一个适合当领导者的节点,然后把这个节点的状态设置成 LEADING 状态。此时,这个节点还不能作为主节点处理写请求,也不能使用领导职能(比如,它没办法阻止其他“领导者”广播提案)。也就是说,集群还没有从故障中恢复过来,而成员发现和数据同步会解决这个问题。
总的来说,成员发现和数据同步不仅让新领导者正式成为领导者,确立了它的领导关系,还解决了各副本的数据冲突,实现了数据副本的一致性。这样一来,集群就能正常处理写请求了。在这句话里:
确立领导关系,也就是在成员发现(DISCOVERY)阶段,领导者和大多数跟随者建立连接,并再次确认各节点对自己当选领导者没有异议,确立自己的领导关系;
处理冲突数据,也就是在数据同步(SYNCHRONIZATION)阶段,领导者以自己的数据为准,解决各节点数据副本的不一致。
对你来说,理解这两点,可以更好地理解 ZooKeeper 怎么恢复故障,以及当主节点崩溃了,哪些数据会丢失,哪些不会,以及背后的原因。也就是说,你能更加深刻地理解 ZooKeeper 的节点故障容错能力。
那么说了这么多,集群具体是怎么从故障中恢复过来的呢?带着这个问题,我们进入今天的学习。

ZAB 集群怎么从故障中恢复过来?

如果我们想把 ZAB 集群恢复到正常状态,那么新领导者就要确立自己的领导关系,成为唯一有效的领导者,然后作为主节点“领导”各备份节点一起处理读写请求。

如何确立领导关系?

那么通过开篇,你可以知道,选举出的领导者,是在成员发现阶段确立领导关系的。
在当选后,领导者会递增自己的任期编号,并基于任期编号值的大小,来和跟随者协商,最终建立领导关系。具体说的话,就是跟随者会选择任期编号值最大的节点,作为自己的领导者,而被大多数节点认同的领导者,将成为真正的领导者。
我举个例子,具体帮你理解一下。
假设一个 ZooKeeper 集群,由节点 A、B、C 组成。其中,领导者 A 已经宕机,C 是新选出来的领导者,B 是新的跟随者(为了方便演示,假设 B、C 已提交提案的事务标识符最大值分别是 <1, 10> 和 <1, 11>,其中 1 是任期编号,10、11 是事务标识符中的计数器值,A 宕机前的任期编号也是 1)。那么 B、C 如何协商建立领导关系呢?
图1
首先,B、C 会把自己的 ZAB 状态设置为成员发现(DISCOVERY),这就表明,选举(ELECTION)阶段结束了,进入了下一个阶段:
图2
在这里,我想补充一下,ZAB 定义了 4 种状态,来标识节点的运行状态。
ELECTION(选举状态):表明节点在进行领导者选举;
DISCOVERY(成员发现状态):表明节点在协商沟通领导者的合法性;
SYNCHRONIZATION(数据同步状态):表明集群的各节点以领导者的数据为准,修复数据副本的一致性;
BROADCAST(广播状态):表明集群各节点在正常处理写请求。
关于这 4 种状态,你知道它们是做什么的就可以了。我就强调一点,只有当集群大多数节点处于广播状态的时候,集群才能提交提案。
接下来,B 会主动联系 C,发送给它包含自己接收过的领导者任期编号最大值(也就是前领导者 A 的任期编号,1)的 FOLLOWINFO 消息。
图3
当 C 接收来自 B 的信息时,它会将包含自己事务标识符最大值的 LEADINFO 消息发给跟随者。
你要注意,领导者进入到成员发现阶段后,会对任期编号加 1,创建新的任期编号,然后基于新任期编号,创建新的事务标识符(也就是 <2, 0>)。
图4
当接收到领导者的响应后,跟随者会判断领导者的任期编号是否最新,如果不是,就发起新的选举;如果是,跟随者返回 ACKEPOCH 消息给领导者。在这里,C 的任期编号(也就是 2)大于 B 接受过的其他领导任期编号(也就是旧领导者 A 的任期编号,1),所以 B 返回确认响应给 C,并设置 ZAB 状态为数据同步。
图5
最后,当领导者接收到来自大多数节点的 ACKEPOCH 消息时,就设置 ZAB 状态为数据同步。在这里,C 接收到了 B 的消息,再加上 C 自己,就是大多数了,所以,在接收到来自 B 的消息后,C 设置 ZAB 状态为数据同步。
图6
现在,ZAB 在成员发现阶段确立了领导者的领导关系,之后领导者就可以行使领导职能了。而这时它首先要解决的就是数据冲突,实现各节点数据的一致性,那么它是怎么做的呢?

如何处理冲突数据?

当进入到数据同步状态后,领导者会根据跟随者的事务标识符最大值,判断以哪种方式处理不一致数据(有 DIFF、TRUNC、SNAP 这 3 种方式,后面我会具体说一说)。
因为 C 已提交提案的事务标识符最大值(也就是 <1, 11>)大于 B 已提交提案的事务标识符最大值(也就是 <1, 10>),所以 C 会用 DIFF 的方式修复数据副本的不一致,并返回差异数据(也就是事务标识符为 <1, 11> 的提案)和 NEWLEADER 消息给 B。
图7
在这里,我想强调一点:B 已提交提案的最大值,也是它最新提案的最大值。因为在 ZooKeeper 实现中,节点退出跟随者状态时(也就是在进入选举前),所有未提交的提案都会被提交。这是 ZooKeeper 的设计,你知道有这么个事就可以了。
然后,B 修复不一致数据,返回 NEWLEADER 消息的确认响应给领导者。
图8
接着,当领导者接收到来自大多数节点的 NEWLEADER 消息的确认响应,将设置 ZAB 状态为广播。在这里,C 接收到 B 的确认响应,加上 C 自己,就是大多数确认了。所以,在接收到来自 B 的确认响应后,C 设置自己的 ZAB 状态为广播,并发送 UPTODATE 消息给所有跟随者,通知它们数据同步已经完成了。
图9
最后当 B 接收到 UPTODATE 消息时,它就知道数据同步完成了,就设置 ZAB 状态为广播。
图10
这个时候,集群就可以正常处理写请求了。
现在,我已经讲完了故障恢复的原理,那接下来,我们就来看一看 ZooKeeper 到底是怎么实现的吧。

ZooKeeper 如何恢复故障?

成员发现

成员发现是通过跟随者和领导者交互来完成的,目标是确保大多数节点对领导者的领导关系没有异议,也就是确立领导者的领导地位。
大概的实现流程,就像下面这样:
图11
为帮你更好地理解这个流程,我们来走一遍核心代码的流程,加深下印象。
第一步,领导者选举结束,节点进入跟随者状态或者领导者状态后,它们会分别设置 ZAB 状态为成员发现。具体来说就是:
跟随者会进入到 Follower.followLeader() 函数中执行,设置 ZAB 状态为成员发现。
self.setZabState(QuorumPeer.ZabState.DISCOVERY);
领导者会进入到 Leader.lead() 函数中执行,并设置 ZAB 状态为成员发现。
self.setZabState(QuorumPeer.ZabState.DISCOVERY);
第二,跟随者会主动联系领导者,发送自己已接受的领导者任期编号最大值(也就是 acceptedEpoch)的 FOLLOWINFO 消息给领导者。
// 跟领导者建立网络连接
connectToLeader(leaderServer.addr, leaderServer.hostname);
connectionTime = System.currentTimeMillis();
// 向领导者报道,并获取领导者的事务标识符最大值
long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);
第三,接收到来自跟随者的 FOLLOWINFO 消息后,在 LearnerHandler.run() 函数中,领导者将创建包含自己事务标识符最大值的 LEADINFO 消息,并响应给跟随者。
// 创建LEADINFO消息
QuorumPacket newEpochPacket = new
QuorumPacket(Leader.LEADERINFO, newLeaderZxid, ver, null);
// 发送LEADINFO消息给跟随者
oa.writeRecord(newEpochPacket, "packet");
第四,接收到来自领导者的 LEADINFO 消息后,跟随者会基于领导者的任期编号,判断领导者是否合法,如果领导者不合法,跟随者发起新的选举,如果领导者合法,响应 ACKEPOCH 消息给领导者。
// 创建ACKEPOCH消息,包含已提交提案的事务标识符最大值
QuorumPacket ackNewEpoch = new QuorumPacket(Leader.ACKEPOCH, lastLoggedZxid, epochBytes, null);
// 响应ACKEPOCH消息给领导者
writePacket(ackNewEpoch, true);
第五,跟随者设置 ZAB 状态为数据同步。
self.setZabState(QuorumPeer.ZabState.SYNCHRONIZATION);
第六,需要你注意的是,在 LearnerHandler.run() 函数中(以及 Leader.lead() 函数),领导者会调用 waitForEpochAck() 函数,来阻塞和等待来自大多数节点的 ACKEPOCH 消息。
ss = new StateSummary(bbepoch.getInt(), ackEpochPacket.getZxid());
learnerMaster.waitForEpochAck(this.getSid(), ss);
第七,当领导者接收到来自大多数节点的 ACKEPOCH 消息后,在 Leader.lead() 函数中,领导者将设置 ZAB 状态为数据同步。
self.setZabState(QuorumPeer.ZabState.SYNCHRONIZATION);
这样,ZooKeeper 就实现了成员发现,各节点就领导者的领导关系达成了共识。
当跟随者和领导者设置 ZAB 状态为数据同步,它们也就是进入了数据同步阶段,那在 ZooKeeper 中数据同步是如何实现的呢?

数据同步

数据同步也是通过跟随者和领导者交互来完成的,目标是确保跟随者节点上的数据与领导者节点上数据是一致的。大概的实现流程,如图所示:
图12
为了方便你理解,咱们一起走一遍核心代码的流程,加深下印象。
第一,在 LearnerHandler.run() 函数中,领导者调用 syncFollower() 函数,根据跟随者的事务标识符值最大值,判断用哪种方式处理不一致数据,把已经提交提案和未提交提案都同步给跟随者:
peerLastZxid = ss.getLastZxid();
boolean needSnap = syncFollower(peerLastZxid, learnerMaster);
在这里,需要你了解领导者向跟随者同步数据的三种方式(TRUNC、DIFF、SNAP),它们是什么含义呢?要想了解这部分内容,你首先要了解一下 syncFollower() 中,3 个关键变量的含义。
peerLastZxid:跟随者节点上,提案的事务标识符最大值。
maxCommittedLog、minCommittedLog:领导者节点内存队列中,已提交提案的事务标识符最大值和最小值。需要你注意的是,maxCommittedLog、minCommittedLog 与 ZooKeeper 的设计有关。在 ZooKeeper 中,为了更高效地复制提案到跟随者上,领导者会将一定数量(默认值为 500)的已提交提案放在内存队列里,而 maxCommittedLog、minCommittedLog 分别标识的是内存队列中,已提交提案的事务标识符最大值和最小值。
说完 3 个变量的含义,我来说说 3 种同步方式。
TRUNC:当 peerLastZxid 大于 maxCommittedLog 时,领导者会通知跟随者丢弃超出的那部分提案。比如,如果跟随者的 peerLastZxid 为 11,领导者的 maxCommittedLog 为 10,那么领导者将通知跟随者丢弃事务标识符值为 11 的提案。
DIFF:当 peerLastZxid 小于 maxCommittedLog,但 peerLastZxid 大于 minCommittedLog 时,领导者会同步给跟随者缺失的已提交的提案,比如,如果跟随者的 peerLastZxid 为 9,领导者的 maxCommittedLog 为 10,minCommittedLog 为 9,那么领导者将同步事务标识符值为 10 的提案,给跟随者。
SNAP:当 peerLastZxid 小于 minCommittedLog 时,也就是说,跟随者缺失的提案比较多,那么,领导者同步快照数据给跟随者,并直接覆盖跟随者本地的数据。
在这里,我想补充一下,领导者先就已提交提案和跟随者达成一致,然后调用 learnerMaster.startForwarding(),将未提交提案(如果有的话)也缓存在发送队列(queuedPackets),并最终复制给跟随者节点。也就是说,领导者以自己的数据为准,实现各节点数据副本的一致的。
需要你注意的是,在 syncFolower() 中,领导者只是将需要发送的差异数据缓存在发送队列(queuedPackets),这个时候还没有发送。
第二,在 LearnerHandler.run() 函数中,领导者创建 NEWLEADER 消息,并缓存在发送队列中。
// 创建NEWLEADER消息
QuorumPacket newLeaderQP = new QuorumPacket(Leader.NEWLEADER, newLeaderZxid, learnerMaster.getQuorumVerifierBytes(), null);
// 缓存NEWLEADER消息到发送队列中
queuedPackets.add(newLeaderQP);
第三,在 LearnerHandler.run() 函数中,领导者调用 startSendingPackets() 函数,启动一个新线程,并将缓存的数据发送给跟随者。
// 发送缓存队列中的数据
startSendingPackets();
第四,跟随者调用 syncWithLeader() 函数,处理来自领导者的数据同步。
// 处理数据同步
syncWithLeader(newEpochZxid);
第五,在 syncWithLeader() 函数,跟随者接收到来自领导者的 NEWLEADER 消息后,返回确认响应给领导者。
writePacket(new QuorumPacket(Leader.ACK, newLeaderZxid, null, null), true);
第六,在 LearnerHandler.run() 函数中(以及 Leader.lead() 函数),领导者等待来自大多数节点的 NEWLEADER 消息的响应。
learnerMaster.waitForNewLeaderAck(getSid(), qp.getZxid());
第七,当领导者接收到来自大多数节点的 NEWLEADER 消息的响应时,在 Leader.lead() 函数中,领导者设置 ZAB 状态为广播状态。
self.setZabState(QuorumPeer.ZabState.BROADCAST);
并在 LearnerHandler.run() 中发送 UPTODATE 消息给所有跟随者,通知它们数据同步已完成了。
queuedPackets.add(new QuorumPacket(Leader.UPTODATE, -1, null, null));
第八,当跟随者接收到 UPTODATE 消息时,就知道自己修复完数据不一致了,可以处理写请求了,就设置 ZAB 状态为广播。
// 数据同步完成后,也就是可以正常处理来自领导者的广播消息了,设置ZAB状态为广播
self.setZabState(QuorumPeer.ZabState.BROADCAST);
你看,这样就确保各节点数据的一致了,接下来,就可以以领导者为主,向其他节点广播消息了。

内容小结

本节课我主要带你了解了 ZAB 如何恢复故障,我希望你明确这样几个重点。
1. 成员发现,是为了建立跟随者和领导者之间的领导者关系,并通过任期编号来确认这个领导者是否为最合适的领导者。
2. 数据同步,是通过以领导者的数据为准的方式,来实现各节点数据副本的一致,需要你注意的是,基于“大多数”的提交原则和选举原则,能确保被复制到大多数节点并提交的提案,就不再改变。
在这里,我想特别强调一下,在 ZooKeeper 的代码实现中,处于提交(Committed)状态的提案是可能会改变的,为什么呢?
在 ZooKeeper 中,一个提案进入提交(Committed)状态,有两种方式:
被复制到大多数节点上,被领导者提交或接收到来自领导者的提交消息(leader.COMMIT)而被提交。在这种状态下,提交的提案是不会改变的。
另外,在 ZooKeeper 的设计中,在节点退出跟随者状态时(在 follower.shutdown() 函数中),会将所有本地未提交的提案都提交。需要你注意的是,此时提交的提案,可能并未被复制到大多数节点上,而且这种设计,就会导致 ZooKeeper 中出现,处于“提交”状态的提案可能会被删除(也就是接收到领导者的 TRUNC 消息而删除的提案)。
更准确的说,在 ZooKeeper 中,被复制到大多数节点上的提案,最终会被提交,并不会再改变;而只在少数节点存在的提案,可能会被提交和不再改变,也可能会被删除。为了帮助你理解,我来举个具体的例子。
如果写请求对应的提案“SET X = 1”已经复制到大多数节点上,那么它是最终会被提交,之后也不会再改变。也就是说,在没有新的 X 赋值操作的前提下,不管节点怎么崩溃、领导者如何变更,你查询到的 X 的值都为 1。
如果写请求对应的提案“SET X = 1”未被复制到大多数节点上,比如在领导者广播消息过程中,领导者崩溃了,那么,提案“SET X = 1”,可能被复制到大多数节点上,并提交和之后就不再改变,也可能会被删除。这个行为是未确定的,取决于新的领导者是否包含该提案。
另外,我想补充下,在 ZAB 中,选举出了新的领导者后,该领导者不能立即处理写请求,还需要通过成员发现、数据同步 2 个阶段进行故障恢复。这是 ZAB 协议的设计决定的,不是所有的共识算法都必须这样,比如 Raft 选举出新的领导者后,领导者是可以立即处理写请求的。
最后,完成数据同步后,节点将进入广播状态,那 ZAB 是如何处理读写请求,又是如何广播消息的呢?下节课,我会重点带你了解这部分内容。

课堂思考

我提到在 ZAB 中,提案提交的大多数原则和领导者选举的大多数原则,确保了被复制到大多数节点的提案就不再改变了。那么你不妨思考和推演一下,这是为什么呢?欢迎在留言区分享你的看法,与我一同讨论。
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 6

提建议

上一篇
加餐 | ZAB协议(一):主节点崩溃了,怎么办?
下一篇
加餐 | ZAB协议(三):如何处理读写请求?
 写留言

精选留言(11)

  • 小波菜
    2020-05-21
    “如果写请求对应的提案“SET X = 1”未被复制到大多数节点上,比如在领导者广播消息过程中,领导者崩溃了,那么,提案“SET X = 1”,可能被复制到大多数节点上,并提交和之后就不再改变,也可能会被删除。这个行为是未确定的,取决于新的领导者是否包含该提案。” 请教韩老师: 这边set x=1只复制到少数节点上,那么这些少数节点的zxid应该是最大,应该回成为新的leader,也就不会丢数据了啊? 然后这个问题又该如何避免呢?
    展开

    作者回复: 加一颗星:),问题1:新领导者不是所有节点中ZXID最大的节点,而是大多数节点中ZXID最大的节点,如果“set x = 1”只复制到少数节点上,ZAB的领导者选举规则,不能保证成为领导者的节点一定是这些“少数节点”。 问题2:对客户端而言,需要支持操作的冥等性,如果写入超时(即在指定时间内,服务器没有成功将指令复制到大多数节点上),重试就可以了。而操作的冥等性,能保证最后的结果是预期的结果(即X的值为1)。

    共 2 条评论
    18
  • Tim
    2020-05-18
    有个问题请教下韩老师,在做故障恢复数据同步时候,如果 minCommittedLog < peerLastZxid < maxCommittedLog, 比如leader 是 【5,6,7,8,9】,而follower是【5,7】,follower中间少了一个zxid 6的事务,这时候数据同步会恢复嘛?谢谢老师解答。

    作者回复: 加一颗星:),不会出现这种情况,ZAB能保证提案的顺序性。

    共 5 条评论
    8
  • 要努力的兵长
    2020-09-10
    如果写请求对应的提案“SET X = 1”未被复制到大多数节点上,比如在领导者广播消息过程中,领导者崩溃了,那么,提案“SET X = 1”,可能被复制到大多数节点上,并提交和之后就不再改变,也可能会被删除。这个行为是未确定的,取决于新的领导者是否包含该提案 ----------像这种 提案的 事务ID明显是最大的吧。 那选举新leader 的时候, 也不可能选举出 没有接受的该提案的那种节点吧 (任期相同的情况下,选举 事务ID最大的 作为领导者)
    展开

    作者回复: 加一颗星:),有可能这个节点也出现了网络故障,没有参与领导者选举,只有“大多数”,才不会再改变。

    6
  • Heaven
    2020-08-19
    少数节点为何我XID最大我不能成为领导者呢?

    作者回复: 加一颗星:),不满足“大多数”原则,共识算法本质上是“多数派”算法。

    共 2 条评论
    4
  • Kvicii.Y
    2020-07-01
    感觉成员发现应该算是选举过后的一个选举补偿,而数据同步则是数据补偿

    作者回复: 加一颗星:),现在看来,这两个阶段其实是可以省去的,比如Raft就没有这两个阶段,技术是在不断发展的。

    3
  • 小麦
    2022-01-21
    【在 ZooKeeper 中,被复制到大多数节点上的提案,最终会被提交】 如果一个提案已经被复制到大多数节点上了,但是在 Leader 向节点发送 commit 之前崩溃了,那么 follower 是没有收到 commit 请求的,那这个提案最终也会被提交吗?为什么?
    共 1 条评论
    2
  • kylexy_0817
    2020-09-05
    韩老师好,“只有当集群大多数节点处于广播状态的时候,集群才能提交提案”,是否意味着BROADCAST广播状态,是会与其它三个状态同时存在的呢?

    作者回复: 加一颗星:),在任意时刻,每个节点只能处于一种状态,但集群中的各节点可能处于不同的状态。

    1
  • 春风
    2020-07-08
    当接收到领导者的响应后,跟随者会判断领导者的任期编号是否最新,如果不是,就发起新的选举; 老师,什么情况下领导者的任期编号会不是最新呢?这个时候发起新的选举,其他节点的状态是不是应该是following状态,zab状态应该是discovery状态,这个时候是怎么响应选举的呢?
    展开

    作者回复: 加一颗星:),问题1:暂时没想到具体例子,我再想想:)。问题2:响应它认为是领导者的节点信息给这个选举状态的节点。

    2
  • simple_孙
    2021-11-13
    ZAB必须有数据同步的操作是不是因为Raft在提交数据的时候,跟随者会检查上一条数据是否提交成功,没成功的话就会重新同步;而ZAB的数据同步就是一个二阶段提交,没法检查上一个位置的同步结果。
    共 1 条评论
  • 我可能是个假开发
    2021-06-30
    应该怎么样理解大多数当选领导呢? 即是LOOKING状态的节点会在一段时间内(多久呢)收集选票?对于epoch相同的情况,按zxid从大到小遍历选票,如果看到某一个zxid的数量满足大多数条件(count(zxid)>(n/2)+1),则投票该zxid中集群id最大的节点为领导者?
  • Geek_672f79
    2021-03-21
    韩老师, 你在ZAB协议(1) 有这么一句话:ZAB 的领导者选举,选举出的是大多数节点中数据最完整的节点。 但在本章有这么一句话 :如果写请求对应的提案“SET X = 1”未被复制到大多数节点上,比如在领导者广播消息过程中,领导者崩溃了,那么,提案“SET X = 1”,可能被复制到大多数节点上,并提交和之后就不再改变,也可能会被删除。这个行为是未确定的,取决于新的领导者是否包含该提案。 我该如何去理解?
    展开
    共 4 条评论