Zookeeper Leader选举

Leader 选举 的原理,也就是大名鼎鼎的 Zab 协议的一部分。

Zab 只是一个协议,或者说只是一个概念,ZooKeeper 是这个协议的具体实现,并不是只有选举用到了 Zab 协议,而是其他地方也用到了。

一、什么是 Leader 选举?

在开始设计之前,我们肯定要先搞懂需求,产品一句话:实现一个 Leader 自动选举的功能。那这句话啥意思呢?

我们先简单回顾一下我们之前所学的 ZooKeeper 知识。

首先 ZooKeeper 是支持集群的,而且它有三个角色:Leader、Follower、Observer。我们也知道这三者之间的区别:只有 Leader 能写操作,Follower 和 Observer 只读,且 Follower 可以参与选主,而 Observer 没权利参与选主。这些知识我们前面都讲解过,如下图:

zk-角色.png

接下来,我们想一个问题:前面说了,只有 Leader 能写操作,那如果 Leader 宕机了,怎么办?这时候整个集群处于亚健康状态,完全不可写了,只能读操作。总不能一直傻傻地等着 Leader 重新起来之后再继续提供写能力吧?所以优秀的产品提了一个需求:当 Leader 宕机后,Follower 要立即开始进行选主,选主的意思就是从 Follower 当中选择一个“最优秀”的出来,让它升级为 Leader

现在需求算是彻底搞懂了,但是我们仍然有很多疑问,比如:我选择哪个 Follower 升级为 Leader 呢?选举流程是咋样的呢?宕机的 Leader 重启来后该何去何从呢?等等一系列问题,接下来就开始逐个攻破~

我们先来看第一个问题:选择哪个 Follower 升级为 Leader 呢?

二、选择哪个 Follower 升级为 Leader?

这个问题,我们大家肯定异口同声地说:当然是哪个 Follower 上的数据最新就哪个 Follower 当 Leader!没错,这是必然的,那我问你:如果多台 Follower 上的数据都一样新,这时候该让谁来当 Leader 呢?我们接下来就好好研究一下这个问题。

现在我们知道了一个条件,那就是:哪个 Follower 上的消息最新就让哪个 Follower 优先被升级为 Leader。 那我们就用 zxid 来标记,zxid 的值越大就代表数据越新。那这个 zxid 啥时候更新呢?也很简单,每一次写请求,就更新一次 zxid,让 zxid 自增 1。

接下来就来到了我们的问题:如果 zxid 一样的话,该怎么选择?

我们在搭建集群的时候都会写一个如下的配置:

server.1= ZooKeeper 1:2888:3888
server.2= ZooKeeper 2:2888:3888
server.3= ZooKeeper 3:2888:3888

我们可以发现server后面有个数字:.1 .2 .3。这是什么呢?这个其实就是一个编号,但是既然要这么写,那就有这么写的意义,这个的意义就在于 Leader 选举会用到。如果 zxid 相同的话,那就代表数据都一样,选谁都行,那索性选择一个 serverId 最大的,也就是选择server.后面数字最大的那个服务,我们也给它起个名字,叫 myid 吧。

现在我们先简单用一句话总结一下:先对比 zxid,zxid 最大的优先被选举,若 zxid 一致,那么就选择一个 myid 最大的。

leader选举-1.png

有了 zxid 和 myid 就够了吗?其实是不完美的,我的需求想要知道目前一共选举多少次了,且当前这次的 zxid 是多少,比如 Leader 宕机了,其他节点进行了重新选主,选完后我希望 zxid 从 0 开始,就好比古代更朝换代,都更朝换代了,我不可能留着上一波的东西,所以我要初始化 zxid。那这里也简单了,加一个字段标记朝代次数不就好了?每次重新选主成功后就给朝代次数加 1 且初始化 zxid。

可以的,但是不够优雅,我们可以把这两个字段合二为一,统称为 zxid。我们 zxid 本身设计是一个自增的 long 类型字段,long 类型是 64 位的,我们可以把前 32 位作为朝代次数,后 32 位作为事务日志id(也就是我们前面一直说的 zxid ),然后这前 32 位结合后 32 位统称为 zxid。

