3. SOFAJRaft源码分析— 是如何进行选举的?
开篇
在上一篇文章当中,我们讲解了NodeImpl在init方法里面会初始化话的动作,选举也是在这个方法里面进行的,这篇文章来从这个方法里详细讲一下选举的过程。
由于我这里介绍的是如何实现的,所以请大家先看一下原理:SOFAJRaft 选举机制剖析 | SOFAJRaft 实现原理
文章比较长,我也慢慢的写了半个月时间~
选举过程分析
我在这里只把有关选举的代码列举出来,其他的代码暂且忽略
NodeImpl#init
public boolean init(final NodeOptions opts) {
		....
    // Init timers
    //设置投票计时器
    this.voteTimer = new RepeatedTimer("JRaft-VoteTimer", this.options.getElectionTimeoutMs()) {
        @Override
        protected void onTrigger() {
            //处理投票超时
            handleVoteTimeout();
        }
        @Override
        protected int adjustTimeout(final int timeoutMs) {
            //在一定范围内返回一个随机的时间戳
            return randomTimeout(timeoutMs);
        }
    };
    //设置预投票计时器
    //当leader在规定的一段时间内没有与 Follower 舰船进行通信时,
    // Follower 就可以认为leader已经不能正常担任旗舰的职责,则 Follower 可以去尝试接替leader的角色。
    // 这段通信超时被称为 Election Timeout
    //候选者在发起投票之前,先发起预投票
    this.electionTimer = new RepeatedTimer("JRaft-ElectionTimer", this.options.getElectionTimeoutMs()) {
        @Override
        protected void onTrigger() {
            handleElectionTimeout();
        }
        @Override
        protected int adjustTimeout(final int timeoutMs) {
            //在一定范围内返回一个随机的时间戳
            //为了避免同时发起选举而导致失败
            return randomTimeout(timeoutMs);
        }
    };
    //leader下台的计时器
    //定时检查是否需要重新选举leader
    this.stepDownTimer = new RepeatedTimer("JRaft-StepDownTimer", this.options.getElectionTimeoutMs() >> 1) {
        @Override
        protected void onTrigger() {
            handleStepDownTimeout();
        }
    };
		....
    if (!this.conf.isEmpty()) {
        //新启动的node需要重新选举
        stepDown(this.currTerm, false, new Status());
    }
		....
}
在这个init方法里面会初始化三个计时器是和选举有关的:
- voteTimer:这个timer负责定期的检查,如果当前的state的状态是候选者(STATE_CANDIDATE),那么就会发起选举
- electionTimer:在一定时间内如果leader没有与 Follower 进行通信时,Follower 就可以认为leader已经不能正常担任leader的职责,那么就会进行选举,在选举之前会先发起预投票,如果没有得到半数以上节点的反馈,则候选者就会识趣的放弃参选。所以这个timer负责预投票
- stepDownTimer:定时检查是否需要重新选举leader,如果当前的leader没有获得超过半数的Follower响应,那么这个leader就应该下台然后重新选举。
RepeatedTimer的分析我已经写好了:2. SOFAJRaft源码分析—JRaft的定时任务调度器是怎么做的?
我们先跟着init方法的思路往下看,一般来说this.conf里面装的是整个集群的节点信息,是不会为空的,所以会调用stepDown,所以先从这个方法看起。
leader下台
private void stepDown(final long term, final boolean wakeupCandidate, final Status status) {
    LOG.debug("Node {} stepDown, term={}, newTerm={}, wakeupCandidate={}.", getNodeId(), this.currTerm, term,
        wakeupCandidate);
    //校验一下当前节点的状态是否有异常,或正在关闭
    if (!this.state.isActive()) {
        return;
    }
    //如果是候选者,那么停止选举
    if (this.state == State.STATE_CANDIDATE) {
        //调用voteTimer的stop方法
        stopVoteTimer();
        //如果当前状态是leader或TRANSFERRING
    } else if (this.state.compareTo(State.STATE_TRANSFERRING) <= 0) {
        //让启动的stepDownTimer停止运作
        stopStepDownTimer();
        //清空选票箱中的内容
        this.ballotBox.clearPendingTasks();
        // signal fsm leader stop immediately
        if (this.state == State.STATE_LEADER) {
            //发送leader下台的事件给其他Follower
            onLeaderStop(status);
        }
    }
    // reset leader_id
    //重置当前节点的leader
    resetLeaderId(PeerId.emptyPeer(), status);
    // soft state in memory
    this.state = State.STATE_FOLLOWER;
    //重置Configuration的上下文
    this.confCtx.reset();
    updateLastLeaderTimestamp(Utils.monotonicMs());
    if (this.snapshotExecutor != null) {
        //停止当前的快照生成
        this.snapshotExecutor.interruptDownloadingSnapshots(term);
    }
    //设置任期为大的那个
    // meta state
    if (term > this.currTerm) {
        this.currTerm = term;
        this.votedId = PeerId.emptyPeer();
        //重设元数据信息保存到文件中
        this.metaStorage.setTermAndVotedFor(term, this.votedId);
    }
    if (wakeupCandidate) {
        this.wakingCandidate = this.replicatorGroup.stopAllAndFindTheNextCandidate(this.conf);
        if (this.wakingCandidate != null) {
            Replicator.sendTimeoutNowAndStop(this.wakingCandidate, this.options.getElectionTimeoutMs());
        }
    } else {
        //把replicatorGroup里面的所有replicator标记为stop
        this.replicatorGroup.stopAll();
    }
    //leader转移的时候会用到
    if (this.stopTransferArg != null) {
        if (this.transferTimer != null) {
            this.transferTimer.cancel(true);
        }
        // There is at most one StopTransferTimer at the same term, it's safe to
        // mark stopTransferArg to NULL
        this.stopTransferArg = null;
    }
    //启动
    this.electionTimer.start();
}
一个leader的下台需要做很多交接的工作:
- 如果当前的节点是个候选人(STATE_CANDIDATE),那么这个时候会让它暂时不要投票
- 如果当前的节点状态是(STATE_TRANSFERRING)表示正在转交leader或是leader(STATE_LEADER),那么就需要把当前节点的stepDownTimer这个定时器给关闭
- 如果当前是leader(STATE_LEADER),那么就需要告诉状态机leader下台了,可以在状态机中对下台的动作做处理
- 重置当前节点的leader,把当前节点的state状态设置为Follower,重置confCtx上下文
- 停止当前的快照生成,设置新的任期,让所有的复制节点停止工作
- 启动electionTimer
调用stopVoteTimer和stopStepDownTimer方法里面主要是调用相应的RepeatedTimer的stop方法,在stop方法里面会将stopped状态设置为ture,并将timeout设置为取消,并将这个timeout加入到cancelledTimeouts集合中去:
如果看了2. SOFAJRaft源码分析—JRaft的定时任务调度器是怎么做的?这篇文章的话,那么下面这段代码应该一看就明白是怎么回事了的。
public void stop() {
    this.lock.lock();
    try {
        if (this.stopped) {
            return;
        }
        this.stopped = true;
        if (this.timeout != null) {
            this.timeout.cancel();
            this.running = false;
            this.timeout = null;
        }
    } finally {
        this.lock.unlock();
    }
}
状态机处理LEADER_STOP事件
在调用NodeImpl的onLeaderStop方法中,实际上是调用了FSMCallerImpl的onLeaderStop方法
NodeImpl#onLeaderStop
private void onLeaderStop(final Status status) {
    this.replicatorGroup.clearFailureReplicators();
    this.fsmCaller.onLeaderStop(status);
}
FSMCallerImpl#onLeaderStop
public boolean onLeaderStop(final Status status) {
    return enqueueTask((task, sequence) -> {
		  //设置当前task的状态为LEADER_STOP
        task.type = TaskType.LEADER_STOP;
        task.status = new Status(status);
    });
}
private boolean enqueueTask(final EventTranslator<ApplyTask> tpl) {
    if (this.shutdownLatch != null) {
        // Shutting down
        LOG.warn("FSMCaller is stopped, can not apply new task.");
        return false;
    }
    //使用Disruptor发布事件
    this.taskQueue.publishEvent(tpl);
    return true;
}
这个方法里像taskQueue队列里面发布了一个LEADER_STOP事件,taskQueue是在FSMCallerImpl的init方法中被初始化的:
public boolean init(final FSMCallerOptions opts) {
    .....
    this.disruptor = DisruptorBuilder.<ApplyTask>newInstance() //
            .setEventFactory(new ApplyTaskFactory()) //
            .setRingBufferSize(opts.getDisruptorBufferSize()) //
            .setThreadFactory(new NamedThreadFactory("JRaft-FSMCaller-Disruptor-", true)) //
            .setProducerType(ProducerType.MULTI) //
            .setWaitStrategy(new BlockingWaitStrategy()) //
            .build();
    this.disruptor.handleEventsWith(new ApplyTaskHandler());
    this.disruptor.setDefaultExceptionHandler(new LogExceptionHandler<Object>(getClass().getSimpleName()));
    this.taskQueue = this.disruptor.start();
    .....
}
在taskQueue中发布了一个任务之后会交给ApplyTaskHandler进行处理
ApplyTaskHandler
private class ApplyTaskHandler implements EventHandler<ApplyTask> {
    // max committed index in current batch, reset to -1 every batch
    private long maxCommittedIndex = -1;
    @Override
    public void onEvent(final ApplyTask event, final long sequence, final boolean endOfBatch) throws Exception {
        this.maxCommittedIndex = runApplyTask(event, this.maxCommittedIndex, endOfBatch);
    }
}
每当有任务到达taskQueue队列的时候会调用ApplyTaskHandler的onEvent方法来处理事件,具体的执行逻辑由runApplyTask方法进行处理
FSMCallerImpl#runApplyTask
private long runApplyTask(final ApplyTask task, long maxCommittedIndex, final boolean endOfBatch) {
    CountDownLatch shutdown = null;
    ...
     switch (task.type) {
         ...
        case LEADER_STOP:
            this.currTask = TaskType.LEADER_STOP;
            doLeaderStop(task.status);
            break;
        ...
    }
		....
}
在runApplyTask方法里会对很多的事件进行处理,我们这里只看LEADER_STOP是怎么做的:
在switch里会调用doLeaderStop方法,这个方法会调用到FSMCallerImpl里面封装的StateMachine状态机的onLeaderStart方法:
private void doLeaderStop(final Status status) {
    this.fsm.onLeaderStop(status);
}
这样就可以对leader停止时进行客制化的处理了。
重置leader
接下来会调用resetLeaderId(PeerId.emptyPeer(), status);方法来重置leader
private void resetLeaderId(final PeerId newLeaderId, final Status status) {
    if (newLeaderId.isEmpty()) {
        //这个判断表示如果当前节点是候选者或者是Follower,并且已经有leader了
        if (!this.leaderId.isEmpty() && this.state.compareTo(State.STATE_TRANSFERRING) > 0) {
            //向状态机装发布停止跟随该leader的事件
            this.fsmCaller.onStopFollowing(new LeaderChangeContext(this.leaderId.copy(), this.currTerm, status));
        }
        //把当前的leader设置为一个空值
        this.leaderId = PeerId.emptyPeer();
    } else {
        //如果当前节点没有leader
        if (this.leaderId == null || this.leaderId.isEmpty()) {
            //那么发布要跟随该leader的事件
            this.fsmCaller.onStartFollowing(new LeaderChangeContext(newLeaderId, this.currTerm, status));
        }
        this.leaderId = newLeaderId.copy();
    }
}
这个方法由两个作用,如果传入的newLeaderId不是个空的,那么就会设置一个新的leader,并向状态机发送一个START_FOLLOWING事件;如果传入的newLeaderId是空的,那么就会发送一个STOP_FOLLOWING事件,并把当前的leader置空。
启动electionTimer,进行leader选举
electionTimer是RepeatedTimer的实现类,在这里我就不多说了,上一篇文章已经介绍过了。
我这里来看看electionTimer的onTrigger方法是怎么处理选举事件的,electionTimer的onTrigger方法会调用NodeImpl的handleElectionTimeout方法,所以直接看这个方法:
NodeImpl#handleElectionTimeout
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有效
预选票preVote
我们在handleElectionTimeout方法中最后调用了preVote方法,接下来重点看一下这个方法。
下面我将preVote拆分成几部分来进行讲解:
NodeImpl#preVote  part1
private void preVote() {
    long oldTerm;
    try {
        LOG.info("Node {} term {} start preVote.", getNodeId(), this.currTerm);
        if (this.snapshotExecutor != null && this.snapshotExecutor.isInstallingSnapshot()) {
            LOG.warn(
                "Node {} term {} doesn't do preVote when installing snapshot as the configuration may be out of date.",
                getNodeId());
            return;
        }
        //conf里面记录了集群节点的信息,如果当前的节点不包含在集群里说明是由问题的
        if (!this.conf.contains(this.serverId)) {
            LOG.warn("Node {} can't do preVote as it is not in conf <{}>.", getNodeId(), this.conf);
            return;
        }
        //设置一下当前的任期
        oldTerm = this.currTerm;
    } finally {
        this.writeLock.unlock();
    }
	  ....
}
这部分代码是一开始进到preVote这个方法首先要经过一些校验,例如当前的节点不能再安装快照的时候进行选举;查看一下当前的节点是不是在自己设置的conf里面,conf这个属性会包含了集群的所有节点;最后设置一下当前的任期后解锁。
NodeImpl#preVote part2
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请求预投票给自己
- 最后因为自己也是集群节点的一员,所以自己也投票给自己
初始化预投票投票箱
初始化预投票箱是调用了Ballot的init方法进行初始化,分别传入新的集群节点信息,和老的集群节点信息
public boolean init(Configuration conf, Configuration oldConf) {
    this.peers.clear();
    this.oldPeers.clear();
    quorum = oldQuorum = 0;
    int index = 0;
    //初始化新的节点
    if (conf != null) {
        for (PeerId peer : conf) {
            this.peers.add(new UnfoundPeerId(peer, index++, false));
        }
    }
    //设置需要多少票数才能成为leader
    this.quorum = this.peers.size() / 2 + 1;
    ....
    return true;
}
我这里为了使逻辑更清晰,假设没有oldConf,省略oldConf相关设置。
这个方法里会遍历所有的peer节点,并将peer封装成UnfoundPeerId插入到peers集合中;然后设置quorum属性,这个属性会在每获得一个投票就减1,当减到0以下时说明获得了足够多的票数,就代表预投票成功。
发起预投票请求
//设置一个回调的类
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);
在构造RequestVoteRequest的时候,会将PreVote属性设置为true,表示这次请求是预投票;设置当前节点为ServerId;传给对方的任期是当前节点的任期加一。最后在发送成功收到响应之后会回调OnPreVoteRpcDone的run方法。
OnPreVoteRpcDone#run
public void run(final Status status) {
    NodeImpl.this.metrics.recordLatency("pre-vote", Utils.monotonicMs() - this.startMs);
    if (!status.isOk()) {
        LOG.warn("Node {} PreVote to {} error: {}.", getNodeId(), this.peer, status);
    } else {
        handlePreVoteResponse(this.peer, this.term, getResponse());
    }
}
在这个方法中如果收到正常的响应,那么会调用handlePreVoteResponse方法处理响应
OnPreVoteRpcDone#handlePreVoteResponse
public void handlePreVoteResponse(final PeerId peerId, final long term, final RequestVoteResponse response) {
    boolean doUnlock = true;
    this.writeLock.lock();
    try {
        //只有follower才可以尝试发起选举
        if (this.state != State.STATE_FOLLOWER) {
            LOG.warn("Node {} received invalid PreVoteResponse from {}, state not in STATE_FOLLOWER but {}.",
                getNodeId(), peerId, this.state);
            return;
        }
        if (term != this.currTerm) {
            LOG.warn("Node {} received invalid PreVoteResponse from {}, term={}, currTerm={}.", getNodeId(),
                peerId, term, this.currTerm);
            return;
        }
        //如果返回的任期大于当前的任期,那么这次请求也是无效的
        if (response.getTerm() > this.currTerm) {
            LOG.warn("Node {} received invalid PreVoteResponse from {}, term {}, expect={}.", getNodeId(), peerId,
                response.getTerm(), this.currTerm);
            stepDown(response.getTerm(), false, new Status(RaftError.EHIGHERTERMRESPONSE,
                "Raft node receives higher term pre_vote_response."));
            return;
        }
        LOG.info("Node {} received PreVoteResponse from {}, term={}, granted={}.", getNodeId(), peerId,
            response.getTerm(), response.getGranted());
        // check granted quorum?
        if (response.getGranted()) {
            this.prevVoteCtx.grant(peerId);
            //得到了半数以上的响应
            if (this.prevVoteCtx.isGranted()) {
                doUnlock = false;
                //进行选举
                electSelf();
            }
        }
    } finally {
        if (doUnlock) {
            this.writeLock.unlock();
        }
    }
}
这里做了3重校验,我们分别来谈谈:
- 第一重试校验了当前的状态,如果不是FOLLOWER那么就不能发起选举。因为如果是leader节点,那么它不会选举,只能stepdown下台,把自己变成FOLLOWER后重新选举;如果是CANDIDATE,那么只能进行由FOLLOWER发起的投票,所以从功能上来说,只能FOLLOWER发起选举。
 从Raft 的设计上来说也只能由FOLLOWER来发起选举,所以这里进行了校验。
