【Zookeeper源码阅读】leader选举源码分析

2,676 阅读9分钟

为了保证Zookeeper集群的一致性,ZAB协议支持了崩溃恢复模式。而崩溃恢复最关键的部分就是leader选举,下面,我们将详细分析leader选举过程。

Zookeeper中文注释源码:github.com/chentianmin…

leader选举原理

leader选举存在与两个阶段中,一个是服务器启动时的leader选举。 一个是运行过程中leader节点宕机导致的leader选举。在开始分析选举的原理之前,先了解几个重要的参数:

  1. 服务器ID(myid):比如有三台服务器,编号分别是 1,2,3。编号越大在选择算法中的权重越大。
  2. 事务id(zxid):值越大说明数据越新,在选举算法中的权重也越大。高32位为epoch,低32位为自增id。
  3. 逻辑时钟(epoch–logicalclock):或者叫投票的次数,同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比,根据不同的值做出不同的判断。
  4. 选举状态
    1. LOOKING:竞选状态。
    2. FOLLOWING:随从状态,同步 leader 状态,参与投票。
    3. OBSERVING:观察状态,同步 leader 状态,不参与投票。
    4. LEADING:领导者状态。

服务器启动时的leader选举

每个节点启动的时候状态都是LOOKING,处于观望状态,接下来就开始进 行选主流程。

若进行Leader选举,则至少需要两台机器,这里选取3台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器Server1启动时,其单独无法进行和完成Leader选举,当第二台服务器Server2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。选举过程如下:

  1. 每个Server发出一个投票。由于是初始情况,Server1Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myidZXIDepoch,使用(myid,ZXID,epoch)来表示, 此时Server1的投票为(1,0,0),Server2的投票为(2,0,0),然后各自将这个投票发给集群中其他机器。

  2. 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票(epoch)、是否来自LOOKING状态的服务器。

  3. 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,PK规则如下:

    1. 优先对比epoch,epoch大的优先级高。
    2. 其次对比ZXIDZXID比较大的服务器优先作为Leader
    3. 如果ZXID相同,那么就比较 myidmyid 较大的服务器作为Leader服务器。 对于Server1而言,它的投票是(1, 0, 0),接收Server2 的投票为(2, 0, 0), 首先会比较两者的epochZXID,均为 0,再比较myid,此时Server2的myid最大,于是更新自己的投票为(2, 0, 0),然后重新投票。对于 Server2而言,其无须更新自己的投票,只是再次向集群中所有机器 发出上一次投票信息即可。
  4. 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于 Server1Server2而言,都统计出集群中已经有两台机器接受了(2, 0, 0)的投票信息,此时便认为已经选出了Leader

  5. 改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader, 就变更为LEADING

运行过程中的leader选举

当集群中的leader服务器出现宕机或者不可用的情况时,那么整个集群将无法对外提供服务,而是进入新一轮的 Leader选举,服务器运行期间的Leader选举和启动时期的Leader选举基本过程是一致的。

  1. 变更状态。Leader挂后,余下的非Observer服务器都会将自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。
  2. 每个Server会发出一个投票。在运行期间,每个服务器上的ZXID可能不同,此时假定Server1ZXID 为123,Server3ZXID为 122。在第一轮投票中,Server1Server3都会投自己,产生投票(1, 123, 0)(3, 122, 0),然后各自将投票发送给集群中所有机器。接收来自各个服务器的投票,与启动时过程相同。
  3. 处理投票。与启动时过程相同,此时,Server1将会成为Leader
  4. 统计投票。与启动时过程相同。
  5. 改变服务器的状态。与启动时过程相同

image.png

leader选举的源码分析

源码分析,最关键的是要找到一个入口,对于zkleader选举,并不由客户端来触发,而是在启动的时候会触发一次选举。因此我们可以直去看启动脚本zkServer.sh 中的运行命令。ZOOMAIN就是QuorumPeerMain。那么我们基于这个入口来看:

QuorumPeerMain.main()

image.png main()方法中,调用了initializeAndRun(args)进行初始化并且运行。

QuorumPeerMain.initializeAndRun()

image.png 这里主要是加载zoo.cfg配置文件,根据配置运行。

QuorumPeerMain.runFromConfig()

image.png

从名字可以看出来,是基于配置文件来进行启动。所以整个方法都是对参数进行解析和设置,因为这些参数暂时还没用, 所以没必要去看。直接看核心的代码quorumPeer.start(),它启动一个线程,那么从这句代码可以看出 QuorumPeer实际是继承了线程。那么它里面一定有一个run()方法。

QuorumPeer.start()

image.png

QuorumPeer.start() 方法,重写了Threadstart()方法。在线程启动之前,会做以下操作:

  1. 通过loadDataBase()恢复快照数据
  2. cnxnFactory.start()启动zkServer,相当于用户可以通过2181这个口进行通信了,这块后续再讲。我们还是以leader选举为主线。

QuorumPeer.startLeaderElection()

image.png

