启动electionTimer,进行leader选举。
一段时间没有leader和follower通信,就会超时,开始选举leader过程。有个超时时间,如果到了这个时间,就会触发一个回调函数。具体如下:
private void handleElectionTimeout() {
boolean doUnlock = true;
this.writeLock.lock();
try {
if (this.state != State.STATE_FOLLOWER) {
return;
}
//如果当前选举没有超时则说明此轮选举有效
if (isCurrentLeaderValid()) {
return;
}
resetLeaderId(PeerId.emptyPeer(), new Status(RaftError.ERAFTTIMEDOUT, "Lost connection from leader %s.",
this.leaderId));
doUnlock = false;
//预投票 (pre-vote) 环节
//候选者在发起投票之前,先发起预投票,
// 如果没有得到半数以上节点的反馈,则候选者就会识趣的放弃参选
preVote();
} finally {
if (doUnlock) {
this.writeLock.unlock();
}
}
}
在这个方法里,首先会加上一个写锁,然后进行校验,最后先发起一个预投票。
校验的时候会校验当前的状态是不是follower,校验leader和follower上次的通信时间是不是超过了ElectionTimeoutMs,如果没有超过,说明leader存活,没必要发起选举;如果通信超时,那么会将leader置空,然后调用预选举。(加写锁,为什么。预投票过程会影响?)
NodeImpl#isCurrentLeaderValid
private boolean isCurrentLeaderValid() {
return Utils.monotonicMs() - this.lastLeaderTimestamp < this.options.getElectionTimeoutMs();
}
复制代码用当前时间和上次leader通信时间相减,如果小于ElectionTimeoutMs(默认1s),那么就没有超时,说明leader有效
预投票过程
private void preVote() {
…
//返回最新的log实体类
final LogId lastLogId = this.logManager.getLastLogId(true);
boolean doUnlock = true;
this.writeLock.lock();
try {
// pre_vote need defense ABA after unlock&writeLock
//因为在上面没有重新加锁的间隙里可能会被别的线程改变了,所以这里校验一下
if (oldTerm != this.currTerm) {
LOG.warn("Node {} raise term {} when get lastLogId.", getNodeId(), this.currTerm);
return;
}
//初始化预投票投票箱
this.prevVoteCtx.init(this.conf.getConf(), this.conf.isStable() ? null : this.conf.getOldConf());
for (final PeerId peer : this.conf.listPeers()) {
//如果遍历的节点是当前节点就跳过
if (peer.equals(this.serverId)) {
continue;
}
//失联的节点也跳过
if (!this.rpcService.connect(peer.getEndpoint())) {
LOG.warn("Node {} channel init failed, address={}.", getNodeId(), peer.getEndpoint());
continue;
}
//设置一个回调的类
final OnPreVoteRpcDone done = new OnPreVoteRpcDone(peer, this.currTerm);
//向被遍历到的这个节点发送一个预投票的请求
done.request = RequestVoteRequest.newBuilder() //
.setPreVote(true) // it's a pre-vote request.
.setGroupId(this.groupId) //
.setServerId(this.serverId.toString()) //
.setPeerId(peer.toString()) //
.setTerm(this.currTerm + 1) // next term,注意这里发送过去的任期会加一
.setLastLogIndex(lastLogId.getIndex()) //
.setLastLogTerm(lastLogId.getTerm()) //
.build();
this.rpcService.preVote(peer.getEndpoint(), done.request, done);
}
//自己也可以投给自己
this.prevVoteCtx.grant(this.serverId);
if (this.prevVoteCtx.isGranted()) {
doUnlock = false;
electSelf();
}
} finally {
if (doUnlock) {
this.writeLock.unlock();
}
}
}
首先会获取最新的log信息,由LogId封装,里面包含两部分,一部分是这个日志的index和写入这个日志所对应当时节点的一个term任期
初始化预投票投票箱
遍历所有的集群节点
如果遍历的节点是当前节点就跳过,如果遍历的节点因为宕机或者手动下线等原因连接不上也跳过
向遍历的节点发送一个RequestVoteRequest请求预投票给自己
最后因为自己也是集群节点的一员,所以自己也投票给自己
在构造RequestVoteRequest的时候,会将PreVote属性设置为true,表示这次请求是预投票;设置当前节点为ServerId;传给对方的任期是当前节点的任期加一。最后在发送成功收到响应之后会回调OnPreVoteRpcDone的run方法。这个方法中如果收到正常的响应,那么会调用handlePreVoteResponse方法处理响应。
这里做了3重校验,我们分别来谈谈:
第一重试校验了当前的状态,如果不是FOLLOWER那么就不能发起选举。因为如果是leader节点,那么它不会选举,只能stepdown下台,把自己变成FOLLOWER后重新选举;如果是CANDIDATE,那么只能进行由FOLLOWER发起的投票,所以从功能上来说,只能FOLLOWER发起选举。
从Raft 的设计上来说也只能由FOLLOWER来发起选举,所以这里进行了校验。
第二重校验主要是校验发送请求时的任期和接受到响应时的任期还是不是一个,如果不是那么说明已经不是上次那轮的选举了,是一次失效的选举
第三重校验是校验响应返回的任期是不是大于当前的任期,如果大于当前的任期,那么重置当前的leader
校验完之后响应的节点会返回一个授权,如果授权通过的话则调用Ballot的grant方法,表示给当前的节点投一票
再看看预投票请求是怎么处理的?
这个方法里面也是蛮有意思的,写的很长,但是逻辑很清楚。
首先调用isActive,看一下当前节点是不是正常的节点,不是正常节点要返回Error信息
将请求传过来的ServerId解析到candidateId实例中
校验当前的节点如果有leader,并且leader有效的,那么就直接break,返回granted为false
如果当前的任期大于请求的任期,那么调用checkReplicator检查自己是不是leader,如果是leader,那么将当前节点从failureReplicators移除,重新加入到replicatorMap中。然后直接break
请求任期和当前任期相等的情况也要校验,只是不用break
如果请求的日志比当前的最新的日志还要新,那么返回granted为true,代表授权成功
checkReplicator会从replicatorMap根据传入的peer实例校验一下是不是为空。因为replicatorMap里面存放了集群所有的节点。然后通过ReplicatorGroupImpl的commonOptions获取当前的Node实例,如果当前的node实例是leader,并且如果存在失败集合failureReplicators中的话就重新添加进replicatorMap中。
投票electself
其实都是和前面预选举的方法preVote重复度很高的。方法太长,所以标了号,从上面号码来一步步讲解:
对当前的节点进行校验,如果当前节点不在集群里面则不进行选举
因为是Follower发起的选举,所以大概是因为要进行正式选举了,把预选举定时器关掉
清空leader再进行选举,注意这里会把votedId设置为当前节点,代表自己参选
开始发起投票定时器,因为可能投票失败需要循环发起投票,voteTimer里面会根据当前的CANDIDATE状态调用electSelf进行选举
调用init方法初始化投票箱,这里和prevVoteCtx是一样的
遍历所有节点,然后向其他集群节点发送RequestVoteRequest请求,这里也是和preVote一样的,请求是被RequestVoteRequestProcessor处理器处理的。
如果有超过半数以上的节点投票选中,那么就调用becomeLeader晋升为leader
预投票和投票的区别可以参见另一篇文章。
晋升leader
投票完毕之后如果收到的票数大于一半,那么就会晋升为leader,调用becomeLeader方法。
这个方法里面首先会停止选举定时器,然后设置当前的状态为leader,并设值任期,然后遍历所有的节点将节点加入到复制集群中,最后将stepDownTimer打开,定时对leader进行校验是不是又半数以上的节点响应当前的leader。
一个leader的下台需要做很多交接的工作:
如果当前的节点是个候选人(STATE_CANDIDATE),那么这个时候会让它暂时不要投票
如果当前的节点状态是(STATE_TRANSFERRING)表示正在转交leader或是leader(STATE_LEADER),那么就需要把当前节点的stepDownTimer这个定时器给关闭
如果当前是leader(STATE_LEADER),那么就需要告诉状态机leader下台了,可以在状态机中对下台的动作做处理
重置当前节点的leader,把当前节点的state状态设置为Follower,重置confCtx上下文
停止当前的快照生成,设置新的任期,让所有的复制节点停止工作
启动electionTimer