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

加餐 | ZAB协议(一):主节点崩溃了,怎么办?

加餐 | ZAB协议(一):主节点崩溃了,怎么办?-极客时间

加餐 | ZAB协议(一):主节点崩溃了,怎么办?

讲述:于航

时长16:11大小14.81M

你好,我是韩健。
咱们都知道,系统在运行中,不可避免会出现各种各样的问题,比如进程崩溃了、服务器死机了,这些问题会导致很严重的后果,让系统没办法运行。学完了 15 讲后,你应该还记得,在 ZAB 中,写请求是必须在主节点上处理的,而且提案的广播和提交,也是由主节点来完成的。既然主节点那么重要,如果它突然崩溃宕机了,该怎么办呢?
答案是选举出新的领导者(也就是新的主节点)。
在我看来,领导者选举,关乎着节点故障容错能力和集群可用性,是 ZAB 协议非常核心的设计之一。你想象一下,如果没有领导者选举,主节点故障了,整个集群都无法写入了,这将是极其严重的灾难性故障。
而对你来说,理解领导者选举(也就是快速领导者选举,Fast Leader Election),能更加深刻地理解 ZAB 协议,并在日常工作中,游刃有余地处理集群的可用性问题。比如如果写请求持续失败,可以先排查下集群的节点状态。
既然领导者选举这么重要,那么 ZAB 是如何选举领导者的呢?带着这个问题,我们进入今天的学习。

ZAB 如何选举领导者?

既然要选举领导者,那就涉及成员身份变更,那么在 ZAB 中,支持哪些成员身份呢?

有哪些成员身份?

ZAB 支持 3 种成员身份(领导者、跟随者、观察者)。
领导者(Leader): 作为主(Primary)节点,在同一时间集群只会有一个领导者。需要你注意的是,所有的写请求都必须在领导者节点上执行。
跟随者(Follower):作为备份(Backup)节点, 集群可以有多个跟随者,它们会响应领导者的心跳,并参与领导者选举和提案提交的投票。需要你注意的是,跟随者可以直接处理并响应来自客户端的读请求,但对于写请求,跟随者需要将它转发给领导者处理。
观察者(Observer):作为备份(Backup)节点,类似跟随者,但是没有投票权,也就是说,观察者不参与领导者选举和提案提交的投票。你可以对比着 Paxos 中的学习者来理解。
需要你注意的是,虽然 ZAB 支持 3 种成员身份,但是它定义了 4 种成员状态。
LOOKING:选举状态,该状态下的节点认为当前集群中没有领导者,会发起领导者选举。
FOLLOWING :跟随者状态,意味着当前节点是跟随者。
LEADING :领导者状态,意味着当前节点是领导者。
OBSERVING: 观察者状态,意味着当前节点是观察者。
为什么多了一种成员状态呢?这是因为 ZAB 支持领导者选举,在选举过程中,涉及了一个过渡状态(也就是选举状态)。
现在,你已经了解了成员身份,那么在 ZAB 中是如何变更成员身份,来选举领导者呢?接下来,我们就来看一下领导者的具体选举过程。

如何选举?

