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<VoteResponse> 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349