【RocketMQ】DLedger選主原始碼分析

shanml發表於2023-02-15

RocketMQ 4.5版本之前,可以採用主從架構進行叢集部署,但是如果master節點掛掉,不能自動在叢集中選舉出新的Master節點,需要人工介入,在4.5版本之後提供了DLedger模式,使用Raft演算法,如果Master節點出現故障,可以自動選舉出新的Master進行切換。

Raft協議

Raft是分散式系統中的一種共識演算法,用於在叢集中選舉Leader管理叢集。Raft協議中有以下角色:

Leader(領導者):叢集中的領導者,負責管理叢集。

Candidate(候選者):具有競選Leader資格的角色,如果叢集需要選舉Leader,節點需要先轉為候選者角色才可以發起競選。

Follower(跟隨者 ):Leader的跟隨者,接收和處理來自Leader的訊息,與Leader之間保持通訊,如果通訊超時或者其他原因導致節點與Leader之間通訊失敗,節點會認為叢集中沒有Leader,就會轉為候選者發起競選,推薦自己成為Leader。

Raft協議中還有一個Term(任期)的概念,任期是隨著選舉的舉行而變化,一般是單調進行遞增,比如說叢集中當前的任期為1,此時某個節點發現叢集中沒有Leader,開始發起競選,此時任期編號就會增加為2,表示進行了新一輪的選舉。一般會為Term較大的那個節點進行投票,當某個節點收到了過半Quorum的投票數(一般是叢集中的節點數/2 + 1),將會被選舉為Leader。

DLedger選主原始碼分析

在Broker啟動的時候,會判斷是否開啟了DLedger,如果開啟會建立角色變更處理器DLedgerRoleChangeHandler, 然後獲取CommitLog轉為DLedgerCommitLog型別,並新增建立的角色變更處理器:

public class BrokerController {
     public boolean initialize() throws CloneNotSupportedException {
        result = result && this.consumerOffsetManager.load();
        result = result && this.subscriptionGroupManager.load();
        result = result && this.consumerFilterManager.load();
        if (result) {
            try {
                this.messageStore = new DefaultMessageStore(this.messageStoreConfig, this.brokerStatsManager, this.messageArrivingListener, this.brokerConfig);
                // 如果開啟了DLeger
                if (messageStoreConfig.isEnableDLegerCommitLog()) {
                    // 建立DLedgerRoleChangeHandler
                    DLedgerRoleChangeHandler roleChangeHandler = new DLedgerRoleChangeHandler(this, (DefaultMessageStore) messageStore);
                    // 獲取CommitLog並轉為DLedgerCommitLog型別,並新增角色變更處理器DLedgerRoleChangeHandler
                    ((DLedgerCommitLog)((DefaultMessageStore) messageStore).getCommitLog()).getdLedgerServer().getdLedgerLeaderElector().addRoleChangeHandler(roleChangeHandler);
                }
                // ...
            } catch (IOException e) {
                result = false;
                log.error("Failed to initialize", e);
            }
        }
        // ...
     }
}

DefaultMessageStore建構函式中可以看到,如果開啟了DLedger,使用的是DLedgerCommitLog,所以上面可以將CommitLog轉換為DLedgerCommitLog

public class DefaultMessageStore implements MessageStore {
    public DefaultMessageStore(final MessageStoreConfig messageStoreConfig, final BrokerStatsManager brokerStatsManager,
        //...
        // 如果開啟DLeger
        if (messageStoreConfig.isEnableDLegerCommitLog()) {
            this.commitLog = new DLedgerCommitLog(this); // 建立DLedgerCommitLog型別的CommitLog
        } else {
            this.commitLog = new CommitLog(this);
        }
        // ...
    }
}

進入到DLedgerCommitLog,可以看到它引用了DLedgerServer,並在start方法中對其進行了啟動,而在DLedgerServer中又啟動了DLedgerLeaderElector進行Leader選舉:

// DLedgerCommitLog
public class DLedgerCommitLog extends CommitLog {
    // DLedgerServer
    private final DLedgerServer dLedgerServer;
    @Override
    public void start() {
        // 啟動DLedgerServer
        dLedgerServer.startup();
    }
}

// DLedgerServer
public class DLedgerServer implements DLedgerProtocolHander {
    public void startup() {
        this.dLedgerStore.startup();
        this.dLedgerRpcService.startup();
        this.dLedgerEntryPusher.startup();
        // 啟動Leader選擇
        this.dLedgerLeaderElector.startup();
        this.executorService.scheduleAtFixedRate(this::checkPreferredLeader, 1000L, 1000L, TimeUnit.MILLISECONDS);
    }
}

DLedgerLeaderElector中,引用了StateMaintainer,並在startup方法中啟動了StateMaintainer,然後遍歷RoleChangeHandler,呼叫其startup進行啟動:

public class DLedgerLeaderElector {
    // 例項化StateMaintainer
    private StateMaintainer stateMaintainer = new StateMaintainer("StateMaintainer", logger);

    public void startup() {
        // 啟動StateMaintainer
        stateMaintainer.start();
        // 遍歷RoleChangeHandler
        for (RoleChangeHandler roleChangeHandler : roleChangeHandlers) {
            // 啟動角色變更處理器
            roleChangeHandler.startup();
        }
    }
}

StateMaintainerDLedgerLeaderElector的內部類,繼承了ShutdownAbleThread,所以這裡其實是開啟了一個執行緒會不斷執行doWork方法,在doWork方法中呼叫了maintainState方法維護狀態:

public class DLedgerLeaderElector {
   public class StateMaintainer extends ShutdownAbleThread {

        @Override public void doWork() {
            try {
                if (DLedgerLeaderElector.this.dLedgerConfig.isEnableLeaderElector()) {
                    DLedgerLeaderElector.this.refreshIntervals(dLedgerConfig);
                    // 維護狀態
                    DLedgerLeaderElector.this.maintainState();
                }
                // 睡眠10ms
                sleep(10);
            } catch (Throwable t) {
                DLedgerLeaderElector.logger.error("Error in heartbeat", t);
            }
        }

    }
}