为了帮你更好地理解 ZAB 的领导者选举,我举个例子演示一下,为了演示方便和更容易理解(我们聚焦最核心的领导者 PK),假设投票信息的格式是 <proposedLeader, proposedEpoch, proposedLastZxid,node>,其中:
proposedLeader,节点提议的,领导者的集群 ID,也就是在集群配置(比如 myid 配置文件)时指定的 ID。
proposedEpoch,节点提议的,领导者的任期编号。
proposedLastZxid,节点提议的,领导者的事务标识符最大值(也就是最新提案的事务标识符)。
node,投票的节点,比如节点 B。
假设一个 ZooKeeper 集群,由节点 A、B、C 组成,其中节点 A 是领导者,节点 B、C 是跟随者(为了方便演示,假设 epoch 分别是 1 和 1,lastZxid 分别是 101 和 102,集群 ID 分别为 2 和 3)。那么如果节点 A 宕机了,会如何选举呢?
图1
首先,当跟随者检测到连接领导者节点的读操作等待超时了,跟随者会变更节点状态,将自己的节点状态变更成 LOOKING,然后发起领导者选举(为了演示方便,我们假设这时节点 B、C 都已经检测到了读操作超时):
图2
接着,每个节点会创建一张选票,这张选票是投给自己的,也就是说,节点 B、C 都“自告奋勇”推荐自己为领导者,并创建选票 <2, 1, 101, B> 和 <3, 1, 102, C>,然后各自将选票发送给集群中所有节点,也就是说,B 发送给 B、C,C 也发送给 B、C。
一般而言,节点会先接收到自己发送给自己的选票(因为不需要跨节点通讯,传输更快),也就是说,B 会先收到来自 B 的选票,C 会先收到来自 C 的选票:
图3
需要你注意的是,集群的各节点收到选票后,为了选举出数据最完整的节点,对于每一张接收到选票,节点都需要进行领导者 PK,也就将选票提议的领导者和自己提议的领导者进行比较,找出更适合作为领导者的节点,约定的规则如下:
优先检查任期编号(Epoch),任期编号大的节点作为领导者;
如果任期编号相同,比较事务标识符的最大值,值大的节点作为领导者;
如果事务标识符的最大值相同,比较集群 ID,集群 ID 大的节点作为领导者。
如果选票提议的领导者,比自己提议的领导者,更适合作为领导者,那么节点将调整选票内容,推荐选票提议的领导者作为领导者。
当节点 B、C 接收到的选票后,因为选票提议的领导者与自己提议的领导者相同,所以,领导者 PK 的结果,是不需要调整选票信息,那么节点 B、C,正常接收和保存选票就可以了。
图4
接着节点 B、C 分别接收到来自对方的选票,比如 B 接收到来自 C 的选票,C 接收到来自 B 的选票:
图5
对于 C 而言,它提议的领导者是 C,而选票(<2, 1, 101, B>)提议的领导者是 B,因为节点 C 的任期编号与节点 B 相同,但节点 C 的事务标识符的最大值比节点 B 的大,那么,按照约定的规则,相比节点 B,节点 C 更适合作为领导者,也就是说,节点 C 不需要调整选票信息,正常接收和保存选票就可以了。
但对于对于节点 B 而言,它提议的领导者是 B,选票(<3, 1, 102, C>)提议的领导者是 C,因为节点 C 的任期编号与节点 B 相同,但节点 C 的事务标识符的最大值比节点 B 的大,那么,按照约定的规则,相比节点 B,节点 C 应该作为领导者,所以,节点 B 除了接收和保存选票信息,还会更新自己的选票为 <3, 1, 102, B>,也就是推荐 C 作为领导者,并将选票重新发送给节点 B、C:
图6
接着,当节点 B、C 接收到来自节点 B,新的选票时,因为这张选票(<3, 1, 102, B>)提议的领导者,与他们提议的领导者是一样的,都是节点 C,所以,他们正常接收和存储这张选票,就可以。
图7
最后,因为此时节点 B、C 提议的领导者(节点 C)赢得大多数选票了(2 张选票),那么,节点 B、C 将根据投票结果,变更节点状态,并退出选举。比如,因为当选的领导者是节点 C,那么节点 B 将变更状态为 FOLLOWING,并退出选举,而节点 C 将变更状态为 LEADING,并退出选举。
图8
你看,这样我们就选举出新的领导者(节点 C),这个选举的过程,很容易理解,我在这里只是假设了一种选举的情况,还会存在节点间事务标识符相同、节点在广播投票信息前接收到了其他节点的投票等情况,这里你可以思考一下,课下自己动手操作一下。
为了帮你在线下更好的阅读代码,自我学习,我想补充一下,逻辑时钟(logicclock)(也就是选举的轮次),会影响选票的有效性,具体来说,逻辑时钟大的节点不会接收来自值小的节点的投票信息。比如,节点 A、B 的逻辑时钟分别为 1 和 2,那么,节点 B 将拒绝接收来自节点 A 的投票信息。
在这里,我想强调的是,领导者选举的目标,是从大多数节点中选举出数据最完整的节点,也就是大多数节点中,事务标识符值最大的节点。另外,ZAB 本质上是通过“见贤思齐,相互推荐”的方式来选举领导者的。也就说,根据领导者 PK,节点会重新推荐更合适的领导者,最终选举出了大多数节点中数据最完整的节点。
当然了,文字和代码是 2 种不同的表达,一些细节,仅仅通过文字是无法表达出来的,所以,为了帮你更通透地理解领导者选举的实现,接下来,我将以最新稳定版的ZooKeeper为例(也就是 3.6.0),具体说一说代码的实现。

ZooKeeper 是如何实现的?

首先,我们来看看,在 ZooKeeper 中是如何实现成员身份的?
在 ZooKeeper 中,成员状态是在 QuorumPeer.java 中实现的,为枚举型变量,就像下面的样子。
public enum ServerState {
LOOKING,
FOLLOWING,
LEADING,
OBSERVING
}
其实,在 ZooKeeper 中,没有直接定义成员身份,而是用了对应的成员状态来表示,比如,处于 FOLLOWING 状态的节点为跟随者。
在这里,我想补充一点,如果你想研究相关成员的功能和实现,那么你可以把对应的成员状态作为切入点来研究,比如,你想研究领导者的功能实现,那么,你可以在代码中搜索 LEADING 关键字,然后研究相应的上下文逻辑,就能得到自己想要的答案了。
如果跟随者将自己的状态从跟随者状态变更为选举状态,这就表示跟随者在发起领导者选举,那么,在 ZooKeeper 中,领导者选举是如何实现的呢?

如何实现选举?

领导者选举,是在 FastLeaderElection.lookForLeader() 中实现的。我来讲一下核心的流程,大概的流程,就像下面的样子。
图9
为帮助你更好的理解这个流程,我们来一起走读下核心代码,加深下印象。
1. 在集群稳定运行时,处于跟随者状态的节点,在 Follower.followLeader() 函数中,周期性地读数据包和处理数据包:
QuorumPacket qp = new QuorumPacket();
while (this.isRunning()) {
// 读取数据包
readPacket(qp);
// 处理数据包
processPacket(qp);
}
2. 当跟随者检测到连接到领导者的读操作超时了(比如领导者节点故障了),这时会抛出异常(Exception),跳出上面的读取数据包和处理数据包的循环,并最终跟随者将节点状态变更为选举状态。
public void run() {
case FOLLOWING:
......
finally {
// 关闭跟随者节点
follower.shutdown();
setFollower(null);
// 设置状态为选举状态
updateServerState();
}
break;
......
}
3. 当节点处于选举状态时,将调用 makeLEStrategy().lookForLeader() 函数(实际对应的函数为 FastLeaderElection.lookForLeader()),发起领导者选举。
setCurrentVote(makeLEStrategy().lookForLeader());
4. 在 FastLeaderElection.lookForLeader() 函数中,节点需要对逻辑时钟(也就是选举的轮次)的值执行加 1 操作,表示我们开启一轮的领导者选举,然后创建投票提案(默认推荐自己为领导者),并通知所有节点:
synchronized (this) {
// 对逻辑时钟的值执行加一操作
logicalclock.incrementAndGet();
// 创建投票提案,并默认推荐自己为领导者
updateProposal(getInitId(), getInitLastLoggedZxid(),
getPeerEpoch());
}
// 广播投票信息给所有节点
sendNotifications();
5. 当节点处于选举状态时,会周期性地从队列中读取接收到的投票信息,直到选举成功。
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) {
// 从队列中读取接收到的投票信息
Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
......
}
6. 当接收到新的投票信息时,节点会进行领导者 PK,来判断谁更适合当领导者,如果投票信息中提议的节点比自己提议的节点,更适合当领导者,更新投票信息,推荐投票信息中提议的节点作为领导者,并广播给所有节点:
else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
// 投票信息中提议的节点比自己提议的节点更适合作为领导者,更新投票信息,并推荐投票信息中提议的节点
updateProposal(n.leader, n.zxid, n.peerEpoch);
// 将新的投票信息广播给所有节点
sendNotifications();
}
7. 如果自己提议的领导者赢得大多数选票,则执行步骤 8,变更节点状态,退出选举;如果自己提议的领导者仍未赢得大多数选票,则执行步骤 5,继续从接收队列中读取新的投票信息。
8. 最后,当节点提议的领导者赢得大多数选票时,根据投票结果,判断当前节点的状态,领导者或跟随者,并变更节点状态,退出选举。
if (voteSet.hasAllQuorums()) {
......
// 根据投票结果,判断并设置节点状态
setPeerState(proposedLeader, voteSet);
// 退出领导者选举
Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);
leaveInstance(endVote);
return endVote;
......
}
需要你注意的是,在这里,我们只是演示一种选举情况,更多的情况,比如接收到来自逻辑时钟比当前节点小的节点的投票信息,再比如接收到来自领导者的投票信息,你可以在课下自己研究一下,遇到问题时,欢迎留言,咱们一起讨论。

