RocketMQ 的DLedger 选主机制

最近在看RocketMQ 的raft实现,名字叫Dledger。找了一篇源码分析的博客发现其中很多细节都解释的不是很清晰。Dledger 选主过程

首先我们要知道RocketMQ Dledger有哪些特性,在github官网上我们可以看到Dledger实现了很多原论文In search of an understandable consensus algorithm中没有的的特性。因为我暂时没有找到官方的设计文档,代码中的注释也暂时不是很完整,因此理解这些特性对于理解源代码有很重要的意义。其中最重要的是Pre-vote 机制。

本文需要读者对Raft算法的原论文有一定的熟悉程度。

Pre-vote机制是在原作者的258页博士论文Ongaro Phd
第九章中提过的,其目标是解决在网络分区发生时,处于少数分区的节点不会一直增加Term, 更具体的应用可以在原论文中搜索得到。

Pre-vote的思想简单来说就是,在Candidate increase term之前,要先在不增加term的情况下看自己是否满足 比majority数量的node要 more up-to-date。如果一个Candidate知道自己一定不可能被选为Leader,那么就会自觉的increase term的值。这是Pre-vote。

而DLedger的实现里还有一个很大和原论文不同的地方。Dledger 会产生更多的Candidate节点。举例来说,当Leader得知有更大term的voting process正在进行,或者具有更大term的leader已经被选举出来之后,Leader并不会退化为follower,而是退化为 "尚未进行pre-vote的Candidate“。 这个体现了Dledger 不同的node有不同的leader preference(使用者可能希望leader 大多数情况都在某一个node上)。而这样的设计可以使以前的Leader能够在因为某些暂时意外原因stepped down之后有机会尽快恢复继续当leader。

主要的handleVote方法

public CompletableFuture handleVote(VoteRequest request, boolean self) {
        //hold the lock to get the latest term, leaderId, ledgerEndIndex
        synchronized (memberState) {
            // PRECONDITION CHECk
            ...

            if (request.getTerm() < memberState.currTerm()) {
                return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_EXPIRED_VOTE_TERM));
            } else if (request.getTerm() == memberState.currTerm()) {
                if (memberState.currVoteFor() == null) {
                    //just let it go
                } else if (memberState.currVoteFor().equals(request.getLeaderId())) {
                    //repeat just let it go
                } else {
                    if (memberState.getLeaderId() != null) {
                        return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_ALREADY_HAS_LEADER));
                    } else {
                        return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_ALREADY_VOTED));
                    }
                }
            } else {
                changeRoleToCandidate(request.getTerm());
                needIncreaseTermImmediately = true;
                return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_TERM_NOT_READY));
            }
            
            ...
            // Check if candidate is more up-to-date than current node

            memberState.setCurrVoteFor(request.getLeaderId());
            return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.ACCEPT));
        }

一个全新的cluster启动

我们假设有4个 node, node 0, 1, 2 从follower转变为candidate的timeout 时间由小到大排序, 优先级由高到低。node 2 和 node 3 优先级可认为相同。

1.刚启动时,根据代码里的定义

node currVotedFor currTerm role lastParseResult
node0 null 0 candidate WAIT_TO_REVOTE
node1 null 0 candidate WAIT_TO_REVOTE
node2 null 0 candidate WAIT_TO_REVOTE
node3 null 0 candidate WAIT_TO_REVOTE
  1. 假设node0 的vote请求最先到达其他节点, 因为node0的初始lastParseResult为WAIT_TO_REVOTE, node0并不会将term + 1。,根据代码, node 1,2,3 分别
  • 判断request.term() 是否等于 currTerm,发现相等
  • 判断currVoteFor 是否是null ,发现是,于是进入接下来的more up-to-date 判断逻辑
  • more up-to-date 判断逻辑 通过,设置currVotedFor为node0, 并返回同意请求。

node0 成为leader,此时状态为

node currVotedFor currTerm role lastParseResult
node0 null 0 leader PASSED
node1 node0 0 candidate WAIT_TO_REVOTE
node2 node0 0 candidate WAIT_TO_REVOTE
node3 node0 0 candidate WAIT_TO_REVOTE
  1. node 123收到来自node0的心跳包之后,发现
  • request.getTerm() == memberState.currTerm()
  • memberState.getLeaderId() == null
    两个条件都满足,根据代码,node 123 退化为 follower 状态。
    此时状态为