比如,zxid 初始化的时候肯定是 0,因为朝代没换过,也没写请求进来过。所以是如下的样子:

00000000000000000000000000000000 00000000000000000000000000000000

比如,这时候客户端发起了 3 次写请求,相当于后 32 位的结果为 3,3 的二进制是 11,所以如下:

00000000000000000000000000000000 00000000000000000000000000000011

再比如这时候 Leader 宕机了,需要重新选举,选举完新 Leader 后,前 32 位要加 1,后 32 位初始化,也就是后 32 位归 0,如下:

00000000000000000000000000000001 00000000000000000000000000000000

到这里,想必你已经明白 zxid 到底是怎么玩的了,一句话:zxid 是一个 long 类型的字段,前 32 位代表朝代,我们这里称为 epoch,后 32 位称为事务次数,也就是写请求次数。

细心的同学肯定在想一个问题:后 32 位是事务次数,那如果 Leader 很坚挺,一直不宕机,写请求又很频繁,把后 32 位都写满了,如下效果:

00000000000000000000000000000001 11111111111111111111111111111111

这时候再有新的写请求进来后会报错吗?不会!会给 epoch 加 1,然后后 32 位归 0。如下:

0000000000000000000000000000010 00000000000000000000000000000000

好了,到目前为止,你应该已经彻底搞懂了 zxid 是什么以及 zxid 什么时候变更。那选主流程是不是也有点变化了呢?我们之前说对比 zxid 和 myid 就行了,但是现在 zxid 的语义发生了变化,zxid 是由两部分组成:epoch(朝代/纪元)和事务日志次数。 现在单纯靠事务日志次数和 myid 来对比肯定是不行的,因为事务日志次数会随着每次选主后而归 0。

那该如何选主呢?也很简单,epoch 越大的就代表越新,因为每次选主都会自增 epoch,那先判断 epoch 不就好啦,epoch 越大的优先被选举;epoch 相同的,再对比 zxid 的后 32 位,也就是事务日志次数,值越大代表写的数据越多,那肯定数据越新;假设事务日志次数也一样,那就按照老套路对比 myid,找到 myid 最大的那个 Server 即可。

画个简易图如下:

持久化-4.png

我们在进入选举流程之前先思考一个问题:选举是很复杂的流程,那么选举肯定要有状态的吧?比如,谁成为了 Leader,谁成为了 Follower,谁成为了 Observer 等,都需要一个字段来标记当前 Server 是什么状态,我们先称这个字段为:选举状态。它包含如下值:

  • LOOKING:竞选状态,也就是说此状态下还没有 Leader 诞生,需要进行 Leader 选举。
  • FOLLOWING:Follower 状态,对应我们之前介绍的 Follower 角色对应的状态,并且它自身是知道 Leader 是谁的。
  • OBSERVING:Observer 状态,对应我们之前介绍的 Observer 角色对应的状态,并且它自身是知道 Leader 是谁的。
  • LEADING:Leader 状态,对应我们之前介绍的 Leader 角色对应的状态。

其实我觉得 ZooKeeper 有一个点的设计很巧妙,那就是 zxid,巧妙采取高低 32 位用一个 long 类型字段代表两种含义,然后通过位运算高效率地进行运算,好处在于不用逻辑啰嗦地维护两个字段,两个字段之间还是有强关联的,还需要保证这两个字段是原子操作。类似这种设计不光是 ZooKeeper 里有,我们 JDK 里的读写锁也是一个字段代表读锁和写锁两种锁,类似的设计还有很多,这种方式还是值得学习和借鉴的。

再看第一个问题:Leader 选举流程到底是咋样的之前,我们需要先搞懂选举的时候都传递哪些参数。

一、投票参数

我们接着上篇提到的需求进行分析,需求就是:要实现一个 Leader 选举功能,我们上一篇也讲解过大致的核心流程和必须字段了,就是对比 zxid + myid 的方式。服务器那么多,且服务器都是自己知道自己的数据,不知道其他服务器的数据( zxid / myid ),那我们该怎么对比 zxid + myid 呢?

