zookeeper leader选举机制
最近看了下zookeeper的源码,先整理下leader选举机制
先看几个关键数据结构和函数
服务可能处于的状态,从名字应该很好理解
public enum ServerState {
LOOKING, FOLLOWING, LEADING, OBSERVING;
}
选票参数,还有Notification,参数也都差不多

static public class ToSend {
long leader; //leader id
long zxid; //选票的zxid
long electionEpoch; //投票轮数
QuorumPeer.ServerState state; //状态
long sid; //投票人id
long peerEpoch; //选票的epoch
}

选票的比较逻辑也很简单,依次比较几个关键字段
protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
...
return ((newEpoch > curEpoch) ||
((newEpoch == curEpoch) &&
((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
}
选举流程
1. 发起投票:
首先投票给自己,然后给所有Acceptor;
2. 等待ack
先判断自身状态
1)如果自身状态不是LOOKING:
说明已经有多数派通过选举结果,直接将选举结果通知给来源方;
2)如果自身状态是LOOKING:
检查投票者状态:
2.1)如果选票是LOOKING发起的,说明当前正在选举,需要收集选票,检查选举条件:
首先检查选票轮数:
如果小于自身投票的轮数,说明收到的选票过期,忽略;
如果大于自身投票的轮数,说明自己已经out:
清空选票箱,根据优先级更新自己的选票,然后notify;
如果等于自身投票的轮数,投票有效:
根据优先级更新自己的选票,然后notify;
将选票投入选票箱;
检查最新选票是否可以通过:
不满足通过条件:
继续等待新的选票:
满足通过条件:
接收新选票,看能否收到优先级更高的选票:
如果有优先级高的选票,继续循环;
否则投票结束,更新状态。
2.2)如果选票是FOLLOWING、LEADING发起的,说明已有多数派通过选举,此时只需确认是否满足多数派:
检查选票是否满足多数派:
2.2.1)满足选票终结。
2.2.2)继续循环等待
代码流程还算清晰,再来考虑下实际投票中可能情况,以上逻辑能否满足:
1. 一台宕机重启的机器加入已有环境
此时肯定有一个多数派接收选票时进入状态1),这个多数派会将当前选举结果返回,这些选票的处理流程都会进入2.2,当所有选票收到时,2.2.1满足,选举结束
2. 一台机器加入正在投票中的环境
如果当前机器的,如果所有server都会接受优先级最高的投票,投票会逐渐收敛,最高优先级最高的选票当选,选举结束;
3. 当集群中多数机器宕机重启
存活的服务发现不满足多数派,改变状态为LOOKING,投票轮数+1, 然后重新开始投票,此时会进入上面情况2。
以上,只要有超过半数的机器存活,经过投票,收敛后,最终会完成投票。
在上一篇文章中我们大致浏览了zookeeper的启动过程,并且提到在Zookeeper的启动过程中leader选举是非常重要而且最复杂的一个环节。那么什么是leader选举呢?zookeeper为什么需要leader选举呢?zookeeper的leader选举的过程又是什么样子的?本文的目的就是解决这三个问题。
首先我们来看看什么是leader选举。其实这个很好理解,leader选举就像总统选举一样,每人一票,获得多数票的人就当选为总统了。在zookeeper集群中也是一样,每个节点都会投票,如果某个节点获得超过半数以上的节点的投票,则该节点就是leader节点了。
国家选举总统是为了选一个最高统帅,治理国家。那么zookeeper集群选举的目的又是什么呢?其实这个要清楚明白的解释还是挺复杂的。我们可以简单点想这个问题:我们有一个zookeeper集群,有好几个节点。每个节点都可以接收请求,处理请求。那么,如果这个时候分别有两个客户端向两个节点发起请求,请求的内容是修改同一个数据。比如客户端c1,请求节点n1,请求是set a = 1; 而客户端c2,请求节点n2,请求内容是set a = 2;
那么最后a是等于1还是等于2呢? 这在一个分布式环境里是很难确定的。解决这个问题有很多办法,而zookeeper的办法是,我们选一个总统出来,所有的这类决策都提交给总统一个人决策,那之前的问题不就没有了么。
那我们现在的问题就是怎么来选择这个总统呢? 在现实中,选择总统是需要宣讲拉选票的,那么在zookeeper的世界里这又如何处理呢?我们还是show code吧。
在QuorumPeer的startLeaderElection方法里包含leader选举的逻辑。Zookeeper默认提供了4种选举方式,默认是第4种: FastLeaderElection。
我们先假设我们这是一个崭新的集群,崭新的集群的选举和之前运行过一段时间的选举是有稍许不同的,后面会提及。
节点状态: 每个集群中的节点都有一个状态 LOOKING, FOLLOWING, LEADING, OBSERVING。都属于这4种,每个节点启动的时候都是LOOKING状态,如果这个节点参与选举但最后不是leader,则状态是FOLLOWING,如果不参与选举则是OBSERVING,leader的状态是LEADING。
开始这个选举算法前,每个节点都会在zoo.cfg上指定的监听端口启动监听(server.1=127.0.0.1:20881:20882),这里的20882就是这里用于选举的端口。
在FastLeaderElection里有一个Manager的内部类,这个类里有启动了两个线程:WorkerReceiver, WorkerSender。为什么说选举这部分复杂呢,我觉得就是这些线程就像左右互搏一样,非常难以理解。顾名思义,这两个线程一个是处理从别的节点接收消息的,一个是向外发送消息的。对于外面的逻辑接收和发送的逻辑都是异步的。
这里配置好了,QuorumPeer的run方法就开始执行了,这里实现的是一个简单的状态机。因为现在是LOOKING状态,所以进入LOOKING的分支,调用选举算法开始选举了:
setCurrentVote(makeLEStrategy().lookForLeader());
而在lookForLeader里主要是干什么呢?首先我们会更新一下一个叫逻辑时钟的东西,这也是在分布式算法里很重要的一个概念,但是在这里先不介绍,可以参考后面的论文。然后决定我要投票给谁。不过zookeeper这里的选举真直白,每个节点都选自己(汗),选我,选我,选我...... 然后向其他节点广播这个选举信息。这里实际上并没有真正的发送出去,只是将选举信息放到由WorkerSender管理的一个队列里。

