Raft Consensus Algorithm - Raft 共識演算法介紹



前言

  • 常在各家產品看到他們對於 cluster 的資訊同步作法,都有提到 Raft;一方面也聽說是因爲 Paxos 太複雜,所以有一個精簡版的做法稱作 Raft。剛好記錄他的相關概念。



Raft 是什麼

  • 一種在 Cluster 內同步資訊、凝聚共識 (Distributed Consensus) 的演算法。

目的

所謂共識 Consensus,
是要讓「外部請求達成的狀態 (State) 在 Cluster 內一致 (Consistency)」。

例如,電商後端 Cluster 保存了目前某商品存貨的狀態,
當不同來源的客戶下單後,要能持續讓 Cluster 內的存貨同步更新到正確的數值,
才不會有「客戶順利下訂,結果卻早就沒有存貨」的情況發生。

Raft 共識演算法,就是為了要為了兼顧「一致性」、「HA」 同時還要「單純易懂」
(兼顧?)


Consistency Model 一致性模型

一致性究竟是什麼?
怎樣算是夠一致?

Model 說明
Strong Consistency 保證系統都能提供最新、一致的資料
Eventual Consistency 遲早系統會提供最新的資料、但保證時序與正確性
Consistent Prefix 不一定是最新的資料,
但對於舊資料一定是一致、已 committed 的
(snapshot、isolation 的概念)
Bounded Staleness 參數化舊版本資料的容許範圍
bounded = ∞ → 退化成 Eventual Consistency
bounded = 0 → 進化成 Strong Consistency
Monotonic Reads 會讀到舊資料,但保證只會越來越新
Read My Writes Client 一定會讀到自己寫入的值、不關心系統如何彼此同步

以球賽結果舉例,假設目前進度與比分如下:

Team \ Round 1 2 3 4 5 6 7 8 9 Total
Team A 0 0 1 0 1 0 0 2
Team B 1 0 1 1 0 2 5

各個不同的「聽眾」(透過收音機) 或是「觀眾」(透過電視、網路平台),
如同 clients,不斷從這個「球賽 Cluster」獲得最新比分的狀態。

所以以各個 Model 來說:

Model 說明
Strong Consistency 所有球迷 (client) 都能得到最新比分
Eventual Consistency 遲早所有球迷都能得到最新比分,不保證時序與正確性
Consistent Prefix 得到正確比分,但時序每個球迷接收到的不一定一樣
Bounded Staleness 參數化「所有球迷所能得到比分的老舊版本」程度
Monotonic Reads 會讀到舊比分,但保證越來越新 (保證時序)
Read My Writes 計分員視角

可以大概想像一些模型的差異:

  • Monotonic Reads:就如同直播延遲的情況,比分一定正確且照順序,但時間會比較延遲。
  • Eventual Consistency: 就像是搶新聞的記者,相對獲得的資訊會快一些,但有可能須要事後修正。

Raft 的一些性質

  • 可以做到 Strong Consistency:也就是以這個演算法同步資訊的 Cluster,
    都可以快速獲得最新、最正確的狀態。

  • 狀態 (State) 統一由 Cluster 內唯一 Leader 為準,且為 Append Only,
    代表資料更新是單向的:Leader 也不能刪除、或去更新原有資料。

  • 假設 Cluster 中沒有叛徒 (Non-Byzantine system)

  • 提供一個大致的架構 (protocol),細節可以依需求調整實作方式,
    例如實作 timeout 的方式、請求與回傳的資料結構等等



Raft 的 3 個角色、2 個階段

3 個角色:LeaderFollowerCandidate

  1. Leader

    • 要求 Follower 更新資料
    • Cluster 中會只有一個 Leader
  2. Follower

    • 可以對 Candidate 投票,並同意 Candidate 的 ACK
    • Cluster 中會有多個 Follower
  3. Candidate:

    • 接收 Follower 投票、並送出 ACK

2 個階段:Election、Log Replication

  1. Election:沒有 Leader 時會進入 Election 階段、部分 Follower 會成為 Candidate
  2. Log Replication:平時溝通、平時的資料更新;不會有 Candidate



一般情況

平時彼此溝通的 Heartbeat:AppendEntries RPC

每一小段時間 (~50ms) Leader 會發送 Heartbeat 給 Follower

Heartbeat 可包含資料 (Log),要求 Follower 更新資料狀態,
此動作實際稱為 AppendEntries RPC (Remote Procedure Call)

  • 如果帶資料,就是要求 Follower 要同步 meta 狀態、以及同步資料狀態
  • 如果沒有資料就是 Heartbeat,只同步 meta 狀態

