09、Zookeeper 源码解析 - FLE(FastLeaderElection)算法集群选举通信原理及流程结构

1.前话

上一篇说明了ZK集群的基本结构和重要组件,分析了在ZK集群启动时哪些重要的组件在起作用,这一篇将会续着上一篇往下开始分析ZK集群选举流程,来分析选举细节、选举流程、机器间如何完成交互通信的以及启动流程中的那些组件在其中起到何种作用。简单来说,便是分析上篇文章说的五个要点中的第二个和第三个要点。至于ZK集群的数据同步放到下篇文章再来分析。

第一篇传送门:(八)Zookeeper原理源码分析之集群启动组件构成、启动流程源码分析和集群配置文件参数分析

注:本篇基于ZK版本3.4.8分析的。

2.集群通信结构

集群通信结构图如下:

 

上面的通信图大致描绘了两个机器在启动流程和准备开始选举流程的通信情况,在图中的A1、A2、A3、A4、B1、B2、B3和B4这八个流程在两台机器里1-4流程可以看成是基本同时执行的,并且A1-B1、A2-B2...这八个流程对所处理的操作是一模一样的,只是触发的时间有点差别而已。因此介绍A1流程就相当于介绍B1流程,以此类推,接下来便详细介绍一下其交互顺序和其中的一些细节:

  • A2流程:FastLeaderElection想要和其它的机器通信,于是会向本类的sendqueue集合添加消息对象,WorkerSender一直轮询这个集合,轮询到有 消息对象后执行A3流程;
  • A3流程:调用QuorumCnxManager对象去发送消息,接着会把消息添加到queueSendMap集合,并判断要发送的机器是否有对应的SendWorker和RecvWorker对去处理这个消息,如果没有则进行A4流程和A5流程;
  • A4流程:A4流程在A5流程之前,连接另一台机器Listener对象里面的ServerSocket对象,并触发B9流程,在机器B创建和机器A进行通信的SendWorker和RecvWorker对;
  • A5流程:判断本机器是否已经初始化了SendWorker和RecvWorker对来和机器B进行通信,如果没有则创建一个SendWorker和RecvWorker对;
  • A6流程:当创建SendWorker成功之后,会轮询queueSendMap集合查看是否有消息需要发送,如果有的话则像机器B的RecvWorker发送消息,从而触发B7流程;
  • A7流程:当机器B执行完B6流程时将会触发A7流程,RecvWorker会一直读取通道的数据,如果有数据则会完全读取放入recvQueue集合中,而WorkerReceiver会一直轮询recvQueue集合,当轮询到有响应消息后会把通知对象放入recvqueue集合中,从而触发A8流程;
  • A8流程:FastLeaderElection在调完A2流程后会一直等待直到recvqueue有响应数据后才会执行后续的选举流程,但后面的选举流程放到下篇来分析吧;
  • A9流程:如果机器B执行了B4流程,相当于机器A执行到了A4流程触发B9一样,Listener将会监听这次连接,进行一定的判断是否要创建SendWorker和RecvWorker对。

看完分析后会发现这几个流程可以一直循环,无穷无尽,当发生了一次新的通信或者选举时将会又一次从A1开启整个流程,执行到最后再次进入选举流程。需要注意的是选举在A1流程便已经开始了

3.集群通信流程

在介绍集群的通信流程时,假设当前的ZK集群有三台机器,SID分别是1、3、5,分别代表机器A、B和C,启动时间基本一致,接下来便按照这个假设基础往下模拟通信流程吧。

3.1 创建通信模块流程

创建通信流程个人认为应该分两个部分,第一个部分是主动向其它的机器发送Socket连接流程,第二个部分则是接收到其它机器的Socket连接请求所执行的监听流程,只有两个流程都执行完才可以构建前面所说的集群通信结构。

3.1.1 发送Socket连接

每台机器在启动时都会主动向配置文件中的集群发送Socket连接请求和通知,大致图如下:

 