maintainState方法中,可以看到對節點的角色進行了判斷:

  1. 如果當前節點是Leader,呼叫maintainAsLeader方法處理;
  2. 如果當前節點是Follower,呼叫maintainAsFollower方法處理;
  3. 如果當前節點是Candidate,呼叫maintainAsCandidate方法處理;
public class DLedgerLeaderElector {
   private void maintainState() throws Exception {
        if (memberState.isLeader()) { // 如果是Leader
            maintainAsLeader();
        } else if (memberState.isFollower()) { // 如果是Follower
            maintainAsFollower();
        } else { // 如果是Candidate
            maintainAsCandidate();
        }
    }
}

MemberState中可以看到role的值預設為CANDIDATE,所以初始狀態下,各個節點的角色為CANDIDATE,接下來進入到maintainAsCandidate方法看下如何發起選舉:

public class MemberState {
    // 預設CANDIDATE角色
    private volatile Role role = CANDIDATE;
}

Candidate發起競選

needIncreaseTermImmediately:預設為false,為true時表示需要增加投票輪次Term的值,並立刻發起新一輪選舉。

currTerm:當前選舉的投票輪次。

LedgerEndIndex:當前記錄的CommitLog日誌的index。

LedgerEndTerm:Leader節點的投票輪次,也就是最近一次Leader選舉成功時的那個Term,會記錄在LedgerEndIndex中。

在Candidate候選者角色下可以選舉Leader,發起選舉的過程如下:

  1. 判斷當時時間是否小於下一次投票開始時間並且needIncreaseTermImmediately為false,如果未到開始時間並且needIncreaseTermImmediately為false,直接返回,等待下一次投票;
  2. 校驗當前角色是否是Candidate,如果不是直接返回;
  3. 由於Leader選舉可能會失敗,所以先判斷上一次選舉的結果,對以下兩個條件進行判斷,滿足兩個條件之一,會增加Terem的值,為新一輪競選做準備,反之使用當前的Term即可:
    (1)如果是WAIT_TO_VOTE_NEXT狀態,也就是等待下一次重新進行選舉;
    (2)needIncreaseTermImmediately為true;
  4. 如果needIncreaseTermImmediately為true,需要重置其狀態為false,並呼叫getNextTimeToRequestVote更新下一次發起選舉的時間;
  5. 呼叫voteForQuorumResponses方法向其他節點發起投票請求;
  6. 處理投票結果,這裡先省略,稍後再講;
public class DLedgerLeaderElector {
    private final MemberState memberState;

    private void maintainAsCandidate() throws Exception {
        // 判斷當時時間是否小於下一次投票開始時間並且needIncreaseTermImmediately為false
        if (System.currentTimeMillis() < nextTimeToRequestVote && !needIncreaseTermImmediately) {
            return;
        }
        long term;
        long ledgerEndTerm;
        long ledgerEndIndex;
        synchronized (memberState) {
            // 如果不是Candidate直接返回
            if (!memberState.isCandidate()) {
                return;
            }
            // 如果上一次選舉Leader之後的結果是等待下一下重新進行選舉或者如果需要立刻增加任期
            if (lastParseResult == VoteResponse.ParseResult.WAIT_TO_VOTE_NEXT || needIncreaseTermImmediately) {
                long prevTerm = memberState.currTerm();
                term = memberState.nextTerm(); // 增加Term
                logger.info("{}_[INCREASE_TERM] from {} to {}", memberState.getSelfId(), prevTerm, term);
                lastParseResult = VoteResponse.ParseResult.WAIT_TO_REVOTE;
            } else {
                // 使用獲取當前的Term
                term = memberState.currTerm();
            }
            // 獲取CommitLog日誌的index
            ledgerEndIndex = memberState.getLedgerEndIndex();
            // 獲取Leader的投票輪次
            ledgerEndTerm = memberState.getLedgerEndTerm();
        }
        // 如果needIncreaseTermImmediately為true
        if (needIncreaseTermImmediately) {
            // 更新下次選舉的時間
            nextTimeToRequestVote = getNextTimeToRequestVote();
            // 恢復needIncreaseTermImmediately的預設狀態
            needIncreaseTermImmediately = false;
            return;
        }
        long startVoteTimeMs = System.currentTimeMillis();
        // 發起投票請求
        final List<CompletableFuture<VoteResponse>> quorumVoteResponses = voteForQuorumResponses(term, ledgerEndTerm, ledgerEndIndex);
       
        // 處理投票結果,先省略
        // ...
    }
}

nextTerm

nextTerm方法用於增加投票輪次Term的值,不過在增加之前會先判斷當前節點維護的叢集中已知的最大Term也就是knownMaxTermInGroup的值是否大於currTerm,如果是則使用knownMaxTermInGroup的值作為下一次投票的輪次,否則才對當前的TermcurrTerm做自增操作:

public class MemberState {
    public synchronized long nextTerm() {
        // 校驗角色
        PreConditions.check(role == CANDIDATE, DLedgerResponseCode.ILLEGAL_MEMBER_STATE, "%s != %s", role, CANDIDATE);
        // 如果已知叢集中最大的Term大於當前的Term,返回叢集中最大的Term大
        if (knownMaxTermInGroup > currTerm) {
            currTerm = knownMaxTermInGroup;
        } else {
            // 否則對Term自增
            ++currTerm;
        }
        currVoteFor = null;
        persistTerm(); // 持久化
        return currTerm;
    }
}

getNextTimeToRequestVote

getNextTimeToRequestVote用於更新下次發起選舉的時間,規則為:當前時間 + 300ms + 隨機值(在最大和最小投票時間間隔之間也就是300-1000之間生成隨機值)。

public class DLedgerLeaderElector {
  // 最小投票時間間隔
  private int minVoteIntervalMs = 300;
  // 最大投票時間間隔
  private int maxVoteIntervalMs = 1000;
  private long getNextTimeToRequestVote() {
        if (isTakingLeadership()) {
            return System.currentTimeMillis() + dLedgerConfig.getMinTakeLeadershipVoteIntervalMs() +
                random.nextInt(dLedgerConfig.getMaxTakeLeadershipVoteIntervalMs() - dLedgerConfig.getMinTakeLeadershipVoteIntervalMs());
        }
        // 當前時間 + 300ms + 隨機值(在最大和最小投票時間間隔之間也就是300-1000之間生成隨機值)
        return System.currentTimeMillis() + minVoteIntervalMs + random.nextInt(maxVoteIntervalMs - minVoteIntervalMs);
  }
}