如果沒有 Leader … 進入 Election!

如果沒有 Leader,就進入 Election 階段,選出 Leader

有參選資格的稱作 Candidate
Candidate 會要求其他 Follower 投票,送出稱為 RequestVote RPC 的請求,
Follower 會回傳投票結果。


但 … 成員要怎麼知道沒有 Leader

  • Follower 超過一定時間 (timeout) 收到「來自 Leader 的 Heartbeat」,
    就認定現在 Cluster 內部失去了 Leader

沒有收到 Heartbeat 可能是因為:

  • Leader Crash
  • 網路出現問題

每個 Follower 會有隨機的 timeout 門檻,範圍大約是 150ms ~ 300ms,
超時之後稱為 electionTimeout

electionTimeout 後的 Follower 隨即成為 Candidate

所以整個 State 大概如下圖

  • 平時只有 LeaderFollower 更新訊息
  • Leader 不見時會進入 Election,此時才會有 Candidate

Note

  1. 上面的圖中有提到 new term、higher term,稍後會說明

下節詳細說明 Election



Election 階段

Follower 觸發 electionTimeout,就會轉為 Candidate
Candidate 送出稱為 RequestVote RPC 的請求,給各個 Follower 要求投票;
Candidate 獲得過半數 cluster 中的投票,就會廣播宣告自己成為 Leader

在選舉過程中,大家都會記錄一個數值 term
這是「Cluster 中已選舉的次數」,可以理解為任期編號; term 是單向遞增的數字。


Candidate 角度

當每次 electionTimeoutFollower 成為 Candidate 時,這個 term 就會 +1
目的是在 Candidate 收到來自其他 CandidateRequestVote 時,
能夠比對誰才是更新的 Candidate

可以想像一種狀況,某 Candidate A 曾經在某次 Election 時 (例如 term = 10) 壞掉了;
剩下的成員相繼無事,完成選舉選出 Leader
過一陣子剛好 Leader 失效,在進入下一輪選舉 (term = 11) 時,
A 此時才回來,這時候比對 term 就知道他是先前已過期的 Candidate
這時候 A 就會放棄 Candidate 的角色。

總而言之,對於某 Candidate 來說,
如果收到對方的 term 「大於等於」 自己的 term
自己就會放棄選舉,成為 Follower


Follower 角度

而對其他 Follower 來說,在自己的 term 時,
最多只會投出一票,給第一個向自己拉票的 Candidate

不過 Follower拒絕以下不合條件的 RequestVote

  1. 對方 term 低於自身 term
  2. 如果 term 相同但是身上的資料長度比自身短 (代表狀態比較老舊)
  3. 本輪已經同意過一次

一輪選舉結果

一個 Candidate 只會有以下三種情況:

  • 要麻獲得過半投票,成為 Leader,然後廣播通知其他 Follower
  • 要麻收到新的 Leader 廣播,自己轉為 Follower
  • 要麻未能收到任何訊息,然後再觸發一次 electionTimeout,繼續當 Candidate

所以理論上,一個失效的 cluster 是有可能無限等待:
Candidate 會一直在觸發 electionTimeout,卡在選舉循環中。


Note

  1. 關於 electionTimeout 的時間尺度,一般情況大概會是:
溝通平均傳遞時間 electionTimeout 平均故障間隔 (MTBF)
0.5ms ~ 20ms 10ms ~ 500ms Days ~ Months



平時溝通階段:Log Replication

前面提到,Leader 會發送 AppendEntries RPC 給所有的 Follower

AppendEntries 包含

  • metadata:terms, index (標示手上已 committed 資料的進度)
  • log entries:實際的資料

如果沒有 log entries (資料),還是會發送 AppendEntries 給各個 Follower
當作 Heartbeat,以讓各個 Follower 知道目前 Leader 狀態正常 (約 50ms 一次)。

如果有 log entries 的話,LeaderFollower 可能資料版本不一致,要怎麼同步呢?


Follower 角度

收到來自 LeaderAppendEntries
比對自己手上的與 Leader 傳來的 termsindex

  • 如果都一致:則直接更新 (append) 資料
  • 如果不一致:則拒絕更新,然後回傳更之前的 index
    來跟 Leader 多次比對「最近一次同步的資料版本」是在哪一個 index;
    然後從該版本之後都直接用 Leader 的資料覆蓋。

Leader 角度