node currVotedFor currTerm role lastParseResult
node0 null 0 leader PASSED
node1 node0 0 follower WAIT_TO_REVOTE
node2 node0 0 follower WAIT_TO_REVOTE
node3 node0 0 follower WAIT_TO_REVOTE
  1. 假设经过一段比较长的时间,所有的通讯全都畅通无阻,没有发生leader选举,心跳正常,log复制进度完全相同, 因此所有的其他candidate会。 连。

  2. 突然node0完全宕机, 丢失所有通讯,并且确保不会重连。这个时候node1 最先因为timeout变为Candidate状态。

node currVotedFor currTerm role lastParseResult
node1 node0 0 candidate WAIT_TO_REVOTE
node2 node0 0 follower WAIT_TO_REVOTE
node3 node0 0 follower WAIT_TO_REVOTE
  1. node1 发出vote() 请求,curVotedFor变成子集,因为是WAIT_TO_REVOTE 状态,并不会将 term + 1。但由于此时 node2, node3 都还认为 leader 仍然在,因此会拒绝node 1 发出的请求。这样的状况一直持续到node 2, node 3 都timeout并变成candidate的状态。这时他们的状态是
node currVotedFor currTerm role lastParseResult
node1 node1 0 candidate WAIT_TO_REVOTE
node2 node0 0 candidate WAIT_TO_REVOTE
node3 node0 0 candidate WAIT_TO_REVOTE
  1. 这个情况和刚开始的情况非常相似,但是我们会发现currVotedFor不再是null了。这个时候我们还是认为node 1 先在maintainAsCandidate() 中提出投票请求.

node1 在统计所有的response之后,(使用如下代码)

lastVoteCost = DLedgerUtils.elapsed(startVoteTimeMs);
VoteResponse.ParseResult parseResult;
if (knownMaxTermInGroup.get() > term) {
    parseResult = VoteResponse.ParseResult.WAIT_TO_VOTE_NEXT;
    nextTimeToRequestVote = getNextTimeToRequestVote();
    changeRoleToCandidate(knownMaxTermInGroup.get());
} else if (alreadyHasLeader.get()) {
    parseResult = VoteResponse.ParseResult.WAIT_TO_VOTE_NEXT;
    nextTimeToRequestVote = getNextTimeToRequestVote() + heartBeatTimeIntervalMs * maxHeartBeatLeak;
} else if (!memberState.isQuorum(validNum.get())) {
    parseResult = VoteResponse.ParseResult.WAIT_TO_REVOTE;
    nextTimeToRequestVote = getNextTimeToRequestVote();
} else if (memberState.isQuorum(acceptedNum.get())) {
    parseResult = VoteResponse.ParseResult.PASSED;
} else if (memberState.isQuorum(acceptedNum.get() + notReadyTermNum.get())) {
    parseResult = VoteResponse.ParseResult.REVOTE_IMMEDIATELY;
} else if (memberState.isQuorum(acceptedNum.get() + biggerLedgerNum.get())) {
    parseResult = VoteResponse.ParseResult.WAIT_TO_REVOTE;
    nextTimeToRequestVote = getNextTimeToRequestVote();
} else {
    parseResult = VoteResponse.ParseResult.WAIT_TO_VOTE_NEXT;
    nextTimeToRequestVote = getNextTimeToRequestVote();
}

// 原文链接:https://blog.csdn.net/prestigeding/article/details/99697323

发现都不满足, 因此走到最后一个默认的else branch里,将lastParseResult 设置成WAIT_TO_VOTE_NEXT。

node currVotedFor currTerm role lastParseResult
node1 node1 0 candidate WAIT_TO_VOTE_NEXT
node2 node0 0 candidate WAIT_TO_REVOTE
node3 node0 0 candidate WAIT_TO_REVOTE
  1. 假设node 1 再一次进入maintainAsCandidate() 方法,这时因为是WAIT_TO_VOTE_NEXT, 所以要把term + 1 后再发起vote,之后状态变为
