Zookeeper选举整体流程源码解析
Posted adj20
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Zookeeper选举整体流程源码解析相关的知识,希望对你有一定的参考价值。
一 Zookeeper选举流程概述
Zookeeper选举机制分为两种:第一次启动和非第一次启动。假设有5个节点,如图1.1所示:
图1.1 ZK集群
1.1 第一次启动
(1)服务器1启动,发起选举。服务器1投自己一票,此时服务器1有一票,未超过半数以上票数,选举无法完成,服务器保持状态为LOOKING;
(2)服务器2启动,再次发起选举。服务器1和服务器2分别半先投给自己1票,服务器1和服务器2会通信,此时服务器1发现服务器2的myid比自己大,因此会将自己的票给服务器2。此时服务器1票数为0,服务器票数为2。服务器2的票数没有达到半数以上,选举无法完成,服务器1和2状态为LOOKING;
(3)服务器3启动,发起一次选举。最终服务1会有0票,服务器2会有0票,服务器3会有3票,此时服务器3的票数超过半数,因此服务器3当选为leader。服务器1和服务器2的状态变为FOLLOWING,服务器3的状态变为LEADING;
(4)随后服务器4和服务器5启动,此时已经有leader了,不再进行选举了,服务器4和服务器5更改状态为FOLLOWING。
1.2 非第一次启动
当集群中的一台服务器无法和leader保持连接时,会进入leader选举,而此时,集群会有以下两种状态:
集群中已经存在leader:针对这种情况,该节点试图去选择leader时,会被其他节点告知当前服务器的leader信息。因此,该服务器仅需要和leader重新建立连接并同步状态即可。
集群中已经不存在leader:假设集群由5个节点组成,SID分别为1,2,3,4,5;ZXID分别为9,9,8,7,6,EPOCH均为1,且此时SID为3的服务器为leader。突然,当3和5服务器发生故障时,会触发leader选举。选举规则为:EPOCH大的直接胜出,如果EPOCH相同,则ZXID大的胜出,如果ZXID相同,则SID大的胜出。
SID:服务器ID,用来标识一台服务器,ID不能重复,且和myid一致
ZXID:事务ID,用来标识一次服务器状态的变更。在某一时刻,集群中的每台机器的ZXID不一定一致
EPOCH:每个leader任期的代号。每投完一次票该数据会增加
二 选举源码概述
源码中的选举整体流程如图2.1所示:
图2.1 源码选举概述图
整个选举过程主要由两个类完成,FastLeaderElection和QuorumCnManager。FastLeaderElection负责接收选票和发送选票; QuorumCnManager负责管理节点之间的选举通信,负责选择票数的传递。
2.1 选举准备源码解析
图2.2
选举代码的入口在QuorumPeerMain中的runFromConfig方法中,如图2.2所示“在完成服务器启动的初始化后,会执行quorumPeer.start(),进入该方法,执行startLeaderElection()方法进入选举流程。
如果当前节点的状态为LOOKING(getPeerState() == ServerState.LOOKING),会创建一个Vote类,该类中有id,zxid,electionEpoch等选举关键信息。随后,如图2.3所示,会通过调用createElectionAlgorithm(electionType)来创建QuorumCnxManager,该类维护了一个接收队列,recvQueue,该队列接收其他节点的投票;也维护了一个queueSendMap,该map的键为服务器ID,值为ArrayBlockingQueue队列,该队列包含了键值所指向的服务器的选票;该类还维护了一个senderWorkerMap,该map存储了一个发送线程,负责将投票发送到其他节点,还存储了一个接收线程,负责接收其他节点的投票。
如图2.3所示,监听类QuorumCnxManager.Listener继承了线程类,在覆写的run方法中,只要未shutdown,会一直等待其他节点发送数据。
图 2.3
2.2 选举执行源码解析
图2.4
执行super.start(),就是执行QuorumPeer.java类中覆写的run()方法,当zookeeper启动后,首先都是Looking状态,通过选举让其中一台服务器成为Leader,其他的成为Follower
图2.5
通过调用setCurrentVote(makeLEStrategy().lookForLeader())进行选举,进入lookForLeader()方法,这里进入FastLeaderElection实现的lookForLeader方法,如图2.6所示:
图2.6
recvset保存了每一个服务器给该节点的合法有效投票,recvset的键为服务器id,value为其他节点给我的投票信息。notTimeout为一次选举的最大等待时间,默认是0.2s。继续往下看,如图2.7所示:
图2.7
每发起一轮选举,都会使logicallock加1,然后调用updateProposal()更新选票信息 。更新完选票信息后,调用sendNotifications()广播选票。如图2.8所示:
图2.8
通过for循环遍历投票参与者,给每台服务器发送选票(就是给SID比自己大的服务器投票),通过new 一个ToSend对象创建发送选票信息,然后将该信息放入队列sendqueue中。该队列中的信息是由WorkerSender(WorkerSender是FastLeaderElection的一个内部类)线程来负责发送的,WorkerSender的run方法如图2.9所示:
图 2.9
通过sendqueue.poll从队列中获取要发送的选票,然后将选票信息传给process方法进行处理,如图2.10:
图2.10
通过manager.toSend发送信息,manager为类QuorumCnManager,负责管理节点之间的选举通信,负责选择票数的传递。 toSend()如图2.11所示:
图2.12
首先通过this.mySid==id判断是不是自己给自己投票,如果是就通过addToRecvQueue将投票信息 放进自己的RecvQueue。如果是发送给其他服务器的选票,如果其他服务器的队列消息已经存现,就将选票信息放入oldq中,否则,则放入新的bq队列中。最后通过connectOne(sid)将消息发送出去。
在connectOne(sid)中会调用conncectOne(long sid, InetSocketAddress electionAddr)与其他节点建立连接,如图2.13所示:
图2.14
如图2.15所示,在conncectOne(long sid, InetSocketAddress electionAddr)方法中与其他节点建立连接,然后通过initiateConnection(sock, id)处理连接。
图2.15
在initiateConnection(sock, id)方法中调用startConnection(sock, id),并在该方法中创建并启动了发送器线程和接收器线程,如图2.16所示:
图2.16
有一处重要的判断if(sid>self.getId()) ,如果要发送的节点的id比自己要大,就直接关闭自己的客户端,意思就是不参与选举了。通过new SendWorker和RecvWorker创建发送和接收线程,并调用start方法启动发送和接收线程。
如图2.17所示,SendWorker会在run方法中一直循环从发送队列SendQueue中,获取发送消息,并调用send(b)进行发送。
图2.17
同样的,如图2.18.在RecvWorker的run方法中,也会一直从输入流中接收消息,调用addToRecvQueue将消息添加的发送队列中,后续会将其他节点给我的票选发送的我这。
图2.18
如图2.19,从 addToRecvQueue(Message msg)方法中,调用了recvQueue.add(msg)将其他节点给我的票选信息加入队列。
图2.19
最后回到FastLeaderElection类中查找WorkerReceiver线程,如图2.20所示,调用manager.pollRecvQueue方法从recvQueue中获取其他节点发送给我的票选信息。
至此,选举流程的源码分析到此结束了, 本文章只分析了一个整体流程的源码,可以帮助大家对选举流程有个整体的把握。有些细节没有去分析,有兴趣的读者可以自己下载源码分析
三 总结
选举源码流程总结如图3.1所示:
图3.1
附Zookeeper入门学习视频:
以上是关于Zookeeper选举整体流程源码解析的主要内容,如果未能解决你的问题,请参考以下文章
《Elasticsearch 源码解析与优化实战》第5章:选主流程
《Elasticsearch 源码解析与优化实战》第5章:选主流程
ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建