博客 / 詳情

返回

基於Raft算法的DLedger-Library分析 | 京東物流技術團隊

1 背景

在分佈式系統應用中,高可用、一致性是經常面臨的問題,針對不同的應用場景,我們會選擇不同的架構方式,比如master-slave、基於ZooKeeper選主。隨着時間的推移,出現了基於Raft算法自動選主的方式,Raft是在Paxos的基礎上,做了一些簡化和限制,比如增加了日誌必須是連續的,只支持領導者、跟隨者和候選人三種狀態,在理解和算法實現上都相對容易許多。

1)DLedger 是openMessaging發佈的一個基於 Raft 實現的JAVA類庫,可以方便引用到系統中,滿足其高可用、高可靠、強一致的需求,其中在RocketMQ中作為消息Broker存儲高可用實現的一種解決方案。

2)Raft將系統中的角色分為領導者(Leader)、跟從者(Follower)和候選人(Candidate):

  • Leader:接受客户端請求,定時發送心跳包,並向Follower同步請求日誌,當日志同步到大多數節點上後告訴Follower提交日誌。
  • Follower:接受並持久化Leader同步的日誌,在Leader告之日誌可以提交之後,提交日誌。
  • Candidate:Leader選舉過程中的臨時角色,該狀態下的節點會發起投票,嘗試選擇自己為主節點,選舉成功後,不會存在該狀態下的節點

2 DLedger架構設計

DLedger 的實現大體可以分為以下兩個部分:

  • 選舉 Leader
  • 日誌複製
  • 其整體架構如下圖

注:圖引用官網

從上面的架構圖中,有兩個核心類:DLedgerLeaderElector 和 DLedgerStore,選舉和文件存儲。選出 leader 後,再由 leader 去接收數據的寫入,同時同步到其他的 follower,這樣就完成了整個 Raft 的寫入過程

3 DLedger選主源碼分析

3.1 下載源碼

從gitGub下載代碼(https://github.com/openmessaging/dledger ),idea引入後,我們發現整個代碼量很小,在分析代碼時比較容易.

3.2 選主流程分析

3.2.1 原理

raft的選主過程實際是一個狀態機的流轉,在集羣啓動時每個節點的等待超時時間時隨機的,在第一個節點超時時間到來,則主動向其他節點發起投票,在收到半數以上的投票後晉升為leader(投票過程是個循環的過程),同時發送心跳請求,其他候選節點收到主節點的請求後,改變自己為follower節點。

  • term: 任期,每一輪投票都是一個任期,默認從0開始
  • Quorum機制:簡單説就是少説半數以上,比如3個節點,2個同意即可
  • 超時時間: 在選舉時,每個節點的超時時間在一定範圍內是隨機的,這樣可以保證能夠順利選舉
3.2.2 代碼分析

整個狀態機的驅動,由線程每個10ms反覆執行DLedgerLeaderElector.maintainState()方法。下面重點來分析其狀態的驅動:

進入到核心方法maintainAsCandidate() :

1.step1 初始化

  • term : 投票輪次。
  • ledgerEndTermLeader: 節點當前的投票輪次。
  • ledgerEndIndex: 當前日誌的最大序列,即下一條日誌的開始 index
  • nextTimeToRequestVote: 下次發起投票的時間(隨機的)
  • needIncreaseTermImmediately:是否立即投票,在後面中會説明

在DLedger中每個節點的初始狀態WAIT\_TO\_REVOTE,所以第一輪只是做了初始化。其中只有 memberState.nextTerm()這個代碼會更改投票輪次

2.step2 投票

進入到核心方法handleVote(),這個方法主要是判斷其他節點請求來後,根據自己的term和請求者的等判斷是否投贊成票

  • ledgerEndIndex因為在日誌複製過程中,每個節點的進度有可能是不一樣的,所以在新的一輪選舉時,這時不能投贊成票的
  • 被選舉者 term 小於 選舉者的term,返回拒絕
  • 被選舉者 term 大於 選舉者的term,則選舉者進行下面操作:

    • 變成candidate(或者保持candidate)
    • 把needIncreaseImmediately設置為true。
    • 返回 REJECT\_TERM\_NOT_READY,這個在後面提到。

這裏補充説明:

選舉者 的下一次狀態循環會進入到maintainAsCandidate()函數,然後因為needIncreaseImmediately為true,所以把term更新,同時重置計時器。 但是並沒有立刻發出投票(此時選舉者 的CurrVoteFor還是null,使得接下來給之前的voting candidate 投贊成票可能)

獲取所有node投票結果後開始計算票數:

3.step3 仲裁

在收到所有節點的投票結果計數後,進行仲裁,這裏主要説明下圖中這個條件

  • acceptNum:同意的數量
  • notReadyTermNum:未準備好的數量(即結果為REJECT\_TERM\_NOT_READY)

這裏沒有重置nextTimeToRequestVote的時間,即刻再發起一次投票。結合上面的説明,這樣保證了被選者能儘快去拿到這些notRead的節點的贊成票。

最終經過多次投票後,當一個node節點獲取到半數以上投票後,更新自己未leader角色,同時向其他node節點發送heartBeat,其他節點在收到心跳信息後,將自己從candidate 變為follower。

3.3 單元測試驗證

3.3.1 編寫單元測試

3.3.2 日誌分析

3.4 應用場景

  1. DLedger 作為 RocketMQ ( version>=4.5.0)的消息存儲已經發布
  2. 基於DLedger 實現多節點的緩存同步更新
  3. 基於日誌複製的副本容錯處理

4 總結

  1. 這裏只簡單分析了選主過程,在閲讀源碼過程中會涉及很多java的基礎及netty的使用,比如AQS、CompletableFuture等,有助於提高我們的編碼能力。
  2. DLedger在初始化時是將節點角色設置為candidate而不是follower 這個和原Raft是不同的地方,在節點角色轉換過程中也稍有差別。

參考文獻

  • https://github.com/openmessaging/dledger/wiki
  • https://www.usenix.org/system/files/conference/atc14/atc14-pa...

作者:京東物流 郭慶海

來源:京東雲開發者社區 自猿其説Tech 轉載請註明來源

user avatar hachimei 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.