由於 Paxos 演算法過於晦澀難懂且難以實現,Diego Ongaro 提出了一種更易於理解和實現並能等價於 Paxos 演算法的共識演算法 - Raft 演算法。
因為 Raft 演算法清晰易懂越來越多的開源專案嘗試引入 Raft 演算法來解決分散式一致性問題。在分散式儲存領域基於 Raft 演算法構建的專案百花齊放,欣欣向榮。
介紹 Raft 演算法的文章早已是汗牛充棟,本文先介紹兩個非常優秀的網站:
The Secret Lives of Data-CN 以圖文方式介紹 Raft 演算法,是非常好的入門材料。將其閱讀完後您大概率已經瞭解了 Raft 演算法,如果您仍有疑問可以回來繼續閱讀本文。
既然您已經回來繼續閱讀,相信您已經瞭解 Raft 演算法中的Leader 選舉、日誌複製等基本概念, 但仍有部分疑惑。沒關係, 接下來我們會解決這些問題。
Raft Scope 是 Raft 官方提供的互動式演示程式,它展示了 Raft 叢集的工作狀態。您可以用它模擬節點當機、心跳超時等各種情況。有了 Raft Scope 我們可以親自“動手” 觀察 Raft 叢集是如何工作、如何處理各種故障的。
遺憾的是這個程式幾乎沒有任何說明非常難以上手。本文接下來將先介紹如何使用 Raft Scope 然後用它模擬幾種 Raft 叢集工作中會遭遇的典型狀況。
Raft Scope 說明
可以看到 Raft Scope 介面由三部分組成。
最下方有兩個滑塊:上面的是進度條您可以拖動它回看剛剛發生過事件,下面的是變速器滑塊越靠左系統執行越慢。
左上角部分是一個由 5 個節點組成的 Raft 叢集,每個圓圈代表叢集中的一個節點。點選節點可以看到它的狀態。對話方塊的右下角有一些按鈕,我們可以點選按鈕模擬各種狀況。我們直接右鍵點選節點也可以看到這些按鈕
這些按鈕的功能是:
- stop: 節點停機
- resume: 啟動停機的節點
- restart: 將節點立即重啟
- time out: 模擬心跳超時,點選按鈕後相應節點會認為 Leader 發生了心跳超時。
- request: 向叢集提交新的資料
節點中間的數字是節點當前的任期號(Term), 節點的顏色似乎同樣是用來表示任期的。
節點可能處於 Follower、Candidate 或者 Leader 狀態。
S2 處於 Candidate 狀態,實心原點表示它現在收到的投票。圖中的兩個原點表示收到了 S2 和 S4 的投票,這 5 個小圓點和叢集中節點的位置是對應的,左下角的小圓點表示 S4, 最上面的小圓點表示 S1。在叢集選舉過程中節點外的動態邊框表示 Election Timeout。
黑色實心邊框表示 S5 是 Leader。Follower 外面的邊框表示 HeartBeat 超時倒數計時。
右上角的表格表示各節點的日誌,每行表示一個節點。
表格最上面的數字是日誌的序號(Log Index)。Log Index 是一個自增且連續的 ID,它可以作為一條日誌唯一標識。節點中最大的 Log Index 也反映了這個節點的狀態機是否與叢集一致。
表格裡的單元格表示日誌項(Entry),其中的數字表示提交日誌的任期(Term)。虛線框表示日誌尚未提交,實線框表示日誌已經提交。
我們可以點選 leader 節點的 request 按鈕來檢視向 Raft 叢集提交資料的過程。
Leader 選舉
Raft Scope 啟動後會立即進行第一次 Leader 選舉,在叢集執行過程任何一個 Follower 出現心跳超時都會引發新一輪選舉。
我們可以點選任意一個 Follower 的 time out 按鈕模擬心跳超時,隨後此 Follower 會發起新一輪選舉。
或者我們可以點選 Leader 的 stop、restart 來模擬 Leader 當機或者重啟,並觀察隨後的叢集選舉過程。
比較奇怪的是, Raft Scope 中的 Leader 節點也可以通過點選 time out 來模擬心跳超時,在實際的 Raft 叢集中 Leader 節點通常不會對自己進行心跳檢測。
Leader 選舉的更多介紹可以檢視:Leader選舉。不過 The Secret Lives of Data 有兩處說的可能不太清楚:
這裡的選舉超時是指新一輪選舉開始時,每個節點隨機思考要不要競選 Leader 的時間,這個時間一般100-到200ms,非常短。
Candidate 發起選舉時會將自身任期(Term)+1並向其它所有節點發出 RequestVote 訊息,這條訊息中包含新任期和 Candidate 節點的最新 Log Index
收到 RequestVote 的節點會進行判斷:
def onRequestVote(self, request_vote)
if request_vote.term <= self.term:
# 若 RequestVote 中的任期小於或等於(<=)當前任期
# 則繼續 Follow 當前 Leader 並拒絕給 RequestVote投票
return False
if request_vote.log_index < self.log_index:
# 若 request_vote 傳送者的 log_index 不如自己新,節點也會拒絕給傳送者投票
# 這種機制確保了已經提交到叢集中的日誌不會丟失,即保證 Raft 演算法的安全性
return False
if self.voted_for is None:
# 若在本 term 中當前節點還未投票,則給 request_vote 的傳送者投票
self.voted_for = request_vote.sender
return True
else:
return False
Follower 超時
現在我們研究一下 The Secret Lives of Data 沒有詳細說明的 Follower 超時處理過程。
我們可以點選任意一個 Follower 的 time out 按鈕模擬心跳超時,隨後此 Follower 會發起新一輪選舉。
根據上文中的 onRequestVote 邏輯,超時的 Follower 的 Log Index 是否與叢集中的大多數節點相同決定了這次選舉的不同結果。
首先來看超時 Follower 的 Log Index 與叢集中大多數相同的情況:
現在我們點選 S5 的 time out 按鈕,隨後我們看到 S5 發起了一輪投票。因為 5 個節點的 Log Index 是一致的, 所以包含原 Leader 在內的大多數節點都投票給了 S5。
現在 S5 成為了新一任 Leader.
接下來我們看另外一種情況。S5 由於網路問題沒有收到帶有 Log Entry 1 的心跳包並導致心跳超時,S5 隨後會發起一次投票:
由於 S5 的 Log Index 比較小其它節點拒絕投票給他,叢集 Leader 和任期不變:
日誌複製
日誌複製的介紹您可以檢視:日誌複製
現在我們進一步探究日誌複製的過程:
- 客戶端將更改提交給 Leader, Leader 會在自己的日誌中寫入一條未提交的記錄(Entry)
- 在下一次心跳時 Leader 會將更改傳送給所有 Follower
- 一旦收到過半節點的確認 Leader 就會提交自己日誌中的記錄4
- 並向客戶端返回寫入成功
- Leader 會在下一次心跳時通知所有節點提交日誌
這裡比較複雜的情況是在第 4 步完成之後 Leader 崩潰。由於此時客戶端已經收到了寫入成功的回覆,所以在選出新的 Leader 之後要繼續完成提交。
在 Leader 提交了自己的日誌後我們立即關掉 Leader:
隨後叢集發起了一次選舉,S3 成為新任 Leader:
可能是因為 Raft Scope 存在 Bug, S3 本應該當選後立即完成提交工作。但是實際上需要我們再一次 Request 之後,日誌1 和日誌 2 才會被一起提交。
腦裂問題
在 Leader 崩潰時可能會有多個節點近乎同時發現心跳超時並轉變為 Candidate 開始選舉:
其它節點投票情況多種多樣,但只要保證獲只有得到過半投票的候選人才能成為 Leader。那麼選舉結果只有兩種可能:
- 有且只有一個候選人獲得過半投票成為 Leader 並開始新的任期
- 沒有一個候選人獲得過半投票,沒有選出 Leader 進入下一輪投票
絕對不會選出多個 Leader
網路分割槽問題
Raft 甚至可以在網路分割槽的情況下正常工作:
在發生網路分割槽後可能存在 3 種情況:
-
任意分割槽中的節點數都不超過一半:這種情況只有叢集被分成 3 個或更多分割槽時才會出現,十分罕見。因為 Leader 選舉和 Commit Log 都需要超過一半節點確認才可以進行,在這種情況下 Raft 叢集不能正常工作。
-
leader 所在的分割槽有超過一半的節點:這種情況視作其它分割槽中的 Follower 當機,系統仍然可以繼續工作。在分割槽修復後,Follower 節點會重新與 Leader 同步。
-
leader 所在分割槽中節點數不超過一半,但存在節點數超過一半的分割槽。這種情況最為複雜:
C、D、E 所在的分割槽節點數超過一半且與原來的 Leader 無法通訊,隨後 C、D、E 在心跳超時後會發起新一輪投票選出新的 Leader 並恢復工作。
原領導者 Node B 仍然會認為自己是叢集的 Leader,但是由於只能與兩個節點通訊(包括自己)無法得到過半節點同意,所以無法完成日誌提交。
在分割槽修復後 Node B 會收到 Node C 的心跳並發現對方的任期(Term)比自己高,Node B 會放棄 Leader 身份轉為 Node C 的 Follower 與它保持同步。
總結
經過本文探討我們可以總結一下 Raft 的一些特性:
- 只要叢集中有超過一半的節點可以正常工作,叢集就可以工作
- 只要寫入成功的資料就不會再丟失
- 任意節點上儲存的狀態可能會落後於叢集共識但是永遠不會出現錯誤的提交。只要系統仍然在正常工作,節點上的狀態一定會在某個時間後與系統共識達成同步,即保證最終一致性
- 只要在某個節點上讀到了某個變更, 在此之後這個節點上永遠可以讀到該變更,即保證單調一致性
推薦閱讀: