在之前的【Zookeeper系列】基本介绍里有提到 ZK 的角色,那篇文章只是简单介绍 Leader
、Follower
和 Observer
这三种角色。那么在一个 ZK 集群中,我怎么知道 ZK 服务是哪一个角色呢?角色是怎么分配的?为什么某个 ZK 是 Follower,而不是 Leader ?结论先行,先简单回答上面的问题。
我怎么知道 ZK 服务是哪一个角色呢? 可以在 ZK 安装包的
bin
路径下执行:zkServer.sh status,可以很清楚的看到打印出来Mode对应的角色,有可能是 leader,follower 或 observer。角色是怎么分配的?为什么某个 ZK 是 Follower,而不是 Leader ? 通过 ZK 内部的选举机制,其细节可继续往下看这篇文章...
[ps.为了降低复杂度,本篇文章并不会深入聊zab协议]
前置内容:
ZXID:当发生写请求的时候,在 ZK 内会发起一个事务,每个事务会分配一个 zxid
作为标识符。zxid 是64位的 long 类型,高32位代表时间戳,低32位可以认为是事务ID,用来标识服务器状态的变更次数
SID:服务器ID。用来标识一台在 ZooKeeper 集群中的机器,每台机器不能重复。注意:这里和启动时指定的myid一致
Epoch:每个 Leader 任期的代号。每次新选举一个 Leader 就会递增1
Observer:不参与投票和选举【换句话说,不参与 Leader 的选举,也不会投票给某个节点】
节点状态:
LEADING:Leader节点的状态
LOOKING:参与选举的状态
FOLLOWING:Follower节点的状态
OBSERVING:Observer节点的状态
好了,前置知识大概这么多,接下来咱们看一下,上面这些东西和选举有什么关系。
选举目的是为了从一堆 ZK 的服务节点中找一个大佬(Leader),然后所有非 Observer 节点都会成为小弟(Follower)。Leader负责读写数据,而 Follower 会分担大佬的负担,分担部分的读请求。
那么问题来了,根据什么条件判断,认为某一台 ZK 服务能成为 Leader 呢?
实际上,选举的规则是按照 Epoch > 事务Id > SID
依次由大到小排序,值最大作为 Leader。
简单来说,选举的时候会比较epoch
的大小,如果所有 ZK 节点的 Leader 任期大小一样,则继续比较 ZXID 的低32位,也就是事务Id大小,如果事务ID大小都一样的话,就会比较服务器ID(SID)
ps. 由于我是基于 Docker 部署 ZK 集群,进入其中的ZK节点,执行 cat /data/version-2/currentEpoch
既可以看到epoch的大小
root@zoo3:/# cat /data/version-2/currentEpoch
5
ZK 集群选举
接下来,看下 ZK 集群是怎样选举 Leader 的,但值得注意的是,选举区分了第一次启动和非第一次启动。第一次选举流程比较简单,但非第一次选举的时候,会涉及到 Leader 宕机或假死的情况,这样的话细节就会多一些。
背景:ZK集群是由5台ZK节点组成,只要集群中过半的节点投票就可选出 Leader
第一次启动
在集群节点启动之初,所有的epoch都是一样的,此时 Leader 还没选举出来,此时不会有写请求进来,所以事务Id也是没有的,所以最终的判断条件是根据SID,也就是节点的ZOO_MY_ID
决定,理论上来说,ZOO_MY_ID 值最大的就会成为Leader,但为什么说是理论上呢?咱们来看下实际的效果。
场景1:
基于docker-compose一次性部署5台ZK节点,参考我另外一篇博客【Zookeeper系列】基于docker-compose快速搭建Zookeeper集群
最终结果是例子中的zoo5成为Leader,符合上面 ZOO_MY_ID 值最大的就会成为 Leader 的说法。
场景2:
先把5台节点都关掉【执行docker-compose stop,模拟集群的节点准备启动的状态】,然后依次执行
docker start zoo1
docker start zoo2
docker start zoo3
集群中有5台节点,先启动3台,符合过半投票的情况,这时候已经可以选举出 Leader,盲猜一下,应该是 zoo3 这台机器成为Leader 了吧?进入 zoo3 容器,执行 zkServer.sh status 查看节点角色。果然,此时的 zoo3 是 Leader
此时,再依次执行 docker start zoo4 和 docker start zoo5,可以发现,Leader依旧是 zoo3。这是因为,当成功选举出 Leader 后,后续启动的 zk 节点都会成为 Follower(现在先不讨论Observer的情况)。
但这种结果并不与选举规则冲突,当 zk 集群内的机器不是同一时刻启动的时候,其大致选举流程是:
先启动 zoo1 ,zoo1会先给自己投票,但由于票数没有过半,所以 zoo1 此时是 LOOKING 状态;
接着启动 zoo2,zoo2一开始也会给自己投票,然后与 zoo1 交换投票信息。zoo1会发现当前的 epoch 和 zxid 都与 zoo2一样,但 zoo2 的 Sid 比自己大,zoo1 就会改票,将自己的那一票给了 zoo2,此时 zoo2 虽然有 zoo1 的投票支持,但投票票数还是没超过一半,不能成为Leader,zoo2 也保持 LOOKING 状态;
再启动 zoo3,同样的 zoo3 也会先给自己投票后,再和 zoo1、zoo2 交换投票信息。zoo1 和 zoo2 都发现与 zoo3 的 epoch 和 zxid 一样大小,但 zoo3 的 Sid 比自己高,所以 zoo1 和 zoo2 都改票投给了 zoo3,此时 zoo3 有了3张票(包括自己自投),超过半数,所以 zoo3 就成为了 LEADING 状态,zoo1 和 zoo2 成为了 FOLLOWING 状态。
接着启动 zoo4 和 zoo5,此时 zoo1、zoo2 和 zoo3 节点都不是 LOOKING 状态,不会交换投票的信息,发现 zoo3 是 LEADING 状态,所以 zoo4 和 zoo5 都投票给 zoo3,并更改自己的状态为 FOLLOWING 状态
但为什么在场景1的时候,不是 zoo3 成为 Leader 而是 zoo5 成为 Leader 呢?这是因为在近乎同一时刻,zk 集群所有的服务都启动了,此时所有节点都是先投票给自己,然后再与其他节点交换信息,发现 zoo5 的 Sid 最大,接着所有的节点都投票给了 zoo5 ,其余节点就都处于 FOLLOWING 状态,而 zoo5 是 LEADING 状态。
非第一次启动
场景1:
假设 zoo3 是 Leader,其余都是 Follower。当 zoo3 宕机,其余节点都变为 FOLLOWING 状态,重新参与选举。
为降低复杂度,将真实的 zxid 简化为只有事务Id。假设 zoo1、zoo2、zoo4、zoo5 的 (epoch,zxid,sid) 分别是 (1,13,1),(1,10,2),(1,13,4),(1,12,5)
此时选举流程是怎样呢?
首先存活节点发现大佬没了,就会将自己的状态更改为 LOOKING 状态,此时所有节点都会给自己投票,然后与其他节点交换投票信息;此时发现大家的epoch一样,则比较彼此的 zxid
很明显,zxid 最大的是 zoo1 和 zoo4,再继续比较彼此的 sid
此时可以轻易得出,zoo4 的 sid 最大,就会将 zoo4 的状态更改为 LEADING。其余节点投票给 zoo4,并更改状态为 FOLLOWING
场景2:
zoo3 为 LEADING 状态,此时 zoo3 假死。如果 zoo3 忽然网络出问题,断开与其他 follower 节点的连接。其他节点以为 zoo3 宕机,则重新选举新 Leader,假设此时 zoo1 成为大佬,此时 zoo3 忽然正常了,那么剩下的 follower 节点会不会蒙圈?怎么会有两个大佬,正所谓一山不能容二虎,我应该听谁?
实际上,zk 有考虑到这种情况,还记得上面说的 epoch 的定义吗?这是每个 Leader 的代号,每更新一次 Leader,epoch 就会递增1。假如 zoo3 假死前,所有的节点的 epoch 都是2。zoo3 假死,zoo3 的 epoch 不会变,因为没想到有其他节点想替代自己的位置嘛。但其他节点选举 zoo1 为 Leader 后,除了 zoo3 ,其他的节点的 epoch 都是3了。此时 zoo3 恢复正常,即使向其他节点同步事务消息,但其余节点发现 zoo3 的 epoch 和自己不一样,就不会认这个大佬,而是认准 zoo1 才是自己的老大。
总结
实际上,zk 集群的选举并不简单,底层选举算法使用到的 ZAB 协议保证分布式消息一致性,本篇文章并没过多描述。对于初学者来说,了解其选举的规则和某些场景下是如何选举,大概了解流程足矣。当实际开发中,为了保证高可用,需要注意的是 ZK 集群节点为奇数。另外,感兴趣的读者可以再去关注 ZAB 协议的细节。
参考资料:
《从Paxos到Zookeeper分布式一致性原理与实践》
如果觉得文章不错的话,麻烦点个赞哈,你的鼓励就是我的动力!对于文章有哪里不清楚或者有误的地方,欢迎在评论区留言~