node currVotedFor currTerm role lastParseResult
node1 node1 1 candidate WAIT_TO_VOTE_NEXT
node2 node0 0 candidate WAIT_TO_REVOTE
node3 node0 0 candidate WAIT_TO_REVOTE
  1. 注意此时node 2, 3 虽然是candidate,但都还没投过票 而node 2, 3 在收到来自node 1 的vote 请求时,发现
  • request.term() > memberState.currTerm()

发现term 不consistent,于是要拒绝,并且执行以下语句

这里我没怎么懂,为什么必须要term 相同才能投票嘞(可能处于性能优化的考虑?)

// 
changeRoleToCandidate(request.getTerm());
needIncreaseTermImmediately = true;

这里changeRoleToCandidate 的作用主要是更新knownMaxTermInGroup的值,needIncreaseTermImmediately 是使得下一次maintainAsCandidate()的时候要把currTerm

  • 如果 knownMaxTermInGroup > currTerm, 则更新为knownMaxTermInGroup
  • 如果 knownMaxTermInGroup = currTerm, 则更新为currTerm + 1

而此时 两件事情发生了

  • node 1 收到 node2 的 REJECT 回复,发现memberState.isQuorum(acceptedNum.get() + notReadyTermNum.get() 成立,parse结果是REVOTE_IMMEDIATELY。立刻再次进入maintainAsCandidate() 方法,准备开始投票

  • node 2 进入maintainAsCandidate() , 因为 needIncreaseTermImmediately 是true,因此要立马把currTerm 更新 1,并且准备开始投票

由于node 2 和node 3 对称,可认为node 3和 node 2 采取了相同的动作。

在大家都还未投票前(也没给自己投票前),此时状态为

node currVotedFor currTerm role lastParseResult
node1 node1 1 candidate REVOTE_IMMEDIATELY
node2 node0 1 candidate WAIT_TO_REVOTE
node3 node0 1 candidate WAIT_TO_REVOTE

那现在发生的事情就很奇怪了,第一种情况,假设 node1 的 vote 请求先到达 node 2 node 3。由于term增加后并没有把 currVotedFor 清空,这会导致node 2, node 3 不能同意该请求,node 1 被拒绝。

第二种情况, 假设node1 的请求在node 2, node 3 先给自己投票之后发出,那么node1 的vote 请求在到达node 2, 3 的时候会看到这样一个状态。node 1 无论如何都没法成功成为leader。

node currVotedFor currTerm role lastParseResult
node1 node1 1 candidate REVOTE_IMMEDIATELY
node2 node2 1 candidate WAIT_TO_REVOTE
node3 node3 1 candidate WAIT_TO_REVOTE

对于node 2,3情况类似。

我认为出现这种情况的主要原因在于:

  • 如果candidate 的 term 大于接收者的 term,那么接收者会返回NOT_READY_TO_VOTE
  • 而当candidate 的 term 等于接收者的 term时,会发现接收者的currVotedFor 要么是接收者自己,要么是上一个term的投票结果(因此是没有意义的结果),反正不会是Candidate自己(即使是也是错误的语义),也会被拒绝。

问题

  • alreadyHasLeader.get() 似乎可以被优化。如果等到所有的follower 都认为没有leader了,有可能中途已经有半数的同意票了但是因为有一个follower还有lead使得再次等待。

  • 每次更新term的时候,votedFor 也应该同时被修改,被改成null,否则就不make sense了。

  • commit 值要怎么保证永远不回退? (找到超过半数的node中所有commit的最大值)

首先所有都被初始化为Candiddate,currTerm是0, curVoted 是null.

  • currentTerm
  • votedFor: 很有可能和原论文中定义不同,follower的votedFor可能是null。
  • commitLogs

volatile states:
commitIndex: index of highest log entry to be commited
lastApplied: index of highest log entry applied to state machine

volatile stats on leaders:

nextIndex: for each server, index of the next log entry to send to that server
matchIndex: for each server, index of highest log entry known to be replicated on server

https://blog.csdn.net/prestigeding/article/details/99697323

你可能感兴趣的:(RocketMQ 的DLedger 选主机制)