前情提要
上一篇我们止步于投票的验证部分,并且讲解了一个完整的接收和发送投票的机制以及,那么我们本篇就接着验证后的合法选票说。同样本篇也会被收录到【Zookeeper 源码解读系列目录】中。
选举的逻辑
那么我们接着上一篇的结尾继续讲解lookForLeader()
方法验证过选票后,又对选票做了什么操作:
public Vote lookForLeader() throws InterruptedException {
/**初始化,略**/
try {
/**已经讲过,略**/
//while会不停的拿别人发来的票
while ((self.getPeerState() == ServerState.LOOKING) &&(!stop)){
//获取其他服务器的投票
Notification n = recvqueue.poll(notTimeout,TimeUnit.MILLISECONDS);
if(n == null){//验证合法性
/**已经讲过,略**/
}
else if(validVoter(n.sid) && validVoter(n.leader)) { //如果收到了合法结果
switch (n.state) {//判断选举状态
case LOOKING:
if (n.electionEpoch > logicalclock.get()) {//如果接收到的周期比自己的高
logicalclock.set(n.electionEpoch);//更新自己的时钟为外来的时钟周期
recvset.clear();//清空自己的选票箱子
//比较选票对应的服务器和本机
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
updateProposal(n.leader, n.zxid, n.peerEpoch);
} else {
//否则投给自己
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
sendNotifications(); //发送投票
} else if (n.electionEpoch < logicalclock.get()) {
//别人投票的周期,比我的小,就 break;直接放弃了,进行下一个循环
/**Log4j**/
break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)) {
//如果周期相等,那就比较sid,Zxid,Epoch
updateProposal(n.leader, n.zxid, n.peerEpoch);
sendNotifications(); //发送投票
}
//保存自己接收的选票
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
/**暂时略,后面讲**/
}
代码很长我们一步一步的讲。首先树立一点:只要是正在选举的服务器应该都是Looking
状态。就是说凡是走到lookForLeader()
这个方法里的就说明自己本身也是Looking
状态。因为外面就是通过switch
分配到Looking
分支进来的。这里的switch
中的分支判断n.state
就是获取的状态。所以这下面的逻辑就是在看n
这张选票对应的机器是什么状态。比如也是Looking
状态,那么就说明, 我本身和对面的机器都是Looking
状态。我们既然是在探究启动的时候选举过程是什么样的,因此我们直接去找到case LOOKING
这个分支就可以了。
选票的比较
首先就是判断谁的时钟周期大if (n.electionEpoch > logicalclock.get())
。如果接收到的投票周期比自己的周期高,更新自己的时钟周期为外来的周期logicalclock.set(n.electionEpoch)
。然后recvset.clear();
清空自己的选票箱子。这里为什么要清空呢?如果时钟周期不一样,那就没有意义了。正常情况下一个集群只有一个周期,如果不相同以大的为准,这一步其实就是在重置所有服务器的时钟周期。重置以后,比较选票对应的服务器和本机if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch()))
。如果选票对应的服务器更强,就更新为选票对应的服务器updateProposal(n.leader, n.zxid, n.peerEpoch);
。否则投给自己updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
,然后发送出去sendNotifications();
。
如果if (n.electionEpoch < logicalclock.get()
别人投票的周期比我的小,就打个Log
。然后什么也不做直接 break;
,相当于放弃了,跳出switch
进行下一次while
循环。
如果时钟周期相等,那就比较sid,zxid,epoch
这三个数据。判断谁的服务器内容是更高等级的数据if (totalOrderPredicate(n.leader,n.zxid,n.peerEpoch,proposedLeader,proposedZxid,proposedEpoch))
。那么我们看n.leader, n.zxid, n.peerEpoch
这些很明显就是外面传递进来的。而proposedLeader, proposedZxid, proposedEpoch
这三个就是当前自己的投票。这三个的值有可能是自己,也有可能会随着投票的进行更新出来的候选者的数据。所以我们看下totalOrderPredicate(***)
这个方法是怎么比较的:
protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
LOG.debug("id: " + newId + ", proposed id: " + curId + ", zxid: 0x" +
Long.toHexString(newZxid) + ", proposed zxid: 0x" + Long.toHexString(curZxid));
//这里getWeight是固定的,默认为1
if(self.getQuorumVerifier().getWeight(newId) == 0){
return false;
}
//这里就是比较的地方
return ((newEpoch > curEpoch) ||
((newEpoch == curEpoch) &&
((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
}
可以看出非常的容易理解:首先比较的就是epoch
,然后比的就是zxid
。如果都一样,那就比sid
谁大。最终如果结果是ture
,说明传入的是比较厉害的,返回true
。如果结果是false
说明自己比较厉害,返回false
。
所以说如果if (totalOrderPredicate(n.leader,n.zxid,n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch))
条件判断为true
,走到这个逻辑里面就说明传递进来的票比较强大。所以更新选票为外来的updateProposal(n.leader, n.zxid, n.peerEpoch);
然后发送投票sendNotifications();
。如果没有进来,那其实就是说没有必要再更新对不对,自己的比较大。
这些比较都过了以后,就会用这句话recvset.put(n.sid,new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
把选票全部都保存到recvset
这个队列里备用,为后面过半机制的验证做准备。但是这里还有一个问题:为什么不是先统计完一共有多少选票了再进行比较?其实recvset
投票箱存的是合法的投票。以上逻辑都是在更新proposed
的选票,也就是在选准leader
是谁。并不会清空外来的选票,除非突然来一个时钟周期更高的,此时自己持有的选票全部都无效了,所以会调用clearAll()
清空选票箱。因为要根据接收到的票决定自己是不是leader
,所以有必要保留所有的票。所以这里没接收一个投票统计一个投票,所以会首先进行选票届的比较。
过半验证
既然票都已经整理好也存好了,根据我们的分析,下面就要开始过滤过半机制验证了:
public Vote lookForLeader() throws InterruptedException {
/**初始化,略**/
try {
/**已经讲过,略**/
while ((self.getPeerState() == ServerState.LOOKING) &&(!stop)){
/**已经讲过,略**/
else if(validVoter(n.sid) && validVoter(n.leader)) {
//如果收到了结果
switch (n.state) {//判断选举状态
case LOOKING:
/**已经讲过,略**/
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
//根据接收到的选票,以及自己的投票,来判断能不能成为leader
if (termPredicate(recvset,
new Vote(proposedLeader, proposedZxid,
logicalclock.get(), proposedEpoch))) {
while((n = recvqueue.poll(finalizeWait,
TimeUnit.MILLISECONDS)) != null){
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)){
recvqueue.put(n);
break;
}
}
if (n == null) {
self.setPeerState((proposedLeader == self.getId()) ?
ServerState.LEADING: learningState());
//将被台服务器最终的选票返回出去
Vote endVote = new Vote(proposedLeader,
proposedZxid,
logicalclock.get(),
proposedEpoch);
leaveInstance(endVote);
return endVote;
}
}
break;
case OBSERVING:
LOG.debug("Notification from observer: " + n.sid);
break;
case FOLLOWING:
case LEADING:
/**不相关,略**/
break;
default:
LOG.warn("Notification state unrecognized: {} (n.state), {} (n.sid)", n.state, n.sid);
break;
}
} else {
/**不相关,略**/
}
}
return null;
} finally {
/**不相关,略**/
}
}
紧接着的if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch)))
就是过半的机制判断,我们进入termPredicate(***)
看下:
protected boolean termPredicate( HashMap<Long, Vote> votes, Vote vote) {
HashSet<Long> set = new HashSet<Long>();
//找到同样的投票
for (Map.Entry<Long,Vote> entry : votes.entrySet()) {
if (vote.equals(entry.getValue())){
set.add(entry.getKey());
}
}
//看这个set中的服务器是不是符合过半机制
return self.getQuorumVerifier().containsQuorum(set);
}
首先说明下参数:votes
是投票箱,vote
是自己的认可的选票(proposed
选票)。方法里面new
了一个set = new HashSet<Long>();
。这个set
和后面的for
循环放一起就很明显看出,这个一块的逻辑主要做的就是把和我投一样票的机器找出来并且加入到一个set
中。然后通过QuorumVerifier
判断是不是set.size() > half
。 如果过了验证就返回true,如果没有过返回false。
假设我们过了过半机制,所以走到这个if
内。那么本台机器就认为是选出了leader
,这里先叫做【准leader】
。紧接着一个while((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null)
是一个验证,它的作用就是:如果我已经找到一个【准leader】
以后,又来了选票怎么办呢?我就得再判断这个选票和我的【准leader】
哪个厉害。如果说新来的选票厉害,那么就把新来的选票放入recvqueue
中,退出这个循环,进入上层循环,再进行一轮【准leader】
的投票。如果来的选票竞争失败了,那么就一直循环,直到无法获取新的选票为止。
选举成功
那么我们跳出while((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null)
以后,看到了if (n == null)
这就说明确实没有票了。此时leader
就选出来了,那么就要开始设置状态了。self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: learningState());
这句话就在对比准leader
的sid
是不是和自己的一样。如果一样,就更新自己服务器的状态到LEADING
,以后的逻辑就是走LEADING
分支了。如果不一样,就根据配置文件的内容设置为相应的FOLLOWING
,或者OBSERVING
状态。最后生成一个新的Vote endVote
设置成选出的leader
的内容,返回出去return endVote
。
那么做完这些,谁是leader
,谁是follower
基本上就确定了。然后就要一直跳出到QuorumPeer.run()
里面,接着执行while(running)
。那么该是LEADING
状态的,就走到case LEADING:
;该是是FOLLOWING
状态的,就去case FOLLOWING:
进行同步数据;OBSERVING
状态的也是一样,等等。这个之前同步数据的那几个博客里已经说过了。那么到这里服务器启动时候的选举逻辑就已经结束了。
服务器变更以后选举的流程
Leader挂了
我们会有这样的情况,启动起来以后,Leader
服务器挂了,这种时候是怎么选举的呢?其实还是在QuorumPeer.run()
里面的while(running)
里面报异常,因为同步数据就是在这里进行的,如果Leader
挂了,数据同步一定会报错。
case FOLLOWING:
try {
LOG.info("FOLLOWING");
setFollower(makeFollower(logFactory));
follower.followLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception",e); //捕获异常
} finally {
follower.shutdown();
setFollower(null);
setPeerState(ServerState.LOOKING); //重置状态,用来参加新的选举
}
break;
其他服务器会在case FOLLOWING:
这里follower.followLeader();
这里报错。因为这里是同步数据的,leader
挂了会导致follower
无法通过这个方法同步数据,然后经过捕获错误。到finally
后重新设定状态为Looking
,然后所有机器一起选举setPeerState(ServerState.LOOKING);
。Observer
也是一样只不过报错的地方是observer.observeLeader();
,但是Observer
不参与选举所以其实对于本节来说无所谓了。
Follower挂了
只有当follower
挂的超过了一半了才会导致重新选举。如果超过一半了这里报错的地方则是case LEADING的leader.lead();
。
while (true) {
Thread.sleep(self.tickTime / 2); //每tickTime/2就执行一次
if (!tickSkip) {
self.tick.incrementAndGet();
}
HashSet<Long> syncedSet = new HashSet<Long>(); //存放还活着的机器id
syncedSet.add(self.getId()); //放入自己
for (LearnerHandler f : getLearners()) {
if (f.synced() && f.getLearnerType() == LearnerType.PARTICIPANT) {
syncedSet.add(f.getSid());
}
f.ping();
}
if (!this.isRunning()) {
shutdown("Unexpected internal error");
return;
}
if (!tickSkip && !self.getQuorumVerifier().containsQuorum(syncedSet)) {
shutdown("Not sufficient followers synced, only synced with sids: [ "
+ getSidSetString(syncedSet) + " ]");
return;
}
tickSkip = !tickSkip;
}
进入lead();
跳过数据同步的地方,走到这个方法里的最后一段逻辑while (true){}
这里。首先我们看到Thread.sleep(self.tickTime / 2)
,这个是为了保证不会立刻就执行探测逻辑用的时间间隔。那么这个while
其实很明显就是leader
在呼叫别的follower
是不是还活着的。往下看有一个set
。首先leader
先把自己放进去了syncedSet.add(self.getId());
,紧跟着就是一个for
循环。我们之前已经分析过,每一个learner
都会有一个LearnerHandler
。这里的for循环就是在找哪些follower
是活的。看里面的if
条件,如果拿出来的f
是活的f.synced()
,并且f
的Type
是Participant
也就是这个判断条件:f.getLearnerType() == LearnerType.PARTICIPANT
成立,那么就把这个机器加到syncedSet
中。如果不是活的那么就不会加到这个syncedSet
中。跳出这个for
循环以后,再往下就又是一个过半验证if (!tickSkip && !self.getQuorumVerifier().containsQuorum(syncedSet))
,意思是:如果syncedSet
里面没有超过一半的机器,那么就是直接shutdown()
。因为这里是一个while(true)
,正常情况下这个线程是不应该结束的。然是现在结束了于是就挑出了leader.lead();
,继续走到finally
里面。
case LEADING:
LOG.info("LEADING");
try {
setLeader(makeLeader(logFactory));
leader.lead(); //内部shutdown()
setLeader(null);
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally { //走到这里
if (leader != null) {
leader.shutdown("Forcing shutdown");
setLeader(null);
}
setPeerState(ServerState.LOOKING);
}
break;
如果leader
活着,先停了leader.shutdown("Forcing shutdown");
,然后把自己作为leader
的位置置空setLeader(null);
,最后再把自己的状态变成Looking
重新对剩下的机器进行选举。总结一下,其实就是leader
在自己运行的时候不断地去ping
自己的follower
如果发现自己的follower
小于等于配置的机器的一半,就自废武功重新举行一个选举,重新确定谁是新的leader
。其实说实在的,如果真的是follower
挂了一半,就已经不是小问题了,但是这样的机制还能保证部分业务的运行的,可能效率会低一些,毕竟少了一些机器。
新加一台机器
当新加入一台服务器的时候,第一件事情是做什么呢?当然是发送投票对不对,因为新加入的机器不知道谁是leader
,肯定会发送自己的投票给所有的机器询问谁是leader
。投票机制都一样主要就是其他机器接到投票后怎么处理的问题,所以我们就得重新找到WorkerReceiver
这个线程类的run()
方法:
public void run() {
Message response;
while (!stop) {
try{
response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS);
if(response == null) continue;
if(!validVoter(response.sid)){ //如果不是合法的的
Vote current = self.getCurrentVote();
ToSend notmsg = new ToSend(ToSend.mType.notification,
current.getId(),
current.getZxid(),
logicalclock.get(),
self.getPeerState(),
response.sid,
current.getPeerEpoch());
sendqueue.offer(notmsg);
} else {
/**不相关,略**/
//解析对方服务器的状态
QuorumPeer.ServerState ackstate = QuorumPeer.ServerState.LOOKING;
switch (response.buffer.getInt()) {
case 0:
ackstate = QuorumPeer.ServerState.LOOKING;
break;
case 1:
ackstate = QuorumPeer.ServerState.FOLLOWING;
break;
case 2:
ackstate = QuorumPeer.ServerState.LEADING;
break;
case 3:
ackstate = QuorumPeer.ServerState.OBSERVING;
break;
default:
continue;
}
//获取对方的其他值
n.leader = response.buffer.getLong();
n.zxid = response.buffer.getLong();
n.electionEpoch = response.buffer.getLong();
n.state = ackstate;
n.sid = response.sid;
if(!backCompatibility){
n.peerEpoch = response.buffer.getLong();
} else {
if(LOG.isInfoEnabled()){
LOG.info("Backward compatibility mode, server id=" + n.sid);
}
n.peerEpoch = ZxidUtils.getEpochFromZxid(n.zxid);
}
/**不相关,略**/
//如果自己的也是Looking,说明正在进行选举
if(self.getPeerState() == QuorumPeer.ServerState.LOOKING){
recvqueue.offer(n);//就先给自己发一份
if((ackstate == QuorumPeer.ServerState.LOOKING)
&& (n.electionEpoch < logicalclock.get())){
//发现对方也是looking,就给对方也发一份我自己的,后面就交给选举算法了
Vote v = getVote();
ToSend notmsg = new ToSend(ToSend.mType.notification,
v.getId(),
v.getZxid(),
logicalclock.get(),
self.getPeerState(),
response.sid,
v.getPeerEpoch());
sendqueue.offer(notmsg);
}
} else {
//如果我的不是looking,那么我就是leader或者follower
Vote current = self.getCurrentVote();
if(ackstate == QuorumPeer.ServerState.LOOKING){
/**Log4j**/
ToSend notmsg;
if(n.version > 0x0) {//这个version是当前数据节点数据内容的版本号
notmsg = new ToSend(
ToSend.mType.notification,
current.getId(),
current.getZxid(),
current.getElectionEpoch(),
self.getPeerState(),
response.sid,
current.getPeerEpoch());
} else {//把本机认为的leader告诉新加的机器
Vote bcVote = self.getBCVote();
notmsg = new ToSend(
ToSend.mType.notification,
bcVote.getId(),
bcVote.getZxid(),
bcVote.getElectionEpoch(),
self.getPeerState(),
response.sid,
bcVote.getPeerEpoch());
}
sendqueue.offer(notmsg);
}
}
}
} catch (InterruptedException e) {
System.out.println("Interrupted Exception while waiting for new message" +
e.toString());
}
}
LOG.info("WorkerReceiver is down");
}
}
这部分代码,我们之前是忽略的,这里拾起来。进入后首先还是接收投票,那么接到数据以后就有一个if(!validVoter(response.sid))
判断投票是不是合法的。如果不是合法的的,比如集群的配置里就没有这个机器,就把自己的投票发给新机器sendqueue.offer(notmsg);
。如果是合法的,那么就先解析对方服务器的状态,是LOOKING
,FOLLOWING
,LEADING
还是OBSERVING
,然后获取对方的其他值sid
、zxid
、electionEpoch
等等。
继续走if(self.getPeerState() == QuorumPeer.ServerState.LOOKING)
,如果本机的也是Looking
,说明整个集群正在进行选举。就先给自己发一份recvqueue.offer(n);
,然后if((ackstate == QuorumPeer.ServerState.LOOKING) && (n.electionEpoch < logicalclock.get()))
判断对方是不是也是Looking
状态。如果发现对方也是Looking
,发现对方也是Looking
,就给对方也发一份本机的投票,后面就交给选举算法了。
如果说if(self.getPeerState() == QuorumPeer.ServerState.LOOKING)
判断:本机不是Looking
状态走到了else
里面。那么本机一定是leader
或者follower
,我就把我认为的leader
的票发给对方,这里的n.version
是一个兼容性检测,其实看内容可以把这个忽略掉。无论是current
还是bcVote
最终都是current
作为选票的内容,最后发送出去sendqueue.offer(notmsg)
,这里就把新的机器的角色确定了。
结束语
那么到这里我们Zookeeper的源码解读就全部结束了。笔者写这一系列的原因是由于公司近一年半以来上了Kafka,而Kafka是严重基于Zookeeper来的。因此借助这个契机好好的研究了一番Zookeeper的源码,并且把自己的心得总结出来,给后来者一个读源码的方向吧。当然笔者只是众多普通的码农之一,在Zookeeper方面不能也不敢自称大牛,只是如同老黄牛一样吭哧吭哧的把Zookeeper源码读了几遍,留下心得而已。如果这些帖子里有错误的地方,欢迎大家指正错误,毕竟有讨论才有进步。后半年笔者可能会使用Spring框架对Kafka做一些功能性的增强和开发,到时候如果有心得一样也会分享出来。当然关于Kafka还有不少的内容和工具可用,比如Kafka tools、Kafka Eagle、Kafka Manager等等吧,开源的收费的市场上数不胜数。但是鞋子合不合适还是自己试试才会知道,所以会参考这些市面上的工具提供的功能作为user case吧,如果遇到什么坑也会分享出来。同样笔者也在构思Spring框架的源码解析,如果有机会也会写一些心得出来。总之感谢大家一路支持,我们以后再见。