傳送投票請求

voteForQuorumResponses方法中,對當前節點維護的叢集中所有節點進行了遍歷,向每一個節點傳送投票請求:

  1. 構建VoteRequest投票請求;
  2. 設定組資訊、LedgerEndIndex、LedgerEndTerm等資訊;
  3. 設定Leader節點ID,也就是當前發起投票請求的節點的ID;
  4. 設定本次選舉的Term資訊;
  5. 設定請求目標節點的ID;
  6. 如果是當前節點自己,直接呼叫handleVote處理投票,否則呼叫dLedgerRpcService的vote方法傳送投票請求;
public class DLedgerLeaderElector {
   private List<CompletableFuture<VoteResponse>> voteForQuorumResponses(long term, long ledgerEndTerm,
        long ledgerEndIndex) throws Exception {
        List<CompletableFuture<VoteResponse>> responses = new ArrayList<>();
        // 遍歷節點
        for (String id : memberState.getPeerMap().keySet()) {
            // 構建投票請求
            VoteRequest voteRequest = new VoteRequest();
            // 設定組
            voteRequest.setGroup(memberState.getGroup());
            voteRequest.setLedgerEndIndex(ledgerEndIndex);
            voteRequest.setLedgerEndTerm(ledgerEndTerm);
            // 設定Leader節點ID
            voteRequest.setLeaderId(memberState.getSelfId());
            // 設定Term資訊
            voteRequest.setTerm(term);
            // 設定目標節點的ID
            voteRequest.setRemoteId(id);
            CompletableFuture<VoteResponse> voteResponse;
            // 如果是當前節點自己
            if (memberState.getSelfId().equals(id)) {
                // 直接呼叫handleVote處理
                voteResponse = handleVote(voteRequest, true);
            } else {
                // 傳送請求
                voteResponse = dLedgerRpcService.vote(voteRequest);
            }
            responses.add(voteResponse);
        }
        return responses;
    }
}

投票請求處理