内容小结

本节课我主要带你了解了 ZAB 是如何选举领导者的,以及在 ZooKeeper 中是如何实现的。我希望你明确这样几个重点。
1. 领导者选举的目标,是选举出大多数节点中数据最完整的节点,也就是大多数节点中事务标识符值最大的节点。
2. 任期编号、事务标识符最大值、集群 ID 的值的大小,决定了哪个节点更适合作为领导者,按照顺序,值大的节点更适合作为领导者。
学到这里,有同学可能会说:“老韩,我研究了一下,领导者是大多数节点中,已提交提案事务标识符最大的节点,因为在领导者选举的实现中,事务标识符采用的是 dataTree.lastProcessedZxid 的值,而这个变量标记的是已提交提案的事务标识符最大值。到底要怎么理解呢?”
我要先为你的探索和思考点个赞,我想说的是,在领导者选举的实现中,事务标识符采用的是 dataTree.lastProcessedZxid 的值。需要你特别注意的是,在跟随者节点正常运行时,dataTree.lastProcessedZxid 表示的是已提交提案的事务标识符最大值,但当跟随者检测到异常,退出跟随者状态时(在 follower.shutdown() 函数中),ZooKeeper 会将所有未提交提案提交,并使用 lastProcessedZxid 表示节点上提案(包括刚提交的提案)的事务标识符的最大值,在接下来的领导者选举中,使用的也是该值,也就是说,ZAB 的领导者选举,选举出的是大多数节点中数据最完整的节点。
为了方便你理解,我举个具体的例子。
A、B、C 三节点,A 是领导者,B、C 是跟随者,B 有 2 个已提交提案 (<1, 1>,<1, 2>),C 有 4 个未提交提案 (<1, 1>,<1, 2>,<1, 3>,<1, 4>),那么当 A 故障后,C 就会当选领导者。因为 C 的 dataTree.lastProcessedZxid 值(也就是 <1, 4>)大于 B 的 dataTree.lastProcessedZxid 值(也就是 <1, 2>)。
最后,你可能会好奇,我为啥会写这么多内容,来分析源码实现,除了因为代码也是一种表达,能有效弥补文字的无法表达的内容之外,还因为对于一个软件来说,最准确、最新的使用手册和技术内幕就是源码。我希望你也能养成阅读源码的习惯,将源码和文档结合起来,来准确理解软件的功能和原理。
选举出了新领导者,它是不是就可以处理写请求了呢?当然不可以,因为 ZAB 集群还需要通过成员发现(Discovery)和数据同步(Synchronization)来恢复故障,然后领导者才能行使“领导”的职能,处理写请求,这也是我会在下一讲重点带你了解的。

课堂思考

既然我提到在 ZAB 协议中,ZAB 协议是通过快速领导者选举,来选举出新的领导者的。那么你不妨想想,在选举中,会出现选票被瓜分、选举失败的问题吗?为什么呢?欢迎在留言区分享你的看法,与我一同讨论。
最后,感谢你的阅读,如果这节课让你有所收获,也欢迎你将它分享给更多的朋友。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 10

提建议

上一篇
15 | ZAB协议:如何实现操作的顺序性?
下一篇
加餐 | ZAB协议(二):如何从故障中恢复?
 写留言