可能有的同学立马想到了:通过网络给发出去,就好比提供一个接口一样,然后把自己服务器的数据带上,请求其他机器的接口进行通知,相当于广播。能想到网络是好的,也确实是通过网络,但是本篇幅不涉及这块内容,等我们彻底剖析完 Leader 选举后,再去剖析网络这一块内容。

现在我们知道了是通过网络广播的,但是收到广播的消息后肯定要和自身对比吧?对比完发现我的 myid 大,那我怎么通知其他机器说:我收到了谁谁谁的消息,然后我比它的 myid 大,我胜出了,我比它更适合当 Leader。还是网络,我还要广播给其他机器。就这样如此反复,那我称这个过程叫投票

那我还有一个问题:你说通过网络传输,然后进行投票。那传输哪些数据呢?就好比你请求接口,参数是什么呢?

首先会有 zxid,对吧?也就是 epoch+写次数。其次会有 myid。这两个是必须的,相信大家肯定都不陌生了。所以目前有如下两个参数。

  • zxid:当前服务器上所保存的数据的最大 zxid。
  • myid:当前服务器的 myid。

这两个参数够了吗?是不是还需要发送被推荐服务器的 zxid 和 myid?也就是:我是谁、我的 zxid 是多少、我推荐谁当 Leader、我推荐 Leader 的 zxid 是多少。所以到目前为止一共四个参数。

  • self_ zxid:当前服务器上所保存的数据的最大 zxid。
  • self_ myid:当前服务器的 myid。
  • vote_id:被推荐服务器的 myid。
  • vote_ zxid:被推荐服务器上所保存数据的最大 zxid。

现在好像是可以满足需求了,但是假设根据这四个参数选举出来 Leader 了,那这时候会更新服务器状态,从 LOOKING 变为 Leader。那是不是也要把这个状态广播给其他服务器,这样其他服务器发现已经有人是 Leader 了,那就把自身改为 Follower。所以我们还缺少一个至关重要的字段:状态。

  • state:当前服务器的状态,包括 LOOKING、FOLLOWING、LEADING、OBSERVING。

这五个参数真的可以了吗?既然我这么问,那肯定是不够的……那还差什么参数呢?依然采取引入问题的方式来补充。我们再想一个问题:比如 N 个节点进行投票,投了 2 轮后某个节点突然宕机了,只是宕机了一个节点,并不影响其他节点继续投票。但是当这个宕机的节点重启回来后再次发起自己的投票,那这个节点的票据还可信吗?肯定是不可信的,因为它落后了几轮投票,它的数据不能保证是准确的。那怎么办?

很简单,我们多加一个参数:投票次数,每一轮投票后,这个次数就加 1,这样每次投票选举的时候就先判断这个投票次数,如果发现别的节点的次数小,那这个节点肯定很不幸发生了意外,导致落后了,那就弃掉这个节点。

还是说刚才的例子:N 个节点投票,投了 2 轮后某个节点突然宕机了,当这个节点重启回来后有两种可能。

  • 第一种可能性:重启回来,状态是 LOOKING,重新发起投票,但是其他节点收到它的投票后发现它的投票次数是 0(因为自己是刚重启回来的,次数肯定是 0,次数是保存在内存中的),比自己的小,直接就舍弃不管了。
  • 第二种可能性:重启回来,收到别人的投票,这时候发现别人的投票次数都比自己的大(因为自己是 0 嘛),那就直接加入到其他节点中,没权利选举为 Leader。

这个投票次数我们也给它起个名,称为逻辑时钟,logicClock

好了,到这里我们的参数就完全定义好了,一共如下六个字段。

  • self_zxid:当前服务器上所保存的数据的最大 zxid。
  • self_myid:当前服务器的 myid。
  • vote_id:被推荐服务器的 myid。
  • vote_zxid:被推荐服务器上所保存数据的最大 zxid。
  • state:当前服务器的状态,包括 LOOKING、FOLLOWING、LEADING、OBSERVING。
  • logicClock:逻辑时钟,保存在内存中,每轮投票后就自增 1,它表示这是该服务器发起的第多少轮投票。

多啰嗦一句,再次强调一个知识点:epoch 和 logicClock 什么区别?

  • 首先,epoch 是全生命周期的,logicClock 是选主时候的生命周期,也就是说 epoch 是全局变量,logicClock 是局部变量。
  • 其次,epoch 是朝代,是纪元,也就是当前 Leader 是第几代,logicClock 是投票次数。举个例子:假设皇帝都是选票选出来的,有可能第一轮选举没出结果,那就得多选几轮,这里的几轮就是 logicClock,选出来皇帝后 epoch 才加 1。epoch 是朝代,logicClock 是大臣投票的次数。可能投了 5 次才选出皇帝来,那 logicClock 就是 5,而 epoch 只加 1。
  • 最后,epoch 是 zxid 的前 32 位,是持久化的。而 logicClock 是局部变量,存到内存里的,选出来主后就归 0 了,相当于方法结束后生命周期就结束了,就是一个局部变量,用于选主,用于给 epoch 加 1。

接下来就正式步入正轨:Leader 选举流程到底是咋样的?

二、Leader 选举流程到底是咋样的?

首先我们能明确的是:每个服务器都单独维护自己的 zxid、 myid 以及 logicClock。我们也知道选举流程的核心比较,就是每次投票的时候 logicClock 都加 1,然后先对比 zxid,zxid 大的就选 zxid,zxid 一样的话就对比 myid,找到 myid 最大的那个节点。

我们目前只知道这种粗粒度的,一些细节我们并不清楚,相当于我们前面所说的内容只是代表这个需求可以实现,但是具体的实现方案还没有梳理,那到底该如何设计呢?

我们可以让每个服务器维护一个票箱,该票箱记录了所收到的选票。比如:服务器 2 投票给了服务器 3,而服务器 3 将宝贵的一票投给了服务器 1,服务器 1 把票投给了自己,那么这时候服务器 1 的投票箱是这样的:(2,3)、(3,1)、(1,1)。如下图所示:

leader选举-3.png

假设这时候服务器 3 反悔了,又重新把票投给了自己,不投给 1 了。那这时候服务器的票箱就是:(2,3)、(3,3)、(1,1)。也就是说服务器保存的票箱里记录的是每一投票者的最后一票。

这时候你肯定有个疑问:怎么把票发出去的呢?开头已经说过了,通过网络发送,广播出去。比如,服务器 3 将票投给自己,那么 (3,3) 这个票据会发给每一个服务器,目标服务器收到后会把票据存到票箱

我们前面说投给 zxid 最大的、投给 myid 最大的,那我怎么知道哪个服务器的 zxid 最大、哪个服务器的 myid 最大呢?也就是说我怎么知道把票投给谁呢?所以,索性第一轮投票我就先投给自己,也就是服务器 1 投给自己、服务器 2 投给自己、服务器 3 也投给自己,以此类推,如下图:

leader选举-4.png

那其他服务器收到各自的投票后该如何处理?这就很简单了,和我们之前设计的流程就大同小异了,我们上面说过参数了,共 6 个: self_ zxid 、self_ myid 、vote_id、vote_ zxid 、state、logicClock。所以,其实就是(self_id,self_ zxid)(vote_id,vote_ zxid)的对比,也就是我们之前说的 zxid 和 myid 的对比。