synchronized(this){
//逻辑时钟
logicalclock++;
//getInitLastLoggedZxid(), getPeerEpoch()这里先不关心是什么,后面会讨论
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
//getInitId() 即是获取选谁,id就是myid里指定的那个数字,所以说一定要唯一
private long getInitId(){
if(self.getQuorumVerifier().getVotingMembers().containsKey(self.getId()))
return self.getId();
else return Long.MIN_VALUE;
}
//发送选举信息,异步发送
sendNotifications();

现在我们去看看怎么把投票信息投递出去。这个逻辑在WorkerSender里,WorkerSender从sendqueue里取出投票,然后交给QuorumCnxManager发送。因为前面发送投票信息的时候是向集群所有节点发送,所以当然也包括自己这个节点,所以QuorumCnxManager的发送逻辑里会判断,如果这个要发送的投票信息是发送给自己的,则不发送了,直接进入接收队列。

public void toSend(Long sid, ByteBuffer b) {
if (self.getId() == sid) {
b.position(0);
addToRecvQueue(new Message(b.duplicate(), sid));
} else {
//发送给别的节点,判断之前是不是发送过
if (!queueSendMap.containsKey(sid)) {
//这个SEND_CAPACITY的大小是1,所以如果之前已经有一个还在等待发送,则会把之前的一个删除掉,发送新的
ArrayBlockingQueue<ByteBuffer> bq = new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY);
queueSendMap.put(sid, bq);
addToSendQueue(bq, b);
} else {
ArrayBlockingQueue<ByteBuffer> bq = queueSendMap.get(sid);
if(bq != null){
addToSendQueue(bq, b);
} else {
LOG.error("No queue for server " + sid);
}
}
//这里是真正的发送逻辑了
connectOne(sid);
}
}

connectOne就是真正发送了。在发送之前会先把自己的id和选举地址发送过去。然后判断要发送节点的id是不是比自己的id大,如果大则不发送了。如果要发送又是启动两个线程:SendWorker,RecvWorker(这种一个进程内许多不同种类的线程,各自干活的状态真的很难理解)。发送逻辑还算简单,就是从刚才放到那个queueSendMap里取出,然后发送。并且发送的时候将发送出去的东西放到一个lastMessageSent的map里,如果queueSendMap里是空的,就发送lastMessageSent里的东西,确保对方一定收到了。
看完了SendWorker的逻辑,再来看看数据接收的逻辑吧。还记得前面提到的有个Listener在选举端口上启动了监听么,现在这里应该接收到数据了。我们可以看到receiveConnection方法。在这里,如果接收到的的信息里的id比自身的id小,则断开连接,并尝试发送消息给这个id对应的节点(当然,如果已经有SendWorker在往这个节点发送数据,则不用了)。
如果接收到的消息的id比当前的大,则会有RecvWorker接收数据,RecvWorker会将接收到的数据放到recvQueue里。
而FastLeaderElection的WorkerReceiver线程里会不断地从这个recvQueue里读取Message处理。在WorkerReceiver会处理一些协议上的事情,比如消息格式等。除此之外还会看看接收到的消息是不是来自投票成员。如果是投票成员,则会看看这个消息里的状态,如果是LOOKING状态并且当前的逻辑时钟比投票消息里的逻辑时钟要高,则会发个通知过去,告诉谁是leader。在这里,刚刚启动的崭新集群,所以逻辑时钟基本上都是相同的,所以这里还没判断出谁是leader。不过在这里我们注意到如果当前节点的状态是LOOKING的话,接收逻辑会将接收到的消息放到FastLeaderElection的recvqueue里。而在FastLeaderElection会从这个recvqueue里读取东西。
这里就是选举的主要逻辑了:totalOrderPredicate

protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {return ((newEpoch > curEpoch) ||
((newEpoch == curEpoch) &&
((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
}

1. 判断消息里的epoch是不是比当前的大,如果大则消息里id对应的server我就承认它是leader
2. 如果epoch相等则判断zxid,如果消息里的zxid比我的大我就承认它是leader
3. 如果前面两个都相等那就比较一下server id吧,如果比我的大我就承认它是leader。
关于前面两个东西暂时我们不去关心它,对于新启动的集群这两者都是相等的。
那这样看来server id的大小也是leader选举的一环啊(有的人生下来注定就不平凡,这都是命啊)。
最后我们来看看,很多文章所介绍的,如果超过一半的人说它是leader,那它就是leader的逻辑吧

private boolean termPredicate(
HashMap<Long, Vote> votes,
Vote vote) { HashSet<Long> set = new HashSet<Long>();
//遍历已经收到的投票集合,将等于当前投票的集合取出放到set中
for (Map.Entry<Long,Vote> entry : votes.entrySet()) {
if (self.getQuorumVerifier().getVotingMembers().containsKey(entry.getKey())
&& vote.equals(entry.getValue())){
set.add(entry.getKey());
}
} //统计set,也就是投某个id的票数是否超过一半
return self.getQuorumVerifier().containsQuorum(set);
} public boolean containsQuorum(Set<Long> ackSet) {
return (ackSet.size() > half);
}

最后一关:如果选的是自己,则将自己的状态更新为LEADING,否则根据type,要么是FOLLOWING,要么是OBSERVING。
到这里选举就结束了。
这里介绍的是一个新集群启动时候的选举过程,启动的时候就是根据zoo.cfg里的配置,向各个节点广播投票,一般都是选投自己。然后收到投票后就会进行进行判断。如果某个节点收到的投票数超过一半,那么它就是leader了。
了解了这个过程,我们来看看另外一个问题:
一个集群有3台机器,挂了一台后的影响是什么?挂了两台呢?
挂了一台:挂了一台后就是收不到其中一台的投票,但是有两台可以参与投票,按照上面的逻辑,它们开始都投给自己,后来按照选举的原则,两个人都投票给其中一个,那么就有一个节点获得的票等于2,2 > (3/2)=1 的,超过了半数,这个时候是能选出leader的。
挂了两台: 挂了两台后,怎么弄也只能获得一张票, 1 不大于 (3/2)=1的,这样就无法选出一个leader了。
在前面介绍时,为了简单我假设的是这是一个崭新的刚启动的集群,这样的集群与工作一段时间后的集群有什么不同呢?不同的就是epoch和zxid这两个参数。在新启动的集群里这两个一般是相等的,而工作一段时间后这两个参数有可能有的节点落后其他节点,至于是为什么,这个还要在后面的存储和处理额胡断请求的文章里介绍。
* 关于逻辑时钟,我们的分布式大牛Leslie Lamport曾写过一篇论文:Time, Clocks, and the Ordering of Events in a Distributed System
zookeeper leader选举机制的更多相关文章
- Zookeeper中的watcher监听和leader选举机制
watcher监听 什么是watcher接口 同一个事件类型在不同的通知状态中代表的含义有所不同,下图列举了常见的通知状态和事件类型. Watcher通知状态与事件类型一览 上图列举了ZooKeepe ...
- 面试官:说一说Zookeeper中Leader选举机制
哈喽!大家好,我是小奇,一位不靠谱的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 今天又是一个阳光明媚的一天,我又 ...
- Zookeeper的选举机制和同步机制超详细讲解,面试经常问到!
前言 zookeeper相信大家都不陌生,很多分布式中间件都利用zk来提供分布式一致性协调的特性.dubbo官方推荐使用zk作为注册中心,zk也是hadoop和Hbase的重要组件.其他知名的开源中间 ...
- zookeeper的选举机制
1)半数机制:集群中半数以上机器存活,集群可用.所以zookeeper适合装在奇数台机器上. 2)Zookeeper虽然在配置文件中并没有指定master和slave.但是,zookeeper工作时, ...
- Zookeeper——分布式一致性协议及Zookeeper Leader选举原理
文章目录 一.引言 二.从ACID到CAP/BASE 三.分布式一致性协议 1. 2PC和3PC 2PC 发起事务请求 事务提交/回滚 3PC canCommit preCommit doCommit ...
- 分布式协调组件Zookeeper之 选举机制与ZAB协议
Zookeeper简介: Zookeeper是什么: Zookeeper 是⼀个分布式协调服务的开源框架. 主要⽤来解决分布式集群中应⽤系统的⼀致性问题, 例如怎样避免同时操作同⼀数据造成脏读的问题. ...
- Zookeeper leader选举
让我们分析如何在ZooKeeper集合中选举leader节点.考虑一个集群中有N个节点.leader选举的过程如下: 所有节点创建具有相同路径 /app/leader_election/guid_ 的 ...
- 服务端相关知识学习(五)之Zookeeper leader选举
在上一篇文章中我们大致浏览了zookeeper的启动过程,并且提到在Zookeeper的启动过程中leader选举是非常重要而且最复杂的一个环节.那么什么是leader选举呢?zookeeper为什么 ...
- zookeeper的leader选举机制个人总结
第一步:每个服务器都首先投自己,格式为<sid,zxid>: 第二步:然后将自己的投票以<sid,zxid>形式发送给其他服务器,这样每个服务器除了自己的投票,还有集群中除了自 ...
随机推荐
- Android手机刘海屏(附工具类)
工具类 根据VIVO.OPPO.华为官方文档,这里整理了一个刘海屏工具类,判断设备是否为刘海屏,其他厂商公布相关方法后也会在此更新. OPPO: /** * OPPO * * @param conte ...
- 内存分配方式,堆区,栈区,new/delete/malloc/free
1.内存分配方式 内存分配方式有三种: [1]从静态存储区域分配.内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量,static变量. [2]在栈上创建.在执行函数时 ...
- python读取两个csv文件数据,进行查找匹配出现次数
现有需求 表1 表2 需要拿表1中的编码去表2中的门票编码列匹配,统计出现的次数,由于表2编码列是区域间,而且列不是固定的,代码如下 #encoding:utf-8 ##导入两个CSV进行比对 imp ...
- Codeforces 5C Longest Regular Bracket Sequence(DP+括号匹配)
题目链接:http://codeforces.com/problemset/problem/5/C 题目大意:给出一串字符串只有'('和')',求出符合括号匹配规则的最大字串长度及该长度的字串出现的次 ...
- python 全栈开发,Day58(bootstrap组件,bootstrap JavaScript 插件,后台模板,图表插件,jQuery插件库,Animate.css,swiper,运行vue项目)
一.bootstrap组件 无数可复用的组件,包括字体图标.下拉菜单.导航.警告框.弹出框等更多功能. 组件和插件的区别? 插件:一个功能,比如js文件 组件:html css js 组件包含插件 面 ...
- java根据word模板导出word文件
1.word模板文件处理,如下图所示在word 文档中填值的地方写入占位变量 2.将word文档另存为xml文件.编辑如下图,找到填写的占位,修改为${bcrxm}格式 3.将文件后缀名改为.ftl文 ...
- javafx的scene大小不能在控制器中设置
做个记录,备忘 遇到一个问题,点击提交,红色标识的VBox大小不能改变.问题如图,代码如下 点击后 fxml结构简化代码: 红色框体对应vboxMax <VBox fx:id="vbo ...
- java:根据利润表计算奖金所得
代码实现: public class Hello { public static void main(String srgs[]) { Lirun(100); Lirun2(100); } publi ...
- BZOJ1297 [SCOI2009]迷路 矩阵乘法
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - BZOJ1297 题意概括 有向图有 N 个节点,从节点 0 出发,他必须恰好在 T 时刻到达节点 N-1. ...
- 013 JstlView
一:InternalResourceViewResolver 1.InternalResourceViewResolver JSP是常见的视图技术,可以使用InternalResourceViewRe ...