leader选举的方法:

  1. 构建当前票据
  2. 获取当前zkServer中的myid对应的ip地址
  3. 创建选举算法

quorumPeer. createElectionAlgorithm()

image.png

根据对应的标识创建选举算法。

FastLeaderElection

image.png

初始化FastLeaderElectionQuorumCnxManager是一个很核心的对象,用来实现领导选举中的网络连接管理功能,这个后面会用到。

FastLeaderElection. starter()

image.png starter()方法里面,设置了一些成员属性,并且构建了两个阻塞队列,分是sendQueuerecvqueue。并且实例化了一个Messager

Messenger

image.png

Messenger里面构建了两个线程,一个是WorkerSender,一个WorkerReceiver。 这两个线程是分别用来发送和接收消息的线程。具做什么,暂时先不分析。

小结

分析到这里,先做一个简单的总结,通过一个流程图把前面部分串联起来。

image.png

ZkServer服务启动的逻辑

在讲leader选举的时候,有一个cnxnFactory.start()方法来启动zk服务,这块具体做了什么呢?我们来分析看看:

runFromConfig中,有构建了一个ServerCnxnFactory

image.png

ServerCnxnFactory.createFactory()

image.png

这个方法里面是根据 ZOOKEEPER_SERVER_CNXN_FACTORY来决定创建NIO server还是Netty Server,而默认情况下,应该是创建一个NIOServerCnxnFactory

QuorumPeer.start()

image.png

我们再回到QuorumPeer.start()方法中,cnxnFactory.start(),应会调用NIOServerCnxnFactory这个类去启动一个线程。

NIOServerCnxnFactory.start()

这里通过thread.start()启动一个线程,那thread是一个什么对象呢?

NIOServerCnxnFactory.configure()

image.png

thread其实构建的是一个zookeeperThread线程,并且线程的参数为this, 表示当前 NIOServerCnxnFactory也是实现了线程的类,那么它必须要重写run()方法。

到此,NIOServer的初始化以及启动过程就完成了。并且对2181的这个 端口进行监听。一旦发现有请求进来,就执行相应的处理即可。这块后续在分析数据同步的时候再做详细了解。

选举流程分析

image.png 前面分析这么多,还没有正式分析到leader选举的核心流程,前期准工作做好了以后,接下来就开始正式分析 leader选举的过程。

很明显,super.start()表示当前类QuorumPeer继承了线程,线程必须重写run()方法,所以我们可以在 QuorumPeer中找到一个 run()方法。

QuorumPeer.run()

image.png

PeerState有几种状态,分别是:

  1. LOOKING,竞选状态。
  2. FOLLOWING,随从状态,同步leader状态,参与投票。
  3. OBSERVING,观察状态,同步leader状态,不参与投票。
  4. LEADING,领导者状态。

对于选举来说,默认都是LOOKING状态,只有LOOKING状态才会去执行选举算法。每个服务器在启动时都会选择 自己做为领导,然后将投票信息发送出去,循环一直到选举出领导为止。

FastLeaderElection.lookForLeader()

leader选举核心代码。

image.png image.png image.png

投票处理的流程图

image.png

FastLeaderElection.termPredicate()

image.png

QuorumMaj. containsQuorum()

image.png

判断当前节点的票数是否是大于一半,默认采用QuorumMaj来实现。

投票的网络通信流程

image.png

image.png

通信过程源码分析

每个 zk 服务启动后创建 socket 监听

image.png

run()方法里面就是创建socket监听。

image.png

FastLeaderElection.lookForLeader

这个方法在前面分析过,里面会调用 sendNotifications 来发送投票请求:

image.png

image.png

FastLeaderElection.sendqueue

sendQueue这个队列的数据,是通过WorkerSender来进行获取并发的。而这个WorkerSender线程,在构建 fastLeaderElection时,会启动。

image.png

QuorumCnxManager.toSend

image.png

startConnection

SendWorker会监听对应sid的阻塞队列,启动的时候回如果队列为空时会重新发送一次最前最后的消息,以防上一次处理是服务器异常退出,造成上一条消息未处理成功;然后就是不停监听队里,发现有消息时调用send()方法RecvWorker:RecvWorker不停监听socketinputstream,读取消息放 到消息接收队列中,消息放入队列中,qcm的流程就完毕了。

leader选举完成之后的处理逻辑

通过lookForLeader方法选举完成以后,会设置当前节点的PeerState, 要么为Leading、要么就是 FOLLOWING、或者OBSERVING。到这里,只是表示当前的leader选出来了,但是QuorumPeer.run()方法里面还没执行完,我们再回过头看看后续的处理过程。

QuorumPeer.run()

image.png

分别来看看caseFOLLOWINGLEADING,会做什么事情:

follower.followLeader()

image.png

makeLeader

初始化一个Leader对象,构建一个LeaderZookeeperServer,用于表leader节点的请求处理服务。

leader.lead()

Leader端, 则通过lead()来处理与Follower的交互。