具体细节如下。

  1. 如果接收到的 logicClock 大于自己的 logicClock,说明该服务器的选举轮次落后于其他服务器的选举轮次,那么先把自己的 logicalclock 更新为收到的,然后立即清空自己维护的票箱,接着对比 myid、zxid、epoch,看看收到的票据和自己的票据哪个更适合作为 Leader,最终再次将自己的投票通过网络广播出去。

    我们来举个例子:

    比如服务器 A 和服务器 B 两个节点,服务器 A 的 logicClock 为 2,服务器 B 的 logicClock 为 3,那么服务器 A 收到了服务器 B 的投票信息,这时候服务器 A 发现服务器 B 的轮次(logicClock)更大,那就代表服务器 A 是落后的,则会立即清空自己维护的票箱并把自己的 logicClock 改为服务器 B 的 logicClock,然后对比 myid、 zxid 和 epoch,看看哪个更适合成为 Leader,最后通过网络广播给其他机器。

  2. 如果接收到的 logicClock 等于自己维护的 logicClock,那就对比二者的 vote_zxid,也就是对比被推荐服务器上所保存数据的最大 zxid。若收到的 vote_zxid 大于自己 vote_zxid,那就将自己票中的 vote_zxid 和 vote_id(被推荐服务器的 myid )改为收到的票中的 vote_zxid 和 vote_id 并通过网络广播出去。另外将收到的票据和自己刚改后的 vote_id 放入自己的票箱。

    我们来举个例子:

    比如服务器 A 和服务器 B 两个节点,服务器 A 和服务器 B 的 logicClock 都为 2,但是服务器 A 的 vote_zxid 是 16,服务器 B 的 vote_zxid 是 20,那么服务器 A 收到了服务器 B 的投票信息,这时候服务器 A 发现服务器 B 的 vote_zxid 比自己的大,那就代表服务器 B 推荐的节点上的数据是最新的,则会将服务器 A 的 vote_zxid 从 16 改为 20,且修改服务器 A 的 vote_id 为服务器 B 的 vote_id,并通过网络广播给其他机器。

  3. 如果接收到的 logicClock 等于自己维护的 logicClock 且二者的 vote_zxid 也一致,那就比较二者的 vote_id,也就是被推荐服务器的 myid。若接收到的投票的 vote_id 大于自己所选的 vote_id,那就将自己票中的 vote_id 改为接收到的票中的 vote_id 并通过网络广播出去。另外,将收到的票据和自己刚改后的 vote_id 放入自己的票箱。

    这个就无需举例了吧~

  4. 如果接收到的 logicClock 小于自己的 logicClock,那么当前服务器直接忽略该投票,继续处理下一个投票。

这个选举投票流程我相信已经很清晰了,唯一有一点瑕疵就是:logicClock 啥时候自增呢?每一轮投票就给 logicClock 加 1,这个后面我们分析源码的时候一目了然。

现在我们设计的 6 个参数已经用上了 5 个了,还差 state 没有用上,state 是什么意思来着?服务器状态,只有 LOOKING 状态才会选举,也就是上述过程都是基于 LOOKING 状态的,那选举完了是不是要更改状态呀?所以 state 参数很简单,就是服务器状态,选举完后会看自身服务器是不是 Leader,如果不是的话就改为 FOLLOWING;如果自身服务器是 Leader,那就将 state 改为 LEADING。

那再看本篇最后一个问题。

要几个服务器同意才能被真正选举呢?假如:服务器 1、服务器 2、服务器 3 参与投票竞选 Leader,这时候服务器 1 投票给了服务器 2,服务器 2 投票给了自己,服务器 3 也投票给了自己,这时候票箱的数据是这样的:(1,2)、(2,2)、(3,3),目前很明显的是服务器 1 有零票,服务器 2 有两票,服务器 3 有一票。相当于没得到全部服务器的认可,因为服务器 3 投给了自己,没有投给服务器 2,那是不是还要继续发起投票重新选举呢?这什么时候是个头呀?

所以不是的!过半即可,也就是过半原则(n+1/2),也就是超过半数以上的节点同意即可被选举为 Leader。举个例子:上面的票箱是 (1,2)、(2,2)、(3,3),一共 3 个服务器,两个服务器都选择了 2 号,3 个服务器的半数以上是3/2 =1,半数以上是1+1=2,我们这里正好有两个服务器同意 2 号当选,所以符合过半原则。

为啥过半原则,后面讲解 Zab 专题的时候细说。

一、集群启动时候选举的流程

先来看第一种 Leader 选举时机:ZooKeeper 服务启动的时候。

在讲解之前,我先来问你一个问题:为什么集群启动时候会进行选举?很简单,因为刚启动的时候,节点状态都是 LOOKING,寻主状态,而且是刚启动,肯定是无主的,需要选举的,所以肯定需要投票进行 Leader 选举。

假设我们要搭建一个 ZooKeeper 集群,集群内有三个 ZooKeeper 服务,分别为:Zookeeper1、Zookeeper2 和 Zookeeper3。按顺序启动,那么哪台机器最有可能被选举为 Leader 呢?为什么?

我先不说答案,我们先分析下集群启动时候的选举流程,然后我们再反过来看这个问题。

首轮投票都会投给自己,因为集群内彼此不知道各自的 zxid 和 myid 等参数,所以 Zookeeper1、Zookeeper2 和 Zookeeper3 的票箱如下图所示:

image.png

由于第一轮,大家都自己投了自己,这时候票箱如下:

服务 票箱
Zookeeper1 (1,1)、(2,2)、(3,3),相当于每人都只有1票,持平
Zookeeper2 (1,1)、(2,2)、(3,3),相当于每人都只有1票,持平
Zookeeper3 (1,1)、(2,2)、(3,3),相当于每人都只有1票,持平

现在每个节点的票数都一样,都是 1 票,所以不分胜负,那只能进行下一轮投票了。那下一轮投给谁呢?我们先把上一篇画的图拿出来重新回忆一下选举流程:

zktest1.png

按照我们在上一篇中的设计来分析的话,先对比 logicClock,优先投给 logicClock 大的;但是现在发现 logicClock 都一样大,也就是走到了上图中的第 6⃣️ 步骤,开始对比 vote_zxid。

vote_zxid 是什么来着?vote_zxid 就是被推荐服务器上所保存数据的最大 zxid。

但是,现在三个 ZooKeeper 服务都把票投递给了自己,并且因为都是刚启动的,还没处理请求,也是首轮选举,所以它们的 epoch 和事物写次数都是 0,也就是说这三个 ZooKeeper 服务的 zxid 也都一样,都是 0。于是就走到了我们上图中的第 7⃣️ 步骤,开始对比 vote_id。

vote_id 就是被推荐服务器的 myid。

那很明显,Zookeeper3 的 myid 是最大的,所以事情变得简单了,终于有结论了。Zookeeper1 和 Zookeeper2 会将票投给 Zookeeper3,而 Zookeeper3 仍然会投给自己。详细核心流程如下。

  1. Zookeeper1 收到 Zookeeper2 和 Zookeeper3 的投票后,发现三者的 logicClock 和 vote_zxid 都一样,所以只能找最大的 myid 进行选举,发现 Zookeeper3 的 myid 是3,而 Zookeeper2 的 myid 是 2,因此 Zookeeper1 将宝贵的一票投给 Zookeeper3。

  2. Zookeeper2 收到 Zookeeper1 和 Zookeeper3 的投票后,发现三者的 logicClock 和 vote_zxid 都一样,所以只能找最大的 myid 进行选举,发现 Zookeeper3 的 myid 是 3,而Zookeeper1 的 myid 是 1,因此 Zookeeper2 将宝贵的一票也投给 Zookeeper3。

  3. Zookeeper3 同理,将票留给自己。

image.png

第二轮投完了,这时候我们看下票箱情况:

服务 票箱
Zookeeper1 (1,3)、(2,3)、(3,3),三个节点都投递给了Zookeeper3
Zookeeper2 (1,3)、(2,3)、(3,3),三个节点都投递给了Zookeeper3
Zookeeper3 (1,3)、(2,3)、(3,3),三个节点都投递给了Zookeeper3

所以毫无争议,大家都认为 Zookeeper3 最适合当 Leader,其实我们本来是过半就行,但是目前大家都同意,那自然最好不过了。Leader 选完了,我们接下来需要干什么?改状态呀!现在状态还是 LOOKING 呢,我们需要给 Zookeeper3 改为 LEADING,而 Zookeeper1 和 Zookeeper2 改为 FOLLOWING 状态,然后 Leader 发起与各个 Follower 之间的心跳。

image.png

好了,目前我们已经讲解完第一个选举时机的全流程了,接下来回归到我们最初的问题:集群内有三个 ZooKeeper 服务,分别为 Zookeeper1、Zookeeper2 和 Zookeeper3,按顺序启动,那么哪台机器最有可能被选举为 Leader 呢?为什么?

按照我们上面的分析,好像最后选出来的是 Zookeeper3 这个服务,但最后选出来的 Leader 真的是 Zookeeper3 吗?错!因为我说了按顺序启动,你想想按顺序,也就是先启动 Zookeeper1,然后 Zookeeper1 开始寻主,发现只有自己,不能构成集群,所以不能升级为 Leader;这时候 Zookeeper2 启动了,也开始寻主、投票。开始都会投给自己,但是第二轮的时候 Zookeeper1 发现 Zookeeper2 的 myid 最大,会把票投给 Zookeeper2,那这时候的结果就是 Zookeeper1 零票、Zookeeper2 两票。按照过半原则,已经符合升级为 Leader 的条件,所以这时候 Zookeeper2 就已经被选为 Leader 了。

有人问:那 Zookeeper3 呢?你想一个问题,选主是毫秒级别的,而服务启动是秒级别的,在 Zookeeper3 启动完成之前,Zookeeper2 早就被选为 Leader 啦!因此,这个问题的答案大概率是 Zookeeper2 被选为 Leader!

那还有人问:Zookeeper3 启动完成后咋办呢?它当然也会进行投票,但是投完后发现 Zookeeper1 和 Zookeeper2 都回应已经有Leader了且 Leader 是 Zookeeper2,那么 Zookeeper3 发现过半的人都回复 Leader 是 Zookeeper2,那就不折腾了,直接作为 Follower 追随 Zookeeper2 这个 Leader。

二、Follower 重启投票流程

我们接着看第二个投票时机:Follower 宕机重启了,这时候该怎么办?

在这之前,我先问个问题:Follower 宕机有啥问题?啥问题也没有,无伤大雅!只是少了一台节点对外提供读能力而已,因为只有 Leader 能处理写请求,所以我们先可以明确一点就是 Follower 意外宕机,其实影响范围不是很大。

好了,回归正题:Follower 重启投票流程。

假设 Zookeeper1 重启了,首先 Follower 重启后的状态是 LOOKING,所以会发起投票,且首轮会把票投给自己然后通过网络广播出去,如下图所示:

image.png

这时候 Zookeeper2 和 Zookeeper3 收到了 Zookeeper1 的投票,会响应给 Zookeeper1 什么呢?

  • 首先,假设 Zookeeper3 是 Leader,那么 Zookeeper3 会给它回复一个:我就是 Leader,你还投什么?直接变 Follower 给我当小弟。

  • 其次,Zookeeper2 收到 Zookeeper1 的投票后,也会回复一个:Zookeeper3 是 Leader,我是 Follower,在当 Zookeeper3 的小弟,你别投了,已经有 Leader 了,你也一起加入吧。

  • 最后,Zookeeper1 收到其他两台机器的回复后,发现 Zookeeper3 就是 Leader 的票数超过一半了,所以 Zookeeper1 心甘情愿地改为 Follower 给 Zookeeper3 当小弟。

具体过程如下图:

image.png

当然不能忘了改状态,给 Zookeeper1 修改状态为 Follower,且 Zookeeper3 会维持 Follower 的心跳。

image.png

上面讲解了 Follower 挂了的投票流程,那么如果是 Leader 挂了,该怎么办?

三、Leader 重启投票流程

Leader 挂了就不是那么简单的事情了,相当于老大没了,这时候所有的写请求都无法得到处理,那就是属于亚健康状态了,所以需要进行选主。那怎么选主呢?我不怕啰嗦,再从头到尾以图文结合的方式阐述下。

首先,假设 Zookeeper3 是 Leader,然后它挂了,所以 Follower1 和 Follower2 发现 Leader 挂了(没心跳了),就会进入 LOOKING 状态,重新进行选举。我们先来看下图(Leader 挂了,Follower 进入 LOOKING 状态):

image.png

然后,两个 LOOKING 状态的 Zookeeper 要重新发起选举,老规矩,还是先投自己,然后通过网络广播出去。如下图:

image.png

接下来又是重复的流程。

  • Zookeeper2 收到 Zookeeper1 的投票,发现二者的 logicClock 和 zxid 都一样,那就对比 myid,结果发现 Zookeeper2 自己的 myid 是大于接收到外部 Zookeeper1 发起的投票的 myid,所以 Zookeeper2 仍然投自己一票并通过网络发给 Zookeeper1。

  • Zookeeper1 同样收到 Zookeeper2 的投票,结果发现二者的 logicClock 和 zxid 都一样,myid 还小于 Zookeeper2 的,那就也将宝贵的一票投给 Zookeeper2。

如下图:

image.png 这时候我们看下票箱情况:

服务 票箱
Zookeeper1 (1,2)、(2,2),Zookeeper2两票,Zookeeper1零票
Zookeeper2 (1,2)、(2,2),Zookeeper2两票,Zookeeper1零票

符合过半原则,所以 Zookeeper2 被选举为 Leader,而 Zookeeper1 成为 Zookeeper2 的 Follower,最后修改各自的状态且在二者之间建立心跳。

image.png

看起来好像一切恢复正常了,那如果 Zookeeper3 重新启动了呢?这时候会怎样?

首先 Zookeeper3 重新启动它的状态会变为 LOOKING,所以它会再次投给自己一票,并通过网络广播给其他人,如下:

image.png

这时候当 Zookeeper1 和 Zookeeper2 收到 Zookeeper3 的广播后,会给予相应的回应。

  • Zookeeper1 会回复:你别投票了,现在整个集群是有主的,Zookeeper2 就是主,我是追随它的 Follower,你也一起加入吧。

  • Zookeeper2 会回复:我就是 Leader,你想谋权?赶紧当我小弟(Follower)追随我,别找事。

image.png

Zookeeper3 收到各位大佬的回复后,发现 Zookeeper1 和 Zookeeper2 都说 Zookeeper2 是 Leader,且集群算上自己就三台机器,符合过半原则了,那就不犹豫了,直接修改自己的状态为 FOLLOWING 去追随 Zookeeper2,然后 Zookeeper2 维护 Zookeeper3 这个 Follower 的心跳。

image.png

看到这里,有的同学可能会产生这样一个疑问。假设写请求刚写到 Leader,尚未同步到各个 Follower 呢,然后 Leader 宕机了,接下来其他 Follower 肯定会选出一个新的 Leader。但这时候恰巧新 Leader 刚选出来,还没接收写请求呢,旧 Leader 就恢复了,别忘了旧 Leader 宕机之前是写成功的,所以旧 Leader 的 zxid 肯定大于新 Leader 的,也就是说旧 Leader 的消息更全、更新,那么旧 Leader 恢复后,会顶替新 Leader 吗?

不会!我不管你 zxid 是不是大,你启动回来后就是 LOOKING 寻主状态,但是集群内已经有新的 Leader 了,旧 Leader 只能抹掉数据同步我新 Leader 的。

你想想,如果让你设计一个 ZooKeeper 的选举功能,你考虑一大堆情况,只会造成代码的复杂性臃肿性,因此没必要为一些非常见场景而破坏整个优雅的项目,情况很多,考虑不完的,何况目前 ZooKeeper 的设计我个人觉得已经很合理了。

一言以蔽之:Leader 选举有三个时机,集群刚启动、Follower 宕机和 Leader 宕机,核心选举流程就是先对比 logicClock ➡️ 再对比 zxid➡️最后对比 myid,选出 Leader 后修改状态以及建立心跳。

posted @ 2023-03-10 22:25  Dazzling!  阅读(339)  评论(0编辑  收藏  举报