为了保证Zookeeper集群的一致性,ZAB协议支持了崩溃恢复模式。而崩溃恢复最关键的部分就是leader选举,下面,我们将详细分析leader选举过程。
Zookeeper中文注释源码:github.com/chentianmin…
leader选举原理
leader选举存在与两个阶段中,一个是服务器启动时的leader选举。 一个是运行过程中leader节点宕机导致的leader选举。在开始分析选举的原理之前,先了解几个重要的参数:
- 服务器ID(myid):比如有三台服务器,编号分别是 1,2,3。编号越大在选择算法中的权重越大。
- 事务id(zxid):值越大说明数据越新,在选举算法中的权重也越大。高32位为
epoch,低32位为自增id。 - 逻辑时钟(epoch–logicalclock):或者叫投票的次数,同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比,根据不同的值做出不同的判断。
- 选举状态
LOOKING:竞选状态。FOLLOWING:随从状态,同步 leader 状态,参与投票。OBSERVING:观察状态,同步 leader 状态,不参与投票。LEADING:领导者状态。
服务器启动时的leader选举
每个节点启动的时候状态都是LOOKING,处于观望状态,接下来就开始进 行选主流程。
若进行Leader选举,则至少需要两台机器,这里选取3台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器Server1启动时,其单独无法进行和完成Leader选举,当第二台服务器Server2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。选举过程如下:
-
每个
Server发出一个投票。由于是初始情况,Server1和Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid、ZXID和epoch,使用(myid,ZXID,epoch)来表示, 此时Server1的投票为(1,0,0),Server2的投票为(2,0,0),然后各自将这个投票发给集群中其他机器。 -
接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票(epoch)、是否来自
LOOKING状态的服务器。 -
处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,PK规则如下:
- 优先对比epoch,epoch大的优先级高。
- 其次对比
ZXID。ZXID比较大的服务器优先作为Leader。 - 如果
ZXID相同,那么就比较myid,myid较大的服务器作为Leader服务器。 对于Server1而言,它的投票是(1, 0, 0),接收Server2 的投票为(2, 0, 0), 首先会比较两者的epoch和ZXID,均为 0,再比较myid,此时Server2的myid最大,于是更新自己的投票为(2, 0, 0),然后重新投票。对于Server2而言,其无须更新自己的投票,只是再次向集群中所有机器 发出上一次投票信息即可。
-
统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于
Server1、Server2而言,都统计出集群中已经有两台机器接受了(2, 0, 0)的投票信息,此时便认为已经选出了Leader。 -
改变服务器状态。一旦确定了
Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader, 就变更为LEADING。
运行过程中的leader选举
当集群中的leader服务器出现宕机或者不可用的情况时,那么整个集群将无法对外提供服务,而是进入新一轮的 Leader选举,服务器运行期间的Leader选举和启动时期的Leader选举基本过程是一致的。
- 变更状态。Leader挂后,余下的非
Observer服务器都会将自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。 - 每个
Server会发出一个投票。在运行期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为123,Server3的ZXID为 122。在第一轮投票中,Server1和Server3都会投自己,产生投票(1, 123, 0),(3, 122, 0),然后各自将投票发送给集群中所有机器。接收来自各个服务器的投票,与启动时过程相同。 - 处理投票。与启动时过程相同,此时,
Server1将会成为Leader。 - 统计投票。与启动时过程相同。
- 改变服务器的状态。与启动时过程相同
leader选举的源码分析
源码分析,最关键的是要找到一个入口,对于zk的leader选举,并不由客户端来触发,而是在启动的时候会触发一次选举。因此我们可以直去看启动脚本zkServer.sh 中的运行命令。ZOOMAIN就是QuorumPeerMain。那么我们基于这个入口来看:
QuorumPeerMain.main()
main()方法中,调用了initializeAndRun(args)进行初始化并且运行。
QuorumPeerMain.initializeAndRun()
这里主要是加载
zoo.cfg配置文件,根据配置运行。
QuorumPeerMain.runFromConfig()
从名字可以看出来,是基于配置文件来进行启动。所以整个方法都是对参数进行解析和设置,因为这些参数暂时还没用, 所以没必要去看。直接看核心的代码quorumPeer.start(),它启动一个线程,那么从这句代码可以看出 QuorumPeer实际是继承了线程。那么它里面一定有一个run()方法。
QuorumPeer.start()
QuorumPeer.start() 方法,重写了Thread的start()方法。在线程启动之前,会做以下操作:
- 通过
loadDataBase()恢复快照数据 cnxnFactory.start()启动zkServer,相当于用户可以通过2181这个口进行通信了,这块后续再讲。我们还是以leader选举为主线。
QuorumPeer.startLeaderElection()
leader选举的方法:
- 构建当前票据
- 获取当前
zkServer中的myid对应的ip地址 - 创建选举算法
quorumPeer. createElectionAlgorithm()
根据对应的标识创建选举算法。
FastLeaderElection
初始化FastLeaderElection,QuorumCnxManager是一个很核心的对象,用来实现领导选举中的网络连接管理功能,这个后面会用到。
FastLeaderElection. starter()
starter()方法里面,设置了一些成员属性,并且构建了两个阻塞队列,分是sendQueue和recvqueue。并且实例化了一个Messager。
Messenger
在Messenger里面构建了两个线程,一个是WorkerSender,一个WorkerReceiver。 这两个线程是分别用来发送和接收消息的线程。具做什么,暂时先不分析。
小结
分析到这里,先做一个简单的总结,通过一个流程图把前面部分串联起来。
ZkServer服务启动的逻辑
在讲leader选举的时候,有一个cnxnFactory.start()方法来启动zk服务,这块具体做了什么呢?我们来分析看看:
在runFromConfig中,有构建了一个ServerCnxnFactory。
ServerCnxnFactory.createFactory()
这个方法里面是根据 ZOOKEEPER_SERVER_CNXN_FACTORY来决定创建NIO server还是Netty Server,而默认情况下,应该是创建一个NIOServerCnxnFactory。
QuorumPeer.start()
我们再回到QuorumPeer.start()方法中,cnxnFactory.start(),应会调用NIOServerCnxnFactory这个类去启动一个线程。
NIOServerCnxnFactory.start()
这里通过thread.start()启动一个线程,那thread是一个什么对象呢?
NIOServerCnxnFactory.configure()
thread其实构建的是一个zookeeperThread线程,并且线程的参数为this, 表示当前 NIOServerCnxnFactory也是实现了线程的类,那么它必须要重写run()方法。
到此,NIOServer的初始化以及启动过程就完成了。并且对2181的这个 端口进行监听。一旦发现有请求进来,就执行相应的处理即可。这块后续在分析数据同步的时候再做详细了解。
选举流程分析
前面分析这么多,还没有正式分析到
leader选举的核心流程,前期准工作做好了以后,接下来就开始正式分析 leader选举的过程。
很明显,super.start()表示当前类QuorumPeer继承了线程,线程必须重写run()方法,所以我们可以在 QuorumPeer中找到一个 run()方法。
QuorumPeer.run()
PeerState有几种状态,分别是:
LOOKING,竞选状态。FOLLOWING,随从状态,同步leader状态,参与投票。OBSERVING,观察状态,同步leader状态,不参与投票。LEADING,领导者状态。
对于选举来说,默认都是LOOKING状态,只有LOOKING状态才会去执行选举算法。每个服务器在启动时都会选择 自己做为领导,然后将投票信息发送出去,循环一直到选举出领导为止。
FastLeaderElection.lookForLeader()
leader选举核心代码。
投票处理的流程图
FastLeaderElection.termPredicate()
QuorumMaj. containsQuorum()
判断当前节点的票数是否是大于一半,默认采用QuorumMaj来实现。
投票的网络通信流程
通信过程源码分析
每个 zk 服务启动后创建 socket 监听
run()方法里面就是创建socket监听。
FastLeaderElection.lookForLeader
这个方法在前面分析过,里面会调用 sendNotifications 来发送投票请求:
FastLeaderElection.sendqueue
sendQueue这个队列的数据,是通过WorkerSender来进行获取并发的。而这个WorkerSender线程,在构建 fastLeaderElection时,会启动。
QuorumCnxManager.toSend
startConnection
SendWorker会监听对应sid的阻塞队列,启动的时候回如果队列为空时会重新发送一次最前最后的消息,以防上一次处理是服务器异常退出,造成上一条消息未处理成功;然后就是不停监听队里,发现有消息时调用send()方法RecvWorker:RecvWorker不停监听socket的inputstream,读取消息放 到消息接收队列中,消息放入队列中,qcm的流程就完毕了。
leader选举完成之后的处理逻辑
通过lookForLeader方法选举完成以后,会设置当前节点的PeerState, 要么为Leading、要么就是 FOLLOWING、或者OBSERVING。到这里,只是表示当前的leader选出来了,但是QuorumPeer.run()方法里面还没执行完,我们再回过头看看后续的处理过程。
QuorumPeer.run()
分别来看看case为FOLLOWING和LEADING,会做什么事情:
follower.followLeader()
makeLeader
初始化一个Leader对象,构建一个LeaderZookeeperServer,用于表leader节点的请求处理服务。
leader.lead()
在 Leader端, 则通过lead()来处理与Follower的交互。