精选留言(20)

  • Geek_yuanhe
    2020-05-11
    韩老师您好,raft算法跟zab的选举区别,可以理解为比较大的区别就是zab是有leader PK,而raft只是先来先得,一旦该节点已经确认投票,后面即使比他任期编号大的选票再来请求投票,也会拒绝,这样理解对么?

    作者回复: 加一颗星:),可以这么理解,Raft领导者选举的关键是随机超时时间、一个节点在一个任期只有一张选票、基于任期编号大小和日志完整度来投票。

    共 4 条评论
    21
  • zyz
    2020-05-09
    根据获取选举票数过半机制的原则,同时服务器数量为奇数,不会出现选举失败的情况

    作者回复: 加一颗星:),其实节点的奇偶数不影响选举结果,影响的是节点故障容错能力,比如,4节点集群和3节点集群的“大多数”分别是3和2,也就是n/2 + 1,都只能容忍1个节点的故障。

    共 2 条评论
    14
  • xzy
    2020-05-25
    你好,请问投票的结果如何同步的呢?当选节点知道自己是 leader,怎么让其他节点也知道呢?

    作者回复: 加一颗星:),可以这么理解,每个节点维护一个投票池,每个投票池都包含自己和其他节点推荐的领导者的节点信息,如果有节点赢得大多数投票,那么这时会判断这个节点是否是自己,如果是自己,那么节点将设置自己的状态为LEADING状态,退出选举;如果不是自己,那么节点将设置自己的状态为FOLLOWING状态,退出选举。

    共 2 条评论
    7
  • DullBird
    2020-05-18
    不会选举失败。假设要瓜分的节点是2个,那么最终这2个还是需要pk一轮。关键是zab的选票不是一张,是改变就可以投出去。

    作者回复: 加一颗星:)

    5
  • 竹马彦四郎的好朋友影...
    2020-05-06
    老师,我想问一下 "选举出了新领导者,它是不是就可以处理写请求了呢?答案是不行的,比如这个时候各节点的数据副本还不一致呢,这就需要对数据做取舍,解决冲突,实现数据副本的一致" 那是不是raft也是如此呢? 就是说raft选出的新的leader也不能立即响应写请求~ 对吗?

    作者回复: 加一颗星:),Raft是可以的,这个特性与ZAB的设计有关,在我看来,这个设计不是很精巧,我会在接下来的加餐中,具体说说。

    共 5 条评论
    6
  • 小波菜
    2020-05-21
    “逻辑时钟(logicclock)(也就是选举的轮次),会影响选票的有效性,具体来说,逻辑时钟大的节点不会接收来自值小的节点的投票信息。比如,节点 A、B 的逻辑时钟分别为 1 和 2,那么,节点 B 将拒绝接收来自节点 A 的投票信息。” 老师我想请教下, 1:逻辑时钟具体工作原理是什么,这边如果A的事务id大于B,B也直接拒绝吗? 2:事务id是如何保证全局单调递增的,类似雪花算法吗?
    展开

    作者回复: 加一颗星:),问题1:主要是为了避免接受到旧的投票信息;会的,具体细节,可参考FastLeaderElection.lookForLeader() 的实现。 问题2:是一个AtomicLong的变量(hzxid),因为领导者的存在,所以事务id,本质上是“单机”的,原子变量就可以了。

    共 2 条评论
    4
  • Kvicii.Y
    2020-06-29
    逻辑时钟到底是什么作用呢?我看到源码有这个东西,但是一直把他当做其他的判断条件,既然和选票PK类似,为什么不把逻辑时钟比较的逻辑加入到选票PK的逻辑totalOrderPredicate中呢?还是说这个逻辑时钟只是代表了机器的标识?

    作者回复: 加一颗星:),可以把逻辑时钟理解为选举的轮次,会影响选票的有效性,主要是为了避免接受到旧的投票信息。

    2
  • 钟友兵
    2020-05-10
    韩老师,如果说投票时,因为网络问题,可能出现接收到的选票出现延迟,比如,节点A只接受到自身的票,没有接收到其他节点的票,其他节点也可能出现接收到的票数不一致的情况,这种情况是如何处理,设置超时时间吗?如果是超时时间,这个值的选取一般有什么原则

    作者回复: 加一颗星:),引入超时,更确切的说是读超时,读超时且没有接收到其他节点的新的选票,重新发送自己的投票,在ZooKeeper中,这个值,初始值为200ms,之后每次超时时,指数退避,增加时长,最大值为60s,具体的实现,可以参考FastLeaderElection.lookForLeader()函数。

    3
  • 宋菁
    2020-05-04
    在网络通讯正常情况下,各个节点都能够收到其他节点的选票,此时必然会选出最终领导者,不会出现选票瓜分的情况,因为即便是两个节点的任期编号和事物标识符一样,集群ID大的仍然会当选,集群ID小的根据规则会选举集群ID大的节点为领导者。

    作者回复: 加一颗星:)

    共 2 条评论
    3
  • arun
    2022-04-11
    老师,逻辑时钟和epoch是同一个么,他们的关系是什么呢?逻辑时钟可以理解为每次leader挂掉重新选举时,逻辑时钟会重置为0,而epoch是每次触发选举都会加1,不知道理解的时候正确,谢谢老师解答
    1
  • Heaven
    2020-08-18
    ZAB协议中,ZAB协议是通过快速领导者选举,来选举出新的领导者的,那么会出现选票会瓜分的情况吗? 必然可能啊,ZAB是一种脱胎于Multi-Paxos的算法,其本质上也是一种投票选举,那么对于这种投票选举,设置不同的选举时间是一种相对较好的选择 看到这个选举突然想到了网络环路中STP的算法解决

    作者回复: 加一颗星:)

    共 2 条评论
    2
  • Geek_c89d45
    2022-07-31 来自广东
    不会出现选票瓜分的情况,因为领导者pk总能比较出优劣,即使epoch相同、zxid相同,最终还有myid兜底,myid是一定不同的。
  • 徐同学呀
    2021-10-08
    原文:当跟随者检测到异常,退出跟随者状态时(在 follower.shutdown() 函数中),ZooKeeper 会将所有未提交提案提交 但是我并没有在源码中找到(3.7.0),哪位大佬找到了可以贴一下局部代码吗
    共 1 条评论
  • 达子不一般
    2021-10-05
    为什么需要领导者pk,pk结果再进行广播? 第一轮投票广播后,每个节点可以从其他节点的response中知道自己是否是数据最完备的节点及数据最完备的节点是谁,此时可以直接更新LEADING和FOLLOWING状态了
  • Jia Tiancai
    2021-07-08
    1. 领导者选举的目标,是选举出大多数节点中数据最完整的节点,也就是大多数节点中事务标识符值最大的节点。 问题1:如果只有少数节点数据更新,选举出来的主节点数据可能不是最完整的吧? 问题2:一个任期内节点可以响应多次,那么就会存在多个节点都获取大多数选票,出现这种情况怎么处理呢?
  • amy
    2021-05-19
    韩老师,也就是在 follower 检测到 leader 崩溃退出后,follower 切换状态为选举状态并生成初始投票时,选取的事务 zxid 是所有接收到的 proposal 中最大的 zxid,尽管这个 proposal 可能还没有被 commit。 比如,除了原来旧的 leader 外,一个 proposal 仅仅只被集群中的一个 follower 收到,在重新选 leader 时,拥有这个 proposal 的 follower 会被选为新的 leader ,因为事务id zxid 最大,然后这个 proposal 会被同步到整个集群。
    共 1 条评论
  • 约书亚
    2021-02-17
    epoch和逻辑时钟是什么关系?zookeeper的paper中的伪代码用round这个变量来代表逻辑时钟,其判断逻辑和文章中说的是一样的。那epoch还有什么用处?能否起到raft中term类似的作用?有点糊涂了。逻辑时钟知否只有在lookForLeader方法中才会+1?
  • Bachue Zhou
    2021-01-27
    感觉这个选举算法好慢,中间有不少通讯其实是可以省略的,过多的通讯在网络较差的情况下必然减慢选举速度,而根据我的理解,迟迟选不出领导者,整个系统就不能写入了,也就是虽然有半数以上节点存活,系统事实上不可用。
    共 2 条评论
  • 李二木
    2020-11-06
    是怎么计算选票的了。每个looking节点是要记录投票信息吗?那looking节点怎么知道投票数超过大多数节点。也就是它怎么知道当前参与所有选举所有节点个数。
  • 西门吹牛
    2020-08-10
    逻辑时钟怎么理解,不是应该所有节点都参与选举吗,选举一次逻辑时钟加1,那所有节点的逻辑时钟不是应该相同吗
    共 1 条评论