Leader 發出 AppendEntries RPC,
並向 Follower 收集 ACK,確認「本次 term」 中、「本次更新」的資料,
都已被多數 (majority) Follower 確認儲存,
本次更新的資料才能算是 Committed

前面提過,一份資料包含

  • metadata (term, index …)
  • log entries

一次 term 中 (時間內都沒有發生 electionTimeout 進入選舉階段),
會有多次資料更新,各個 Follower 會用 index 變數來確認目前儲存進度。

另外,值得注意的是:

  • 完成 Commit 的資料才會回傳給 client。

也就是還在確認狀態、未被多數 Follower commit 的資料,
在外部 client 發出請求的時候,cluster 的回應不會包含這些資料。


Log Replication 時各種狀況

在同步資料的過程中,可能會遇到不同的狀況、不同的 Follower 的狀態:

  1. 方格裡面的數字是 term,代表對應的資料是在哪一輪選舉時更新的。
  2. 最上方的一排數字是 log index,代表不同時序的資料 append 的順序
  3. 左邊 (a) ~ (f) 是不同情況的 Follower,以下分別說明

(a) Follower 資料落後一版

FollowerLeader 都是在同一輪選舉中 (term = 6)。

Follower 在收到來自 LeaderAppendEntries 時,
會發現自己少了 index = 10 的資料,隨即補上 Leader 的新資料。

(b) Follower 資料嚴重落後

Follower 在先前的選舉 term = 4 時就失效了,現在才回來。

在收到來自 LeaderAppendEntries 後,
會發現自己的 log index 停留在 4
隨即補上 Leader 的新資料:index 5~10

(c) Follower 超前資料

Leader term 相同;

資料狀態必須以 Leader 為準,超出的資料就直接丟棄。

所以此 Follower 必須捨棄 index = 11 的資料。

(d) FollowertermLeader 還更新

代表現在的 Leader 是過時的 Leader
此時用有更新的 term (= 7) 的 Follower
會取而代之,直接變成新的 Leader

原有的 Leader 轉為 Follower
然後要把落後的 term = 7 的資料補足。

(e) Follower 的資料落後,而且版本不一致

Follower index = 6 & 7 的資料跟 Leader 不一致,
還停留在該 term = 4 的階段。

當接收到 LeaderAppendEntries RPC 後,
就會發現狀態在 index = 6 開始不一致。
此時就要捨棄原本 index = 6 以後的資料,
Leader 的資料對齊。

(f) Follower 身上有很多過時資料

這有可能是先前已經失效的 Leader,身上有 term = 2 & 3 的資料。

一加入這個 cluster,馬上就會發現 index4 ~ 11 都是過時的,
因為與 Leader 相比,term 完全不同。

此時就會在 AppendEntries RPC 之後把跟現在 Leader 不同的資料都捨棄,
Leader 的資料為準。

也就是 index = 4 ~ 11 的資料都替換成 Leader 身上的版本。



補充

當資料增長 … Snapshot

當資料大到一定程度時,
可用 Snapshot 將舊資料另外保存,以縮小現有的 Log Entries。


當成員需要更新 … Joint Consensus

Cluster 的成員如果需要更新,例如更新版本、或更新設定檔等,
需要使用兩階段 commit 的方法來確保系統的安全性、與資料正確性。

這兩階段可以有多種實作方式:
例如,某些系統會在第一階段禁用舊設定,停止處理 client 的請求,
然後在第二階段啟用新設定。

而在 Raft 中,系統先切換到一個稱為 Joint Consensus 的過渡性狀態,
當該所有成員的設定都換成新設定後,再恢復原本的同步流程。

這樣的做法是為了避免中斷服務

Joint Consensus 簡單來說是:
將新舊版的 Server 都納入當前的運作;並且 log entries 會同時紀錄新舊的版本狀態,
方便 cluster 辨識不同狀態的成員,以及同步資料。

在過程中,只有同時具備新舊資料狀態的成員才有可能被選為 Leader


動畫幫助理解

可以參考這兩個站,

  1. https://raft.github.io/
  2. http://thesecretlivesofdata.com/raft/

第一個可以依照自己想看的情境去測試 Raft 的運作;
第二個是一個教學流程,看完可以先有比較直觀的理解。




REF

  1. https://raft.github.io/raft.pdf
  2. https://ithelp.ithome.com.tw/articles/10217086
  3. https://raft.github.io/
  4. http://thesecretlivesofdata.com/raft/

主題 StackJimmy 設計