- 第二重校验主要是校验发送请求时的任期和接受到响应时的任期还是不是一个,如果不是那么说明已经不是上次那轮的选举了,是一次失效的选举
- 第三重校验是校验响应返回的任期是不是大于当前的任期,如果大于当前的任期,那么重置当前的leader
校验完之后响应的节点会返回一个授权,如果授权通过的话则调用Ballot的grant方法,表示给当前的节点投一票
Ballot#grant
public void grant(PeerId peerId) {
    this.grant(peerId, new PosHint());
}
public PosHint grant(PeerId peerId, PosHint hint) {
    UnfoundPeerId peer = findPeer(peerId, peers, hint.pos0);
    if (peer != null) {
        if (!peer.found) {
            peer.found = true;
            this.quorum--;
        }
        hint.pos0 = peer.index;
    } else {
        hint.pos0 = -1;
    }
    ....
    return hint;
}
grant方法会根据peerId去集群集合里面去找被封装的UnfoundPeerId实例,然后判断一下,如果没有被记录过,那么就将quorum减一,表示收到一票,然后将found设置为ture表示已经找过了。
在查找UnfoundPeerId实例的时候方法里面做了一个很有趣的设置:
首先在存入到peers集合里面的时候是这样的:
int index = 0;
for (PeerId peer : conf) {
    this.peers.add(new UnfoundPeerId(peer, index++, false));
}
这里会遍历conf,然后会存入index,index从零开始。
然后在查找的时候会传入peerId和posHint还有peers集合:
private UnfoundPeerId findPeer(PeerId peerId, List<UnfoundPeerId> peers, int posHint) {
    if (posHint < 0 || posHint >= peers.size() || !peers.get(posHint).peerId.equals(peerId)) {
        for (UnfoundPeerId ufp : peers) {
            if (ufp.peerId.equals(peerId)) {
                return ufp;
            }
        }
        return null;
    }
    return peers.get(posHint);
}
这里传入的posHint默认是-1 ,即如果是第一次传入,那么会遍历整个peers集合,然后一个个比对之后返回。
因为PosHint实例会在调用完之后将pos0设置为peer的index,如果grant方法不是第一次调用,那么在调用findPeer方法的时候就可以直接通过get方法获取,不用再遍历整个集合了。
这种写法也可以运用到平时的代码中去。
调用了grant方法之后会调用Ballot的isGranted判断一下是否达到了半数以上的响应。
Ballot#isGranted
public boolean isGranted() {
    return this.quorum <= 0 && oldQuorum <= 0;
}
即判断一下投票箱里面的票数是不是被减到了0。如果返回是的话,那么就调用electSelf进行选举。
选举的方法暂时先不看,我们来看看预选举的请求是怎么被处理的
响应RequestVoteRequest请求
RequestVoteRequest请求的处理器是在RaftRpcServerFactory的addRaftRequestProcessors方法中被安置的,具体的处理时RequestVoteRequestProcessor。
具体的处理方法是交由processRequest0方法来处理的。
RequestVoteRequestProcessor#processRequest0
public Message processRequest0(RaftServerService service, RequestVoteRequest request, RpcRequestClosure done) {
    //如果是预选举
    if (request.getPreVote()) {
        return service.handlePreVoteRequest(request);
    } else {
        return service.handleRequestVoteRequest(request);
    }
}
因为这个处理器可以处理选举和预选举,所以加了个判断。预选举的方法交给NodeImpl的handlePreVoteRequest来具体实现的。
NodeImpl#handlePreVoteRequest
public Message handlePreVoteRequest(final RequestVoteRequest request) {
    boolean doUnlock = true;
    this.writeLock.lock();
    try {
        //校验这个节点是不是正常的节点
        if (!this.state.isActive()) {
            LOG.warn("Node {} is not in active state, currTerm={}.", getNodeId(), this.currTerm);
            return RpcResponseFactory.newResponse(RaftError.EINVAL, "Node %s is not in active state, state %s.",
                getNodeId(), this.state.name());
        }
        final PeerId candidateId = new PeerId();
        //发送过来的request请求携带的ServerId格式不能错
        if (!candidateId.parse(request.getServerId())) {
            LOG.warn("Node {} received PreVoteRequest from {} serverId bad format.", getNodeId(),
                request.getServerId());
            return RpcResponseFactory.newResponse(RaftError.EINVAL, "Parse candidateId failed: %s.",
                request.getServerId());
        }
        boolean granted = false;
        // noinspection ConstantConditions
        do {
            //已经有leader的情况
            if (this.leaderId != null && !this.leaderId.isEmpty() && isCurrentLeaderValid()) {
                LOG.info(
                    "Node {} ignore PreVoteRequest from {}, term={}, currTerm={}, because the leader {}'s lease is still valid.",
                    getNodeId(), request.getServerId(), request.getTerm(), this.currTerm, this.leaderId);
                break;
            }
            //请求的任期小于当前的任期
            if (request.getTerm() < this.currTerm) {
                LOG.info("Node {} ignore PreVoteRequest from {}, term={}, currTerm={}.", getNodeId(),
                    request.getServerId(), request.getTerm(), this.currTerm);
                // A follower replicator may not be started when this node become leader, so we must check it.
                  //那么这个节点也可能是leader,所以校验一下请求的节点是不是复制节点,重新加入到replicatorGroup中
                checkReplicator(candidateId);
                break;
            } else if (request.getTerm() == this.currTerm + 1) {
                // A follower replicator may not be started when this node become leader, so we must check it.
                // check replicator state
                //因为请求的任期和当前的任期相等,那么这个节点也可能是leader,
                 // 所以校验一下请求的节点是不是复制节点,重新加入到replicatorGroup中
                checkReplicator(candidateId);
            }
            doUnlock = false;
            this.writeLock.unlock();
            //获取最新的日志
            final LogId lastLogId = this.logManager.getLastLogId(true);
            doUnlock = true;
            this.writeLock.lock();
            final LogId requestLastLogId = new LogId(request.getLastLogIndex(), request.getLastLogTerm());
            //比较当前节点的日志完整度和请求节点的日志完整度
            granted = requestLastLogId.compareTo(lastLogId) >= 0;
            LOG.info(
                "Node {} received PreVoteRequest from {}, term={}, currTerm={}, granted={}, requestLastLogId={}, lastLogId={}.",
                getNodeId(), request.getServerId(), request.getTerm(), this.currTerm, granted, requestLastLogId,
                lastLogId);
        } while (false);//这个while蛮有意思,为了用break想尽了办法
        return RequestVoteResponse.newBuilder() //
            .setTerm(this.currTerm) //
            .setGranted(granted) //
            .build();
    } finally {
        if (doUnlock) {
            this.writeLock.unlock();
        }
    }
}
这个方法里面也是蛮有意思的,写的很长,但是逻辑很清楚。
- 首先调用isActive,看一下当前节点是不是正常的节点,不是正常节点要返回Error信息
- 将请求传过来的ServerId解析到candidateId实例中
- 校验当前的节点如果有leader,并且leader有效的,那么就直接break,返回granted为false
- 如果当前的任期大于请求的任期,那么调用checkReplicator检查自己是不是leader,如果是leader,那么将当前节点从failureReplicators移除,重新加入到replicatorMap中。然后直接break
- 请求任期和当前任期相等的情况也要校验,只是不用break
- 如果请求的日志比当前的最新的日志还要新,那么返回granted为true,代表授权成功
这里有一个有意思的地方是,因为java中只能在循环中goto,所以这里使用了do-while(false)只做单次的循环,这样就可以do代码块里使用break了。
下面稍微看一下checkReplicator:
NodeImpl#checkReplicator
private void checkReplicator(final PeerId candidateId) {
    if (this.state == State.STATE_LEADER) {
        this.replicatorGroup.checkReplicator(candidateId, false);
    }
}
这里判断一下是不是leader,然后就会调用ReplicatorGroupImpl的checkReplicator
ReplicatorGroupImpl#checkReplicator
private final ConcurrentMap<PeerId, ThreadId> replicatorMap      = new ConcurrentHashMap<>();
private final Set<PeerId>                     failureReplicators = new ConcurrentHashSet<>();
public void checkReplicator(final PeerId peer, final boolean lockNode) {
    //根据传入的peer获取相应的ThreadId
    final ThreadId rid = this.replicatorMap.get(peer);
    // noinspection StatementWithEmptyBody
    if (rid == null) {
        // Create replicator if it's not found for leader.
        final NodeImpl node = this.commonOptions.getNode();
        if (lockNode) {
            node.writeLock.lock();
        }
        try {
            //如果当前的节点是leader,并且传入的peer在failureReplicators中,那么重新添加到replicatorMap
            if (node.isLeader() && this.failureReplicators.contains(peer) && addReplicator(peer)) {
                this.failureReplicators.remove(peer);
            }
        } finally {
            if (lockNode) {
                node.writeLock.unlock();
            }
        }
    } else { // NOPMD
        // Unblock it right now.
        // Replicator.unBlockAndSendNow(rid);
    }
}
checkReplicator会从replicatorMap根据传入的peer实例校验一下是不是为空。因为replicatorMap里面存放了集群所有的节点。然后通过ReplicatorGroupImpl的commonOptions获取当前的Node实例,如果当前的node实例是leader,并且如果存在失败集合failureReplicators中的话就重新添加进replicatorMap中。
ReplicatorGroupImpl#addReplicator
public boolean addReplicator(final PeerId peer) {
    //校验当前的任期
    Requires.requireTrue(this.commonOptions.getTerm() != 0);
    //如果replicatorMap里面已经有这个节点了,那么将它从failureReplicators集合中移除
    if (this.replicatorMap.containsKey(peer)) {
        this.failureReplicators.remove(peer);
        return true;
    }
    //赋值一个新的ReplicatorOptions
    final ReplicatorOptions opts = this.commonOptions == null ? new ReplicatorOptions() : this.commonOptions.copy();
    //新的ReplicatorOptions添加这个PeerId
    opts.setPeerId(peer);
    final ThreadId rid = Replicator.start(opts, this.raftOptions);
    if (rid == null) {
        LOG.error("Fail to start replicator to peer={}.", peer);
        this.failureReplicators.add(peer);
        return false;
    }
    return this.replicatorMap.put(peer, rid) == null;
}
addReplicator里面主要是做了两件事:1. 将要加入的节点从failureReplicators集合里移除;2. 将要加入的节点放入到replicatorMap集合中去。
投票electSelf
private void electSelf() {
    long oldTerm;
    try {
        LOG.info("Node {} start vote and grant vote self, term={}.", getNodeId(), this.currTerm);
        //1. 如果当前节点不在集群里面则不进行选举
        if (!this.conf.contains(this.serverId)) {
            LOG.warn("Node {} can't do electSelf as it is not in {}.", getNodeId(), this.conf);
            return;
        }
        //2. 大概是因为要进行正式选举了,把预选举关掉
        if (this.state == State.STATE_FOLLOWER) {
            LOG.debug("Node {} stop election timer, term={}.", getNodeId(), this.currTerm);
            this.electionTimer.stop();
        }
        //3. 清空leader
        resetLeaderId(PeerId.emptyPeer(), new Status(RaftError.ERAFTTIMEDOUT,
            "A follower's leader_id is reset to NULL as it begins to request_vote."));
        this.state = State.STATE_CANDIDATE;
        this.currTerm++;
        this.votedId = this.serverId.copy();
        LOG.debug("Node {} start vote timer, term={} .", getNodeId(), this.currTerm);
        //4. 开始发起投票定时器,因为可能投票失败需要循环发起投票
        this.voteTimer.start();
        //5. 初始化投票箱
        this.voteCtx.init(this.conf.getConf(), this.conf.isStable() ? null : this.conf.getOldConf());
        oldTerm = this.currTerm;
    } finally {
        this.writeLock.unlock();
    }
    final LogId lastLogId = this.logManager.getLastLogId(true);
    this.writeLock.lock();
    try {
        // vote need defense ABA after unlock&writeLock
        if (oldTerm != this.currTerm) {
            LOG.warn("Node {} raise term {} when getLastLogId.", getNodeId(), this.currTerm);
            return;
        }
        //6. 遍历所有节点
        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 OnRequestVoteRpcDone done = new OnRequestVoteRpcDone(peer, this.currTerm, this);
            done.request = RequestVoteRequest.newBuilder() //
                .setPreVote(false) // It's not a pre-vote request.
                .setGroupId(this.groupId) //
                .setServerId(this.serverId.toString()) //
                .setPeerId(peer.toString()) //
                .setTerm(this.currTerm) //
                .setLastLogIndex(lastLogId.getIndex()) //
                .setLastLogTerm(lastLogId.getTerm()) //
                .build();
            this.rpcService.requestVote(peer.getEndpoint(), done.request, done);
        }
        this.metaStorage.setTermAndVotedFor(this.currTerm, this.serverId);
        this.voteCtx.grant(this.serverId);
        if (this.voteCtx.isGranted()) {
            //7. 投票成功,那么就晋升为leader
            becomeLeader();
        }
    } finally {
        this.writeLock.unlock();
    }
}
不要看这个方法这么长,其实都是和前面预选举的方法preVote重复度很高的。方法太长,所以标了号,从上面号码来一步步讲解:
- 对当前的节点进行校验,如果当前节点不在集群里面则不进行选举
- 因为是Follower发起的选举,所以大概是因为要进行正式选举了,把预选举定时器关掉
- 清空leader再进行选举,注意这里会把votedId设置为当前节点,代表自己参选
- 开始发起投票定时器,因为可能投票失败需要循环发起投票,voteTimer里面会根据当前的CANDIDATE状态调用electSelf进行选举
- 调用init方法初始化投票箱,这里和prevVoteCtx是一样的
- 遍历所有节点,然后向其他集群节点发送RequestVoteRequest请求,这里也是和preVote一样的,请求是被RequestVoteRequestProcessor处理器处理的。
- 如果有超过半数以上的节点投票选中,那么就调用becomeLeader晋升为leader
我先来看看RequestVoteRequestProcessor怎么处理的选举:
在RequestVoteRequestProcessor的processRequest0会调用NodeImpl的handleRequestVoteRequest来处理具体的逻辑。
处理投票请求
NodeImpl#handleRequestVoteRequest
public Message handleRequestVoteRequest(final RequestVoteRequest request) {
    boolean doUnlock = true;
    this.writeLock.lock();
    try {
        //是否存活
        if (!this.state.isActive()) {
            LOG.warn("Node {} is not in active state, currTerm={}.", getNodeId(), this.currTerm);
            return RpcResponseFactory.newResponse(RaftError.EINVAL, "Node %s is not in active state, state %s.",
                getNodeId(), this.state.name());
        }
        final PeerId candidateId = new PeerId();
        if (!candidateId.parse(request.getServerId())) {
            LOG.warn("Node {} received RequestVoteRequest from {} serverId bad format.", getNodeId(),
                request.getServerId());
            return RpcResponseFactory.newResponse(RaftError.EINVAL, "Parse candidateId failed: %s.",
                request.getServerId());
        }
        // noinspection ConstantConditions
        do {
            // check term
            if (request.getTerm() >= this.currTerm) {
                LOG.info("Node {} received RequestVoteRequest from {}, term={}, currTerm={}.", getNodeId(),
                        request.getServerId(), request.getTerm(), this.currTerm);
                //1. 如果请求的任期大于当前任期
                // increase current term, change state to follower
                if (request.getTerm() > this.currTerm) {
                    stepDown(request.getTerm(), false, new Status(RaftError.EHIGHERTERMRESPONSE,
                        "Raft node receives higher term RequestVoteRequest."));
                }
            } else {
                // ignore older term
                LOG.info("Node {} ignore RequestVoteRequest from {}, term={}, currTerm={}.", getNodeId(),
                    request.getServerId(), request.getTerm(), this.currTerm);
                break;
            }
            doUnlock = false;
            this.writeLock.unlock();
            final LogId lastLogId = this.logManager.getLastLogId(true);
            doUnlock = true;
            this.writeLock.lock();
            // vote need ABA check after unlock&writeLock
            if (request.getTerm() != this.currTerm) {
                LOG.warn("Node {} raise term {} when get lastLogId.", getNodeId(), this.currTerm);
                break;
            }
            //2. 判断日志完整度
            final boolean logIsOk = new LogId(request.getLastLogIndex(), request.getLastLogTerm())
                .compareTo(lastLogId) >= 0;
            //3. 判断当前的节点是不是已经投过票了
            if (logIsOk && (this.votedId == null || this.votedId.isEmpty())) {
                stepDown(request.getTerm(), false, new Status(RaftError.EVOTEFORCANDIDATE,
                    "Raft node votes for some candidate, step down to restart election_timer."));
                this.votedId = candidateId.copy();
                this.metaStorage.setVotedFor(candidateId);
            }
        } while (false);
        return RequestVoteResponse.newBuilder() //
            .setTerm(this.currTerm) //
            //4.同意投票的条件是当前的任期和请求的任期一样,并且已经将votedId设置为请求节点
            .setGranted(request.getTerm() == this.currTerm && candidateId.equals(this.votedId)) //
            .build();
    } finally {
        if (doUnlock) {
            this.writeLock.unlock();
        }
    }
}
这个方法大致也和handlePreVoteRequest差不多。我这里只分析一下我标注的。
- 这里是判断当前的任期是小于请求的任期的,并且调用stepDown将请求任期设置为当前的任期,将当前的状态设置被Follower
- 作为一个leader来做日志肯定是要比被请求的节点完整,所以这里判断一下日志是不是比被请求的节点日志完整
- 如果日志是完整的,并且被请求的节点没有投票给其他的候选人,那么就将votedId设置为当前请求的节点
- 给请求发送响应,同意投票的条件是当前的任期和请求的任期一样,并且已经将votedId设置为请求节点
晋升leader
投票完毕之后如果收到的票数大于一半,那么就会晋升为leader,调用becomeLeader方法。
private void becomeLeader() {
    Requires.requireTrue(this.state == State.STATE_CANDIDATE, "Illegal state: " + this.state);
    LOG.info("Node {} become leader of group, term={}, conf={}, oldConf={}.", getNodeId(), this.currTerm,
        this.conf.getConf(), this.conf.getOldConf());
    // cancel candidate vote timer
    //晋升leader之后就会把选举的定时器关闭了
    stopVoteTimer();
    //设置当前的状态为leader
    this.state = State.STATE_LEADER;
    this.leaderId = this.serverId.copy();
    //复制集群中设置新的任期
    this.replicatorGroup.resetTerm(this.currTerm);
    //遍历所有的集群节点
    for (final PeerId peer : this.conf.listPeers()) {
        if (peer.equals(this.serverId)) {
            continue;
        }
        LOG.debug("Node {} add replicator, term={}, peer={}.", getNodeId(), this.currTerm, peer);
        //如果成为leader,那么需要把自己的日志信息复制到其他节点
        if (!this.replicatorGroup.addReplicator(peer)) {
            LOG.error("Fail to add replicator, peer={}.", peer);
        }
    }
    // init commit manager
    this.ballotBox.resetPendingIndex(this.logManager.getLastLogIndex() + 1);
    // Register _conf_ctx to reject configuration changing before the first log
    // is committed.
    if (this.confCtx.isBusy()) {
        throw new IllegalStateException();
    }
    this.confCtx.flush(this.conf.getConf(), this.conf.getOldConf());
    //如果是leader了,那么就要定时的检查不是有资格胜任
    this.stepDownTimer.start();
}
这个方法里面首先会停止选举定时器,然后设置当前的状态为leader,并设值任期,然后遍历所有的节点将节点加入到复制集群中,最后将stepDownTimer打开,定时对leader进行校验是不是又半数以上的节点响应当前的leader。
好了,到这里就讲完了,希望下次还可以see you again~
3. SOFAJRaft源码分析— 是如何进行选举的?的更多相关文章
- 4. SOFAJRaft源码分析— RheaKV初始化做了什么?
		前言 由于RheaKV要讲起来篇幅比较长,所以这里分成几个章节来讲,这一章讲一讲RheaKV初始化做了什么? 我们先来给个例子,我们从例子来讲: public static void main(fin ... 
- 6. SOFAJRaft源码分析— 透过RheaKV看线性一致性读
		开篇 其实这篇文章我本来想在讲完选举的时候就开始讲线性一致性读的,但是感觉直接讲没头没尾的看起来比比较困难,所以就有了RheaKV的系列,这是RheaKV,终于可以讲一下SOFAJRaft的线性一致性 ... 
- 8. SOFAJRaft源码分析— 如何实现日志复制的pipeline机制?
		前言 前几天和腾讯的大佬一起吃饭聊天,说起我对SOFAJRaft的理解,我自然以为我是很懂了的,但是大佬问起了我那SOFAJRaft集群之间的日志是怎么复制的? 我当时哑口无言,说不出是怎么实现的,所 ... 
- 9. SOFAJRaft源码分析— Follower如何通过Snapshot快速追上Leader日志?
		前言 引入快照机制主要是为了解决两个问题: JRaft新节点加入后,如何快速追上最新的数据 Raft 节点出现故障重新启动后如何高效恢复到最新的数据 Snapshot 源码分析 生成 Raft 节点的 ... 
- 2. SOFAJRaft源码分析—JRaft的定时任务调度器是怎么做的?
		看完这个实现之后,感觉还是要多看源码,多研究.其实JRaft的定时任务调度器是基于Netty的时间轮来做的,如果没有看过Netty的源码,很可能并不知道时间轮算法,也就很难想到要去使用这么优秀的定时调 ... 
- 7. SOFAJRaft源码分析—如何实现一个轻量级的对象池?
		前言 我在看SOFAJRaft的源码的时候看到了使用了对象池的技术,看了一下感觉要吃透的话还是要新开一篇文章来讲,内容也比较充实,大家也可以学到之后运用到实际的项目中去. 这里我使用Recyclabl ... 
- ZOOKEEPER3.3.3源码分析(四)对LEADER选举过程分析的纠正
		很抱歉,之前分析的zookeeper leader选举算法有误,特此更正说明. 那里面最大的错误在于,leader选举其实不是在大多数节点通过就能选举上的,这一点与传统的paxos算法不同,因为如果这 ... 
- 1. SOFAJRaft源码分析— SOFAJRaft启动时做了什么?
		我们这次依然用上次的例子CounterServer来进行讲解: 我这里就不贴整个代码了 public static void main(final String[] args) throws IOEx ... 
- 5. SOFAJRaft源码分析— RheaKV中如何存放数据?
		概述 上一篇讲了RheaKV是如何进行初始化的,因为RheaKV主要是用来做KV存储的,RheaKV读写的是相当的复杂,一起写会篇幅太长,所以这一篇主要来讲一下RheaKV中如何存放数据. 我们这里使 ... 
随机推荐
- ReactNative之Redux详解
			用redux有一段时间了,感觉还是有必要把其相关的知识点系统的总结一下的,毕竟好记性不如烂笔头.上篇博客更新了关于<ES6中的迭代器.Generator函数以及Generator函数的异步操作& ... 
- MAC sublime常用快捷键(慢慢补)
			1、 FN + 左方向键:向左选择一行 2、FN + 右方向键:向右选择一行 3、FN + 上方向键:跳到页头 4、FN + 下方向键:跳到页尾 5、FN + SHIFT + 左方向键|上方向键:从当 ... 
- JDK 13 都已经发布了,Java 8 依然是最爱
			在 JDK 版本的世界里,从来都是 Oracle 发他的新版本,我们继续用我们的老版本.三年之前用 JDK 7,后来终于升级到了 JDK 8.自从升级了没多久,JDK 就开始了半年发一个新版本的节奏, ... 
- Java常识及数据类型
			上次介绍完了JDK的下载,安装,以及配置了环境变量 .这次我们来讲讲Java的常识及Java的数据类型; 常见Java开发工具 编辑器: 1:UltraEdit; 2:EditPlus等; 集成开发环 ... 
- Kotlin学习系列(三)
			类声明 Kotlin使用class关键字声明类: class Invoice{ } Kotlin类声明基本包括header与body: [<Modifier>] <class> ... 
- xampp修改mysql 启动脚本
			打开xmapp,点击mysql对应的config按钮进入my.ini文件,如图所示: 修改mysqld服务的port参数3306为你想要设置的port,如图2所示: 重新启动mysql服务即可用客户端 ... 
- centos6.9实时查看tomcat运行日志
			1.切换到tomcat的logs目录下 cd /usr/local/apache-tomcat-/logs 2.执行命令,查看日志 tail -f catalina.out 3.退出 Ctrl+c 
- uC/OS-III 软件定时器(三)
			软件定时器是uC/OS 操作系统的一个内核对象,软件定时器是基于时钟节拍和系统管理创建的软件性定时器,理论上可以创建无限多个,操作简单,但精准度肯定比硬件定时稍逊一筹. 原理和实现过程 要用到的函数: ... 
- Spring IOC(2)----如何注册bean定义
			前面说到IOC容器在刷新之前的一些初始化工作,现在来看看在refresh()方法中,是怎样来加载注册我们自己的bean定义的. refresh()方法中有很多功能,从注释中就可以看出来 我们本次重点关 ... 
- grep、正则表达式
			1.grep :文本搜索工具 -i:忽略大小写--color:匹配到的内容高亮显示-v:显示没有被模式匹配到的行-o:只显示被模式匹配到的字符串-n:显示匹配到行的行号-E:使用扩展正则表达式-A:后 ... 