其他節點收到投票請求後,對請求的處理在handleVote方法中:

  1. 判斷髮起投票的節點是否在當前節點的叢集中,如果不在叢集中,拒絕投票,返回狀態為REJECT_UNKNOWN_LEADER
  2. 如果不是當前節點發起的請求,但是請求中攜帶的LeaderID與當前節點ID一致,拒絕投票,返回狀態為REJECT_UNEXPECTED_LEADER
  3. 對比請求中的攜帶的LedgerEndTerm與當前節點記錄的LedgerEndTerm
  • 小於:說明請求的LedgerEndTerm比較落後,拒絕投票REJECT_EXPIRED_LEDGER_TERM
  • 相等,但是LedgerEndIndex小於當前節點維護的LedgerEndIndex:說明發起請求的節點日誌比較落後,拒絕投票,返回REJECT_SMALL_LEDGER_END_INDEX
  • 其他情況:繼續下一步;
  1. 對比請求中的Term與當前節點的Term大小:

    • 小於:說明請求中的Term比較落後,拒絕投票返回狀態為REJECT_EXPIRED_LEDGER_TERM
    • 相等:如果當前節點還未投票或者剛好投票給發起請求的節點,進入下一步;如果已經投票給某個Leader,拒絕投票返回REJECT_ALREADY_HAS_LEADER;除此之外其他情況返回REJECT_ALREADY_VOTED
    • 大於:說明當前節點的Term過小已經落後於最新的Term,呼叫changeRoleToCandidate方法將當前節點更改為Candidate角色,needIncreaseTermImmediately置為true,返回REJECT_TERM_NOT_READY表示當前節點還未準備好進行投票;

    呼叫changeRoleToCandidate方法時傳入了請求中攜帶的Term的值,在方法內會與當前節點已知的最大Term的值knownMaxTermInGroup做對比,如果knownMaxTermInGroup比請求中的Term小,會更新為請求中的Term的值,在上面maintainAsCandidate方法中可以知道,如果needIncreaseTermImmediately`置為true,會呼叫nextTerm增加Term,nextTerm方法上面也提到過,這個方法中會判斷knownMaxTermInGroup是否大於當前的Term如果是返回knownMaxTermInGroup的值,所以如果當前節點的Term落後於發起選舉的Term,不能進行投票,需要在下次更新Term的值後,與發起Leader選舉的Term一致時才可以投票;

  2. 如果請求中的TERM小於當前節點的LedgerEndTerm,拒絕投票,返回REJECT_TERM_SMALL_THAN_LEDGER

  3. 投票給發起請求的節點,設定CurrVoteFor的值為發起請求的節點ID,並返回ACCEPT接受投票狀態

public CompletableFuture<VoteResponse> handleVote(VoteRequest request, boolean self) {
        // 加鎖
        synchronized (memberState) {
            // 判斷髮起投票的節點是否在當前節點的叢集中
            if (!memberState.isPeerMember(request.getLeaderId())) {
                logger.warn("[BUG] [HandleVote] remoteId={} is an unknown member", request.getLeaderId());
                return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_UNKNOWN_LEADER));
            }
            // 如果不是當前節點發起的請求,但是請求中的LeaderID與當前節點一致
            if (!self && memberState.getSelfId().equals(request.getLeaderId())) {
                logger.warn("[BUG] [HandleVote] selfId={} but remoteId={}", memberState.getSelfId(), request.getLeaderId());
                return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_UNEXPECTED_LEADER));
            }
            // 如果請求中的LedgerEndTerm小於當前節點的LedgerEndTerm,說明請求的Term已過期
            if (request.getLedgerEndTerm() < memberState.getLedgerEndTerm()) {
                // 返回REJECT_EXPIRED_LEDGER_TERM
                return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_EXPIRED_LEDGER_TERM));
            } else if (request.getLedgerEndTerm() == memberState.getLedgerEndTerm() && request.getLedgerEndIndex() < memberState.getLedgerEndIndex()) { // 如果LedgerEndTerm一致但是請求中的LedgerEndIndex小於當前節點的
                // 返回REJECT_SMALL_LEDGER_END_INDEX
                return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_SMALL_LEDGER_END_INDEX));
            }
            // 如果請求中的TERM小於當前節點的Term
            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()) { // 如果請求中的TERM等於當前節點的Term
                if (memberState.currVoteFor() == null) { // 如果當前節點還未投票
                    //let it go
                } else if (memberState.currVoteFor().equals(request.getLeaderId())) { // 如果當前節點剛好投票給發起請求的節點
                    //repeat just let it go
                } else {
                    if (memberState.getLeaderId() != null) { // 如果已經有Leader
                        // 返回REJECT_ALREADY_HAS_LEADER,表示已投過票
                        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 {
                // 走到這裡表示請求中的Term大於當前節點記錄的Term
                // 當前節點更改為Candidate角色
                changeRoleToCandidate(request.getTerm());
                // needIncreaseTermImmediately置為true,在下次執行時增加Term
                needIncreaseTermImmediately = true;
                // 返回REJECT_TERM_NOT_READY
                return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_TERM_NOT_READY));
            }
            // 如果請求中的TERM小於當前節點的LedgerEndTerm
            if (request.getTerm() < memberState.getLedgerEndTerm()) {
                return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.getLedgerEndTerm()).voteResult(VoteResponse.RESULT.REJECT_TERM_SMALL_THAN_LEDGER));
            }
            if (!self && isTakingLeadership() && request.getLedgerEndTerm() == memberState.getLedgerEndTerm() && memberState.getLedgerEndIndex() >= request.getLedgerEndIndex()) {
                return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_TAKING_LEADERSHIP));
            }
            // 投票給發起請求的節點
            memberState.setCurrVoteFor(request.getLeaderId());
            // 返回ACCEPT接收投票的狀態
            return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.ACCEPT));
        }
    }

處理投票響應結果

回到maintainAsCandidate方法,繼續看處理投票結果的部分,給叢集中每個節點發起投票請求之後,會等待每個請求返回響應,並進行處理:

  1. ACCEPT:表示同意投票給當前節點,接受投票的節點數量acceptedNum加1;
  2. REJECT_ALREADY_VOTED或者REJECT_TAKING_LEADERSHIP:表示拒絕投票給當前節點;
  3. REJECT_ALREADY_HAS_LEADER:表示已經投票給了其他節點,alreadyHasLeader設定為true;
  4. REJECT_EXPIRED_VOTE_TERM:表示反映響應的節點的Term比當前節點的大,此時判斷返回的Term是否大於當前節點記錄的最大Term的值,如果是對knownMaxTermInGroup進行更新;
  5. REJECT_SMALL_LEDGER_END_INDEX:表示返回響應節點的LedgerEndIndex比當前節點的大,biggerLedgerNum加1;
  6. REJECT_TERM_NOT_READY:表示有節點還未準備好進行投票,notReadyTermNum加1;
public class DLedgerLeaderElector {
    private void maintainAsCandidate() throws Exception {
        // ...
        
        // 發起投票
        final List<CompletableFuture<VoteResponse>> quorumVoteResponses = voteForQuorumResponses(term, ledgerEndTerm, ledgerEndIndex);
        final AtomicLong knownMaxTermInGroup = new AtomicLong(term);
        final AtomicInteger allNum = new AtomicInteger(0);
        final AtomicInteger validNum = new AtomicInteger(0);
        // 記錄接受投票的節點個數
        final AtomicInteger acceptedNum = new AtomicInteger(0);
        final AtomicInteger notReadyTermNum = new AtomicInteger(0);
        final AtomicInteger biggerLedgerNum = new AtomicInteger(0);
        // 記錄是否有節點投票給了其他節點
        final AtomicBoolean alreadyHasLeader = new AtomicBoolean(false);

        CountDownLatch voteLatch = new CountDownLatch(1);
        // 處理投票響應結果
        for (CompletableFuture<VoteResponse> future : quorumVoteResponses) {
            future.whenComplete((VoteResponse x, Throwable ex) -> {
                try {
                    if (ex != null) {
                        throw ex;
                    }
                    logger.info("[{}][GetVoteResponse] {}", memberState.getSelfId(), JSON.toJSONString(x));
                    if (x.getVoteResult() != VoteResponse.RESULT.UNKNOWN) {
                        validNum.incrementAndGet();
                    }
                    synchronized (knownMaxTermInGroup) {
                        switch (x.getVoteResult()) { // 判斷投票結果
                            case ACCEPT: // 如果接受投票
                                acceptedNum.incrementAndGet();
                                break;
                            case REJECT_ALREADY_VOTED: // 拒絕投票
                            case REJECT_TAKING_LEADERSHIP:
                                break;
                            case REJECT_ALREADY_HAS_LEADER: // 如果已經投票了其他節點
                                alreadyHasLeader.compareAndSet(false, true);
                                break;
                            case REJECT_TERM_SMALL_THAN_LEDGER:
                            case REJECT_EXPIRED_VOTE_TERM: // 如果響應中的Term大於當前節點傳送請求時的Term
                                // 判斷響應中的Term是否大於當前節點已知的最大Term
                                if (x.getTerm() > knownMaxTermInGroup.get()) { 
                                    knownMaxTermInGroup.set(x.getTerm()); // 進行更新
                                }
                                break;
                            case REJECT_EXPIRED_LEDGER_TERM:
                            case REJECT_SMALL_LEDGER_END_INDEX:// 如果返回響應節點的LedgerEndIndex比當前節點的大
                                biggerLedgerNum.incrementAndGet();
                                break;
                            case REJECT_TERM_NOT_READY: // 如果還未準備好
                                notReadyTermNum.incrementAndGet();
                                break;
                            default:
                                break;
                        }
                    }
                    if (alreadyHasLeader.get()
                        || memberState.isQuorum(acceptedNum.get())
                        || memberState.isQuorum(acceptedNum.get() + notReadyTermNum.get())) {
                        voteLatch.countDown();
                    }
                } catch (Throwable t) {
                    logger.error("vote response failed", t);
                } finally {
                    allNum.incrementAndGet();
                    if (allNum.get() == memberState.peerSize()) {
                        voteLatch.countDown();
                    }
                }
            });

        }
        // 判斷投票結果,先省略....
    }
}

經過以上處理,等待(2000 + 一個隨機數)毫秒之後,接下來判斷本次選舉是否成功:

  1. 如果其他節點返回的響應中有比當前節點的Term大的,說明本次選舉的Term已經落後其他節點,使用請求中返回的較大的那個Term作為下次競選的Term,並計算下次發起選舉的時間,等待下一次進行選舉,對應狀態為WAIT_TO_VOTE_NEXT
  2. 如果有節點已經投票給了其他節點,說明有其他節點在競爭Leader,需要等待下一次進行選舉,對應狀態為WAIT_TO_REVOTE
  3. 如果收到的有效投票數未過半,等待下一次進行選舉,對應狀態為WAIT_TO_REVOTE
  4. 如果收到有效投票數減去比當前節點ledgerEndIndex大的節點個數未過半,如果走到這一個條件中,說明收到有效投票個數是過半的(如果未過半會先進入條件3),但是其中有些節點的ledgerEndIndex大於當前節點,將這部分減去之後還未過半,意味著本次選舉沒獲得過半的有效投票數不能成功選舉,等待下一次進行選舉,對應狀態為WAIT_TO_REVOTE
  5. 如果收到的投票數過半也就是達到了Quorum,本輪選舉成功,對應狀態PASS;
  6. 如果接受投票的節點個數+未準備好的節點數過半,表示有部分節點Term落後,與當前選舉的Term不一致,立刻進行下一次投票,對應狀態為REVOTE_IMMEDIATELY;
  7. 其他情況,等待下一次投票,對應狀態為WAIT_TO_VOTE_NEXT;

如果進入到第6步,意味著選舉成功,此時會呼叫changeRoleToLeader轉為Leader角色:

public class DLedgerLeaderElector {
    private void maintainAsCandidate() throws Exception {
        // 發起投票
        // ...
        // 處理投票響應結果
        // ...
        try {
            // 等待
            voteLatch.await(2000 + random.nextInt(maxVoteIntervalMs), TimeUnit.MILLISECONDS);
        } catch (Throwable ignore) {
        }
        lastVoteCost = DLedgerUtils.elapsed(startVoteTimeMs);
        VoteResponse.ParseResult parseResult;
        // 判斷投票結果
        if (knownMaxTermInGroup.get() > term) { // 1.如果其他節點返回的響應中有比當前節點的Term大的
            // 等待下一次選舉
            parseResult = VoteResponse.ParseResult.WAIT_TO_VOTE_NEXT; 
            // 計算下次發起選舉的時間
            nextTimeToRequestVote = getNextTimeToRequestVote(); 
            // 轉為Candidate,傳入knownMaxTermInGroup,下次會使用knownMaxTermInGroup的值進選舉
            changeRoleToCandidate(knownMaxTermInGroup.get()); 
        } else if (alreadyHasLeader.get()) {// 2.如果有節點已經投票給了其他節點
            parseResult = VoteResponse.ParseResult.WAIT_TO_REVOTE;
            // 設定下次選舉時間
            nextTimeToRequestVote = getNextTimeToRequestVote() + heartBeatTimeIntervalMs * maxHeartBeatLeak;
        } else if (!memberState.isQuorum(validNum.get())) { // 3.如果收到的有效投票數未過半
            parseResult = VoteResponse.ParseResult.WAIT_TO_REVOTE;
            nextTimeToRequestVote = getNextTimeToRequestVote();
        } else if (!memberState.isQuorum(validNum.get() - biggerLedgerNum.get())) {// 4.如果收到有效投票數減去比當前節點ledgerEndIndex大的節點個數未過半
            parseResult = VoteResponse.ParseResult.WAIT_TO_REVOTE;
            nextTimeToRequestVote = getNextTimeToRequestVote() + maxVoteIntervalMs;
        } else if (memberState.isQuorum(acceptedNum.get())) {
            // 5.投票數如果達到了Quorum,選舉透過
            parseResult = VoteResponse.ParseResult.PASSED; 
        } else if (memberState.isQuorum(acceptedNum.get() + notReadyTermNum.get())) {
            // 6. 如果接受投票數+未準備好的節點數過半,立刻進行下一次投票
            parseResult = VoteResponse.ParseResult.REVOTE_IMMEDIATELY;
        } else {
            // 7.其他情況,等待下一次投票
            parseResult = VoteResponse.ParseResult.WAIT_TO_VOTE_NEXT;
            nextTimeToRequestVote = getNextTimeToRequestVote();
        }
        lastParseResult = parseResult;
        logger.info("[{}] [PARSE_VOTE_RESULT] cost={} term={} memberNum={} allNum={} acceptedNum={} notReadyTermNum={} biggerLedgerNum={} alreadyHasLeader={} maxTerm={} result={}",
            memberState.getSelfId(), lastVoteCost, term, memberState.peerSize(), allNum, acceptedNum, notReadyTermNum, biggerLedgerNum, alreadyHasLeader, knownMaxTermInGroup.get(), parseResult);
        // 如果選舉透過
        if (parseResult == VoteResponse.ParseResult.PASSED) {
            logger.info("[{}] [VOTE_RESULT] has been elected to be the leader in term {}", memberState.getSelfId(), term);
            // 選舉成功,轉為Leader角色
            changeRoleToLeader(term);
        }
    }
}

成為Leadr

節點收到叢集中大多數投票後,呼叫changeRoleToLeader方法轉為Leader:

  1. 呼叫changeToLeader方法將角色更改為Leader;
  2. 呼叫handleRoleChange方法觸發角色變更事件;
public class DLedgerLeaderElector {
    public void changeRoleToLeader(long term) {
        synchronized(this.memberState) {
            // 如果Term一致
            if (this.memberState.currTerm() == term) {
                // 轉為Leader角色
                this.memberState.changeToLeader(term);
                this.lastSendHeartBeatTime = -1L;
                // 觸發角色改變事件
                this.handleRoleChange(term, Role.LEADER);
                logger.info("[{}] [ChangeRoleToLeader] from term: {} and currTerm: {}", new Object[]{this.memberState.getSelfId(), term, this.memberState.currTerm()});
            } else {
                logger.warn("[{}] skip to be the leader in term: {}, but currTerm is: {}", new Object[]{this.memberState.getSelfId(), term, this.memberState.currTerm()});
            }

        }
    }
}

成為Leader角色之後,下次執行StateMaintainer的doWork方法時,呼叫DLedgerLeaderElector.this.maintainState()後,會進入到Leader角色的處理邏輯,也就是maintainAsLeader方法中,在方法中會判斷上次傳送心跳的時間是否大於心跳傳送間隔,如果是做如下處理:

  1. 校驗是否是Leader角色,如果不是直接返回;
  2. 呼叫sendHeartbeats方法,向叢集中其他節點傳送心跳包;

也就是說,如果某個節點成為了Leader角色,會定期執行進入到maintainAsLeader方法中,如果距離上次傳送心跳的時間超過了心跳傳送間隔,向其他節點傳送心跳包保持通訊。

public class DLedgerLeaderElector {
    private void maintainAsLeader() throws Exception {
        // 如果上次傳送心跳的時間大於心跳傳送間隔
        if (DLedgerUtils.elapsed(lastSendHeartBeatTime) > heartBeatTimeIntervalMs) {
            long term;
            String leaderId;
            synchronized (memberState) {
                // 校驗是否是Leader
                if (!memberState.isLeader()) {
                    return;
                }
                term = memberState.currTerm();
                leaderId = memberState.getLeaderId();
                // 更新傳送心跳的時間
                lastSendHeartBeatTime = System.currentTimeMillis();
            }
            // 向叢集中其他節點傳送心跳包
            sendHeartbeats(term, leaderId);
        }
    }
}

計算時間差

DLedgerUtilselapsed方法用於計算時間差,使用當前時間減去引數傳入的時間,後面會看到某些情況下會將上次傳送心跳的時間置為-1,這裡相減之後,返回值會大於心跳時間間隔,所以會立刻傳送心跳包:

public class DLedgerUtils {
    public static long elapsed(long start) {
        return System.currentTimeMillis() - start;
    }
}

傳送心跳包

sendHeartbeats方法中遍歷當前節點維護的叢集中所有節點,向除自己以外的其他節點傳送心跳請求:

  1. 構建HeartBeatRequest請求;
  2. 請求中設定本次選舉的相關資訊,包括組資訊、當前節點的ID、目標節點的ID、LeaderID、當前的Term;
  3. 呼叫heartBeat傳送心跳請求;
  4. 處理心跳請求返回的響應資料,這個稍後再講;
public class DLedgerLeaderElector {
    private void sendHeartbeats(long term, String leaderId) throws Exception {
        final AtomicInteger allNum = new AtomicInteger(1);
        final AtomicInteger succNum = new AtomicInteger(1);
        final AtomicInteger notReadyNum = new AtomicInteger(0);
        final AtomicLong maxTerm = new AtomicLong(-1);
        final AtomicBoolean inconsistLeader = new AtomicBoolean(false);
        final CountDownLatch beatLatch = new CountDownLatch(1);
        long startHeartbeatTimeMs = System.currentTimeMillis();
        // 遍歷叢集中的節點
        for (String id : memberState.getPeerMap().keySet()) {
            // 如果是當前節點自己,跳過
            if (memberState.getSelfId().equals(id)) {
                continue;
            }
            // 構建心跳請求
            HeartBeatRequest heartBeatRequest = new HeartBeatRequest();
            heartBeatRequest.setGroup(memberState.getGroup()); // 設定組資訊
            heartBeatRequest.setLocalId(memberState.getSelfId());// 設定當前節點的ID
            heartBeatRequest.setRemoteId(id);// 設定目標節點的ID
            heartBeatRequest.setLeaderId(leaderId); // 設定LeaderID
            heartBeatRequest.setTerm(term); // 設定Term
            // 傳送心跳請求
            CompletableFuture<HeartBeatResponse> future = dLedgerRpcService.heartBeat(heartBeatRequest);
            // 先省略心跳響應處理
            // ...
        }
        // ...
    }
}

心跳請求處理

lastLeaderHeartBeatTime:最近一次收到Leader心跳請求的時間,用於在Follower角色下判斷心跳時間是否超時使用,如果長時間未收到心跳包,會認為Master故障,轉為Candidate角色進行競選。

叢集中其他節點收到心跳請求後,對請求的處理在handleHeartBeat方法中:

  1. 判斷髮送心跳請求的節點是否在當前節點維護的叢集中,如果不在返回狀態為UNKNOWN_MEMBER

  2. 判斷心跳請求中攜帶的LeaderID是否是當前節點,如果是,返回UNEXPECTED_MEMBER

  3. 對比請求中攜帶的Term與當前節點的Term:

    • 小於:返回EXPIRED_TERM表示請求中的Term已過期;
    • 相等:如果請求中的LeaderID與當前節點維護的LeaderID一致,表示之前已經同意節點成為Leader,更新收到心跳包的時間lastLeaderHeartBeatTime為當前時間,返回成功即可;
  4. 再次對比請求中的Term與當前節點的Term:

    (1)小於:說明請求的Term已落後,返回EXPIRED_TERM;

    (2)相等:

    • 如果當前節點記錄的LeaderId為空,呼叫changeRoleToFollower轉為Follower角色,返回成功
    • 如果請求中的LeaderId與當前節點的Leader一致,表示之前已經同意節點成為Leader,更新收到心跳包的時間lastLeaderHeartBeatTime為當前時間,返回成功;
    • 其他情況:主要是為了容錯處理,返回INCONSISTENT_LEADER

    (3)大於:說明當前節點Term比較落後,此時呼叫changeRoleToCandidate轉為Candidate角色,然後將needIncreaseTermImmediately置為true,返回TERM_NOT_READY,表示未準備好(與選舉投票時的處理邏輯一致);

public class DLedgerLeaderElector {
    public CompletableFuture<HeartBeatResponse> handleHeartBeat(HeartBeatRequest request) throws Exception {
        // 判斷髮送心跳請求的節點是否在叢集中
        if (!memberState.isPeerMember(request.getLeaderId())) {
            logger.warn("[BUG] [HandleHeartBeat] remoteId={} is an unknown member", request.getLeaderId());
            // 返回UNKNOWN_MEMBER
            return CompletableFuture.completedFuture(new HeartBeatResponse().term(memberState.currTerm()).code(DLedgerResponseCode.UNKNOWN_MEMBER.getCode()));
        }
        // 判斷心跳請求中攜帶的LeaderID是否是當前節點
        if (memberState.getSelfId().equals(request.getLeaderId())) {
            logger.warn("[BUG] [HandleHeartBeat] selfId={} but remoteId={}", memberState.getSelfId(), request.getLeaderId());
            // 返回UNEXPECTED_MEMBER
            return CompletableFuture.completedFuture(new HeartBeatResponse().term(memberState.currTerm()).code(DLedgerResponseCode.UNEXPECTED_MEMBER.getCode()));
        }
        // 對比Term
        if (request.getTerm() < memberState.currTerm()) { // 請求中的Term如果小於當前節點的Term
            // 返回EXPIRED_TERM表示請求中的Term已過期
            return CompletableFuture.completedFuture(new HeartBeatResponse().term(memberState.currTerm()).code(DLedgerResponseCode.EXPIRED_TERM.getCode()));
        } else if (request.getTerm() == memberState.currTerm()) { // 如果相等
            // 如果請求中的LeaderID與當前節點維護的LeaderID一致,表示已經同意節點成為Leader
            if (request.getLeaderId().equals(memberState.getLeaderId())) { 
                // 更新為當前時間
                lastLeaderHeartBeatTime = System.currentTimeMillis();
                // 返回成功
                return CompletableFuture.completedFuture(new HeartBeatResponse());
            }
        }
        synchronized (memberState) {
            // 請求中的Term如果小於當前節點的Term
            if (request.getTerm() < memberState.currTerm()) { 
                // 返回EXPIRED_TERM
                return CompletableFuture.completedFuture(new HeartBeatResponse().term(memberState.currTerm()).code(DLedgerResponseCode.EXPIRED_TERM.getCode()));
            } else if (request.getTerm() == memberState.currTerm()) {  // 請求中的Term於當前節點的Term相等
                if (memberState.getLeaderId() == null) { // 如果當前節點記錄的LeaderId為空
                    // 轉為Follower角色
                    changeRoleToFollower(request.getTerm(), request.getLeaderId());
                    // 返回成功
                    return CompletableFuture.completedFuture(new HeartBeatResponse());
                } else if (request.getLeaderId().equals(memberState.getLeaderId())) { // 如果請求中的LeaderId與當前節點的Leader一致
                    lastLeaderHeartBeatTime = System.currentTimeMillis();
                    // 返回成功
                    return CompletableFuture.completedFuture(new HeartBeatResponse());
                } else {
                    logger.error("[{}][BUG] currTerm {} has leader {}, but received leader {}", memberState.getSelfId(), memberState.currTerm(), memberState.getLeaderId(), request.getLeaderId());
                    // 返回INCONSISTENT_LEADER
                    return CompletableFuture.completedFuture(new HeartBeatResponse().code(DLedgerResponseCode.INCONSISTENT_LEADER.getCode()));
                }
            } else { // 如果請求中的Term大於當前節點的Term
                // 轉為Candidate
                changeRoleToCandidate(request.getTerm());
                // needIncreaseTermImmediately置為true
                needIncreaseTermImmediately = true;
                // 返回TERM_NOT_READY
                return CompletableFuture.completedFuture(new HeartBeatResponse().code(DLedgerResponseCode.TERM_NOT_READY.getCode()));
            }
        }
    }
}

心跳響應結果處理

回到sendHeartbeats方法,當請求返回響應之後,會對返回響應狀態進行判斷:

  1. SUCCESS:表示成功,記錄心跳傳送成功的節點個數,succNum加1;
  2. EXPIRED_TERM:表示當前節點的Term已過期落後於其他節點,將較大的那個Term記錄在maxTerm中;
  3. INCONSISTENT_LEADER:將inconsistLeader置為true;
  4. TERM_NOT_READY:表示有節點還未準備好,也就是Term較小,此時記錄未準備節點的數量,notReadyNum加1;

接下來根據上面的處理結果進行判斷:

  1. 如果叢集中過半節點對心跳包返回了成功的狀態,更新心跳包成功的時間lastSendHeartBeatTime的值;
  2. 如果未過半進行以下判斷:
    • 如果成功的個數+未準備好的個數過半,lastSendHeartBeatTime值置為-1,下次進入maintainAsLeader方法會認為已經超過心跳傳送時間間隔,所以會立刻傳送心跳包;
    • 如果maxTerm值大於當前節點的Term,表示當前節點Term已過期,呼叫changeRoleToCandidate轉為Candidate,並使用maxTerm做為下次選舉的Term,等待下次選舉;
    • inconsistLeader為true,呼叫changeRoleToCandidate轉為Candidate,等待下次選舉;
    • 如果上次成功傳送心跳的時間大於maxHeartBeatLeak(最大心跳時間) * heartBeatTimeIntervalMs(心跳傳送間隔),呼叫changeRoleToCandidate轉為Candidate,等待下次選舉;
public class DLedgerLeaderElector {
    private void sendHeartbeats(long term, String leaderId) throws Exception {
        final AtomicInteger allNum = new AtomicInteger(1);
        final AtomicInteger succNum = new AtomicInteger(1);
        final AtomicInteger notReadyNum = new AtomicInteger(0);
        final AtomicLong maxTerm = new AtomicLong(-1);
        final AtomicBoolean inconsistLeader = new AtomicBoolean(false);
        final CountDownLatch beatLatch = new CountDownLatch(1);
        long startHeartbeatTimeMs = System.currentTimeMillis();
        // 遍歷叢集中的節點
        for (String id : memberState.getPeerMap().keySet()) {
            // 如果是當前節點自己,跳過
            if (memberState.getSelfId().equals(id)) {
                continue;
            }
            // 構建心跳請求
            HeartBeatRequest heartBeatRequest = new HeartBeatRequest();
            heartBeatRequest.setGroup(memberState.getGroup()); // 設定組資訊
            heartBeatRequest.setLocalId(memberState.getSelfId());// 設定當前節點的ID
            heartBeatRequest.setRemoteId(id);// 設定目標節點的ID
            heartBeatRequest.setLeaderId(leaderId); // 設定LeaderID
            heartBeatRequest.setTerm(term); // 設定Term
            // 傳送心跳
            CompletableFuture<HeartBeatResponse> future = dLedgerRpcService.heartBeat(heartBeatRequest);
            future.whenComplete((HeartBeatResponse x, Throwable ex) -> {
                try {
                    if (ex != null) {
                        memberState.getPeersLiveTable().put(id, Boolean.FALSE);
                        throw ex;
                    }
                    switch (DLedgerResponseCode.valueOf(x.getCode())) {
                        case SUCCESS: // 如果成功
                            // 記錄成功的數量
                            succNum.incrementAndGet();
                            break;
                        case EXPIRED_TERM: // 如果Term過期
                            // 使用響應返回的較大的那個Term記錄在maxTerm中
                            maxTerm.set(x.getTerm());
                            break;
                        case INCONSISTENT_LEADER: // 如果是INCONSISTENT_LEADER
                            // inconsistLeader置為true
                            inconsistLeader.compareAndSet(false, true);
                            break;
                        case TERM_NOT_READY:// 如果未準備
                            // 記錄未準備節點的數量
                            notReadyNum.incrementAndGet();
                            break;
                        default:
                            break;
                    }
                   // ...
                } catch (Throwable t) {
                    logger.error("heartbeat response failed", t);
                } finally {
                    allNum.incrementAndGet();
                    if (allNum.get() == memberState.peerSize()) {
                        beatLatch.countDown();
                    }
                }
            });
        }
        beatLatch.await(heartBeatTimeIntervalMs, TimeUnit.MILLISECONDS);
        // 如果叢集中過半節點對心跳包返回了成功的狀態
        if (memberState.isQuorum(succNum.get())) {
            // 記錄心跳成功的時間
            lastSuccHeartBeatTime = System.currentTimeMillis();
        } else {
            logger.info("[{}] Parse heartbeat responses in cost={} term={} allNum={} succNum={} notReadyNum={} inconsistLeader={} maxTerm={} peerSize={} lastSuccHeartBeatTime={}",
                memberState.getSelfId(), DLedgerUtils.elapsed(startHeartbeatTimeMs), term, allNum.get(), succNum.get(), notReadyNum.get(), inconsistLeader.get(), maxTerm.get(), memberState.peerSize(), new Timestamp(lastSuccHeartBeatTime));
            if (memberState.isQuorum(succNum.get() + notReadyNum.get())) { // 如果成功的個數+未準備好的個數
                // 上次傳送心跳的時間間隔置為-1
                lastSendHeartBeatTime = -1;
            } else if (maxTerm.get() > term) { // 如果Term資訊過期
                // 轉為Candidate重新選舉,這裡傳入的是maxTerm的值
                changeRoleToCandidate(maxTerm.get()); 
            } else if (inconsistLeader.get()) { // 如果inconsistLeader為true
                 // 轉為Candidate重新選舉
                changeRoleToCandidate(term);
            } else if (DLedgerUtils.elapsed(lastSuccHeartBeatTime) > maxHeartBeatLeak * heartBeatTimeIntervalMs) {
                // 如果上次成功傳送心跳的時間大於 最大心跳時間* 心跳傳送間隔
                changeRoleToCandidate(term);
            }
        }
    }
}

Follower

當節點收到心跳包並同意發起選舉的節點成為Leader時,會轉為Follower角色,在下次執行doWork方法時會進入到maintainAsFollower的處理邏輯,會判斷上次收到心跳包的實際是否超過了兩倍的傳送心跳間隔,如果超過,判斷當前節點是否是Follower並且上次收到心跳包的時間大於最大心跳時間 * 每次傳送心跳的時間間隔,如果成立,會呼叫changeRoleToCandidate方法 轉為Candidate發起競選,也就是說如果Follower節點長時間未收到Leader節點的心跳請求,會認為Leader出現了故障,所以會轉為Candidate重新發起競選

public class DLedgerLeaderElector {
    private void maintainAsFollower() {
        // 上次收到心跳包的時間是否超過了兩倍的傳送心跳間隔
        if (DLedgerUtils.elapsed(lastLeaderHeartBeatTime) > 2 * heartBeatTimeIntervalMs) {
            synchronized (memberState) {
                // 如果是Follower並且上次收到心跳包的時間大於 最大心跳時間 * 每次傳送心跳的時間間隔
                if (memberState.isFollower() && (DLedgerUtils.elapsed(lastLeaderHeartBeatTime) > maxHeartBeatLeak * heartBeatTimeIntervalMs)) {
                    logger.info("[{}][HeartBeatTimeOut] lastLeaderHeartBeatTime: {} heartBeatTimeIntervalMs: {} lastLeader={}", memberState.getSelfId(), new Timestamp(lastLeaderHeartBeatTime), heartBeatTimeIntervalMs, memberState.getLeaderId());
                    // 轉為Candidate發起競選
                    changeRoleToCandidate(memberState.currTerm());
                }
            }
        }
    }
}

總結

參考

【中介軟體興趣圈】原始碼分析 RocketMQ DLedger 多副本之 Leader 選主

RocketMQ原始碼分析之Dledger模式

RocketMQ版本:4.9.3

相關文章