可以看到途中有六个向外的接头,这些箭头代表机器启动时尝试给和其它机器发送Socket连接并发送通知的流程,并且这六个步骤由于启动时间基本一致,所以执行时间也差不多。接下来看下途中的流程结果:

  • 1-3流程:机器A向机器B发送Socket请求并尝试发送通知给B,但此时会发送失败;
  • 1-5流程:机器A向机器C发送Socket请求并尝试发送通知给C,但发送通知失败;
  • 3-1流程:机器B向机器A发送Socket请求并尝试发送通知给A,发送成功;
  • 3-5流程:机器B向机器C发送Socket请求并尝试发送通知给C,但发送通知失败;
  • 5-1流程:机器C向机器A发送Socket请求并尝试发送通知给A,发送成功;
  • 5-3流程:机器C向机器B发送Socket请求并尝试发送通知给B,发送成功;
  • 三个Vote流程:每台机器都会向自己发送一条通知,告诉本机选举对象先给自己投一票。

**结果分析:**仔细看途中的流程会发现一个有趣的现象,SID大的发给小的都成功了,SID小的发给大的都失败了,这是因为在发送完Socket连接后会尝试给连接机器发送一个通知,但在发送前判断发送机器的SID是否比本机器大,如果大的话则不发送请求,直接关闭Socket,如果本机器的SID比另外一台大的话则继续发送把投票投给本机器的通知。

**总结:**启动时的发送Socket连接流程SID大的将会给SID小于或等于本机器SID的发送Socket连接并发送我把票投给自己的通知,SID小的只会和SID大的连接一下随后关闭Socket连接;

经过这个流程后,SID大的机器将会主动创建和SID小的通信SendWorker和RecvWorker对。结果图如下:

 

SID最小的A机器senderWorkerMap为空,B机器的senderWorkerMap只有A的通信对,而C机器的senderWorkerMap有A和B的通信对。

3.1.2 接收监听Socket连接

在上一章中提到了每台机器会创建一个Listener用来监听其它机器的Socket连接本机器情况,在发送Socket连接流程中提到了每个机器都会向其它的机器发送Socket连接,有Socket连接时都会触发本机的ServerSocket服务监听。监听结果图如下:

 

上图中只有三个成功触发流程,但在刚刚的发送Socket流程中明明有六个Socket发送,应该有六个成功触发流程才对呀,这是咋回事呢?不急,先看下图中触发结果:

  • 3-1流程:机器B发送给机器A的Socket通信成功触发;
  • 5-1流程:机器C发送给机器A的Socket通信成功触发;
  • 5-3流程:机器C发送给机器B的Socket通信成功触发;
  • 其余的1-3、1-5和3-5流程发送Socket通信监听流程属于连接重置流程了,稍后分析。

**结果分析:**从结果来看SID大的发送完Socket连接请求后将会在SID小的机器上触发监听并生成SendWorker和RecvWorker通信对,如果SID小的机器上原来就有发送Socket连接的通信对则会覆盖。

**总结:**无论是主动的创建SendWorker和RecvWorker通信对还是监听式的创建,主动权永远在都在SID大的机器上,SID大的机器主动发送通知创建通信对,而SID小的又因为SID大的机器主动发送Socket连接而导致被动的创建通信对。

经过上两个流程后,各个机器中的senderWorkerMap状况:

 

经过主动和被动的创建通信对,集群中的每个机器都会有另外机器的通信对,以保持进行选举时的投票通知。

3.1.3 Socket连接重置

此次我们模拟的选举通信的例子是三台机器几乎同时启动,因此向另外的机器发送Socket连接等操作都可以看成是同时操作的,操作完之后便形成了每个机器中都含有另外一些机器的通信对。但是加入在运行过程中机器A宕机重启了或者机器SID大的先启动,随后SID小的再启动,此时有一方都没启动,按照刚刚的两个流程来走肯定是走不通的。

因此便有了Socket连接重置操作,这样可以确保后面启动的机器也可以和先启动的机器成功创建通信对。假设机器A和C已经启动成功了,但是还在选举通信的创建通信对过程,此时大致流程图如下:

 

在前面出现过的Complete、Over和Trigger之外又多了Reset事件,在这里面代表了Socket连接重置事件,这张图是更接近于平时正常使用ZK集群的流程,因为正常启动流程下几台机器的启动时间基本上是异步的。接下来详细分析一下图中流程:

1、 S1:3-1流程:假设B机器首先向SID小的A机器发送了连接Socket请求,此时会在B机器上生成和A机器通信的通信对,而A机器因为B机器Socket连接进来且B机器的SID大于A机器的SID,因此会触发处理事件,从而在A机器上生成和B机器通信的通信对;
2、 S2:3-5流程:B机器向A机器发送Socket连接请求,但由于C机器的SID比B机器的SID要大,因此B机器会发送通知失败,但C机器依然可以接收到Socket请求,且在C机器上会触发重置Socket连接的操作,将本地和机器B通信的通信对(如果以前有的话)关闭,并重新连接B机器,创建和B机器通信的通信对;
3、 S3:5-3流程:当S2的重置事件触发后C机器会向B机器发送连接Socket请求,满足了SID大的机器连接SID小的机器情景,因此会在B机器上触发事件,生成和C机器通信的通信对;

**结果分析:**当SID小的机器B向SID大的机器C发送连接Socket请求时,将会在C机器上触发重置事件,将本机器和B通信的通信对重置并重新发送连接Socket请求,从而满足SID大的机器向SID小的机器发送Socket请求,在B机器上创建通信的通信对。

**结论:**经过上述流程后可以发现重置操作类似于SID小的机器要求SID大的机器再次主动联系本机器,从而满足SID大的机器主动发送Socket连接情景,在两台机器上彼此创建通信的通信对,可以把重置操作看成主动通信和被动触发的转换操作,将主动发送的转变成被动接收的,将被动接收的转变成主动发送的。

3.2 选举通信流程

3.2.1 选举规则

前面建立通信对流程是选举通信流程的必备前提,完成了前面通信对的建立便可以开始进行选举投票交换了。FLE选举的规则如下,且优先级从上到下排列:

1、 Epoch谁大谁当选:Epoch类似于古代的朝代,每更替一个朝代Epoch就会+1,正常的ZK集群机器运行时所有的机器Epoch都是一样的,说明在同一个选举结果中当某个机器收到Epoch要大的说明因为某种意外新的选举结果未收到,因此收到新的朝代信息直接变成Follower加入;
2、 zxid谁大谁当选:在ZK集群中zxid是全局唯一的,处理请求时都是往上递增的,因此选举时谁的zxid要大说明谁接收的信息最多,应该成为Leader以便最大限度的减少请求的丢失;
3、 myid谁大谁当选:在初始阶段Epoch和zxid都是一样的,因此需要有一个最基本的值来衡量谁来当Leader,这个最基本的值便是每台机器都拥有的myid;

3.2.2 涉及参数

选举流程会涉及如下几个参数交换:

  • proposedLeader:当前机器选举Leader的myid,最开始每台机器都会把这个值设置为自己的myid;
  • proposedZxid:当前机器选举Leader的zxid,这里面包含了本机的epoch信息,最开始的值为自己机器的zxid;
  • proposedEpoch:当前机器的迭代数(朝代数),最开始为本机器初始化的迭代数,但在进行选举时发送给其它机器值为logicalclock逻辑迭代数;
  • electionEpoch:接收通知时使用的参数,对应于发送通知机器的logicalclock值;
  • peerEpoch:接收通知时使用的参数,对应于发送通知机器的proposedEpoch迭代数。

3.2.3 选举流程

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

 

流程分析如下:

1、 成功建立通信对后将会从queueSendMap集合中取出通知发送给集群中的每个机器告诉它们选举自己当选Leader,并把自己的proposedLeader、proposedZxid和proposedEpoch等值信息放到通知中,以便其它机器收到后作比对如机器A最开始就会向B和C机器发送选举自己当Leader的通知,B和C机器也会这样做;
2、 先从机器A分析起,在这个步骤下,机器A、B和C都接收到了其它机器的选举通知,并使用前面所说的选举规则进行过判断假设A先收到B的通知,先把选举投给了B,但是后面又收到了C的通知,最后把投票改投给了C,如果是先收到C的通知,那么B的通知将会被忽略,最终都会把票投给C机器;
3、 B机器会收到A和C的通知,但是A的通知会被忽略,因为根据FLE算法的规则A的权重比B的小,但是C的权重比B要大,因此B最终会把投票投给机器C;
4、 此时集群内的三台机器已经收到了全部通知并把票已经给投了,最终算上C自己的投票,C的投票是肯定超过总数的半数的于是C将自己的状态改成Leader,而A和B则把自己的状态改成Follower,修改的依据便是判断超过半数的机器信息myid是否和本机器相同,相同则是Leader,否则是Follower;

关于流程的分析便到此结束,下一篇将会分析选举流程的源码。