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();
}
}
}
StateMaintainer
是DLedgerLeaderElector
的內部類,繼承了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
方法中,可以看到對節點的角色進行了判斷:
- 如果當前節點是Leader,呼叫
maintainAsLeader
方法處理; - 如果當前節點是Follower,呼叫
maintainAsFollower
方法處理; - 如果當前節點是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,發起選舉的過程如下:
- 判斷當時時間是否小於下一次投票開始時間並且
needIncreaseTermImmediately
為false,如果未到開始時間並且needIncreaseTermImmediately
為false,直接返回,等待下一次投票; - 校驗當前角色是否是
Candidate
,如果不是直接返回; - 由於Leader選舉可能會失敗,所以先判斷上一次選舉的結果,對以下兩個條件進行判斷,滿足兩個條件之一,會增加Terem的值,為新一輪競選做準備,反之使用當前的Term即可:
(1)如果是WAIT_TO_VOTE_NEXT
狀態,也就是等待下一次重新進行選舉;
(2)needIncreaseTermImmediately
為true; - 如果
needIncreaseTermImmediately
為true,需要重置其狀態為false,並呼叫getNextTimeToRequestVote
更新下一次發起選舉的時間; - 呼叫
voteForQuorumResponses
方法向其他節點發起投票請求; - 處理投票結果,這裡先省略,稍後再講;
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
方法中,對當前節點維護的叢集中所有節點進行了遍歷,向每一個節點傳送投票請求:
- 構建VoteRequest投票請求;
- 設定組資訊、LedgerEndIndex、LedgerEndTerm等資訊;
- 設定Leader節點ID,也就是當前發起投票請求的節點的ID;
- 設定本次選舉的Term資訊;
- 設定請求目標節點的ID;
- 如果是當前節點自己,直接呼叫
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
方法中:
- 判斷髮起投票的節點是否在當前節點的叢集中,如果不在叢集中,拒絕投票,返回狀態為
REJECT_UNKNOWN_LEADER
; - 如果不是當前節點發起的請求,但是請求中攜帶的LeaderID與當前節點ID一致,拒絕投票,返回狀態為
REJECT_UNEXPECTED_LEADER
; - 對比請求中的攜帶的
LedgerEndTerm
與當前節點記錄的LedgerEndTerm
:
- 小於:說明請求的LedgerEndTerm比較落後,拒絕投票
REJECT_EXPIRED_LEDGER_TERM
; - 相等,但是LedgerEndIndex小於當前節點維護的LedgerEndIndex:說明發起請求的節點日誌比較落後,拒絕投票,返回
REJECT_SMALL_LEDGER_END_INDEX
; - 其他情況:繼續下一步;
-
對比請求中的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一致時才可以投票; - 小於:說明請求中的Term比較落後,拒絕投票返回狀態為
-
如果請求中的TERM小於當前節點的LedgerEndTerm,拒絕投票,返回
REJECT_TERM_SMALL_THAN_LEDGER
; -
投票給發起請求的節點,設定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
方法,繼續看處理投票結果的部分,給叢集中每個節點發起投票請求之後,會等待每個請求返回響應,並進行處理:
ACCEPT
:表示同意投票給當前節點,接受投票的節點數量acceptedNum
加1;REJECT_ALREADY_VOTED
或者REJECT_TAKING_LEADERSHIP
:表示拒絕投票給當前節點;REJECT_ALREADY_HAS_LEADER
:表示已經投票給了其他節點,alreadyHasLeader
設定為true;REJECT_EXPIRED_VOTE_TERM
:表示反映響應的節點的Term比當前節點的大,此時判斷返回的Term是否大於當前節點記錄的最大Term的值,如果是對knownMaxTermInGroup
進行更新;REJECT_SMALL_LEDGER_END_INDEX
:表示返回響應節點的LedgerEndIndex
比當前節點的大,biggerLedgerNum加1;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 + 一個隨機數)毫秒之後,接下來判斷本次選舉是否成功:
- 如果其他節點返回的響應中有比當前節點的Term大的,說明本次選舉的Term已經落後其他節點,使用請求中返回的較大的那個Term作為下次競選的Term,並計算下次發起選舉的時間,等待下一次進行選舉,對應狀態為
WAIT_TO_VOTE_NEXT
; - 如果有節點已經投票給了其他節點,說明有其他節點在競爭Leader,需要等待下一次進行選舉,對應狀態為
WAIT_TO_REVOTE
; - 如果收到的有效投票數未過半,等待下一次進行選舉,對應狀態為
WAIT_TO_REVOTE
; - 如果收到有效投票數減去比當前節點ledgerEndIndex大的節點個數未過半,如果走到這一個條件中,說明收到有效投票個數是過半的(如果未過半會先進入條件3),但是其中有些節點的ledgerEndIndex大於當前節點,將這部分減去之後還未過半,意味著本次選舉沒獲得過半的有效投票數不能成功選舉,等待下一次進行選舉,對應狀態為
WAIT_TO_REVOTE
; - 如果收到的投票數過半也就是達到了Quorum,本輪選舉成功,對應狀態
PASS
; - 如果接受投票的節點個數+未準備好的節點數過半,表示有部分節點Term落後,與當前選舉的Term不一致,立刻進行下一次投票,對應狀態為
REVOTE_IMMEDIATELY
; - 其他情況,等待下一次投票,對應狀態為
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:
- 呼叫
changeToLeader
方法將角色更改為Leader; - 呼叫
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
方法中,在方法中會判斷上次傳送心跳的時間是否大於心跳傳送間隔,如果是做如下處理:
- 校驗是否是Leader角色,如果不是直接返回;
- 呼叫
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);
}
}
}
計算時間差
DLedgerUtils
的elapsed
方法用於計算時間差,使用當前時間減去引數傳入的時間,後面會看到某些情況下會將上次傳送心跳的時間置為-1,這裡相減之後,返回值會大於心跳時間間隔,所以會立刻傳送心跳包:
public class DLedgerUtils {
public static long elapsed(long start) {
return System.currentTimeMillis() - start;
}
}
傳送心跳包
在sendHeartbeats
方法中遍歷當前節點維護的叢集中所有節點,向除自己以外的其他節點傳送心跳請求:
- 構建
HeartBeatRequest
請求; - 請求中設定本次選舉的相關資訊,包括組資訊、當前節點的ID、目標節點的ID、LeaderID、當前的Term;
- 呼叫
heartBeat
傳送心跳請求; - 處理心跳請求返回的響應資料,這個稍後再講;
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
方法中:
-
判斷髮送心跳請求的節點是否在當前節點維護的叢集中,如果不在返回狀態為
UNKNOWN_MEMBER
; -
判斷心跳請求中攜帶的LeaderID是否是當前節點,如果是,返回
UNEXPECTED_MEMBER
; -
對比請求中攜帶的Term與當前節點的Term:
- 小於:返回
EXPIRED_TERM
表示請求中的Term已過期; - 相等:如果請求中的LeaderID與當前節點維護的LeaderID一致,表示之前已經同意節點成為Leader,更新收到心跳包的時間
lastLeaderHeartBeatTime
為當前時間,返回成功即可;
- 小於:返回
-
再次對比請求中的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方法,當請求返回響應之後,會對返回響應狀態進行判斷:
SUCCESS
:表示成功,記錄心跳傳送成功的節點個數,succNum加1;EXPIRED_TERM
:表示當前節點的Term已過期落後於其他節點,將較大的那個Term記錄在maxTerm中;INCONSISTENT_LEADER
:將inconsistLeader置為true;TERM_NOT_READY
:表示有節點還未準備好,也就是Term較小,此時記錄未準備節點的數量,notReadyNum加1;
接下來根據上面的處理結果進行判斷:
- 如果叢集中過半節點對心跳包返回了成功的狀態,更新心跳包成功的時間lastSendHeartBeatTime的值;
- 如果未過半進行以下判斷:
- 如果成功的個數+未準備好的個數過半,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版本:4.9.3