加餐 | ZAB协议(二):如何从故障中恢复?
下载APP
关闭
渠道合作
推荐作者
加餐 | ZAB协议(二):如何从故障中恢复?
2020-05-17 韩健 来自北京
《分布式协议与算法实战》
课程介绍
讲述:于航
时长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 状态为成员发现。
领导者会进入到 Leader.lead() 函数中执行,并设置 ZAB 状态为成员发现。
第二,跟随者会主动联系领导者,发送自己已接受的领导者任期编号最大值(也就是 acceptedEpoch)的 FOLLOWINFO 消息给领导者。
第三,接收到来自跟随者的 FOLLOWINFO 消息后,在 LearnerHandler.run() 函数中,领导者将创建包含自己事务标识符最大值的 LEADINFO 消息,并响应给跟随者。
第四,接收到来自领导者的 LEADINFO 消息后,跟随者会基于领导者的任期编号,判断领导者是否合法,如果领导者不合法,跟随者发起新的选举,如果领导者合法,响应 ACKEPOCH 消息给领导者。
第五,跟随者设置 ZAB 状态为数据同步。
第六,需要你注意的是,在 LearnerHandler.run() 函数中(以及 Leader.lead() 函数),领导者会调用 waitForEpochAck() 函数,来阻塞和等待来自大多数节点的 ACKEPOCH 消息。
第七,当领导者接收到来自大多数节点的 ACKEPOCH 消息后,在 Leader.lead() 函数中,领导者将设置 ZAB 状态为数据同步。
这样,ZooKeeper 就实现了成员发现,各节点就领导者的领导关系达成了共识。
当跟随者和领导者设置 ZAB 状态为数据同步,它们也就是进入了数据同步阶段,那在 ZooKeeper 中数据同步是如何实现的呢?
数据同步
数据同步也是通过跟随者和领导者交互来完成的,目标是确保跟随者节点上的数据与领导者节点上数据是一致的。大概的实现流程,如图所示:
图12
为了方便你理解,咱们一起走一遍核心代码的流程,加深下印象。
第一,在 LearnerHandler.run() 函数中,领导者调用 syncFollower() 函数,根据跟随者的事务标识符值最大值,判断用哪种方式处理不一致数据,把已经提交提案和未提交提案都同步给跟随者:
在这里,需要你了解领导者向跟随者同步数据的三种方式(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 消息,并缓存在发送队列中。
第三,在 LearnerHandler.run() 函数中,领导者调用 startSendingPackets() 函数,启动一个新线程,并将缓存的数据发送给跟随者。
第四,跟随者调用 syncWithLeader() 函数,处理来自领导者的数据同步。
第五,在 syncWithLeader() 函数,跟随者接收到来自领导者的 NEWLEADER 消息后,返回确认响应给领导者。
第六,在 LearnerHandler.run() 函数中(以及 Leader.lead() 函数),领导者等待来自大多数节点的 NEWLEADER 消息的响应。
第七,当领导者接收到来自大多数节点的 NEWLEADER 消息的响应时,在 Leader.lead() 函数中,领导者设置 ZAB 状态为广播状态。
并在 LearnerHandler.run() 中发送 UPTODATE 消息给所有跟随者,通知它们数据同步已完成了。
第八,当跟随者接收到 UPTODATE 消息时,就知道自己修复完数据不一致了,可以处理写请求了,就设置 ZAB 状态为广播。
你看,这样就确保各节点数据的一致了,接下来,就可以以领导者为主,向其他节点广播消息了。
内容小结
本节课我主要带你了解了 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 - Tim2020-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 - Heaven2020-08-19少数节点为何我XID最大我不能成为领导者呢?
作者回复: 加一颗星:),不满足“大多数”原则,共识算法本质上是“多数派”算法。
共 2 条评论4 - Kvicii.Y2020-07-01感觉成员发现应该算是选举过后的一个选举补偿,而数据同步则是数据补偿
作者回复: 加一颗星:),现在看来,这两个阶段其实是可以省去的,比如Raft就没有这两个阶段,技术是在不断发展的。
3 - 小麦2022-01-21【在 ZooKeeper 中,被复制到大多数节点上的提案,最终会被提交】 如果一个提案已经被复制到大多数节点上了,但是在 Leader 向节点发送 commit 之前崩溃了,那么 follower 是没有收到 commit 请求的,那这个提案最终也会被提交吗?为什么?共 1 条评论2
- kylexy_08172020-09-05韩老师好,“只有当集群大多数节点处于广播状态的时候,集群才能提交提案”,是否意味着BROADCAST广播状态,是会与其它三个状态同时存在的呢?
作者回复: 加一颗星:),在任意时刻,每个节点只能处于一种状态,但集群中的各节点可能处于不同的状态。
1 - 春风2020-07-08当接收到领导者的响应后,跟随者会判断领导者的任期编号是否最新,如果不是,就发起新的选举; 老师,什么情况下领导者的任期编号会不是最新呢?这个时候发起新的选举,其他节点的状态是不是应该是following状态,zab状态应该是discovery状态,这个时候是怎么响应选举的呢?展开
作者回复: 加一颗星:),问题1:暂时没想到具体例子,我再想想:)。问题2:响应它认为是领导者的节点信息给这个选举状态的节点。
2 - simple_孙2021-11-13ZAB必须有数据同步的操作是不是因为Raft在提交数据的时候,跟随者会检查上一条数据是否提交成功,没成功的话就会重新同步;而ZAB的数据同步就是一个二阶段提交,没法检查上一个位置的同步结果。共 1 条评论
- 我可能是个假开发2021-06-30应该怎么样理解大多数当选领导呢? 即是LOOKING状态的节点会在一段时间内(多久呢)收集选票?对于epoch相同的情况,按zxid从大到小遍历选票,如果看到某一个zxid的数量满足大多数条件(count(zxid)>(n/2)+1),则投票该zxid中集群id最大的节点为领导者?
- Geek_672f792021-03-21韩老师, 你在ZAB协议(1) 有这么一句话:ZAB 的领导者选举,选举出的是大多数节点中数据最完整的节点。 但在本章有这么一句话 :如果写请求对应的提案“SET X = 1”未被复制到大多数节点上,比如在领导者广播消息过程中,领导者崩溃了,那么,提案“SET X = 1”,可能被复制到大多数节点上,并提交和之后就不再改变,也可能会被删除。这个行为是未确定的,取决于新的领导者是否包含该提案。 我该如何去理解?展开共 4 条评论