选举主流程
上一篇文章中我们简要的介绍了集群的启动过程,主要有两步涉及的群主选举
1 | public synchronized void start() { |
再深入代码发现FastLeaderElection里面也是多个Thread类,所以这两个步骤很多过程是并行执行的。我们先看第一步中,串行部分:
1 | public synchronized void startLeaderElection() { |
创建群主选举的通信管理器,并开启选举通信,listener.start()
启动选举也就是FastLeaderElection.start()
也就是最开始的QuorumPeer.start();
选举的通信管理我们单独作为了一个章节来讲,有兴趣的可以看源码解析三、选举中的通信.我们这里只讲FastLeaderElection启动过程、QuorumPeer.run方法
FastLeaderElection
主要属性如下:
1 | QuorumCnxManager manager;// 通信管理器 |
starter 初始化,票据和自己的zxid都为-1 ,初始化发送和接收队列。
1 | private void starter(QuorumPeer self, QuorumCnxManager manager) { |
Messenger
主要工作的类还是message,这是一个多线程实现的消息管理器
1 | protected class Messenger { |
WorkerReceiver
WorkerReceiver主要功能都在run方法
1 | public void run() { |
过程总结如下:
- 从选举通信消息管理器中拿到消息
- 组装成投票信息
- 验证消息的sid是否合法,不合法则返回给对方一个消息,将其放入sendqueue
- 判断自身的状态,如果是LOOKING,则将这条消息放到接受队列recvqueue
- 如果对端也是LOOKING状态,且对方的Epoch比我们的旧,则发送我们的投票,尝试让其更新,将其放入sendqueue
- 如果本节点应不是LOOKING状态,且对端状态是LOOKING,则发送我们的投票信息,将其放入sendqueue
如果本身是LEADING状态,有可能会触发自身状态的改变,这个留到后面讲
WorkerSender
相对于WorkerReceiver,WorkerSender更简单,就是将需要发送的消息从sendqueue放到manager里的queueSendMap准备发送。
1 | // 拿到消息然后发送 |
主线程中的群主选举
主要集中在run方法中,由于run方法是在节点运行的所有过程,因此我们这里只列举与群主选举有关的部分:
代码分析
1 |
|
于是我们的目光转到setCurrentVote(makeLEStrategy().lookForLeader());
其中makeLEStrategy()是获取选举算法,我们在上一步createElectionAlgorithm已经设置了,也就是FastLeaderElection。
我们选举真正的核心是lookForLeader()方法:
1 | /** |
过程总结
看完代码,我们再来总结一下流程
- 初始化自己的选票
- 广播自己的选票
- 从recvqueue拿到下一个选票
- 如果消息为空,那么就再广播一次,这是因为有可能其他节点没有收到消息。
- 验证选票的正确性,一是验证当前候选人和上一轮候选人是否包括该sid,二是验证当前候选人和上一轮候选人是否包括该选票的推举人。
- 对选票做处理:
- LOOKING状态的选票
- 如果选票的逻辑始终大于当前节点的逻辑时钟,则说明当前节点慢多个轮次。则重置当前节点的时钟,并清除之前的选票,然后广播更新后的选票
- 将选票放入recvset,注意每个服务器都只有一份,所以重复发没有副作用。
- 统计选票(针对当前选票),也就是当前别选举的节点的投票。如果该投票仲裁有效(也就是当前投票数超过一半)看是否有新的选票进来,如果没有更好的选票,则结束选票,选择当前所选举的Leader
- FELLOW或者LEADER的选票
- 如果轮次是一样的,进行统计所有的选票,如果成功,则返回。
- 再对不在LOOKING状态的选票进行一次统计,并设置新的轮次。这个针对新节点加入已经稳定后的集群。
- 没有结束则继续获取选票。
- LOOKING状态的选票
[图片上传失败…(image-2d2cd3-1606390914706)]
一些参数介绍:
logicalclock(当前节点)=electionEpoch(当前的投票) // 选举的轮次,默认是0,每执行一次选举都会++
proposedEpoch(当前的节点)=peerEpoch(当前的投票) // 每次leader选举完成之后,都会选举出一个新的
peerEpoch,作为该Leader轮次,用来标记事务请求所属的轮次。currentEpoch 如果不是Leader的时候,一直是初始化的Epoch,如果是leader,成为leader之后的最新的轮次
投票pk
- 如果当前票据的Leader轮次大于当前节点的Leader轮次,则返回true;
- 如果当前票据和当前节点自己的票据Leader轮次相等,则比较事物id zxid,如果当前票据更大,则返回true;
- 如果当前票据和当前节点自己的票据Leader轮次和事物zxid都相等,则比较两者的sid 如果当前sid更大,返回true;
- 其他情况返回false,也就是不用更新选票。
流程总结
结合另一章节介绍的选举中的通信,我们可以梳理出,选举过程中所有组件的交互关系。一共三个组件QuorumCnxManager、FastLeaderElection、QuorumPeer。
都是作为一个或多个线程来并行处理,从而选出最后的主节点:
脑裂问题
之所以没有单独放到一个章节来讲,主要是因为zookeeper如今的版本已基本解决了脑裂的问题。
什么是脑裂
很多集群模式工作的组件比如zookeeper、elasticSearch,他们通常有一个master节点,负责调度或者说负责管理、通信,总之一定有一个中心节点(可以看作是集群的大脑)。在正常工作的情况下,一般没有问题,但如果出现通信异常、程序异常
的情况,就是出现两个master节点,这就是脑裂现象。
如何产生
- 集群部分通信断开
假设我们有六个节点,一个网段有三台,某一刻,两个网段的连接断开:
在如上的情况下,网络断开后,另一个网段就会有可能自主的产生一个新的master节点,这样就发生了脑裂
2. master节点断开后又连上
假设我们有四个节点,在某个瞬间,master节点断开通信,或者延迟。那么这个时候其他节点会重新选举出一个新的master节点,等到master节点恢复正常,它会认为它依然是master节点,这个时候集群出现了两个,也就是脑裂。
解决方案
在当前版本中,zookeeper已经解决以上两个问题,如果依照上文看懂了zookeeper的源码,那应该就能给出答案。
- 使用仲裁机制Quorum,也就是master节点产生,必须获得半数以上的节点
如上网段二是三台机器,那么就完全没有办法选举出master节点,注意这里是半数以上>,而不是半数或者以上>=。 - 使用epch机制,每次选举都会累加
在上述情况二中,master断开,其他三个节点重新选举,选票为3超过了一半有效,且epoch+1,等之前的master重新恢复之后,它的epoch已经是旧的了,所以其他三个节点都不会认同,而是会触发其(旧master)更新,这样就防止了脑裂