由於 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 的一些特性:
- 只要集群中有超過一半的節點可以正常工作,集群就可以工作
- 只要寫入成功的數據就不會再丟失
- 任意節點上保存的狀態可能會落后於集群共識但是永遠不會出現錯誤的提交。只要系統仍然在正常工作,節點上的狀態一定會在某個時間后與系統共識達成同步,即保證最終一致性
- 只要在某個節點上讀到了某個變更, 在此之后這個節點上永遠可以讀到該變更,即保證單調一致性
推薦閱讀: