17、Zookeeper 源码解析 - ZAB协议中FLE选举通信流程

1. ZAB协议

如果说到ZookeeperFLE选举算法和广播通信,那就绕不开Zookeeper的底层通信协议ZAB

ZAB协议全称为Zookeeper Atomic BroadCast,即Zookeeper原子广播,Zookeeper则是通过ZAB协议来保证分布式事务的最终一致性。这里需要注意,ZK只能保证最终一致性,而不是强一致性,即在某一时刻读取任意两个机器的数据,可能是不一样的。

ZAB协议使用的是主备系统架构模型,只有Leader会进行真正的写事务处理,FollowerObserver则负责提供读操作和与Leader进行数据同步。(FollowerObserver实际差别挺大的,后面再仔细分析)

ZAB协议共分成了三个阶段:

1、 发现:指的就是实际的选举流程,此流程会在集群机器中选举出LeaderFollowerObserver,且Leader会维护一份可用的集群客户端通信对;
2、 同步:在集群中选举出Leader后,Leader将本身的数据同步给集群内的其它机器,实现集群多副本,保证可用性;
3、 广播:在集群完成选举和数据同步后,集群就可以正式对客户端提供功能了,此时客户端对集群的写请求都会经过LeaderLeader再对集群广播Proposal事务请求,完成集群对客户端的请求同步(实际上还有ackcommit等流程,但不是本篇重点,所以忽略);

ZAB协议也可以分为两个模式:

1、 恢复模式:ZAB协议的发现和同步阶段,在代码中可以称为LOOKING状态;
2、 广播模式:ZAB协议的广播阶段,在代码中表现为LEADINGFOLLOWINGOBSERVING状态;

ZAB协议的内容很多,如果要一篇文章就全部搞定,估计看完都得2-3个小时,更别说要一次性消化完了。因此本篇就只分析一下发现阶段,并主要着重于选举算法的实现和一些细节,并思考解答一下该流程所涉及的一些常见问题。

2. 集群配置

在开始选举前先把关键的集群配置说明一下:

# 选择leader选举算法
electionAlg=3

# 集群类型
peerType=participant

# 和参与者一样,观察者现在默认将事务日志以及数据快照写到磁盘上
syncEnabled=true

# 配置的集群通信机器信息
server.sid1=host:port:electionPort:type
server.sid2=host:port:electionPort

参数说明:

  • electionAlg:选择Leader的选举算法:0对应于原始的基于UDP的版本;1对应于快速Leader选举基于UDP的无身份验证的版本;2对应于快速Leader选举有基于UDP的身份验证的版本;3对应于快速Leader选举基于TCP的版本,简称FLE。默认为3,FLE也是最常用的算法;
  • peerType:集群类型,observer或者participant;
  • syncEnabled:和参与者一样,观察者现在默认将事务日志以及数据快照写到磁盘上,这将减少观察者在服务器重启时的恢复时间。将其值设置为false可以禁用该特性。默认值是true;
  • server:配置的server信息,需要把自身也配进去,有三种配置方式,sid1则是本机器的编号,一般配置为整数,port为配置该server和集群中的Leader交换信息所使用的的端口,electionPort为配置选举leader时所使用的端口,type的类型是observer或participant。

server的配置方式我们可以暂时推断出,集群内新加入的机器是一定要有目标集群的所有机器地址+端口信息的,否则新加入的机器无法向集群的其它机器发送消息。需要注意的是,如果server不配置electionPort将无法参与集群的选举通信,type不配置默认为participant类型。

participantobserver数量也有限制,当participant的数量小于2observer数量只要大于0启动就会报错,participant的数量不是奇数日志会进行提示。

3. 选举发现

发现阶段ZK做了很多有意思的小设计,但这些小设计如果没注意就很容易踩坑,接下来在介绍选举流程时会一一把小设计引出来。

假设该阶段的选举算法使用了默认的FLE算法,FLE算法的全称为Fast Leader Election,即快速选择Leader算法,顾名思义,该算法的核心便是快速选择Leader,那么该算法是如何保证可以快速确定Leader的?接下来一起慢慢解开该算法的面纱。

3.1 选举的三个条件参数

在开始正式的选举流程分析前,先需要知道哪些参数是选举流程中最为重要的,三个参数说明如下:

1、 epoch:类似于古代的朝代,每更替一个朝代epoch就会+1,在ZK中每进行一轮新的选举epoch也会+1,初始值为0;
2、 zxid:在ZK集群中zxid是全局唯一的,每来一个请求zxid就会+1,初始值为0;
3、 myid:机器最基本的标识,如果myid配成1server也必须得配置一个server.1,选举时等同于本机器的sid;

3.2 选举核心规则

优先级从上到下:

1、 epoch谁大谁当选:正常的ZK集群机器运行时所有的机器epoch都是一样的当某个机器收到epoch要大的投票信息说明本机器因为某种意外新的选举结果未收到,因此本机器收到新的朝代信息直接变成Follower加入;
2、 zxid谁大谁当选:如果epoch相同,选举时谁的zxid要大说明谁接收的信息最多,应该让其成为Leader以便最大限度的减少请求的丢失;
3、 myid谁大谁当选:在初始阶段epochzxid都是一样的,因此需要有一个最基本的标准来衡量谁来当Leader,这个最基本的标准便是每台机器都拥有的myid在选举时也会以SID的别名来称呼;

换成通俗一点的话来讲就是:

1、 epoch谁大谁当选:周星驰的《九品芝麻官》里有句话是“你竟然用前朝的剑来斩本朝的官?”这句话的隐藏意思就是前朝的权利在本朝等于无效,应当以本朝的权利优先,不管是前朝的尚方宝剑还是前朝的皇帝;在ZK中也是如此,不管上一个epoch值机器是FollowerLeader,只要机器的epoch比集群最新的epoch要小,就说明需要无条件服从新的epoch
2、 zxid谁大谁当选:当以这个条件为判定标准,就说明参与选举的机器都是同一个epoch值,当集群处理一个客户端请求时zxid便会+1,因此zxid可以看成是处理事务的能力,zxid的值越大,就说明该机器接触过的事情多,能力大,在同一个选举朝代中,能力越大肯定会选举该机器当选Leader;从本质上来看,这里的能力越大就是现实中的网络更好或机器算力更强;
3、 myid谁大谁当选:如一开始所说,初始阶段epochzxid肯定所有机器都是一样的,此时来选举谁当Leader,就一定有个预先设定的值来做判断就如一个部门刚刚成立,所有人互不相识,需要一个Leader,但投票总要有根据的,比如每个人手上都有其他人的简历,根据简历再去投票谁更适合当Leader;而在ZK集群中,myid的大小就代表着简历内容的优劣,作为兜底的判断条件来选举Leader

因此从这些选举的核心规则我们就能轻易得知:

  • 集群的Leader一般而言算力和性能会比其它的机器要更好;
  • 在初始化集群时,给算力和网络最好的机器设置最大的myid可以有效提高集群的处理能力;

3.3 建立通信对

3.3.1 流程分析

在开始集群选举Leader流程前有一个必要的流程:确定集群内有哪些机器参与选举并和这些机器建立通信对。ZK在建立通信对这上面花了点小设计,假设现在参与投票的三台机器还是下面的server配置:

server.1=host:port:electionPort
server.3=host:port:electionPort
server.5=host:port:electionPort

配置启动后集群内的机器会向server列表的所有机器都发送投票给自己的消息,这个流程分两个阶段,第一阶段通信图如下:

 

其中每台机器都会先给自己投一票,然后再和其它的机器建立Socket通信去发送自己的投票信息,从图中可以看到,SID大的机器向SID小的机器建立通信Socket都成功了,但SID小的机器向SID大的机器建立通信Socket都被关闭了。

经过第一阶段,每台机器的通信对集合情况如下:

 

可以看到第一阶段后,SID最小的机器没有大SID机器的通信对,小SID机器的通信对建立会在第二阶段完成,通信图如下:

 

在第一阶段SID大的机器主动把SID小的机器通信Socket关闭了,但由于在第一阶段SID大的机器向SID小的机器发送过建立通信对的操作,所以第二阶段中SID小的机器将会由于被连接而和SID大的机器建立通信对。经过第二阶段通信对集合情况如下:

 

经过了这两个阶段后集群内的所有机器才算完整的建立了通信网络。

注:上面表示的都是participant之间的建立通信对流程,如果是observerobserver之间是不会互相建立通信对的。

3.3.2 思考与总结

看完上述流程是不是觉得很困惑?为什么ZK实现的ZAB协议在发现阶段建立通信对时一定要SID大的机器来主动连接建立通信对,SID小的机器为什么对SID大的机器毫无主动权?

个人觉得如果要解答这个问题,关键有两个点:

1、 SID唯一性:在ZK的机器上,如果配置了两个SID一样的server配置,那么有一个一定会覆盖另外一个,并且由于文件会转成Properties对象,因此key的顺序也是没办法控制的基于这个原因,新的SID要避免覆盖的问题,值最好一直往上增加,如果使用数字间隔的方式来预留位置,过于复杂也不便于后续维护;基于这个原因ZKSID大的机器赋予了更多的主动性,目的在于鼓励开发者按SID递增的方式去配置;
2、 集群新增机器:这是解答上面这些问题最核心的点,简单来说就是原集群只包含了A-B-C三台机器,现在需要新增第四台机器D,由于A-B-C集群不知道D机器的信息,因为一开始的配置信息是不会把D配置进去的,只有D知道A-B-C集群,此时如果DSID最大,即使A-B-C不知道D机器,D建立通信对时也可以掌握主动权,和A-B-C集群的三台机器各自建立通信对;如果DSID小,将无法顺利和A-B-C集群建立通信对,这个流程后续会分析;

从上述这两点可以得出以下结论:

1、 ZK是推荐开发者配置SID时递增,不推荐使用数字间隔的配置方式;
2、 为了让开发者遵循第一点,ZKSID大的机器赋予了更多的主动权,特别是新增机器去融入原有集群的情况,新增的机器只能配置更大的SID才行,配置更小的SID将无法融入原集群;

3.4 选举流程

假设现在有三台机器,三台机器的server配置如下:

server.1=host:port:electionPort
server.3=host:port:electionPort
server.5=host:port:electionPort

3.4.1 流程分析

在选举时,机器A先回复集群通知,随后机器B回复集群通知。

其大致选举通信流程图如下:

 

流程分析如下:

1、 开始Leader选举时,每台机器都会发送给集群中的其它机器告诉它们选举自己当选Leader,并把自己的myidzxidepoch等值信息放到通知中,以便其它机器收到后作比对;
2、 机器ABC都接收到了各自的选举通知,并使用前面所说的选举规则进行过判断这里假设A先收到B的通知,先把选举投给了B,但是后面又收到了C的通知,最后把投票改投给了C,如果是先收到C的通知,那么B的通知将会被忽略,最终A都会把票投给C机器;
3、 B机器会收到AC的通知,但是A的通知会被忽略,因为根据FLE算法的规则A的权重比B的小,但是C的权重比B要大,因此B最终会把投票投给机器C
4、 此时集群内的三台机器已经收到了全部通知并把票已经给投了,最终算上C自己的投票,就算有其中一台机器没有回复,C的投票是肯定超过总数的半数的于是C将自己的状态改成Leader,而AB则把自己的状态改成Follower,修改的依据便是判断超过半数的机器信息myid是否和本机器相同,相同则是Leader,否则是Follower

3.4.2 思考与总结

单看上述流程可能会像看流水账一样,都看得懂,但感觉都比较死,都是一些约定俗成的流程。但其实如果站在现实角度看,这个选举算法也是很有意思的,比如现实中有以下场景:

现在公司有个新项目需要攻坚,新拉了一个攻坚组,组内只有三个人A、B和C
原来三个人都没当过组长,现在需要这三个人自行协商谁来当组长

可以采取类似FLE的方式来进行推选:

1、 每个人向其他的人分发一下自己的工作简历,这个动作就类似于流程中的把当选Leader的投票投给自己;
2、 每个人再根据工作简历判断自己相比其他的人合不合适当选组长,判断的条件有自身技能(类似于SID)、接手过的项目(类似于zxid)又或者工作经验(类似于epoch)等;
3、 如果A对比完自己和BC后,觉得C综合来看是三个人中最合适的,A就会把票投给C;而B先对比完A后觉得自己比A更有竞争力,于是便忽略A,随后再与C对比,最后发现C更适合,再推举C当组长,C同理;
4、 经过上面三个投票对比流程后,最后C的票数稳定过半,自然当选了组长;

以上面这种方式可能更容易理解到FLE算法为什么叫快速选举Leader算法,个人认为其核心思想在于判断条件直观明显,对于不符合当选Leader的机器交互尽可能少。这样才能在短时间内不需要很多算力性能便可以选举出集群Leader

4. 发现选举阶段相关问题

或许看完了上面的发现阶段会觉得自己懂了但好像又没有完全懂,考验对部分知识是否掌握的唯一标准就是答疑解惑。比如我在学完发现阶段后产生了些许疑惑,网上也有选举相关的面试问题,接下来尝试一一解答这些问题。

4.1 新增Follower融入集群

学习ZK或面试的时候经常会碰到以下这个问题:

如果集群选举完毕后,中途要加入Follower机器参与集群工作,此时如何通信的?

接下来便来解答一下这个问题,还是按上面的流程,将其分为三个部分:

1、 配置说明;
2、 建立通信对;
3、 选举流程;

4.1.1 建立通信对

ZK集群可支持新增FollowerObserver机器角色,新增这两种角色前有个绕不开的流程:和集群机器建立通信对。前面说过只能由SID大的机器主动向SID小的机器有个小设计,这个小设计在新增机器的场景就尤为重要。

4.1.1.1 流程分析

假设还是原来的A-B-C集群,该集群已经选举出了Leader并正常运行,每个机器的server配置都如下:

server.2=host:port:electionPort
server.4=host:port:electionPort
server.6=host:port:electionPort

现在需要对集群进行扩容新增两台Follower机器,两个机器的server主要配置如下:

# 机器D
server.1=host:port:electionPort
# 机器E
server.7=host:port:electionPort
# 原集群配置
server.2=host:port:electionPort
server.4=host:port:electionPort
server.6=host:port:electionPort

此时启动两台机器,第一阶段连接情况如下图:

 

SID最小的D机器,连接A-B-C集群的任意一台机器都失败了,而SID最大的E机器连接任意一台机器都成功了,通信对建立情况如下:
 

接下来便是二阶段的触发连接,触发连接情况如下图:

 

有没有觉得很奇怪,为什么D机器没有触发建立通信对的情况?再来看下通信对图:

 

这一看明显不对劲了,只有机器E成功和现有集群A-B-C建立起了通信对,而D则只和机器E成功建立了通信对,这一现象明显和之前说的流程不一致,按之前的流程来说应该五台机器都会建立彼此的通信对。那么为什么会产生这种现象呢?

4.1.1.2 思考与总结

其核心问题就在于前面提到过的ZKSID大的机器赋予了更多的主动权,相反,SID小的机器就会很被动。解释如下:

  • 机器E:由于E的SID比现有集群机器都要大,因此连接和触发操作都是可以正常完成的。A-B-C集群的server配置没有机器E的信息,但机器E的server配置有A-B-C集群的机器信息,当机器E向A-B-C集群的三台机器发送建立通信对的操作时,由于SID大,因此机器E可以掌握主动权,第一阶段先与server配置的机器建立通信对,随后SID小的机器在第二阶段触发与机器E建立通信对;
  • 机器D:由于D的SID在所有的机器中是最小的,因此D的主动权是最差的,无法向其它的机器主动建立通信对。A-B-C机器的server配置中没有D的机器信息,所以A-B-C三台机器无法主动向D发起建立通信对的操作,因此D也无法触发与A-B-C三台机器建立通信对的操作;但E的server配置是有D机器的,因此D可以顺利触发与E建立通信对。

在新增集群机器的这种场景下,该案例更容易证明前面对于集群建立通信对流程的思考总结,在这种逻辑下ZK集群的SID一定是递增的,如果把新机器的SID弄成最小的或者不是最大的,新的机器将无法和现有集群的所有机器建立通信对。假设建立通信对的机器没有超过server数量/2,那么该机器就永远处于LOOKING阶段,永远不会正式承担集群的工作。

4.1.2 新增机器选举流程

4.1.2.1 流程分析

既然说明了机器要参与选举,那么机器的角色就一定是Follower,因为集群如果正常运行,是不会突然变更Leader的,添加的机器只能是Follower了。假设现在添加的机器就是前面提到过的DE机器,通信对建立流程按照前面的分析已经完成,现在已经进入了投票流程,其投票流程图如下:

 

1、FLE算法中,第一步的操作永远都是向server配置的所有机器发送把票投给自己的消息,无论是同时启动还是后续新增的机器启动;但前提是本机器和其它的机器已经建立了通信对,由于D只和E建立了通信对,因此D只会向E发送自己的投票消息,而E则会向其它的四台机器发送自己的投票消息;此时也会发送机器本身的epochzxidmyid,但这三个参数在该场景下不会被使用到;
2、 当集群A-B-C的任意机器接收到E的投票消息后,由于E不在A-B-Cserver配置中,因此A-B-C集群所有机器将会直接向E发送本机器已产生的投票信息,而不会做任何逻辑判断,在这一步机器E将会得知集群Leader的信息,当先后收到的消息达到或大于server数量/2时,选举成功,state成为Follower;但机器D由于无法和A-B-C集群进行通信,因此暂时无法得知集群Leader的信息,但前面收到了E的投票信息,D会暂时选举E当选Leader
3、 由于机器D除了最开始收到过E的投票信息后,再也没有收到其它的投票信息,因此D会再次向server的所有机器发送投票给E的消息,实际上只能发给E,因为只有E的通信对;当E收到D的消息后,将会直接把本机器的Leader投票发送给DD收到Leader投票后,会把之前投给E的票改为投给新的Leader,但由于只会收到E的投票信息,投票数量永远不会超过server数量/2=2,因此机器D会一直处于LOOKING状态,重复第三步;

4.1.2.2 思考与总结

可以从上面的流程看出如果把SID的值设置的比要参与集群的大部分SID都要小的话就会导致机器完全融入不进去,会一直处于LOOKING状态,循环往复。更可怕的是D机器会一直向有通信对的E发送选举消息,E机器选举结束后通信对不会销毁,因此会一直接收消息,当消息堆积到100时就会把前一个消息移除再接收新的,但机器E还是会一直接收D的不必要消息。

因此如果集群要加入新的Follower机器,SID最好要比现有集群的所有机器都大,这样才能顺利和集群的所有机器建立起通信对,并融入集群分担集群工作压力。

看网上有些文章说当新加入集群的机器成功选举工作后,需要把原集群机器也重启,但实际这样的操作是没必要的,在ZK集群的配置文件中配置server信息,目的是为了选举时能让机器和server中的配置建立通信对,如果新加入的机器严格按照SID递增的方式配置,那么所有机器是肯定可以顺利全部建立各自的通信对,所以完全无需再修改原集群机器再去重启。当然,把zoo.cfg配置文件修改成一样的,这个操作是推荐做的,保持机器配置的统一性,方便后续维护。

4.2 新增Observer融入集群

网上有文章会说Observer不会参与选举,但Observer机器不参与选举是怎么融入集群的?又是怎么从LOOKING状态转变为OBSERVING的呢?“Observer不参与选举”这种说法是不严谨的,Observer肯定是通过选举才能融入集群,只是Observer不具备选举权,“不能参与选举”和“不具备选举权”差别很大。

说到选举核心规则就避不开这三个参数:epochzxidmyid,在选举时比对的就是这三个值的大小,当某个机器被集群超过半数的机器选举为Leader时,那么该机器将会成为LeaderObserver不具备选举权就体现在这两个方面:

1、 Observer在参与选举流程时这三个参数都是Integer.MIN_VALUE整数的最小值,所以先接收了哪个机器通知就选举那个机器当选Leader,随后碰到更大的值,再更换投票;
2、 Observer向其它参与选举的机器发送消息时,由于不具备选举权,其它的机器会直接把自身的投票信息返回给ObserverObserver收到后,再更新自身的投票信息;

如果Observer要融入集群,这种情况相对于Follower而言算是比较简单的,只需要两步便可以让Observer机器从LOOKING状态变成OBSERVING。假设还是原来的A-B-C集群,该集群已经选举出了Leader并正常运行,每个机器的server配置都如下:

server.2=host:port:electionPort
server.4=host:port:electionPort
server.6=host:port:electionPort

现在需要对集群进行扩容新增1台Observer机器,两个机器的server主要配置如下:

# 机器E
server.7=host:port:electionPort:observer
# 原集群配置
server.2=host:port:electionPort
server.4=host:port:electionPort
server.6=host:port:electionPort

配置完上面的配置后启动机器E,流程图如下:

 

流程是十分简单的,由于E机器的sid大,因此是可以准确和A-B-C集群进行通信的。共两步:

1、 启动选举流程后的通用流程,会向参与选举的机器发送投票给自己的消息;
2、 集群内参与选举的机器判断E不是participant,直接向E回复自身的投票信息;E机器接收到超过半数的投票信息后,便成功选举出Leader,同时自身的状态修改为OBSERVING

集群正常运行再去新增Observer的流程是十分简单的,其实就算是Observerparticipant同时启动,对于Observer而言,流程还是非常简单,依然只会接收其它机器的投票信息,只是Observer接收到不同的投票信息后会比较并更改自身的投票信息。

4.3 机器宕机重新选举相关

其实如果前面的那些流程都了解了,相关机器宕机再启动无非就是再走一遍之前的逻辑,机器宕机最大的影响还是在于某个机器宕机后对于整个集群而言,吞吐量和可用性是否有什么变化。接下来分别聊下这三种情况。

4.3.1 Leader宕机

最严重的情况,整个集群在新的Leader被选举出来前都无法提供服务,也就是整个集群会短暂的不可用。Leader宕机的选举流程和前面说的发现阶段选举流程基本差不多,只是原来的通信对会销毁再重新创建一次。

只要集群的机器数量是奇数,这种情况是不会发生脑裂的。假设原集群有5participant,然后Leader宕机了,此时只剩下4台机器选举,剩下的机器想要当选Leader需要得票>=3,所以4台机器各占2票这种情况会被下一轮投票给纠正,最后只能有一台新的Leader机器诞生。

4.3.2 Follower宕机

集群可以正常提供服务,只是集群的吞吐量会受到影响,比如同步数据时,会一直少一个机器进行ack,变相的减少了吞吐量。假设还是5台机器集群,最多允许宕机2台机器,如果集群没有3台正常进行ack的机器,那么集群将不可用。即奇数数量的集群最大可允许宕机数为n/2

集群宕机数量示例:

1、 总共3台机器,最多1台机器宕机;
2、 总共5台机器,最多2台机器宕机;
3、 总共7台机器,最多3台机器宕机;
4、 总共9台机器,最多4台机器宕机;
5、 …;

4.3.3 Observer宕机

仅会影响读操作的吞吐量,对于集群的写操作无影响,甚至从某一种角度来看会提升集群的写操作吞吐量,为何这样说?

Observer接收写操作大致流程:Observer接收到写操作后会把写操作发给LeaderLeader再和其它的Follower进行ack半数确认,最后commit并同步给集群所有的Follower机器,并发送INFORMObserverObserver再同步本机数据。

所以无论写操作是Observer接收还是Follower接收,都会经过Leader,对于Leader而言没有任何变化,有Observer时,Leader还要向Observer发送INFORM消息去同步消息,如果没有Observer,发送INFORM消息这一步对于Leader而言则可以省略,Leader就能有更多的算力去处理和Follower的交互及客户端的操作。

所以Observer不是任何情况都可以无脑新增的,需要对读写操作的比例有个大致的判断才能决定新增Follower还是Observer

但回归现实来说,写操作肯定是占少数的,提升了读操作效率,从整体而言确实就是提升了集群的吞吐量。且新增Observer是最简单、对集群影响最小的操作,如果要新增Follower机器,一次性需要新增两台以保证集群数量为奇数,但Observer可以新增任意台,宕机后对集群的影响也是最小的。