Raft 論文研讀
說明:本文為論文 《 In Search of an Understandable Consensus Algorithm (Extended Version) 》 的個人理解,難免有理解不到位之處,歡迎交流與指正 。
論文地址:Raft Paper
1. 複製狀態機
複製狀態機 (Replicated state machine)
方法在分散式系統中被用於解決 容錯問題 ,這種方法中,一個叢集中各伺服器有相同狀態的副本,並且在一些伺服器當機的情況下也可以正常執行 。
如上圖所示,每臺伺服器都儲存一個包含一系列命令的 日誌 ,並且按照日誌的順序執行。每臺伺服器都順序執行相同日誌上的命令,因此它們可以保證相同的狀態 。
一致性演算法 的工作就是保證複製日誌的相同 。一臺伺服器上,一致性模組接收 client 的請求命令並將其寫入到自己的日誌中,它和其他伺服器上一致性模組通訊來保證叢集中伺服器的日誌都相同 。命令被正確地複製後,每一個伺服器的狀態機按照日誌順序執行它們,最後將輸出結果返回給 client 。
因此,伺服器叢集看起來就是一個高可用的狀態機,只要叢集中大多數機器可以正常執行,就可以保證可用性 。
關於複製狀態機的更詳細內容,可以閱讀 VM-FT ,不過 Raft 應用到的複製狀態機一般是應用級複製,不必達到像 VM-FT 那樣的機器級複製 。
可將複製狀態機應用於 MapReduce 的 master 、GFS 的 master 以及 VM-FT 的儲存伺服器 。
2. Raft 簡介
Raft 是一種為了管理複製日誌的一致性演算法 。為了提高 可理解性 :
- 將問題分解為:領導人選舉、日誌複製、安全性和角色轉變等部分
- 通過減少狀態的數量來簡化需要考慮的狀態空間
Raft 演算法在許多方面都和現有的一致性演算法很相似,但也有獨特的特性:
- 強領導性:和其他一致性演算法相比,Raft 使用一種更強的領導能力形式。比如,日誌條目只從領導者傳送給其他的伺服器。
- 領導選舉:Raft 演算法使用一個隨機計時器來選舉領導者,可有效解決選舉時候的衝突問題 。
- 成員關係調整:Raft 使用一種共同一致的方法來處理叢集成員變換的問題,在這種方法下,處於調整過程中的兩種不同的配置叢集中大多數機器會有重疊,這就使得叢集在成員變換的時候依然可以繼續工作。
一個 Raft 叢集必須包含奇數個伺服器,若包含 2f+1 臺伺服器,則可以容忍 f 臺伺服器的故障 。( 為了保留多數伺服器線上,以正常完成日誌提交和 leader 選舉 )
3. 領導人選舉
Raft 通過選舉一個 leader ,然後給予它全部的管理日誌的許可權,以此來實現一致性 。
一個伺服器處於三種狀態之一:leader
、follower
和 candidate
:
- leader:系統中只能有一個 leader ,它接收 client 的請求( 若 client 和 follower 聯絡,follower 會把請求重定向給 leader ),並將日誌複製到 follower ;leader 當機或與其他伺服器斷開連線後會選舉新的 leader 。
- follower:follower 不會傳送任何請求,只會響應來自 leader 或 candidate 的請求 。
- candidate:一個 follower 在選舉超時後就會變成 candidate ,此後它將進行選舉,獲得多於半數票數即成為 leader 。
一次選舉開始對應這一個新的 term (任期)
,term 使用連續的整數標記 ,一個 term 最多有一個 leader 。
Raft 會使用一種 心跳機制
來觸發領導人選舉,當 leader 在位時,它週期性地傳送心跳包(不含 log entry 的 AppendEntries RPC
請求) 給 follower ,若 follower 在一段時間內未接收到心跳包( 選舉超時
),則認為系統中沒有 leader ,此時該 follower 會發起選舉 。
當系統啟動時,所有伺服器都是 follower ,第一個選舉超時的發起選舉 。
3.1 選舉過程
- candidate
- 選舉超時的 follower 增加當前 term 號,轉換到 candidate 狀態
- candidate 並行地向其他伺服器傳送
RequestVote RPC
請求 - candidate 保持狀態一直到:
- 若 candidate 獲取了超過半數伺服器的選票,則成為 leader ,並開始向所有 follower 傳送心跳包
- 若 candidate 在等候投票過程中接收到來自其他伺服器的心跳包,表示已有 leader 被選舉。若心跳包 term 不小於此 candidate 當前 term,則此 candidate 承認新 leader 合法並回到 follower 狀態;否則此 candidate 拒絕此次心跳包並保持 candidate 狀態
- 若有同時有多個 candidate ,它們可能都無法獲得超過半數的投票(
split vote
問題 ),則所有 candidate 都超時,並增加 term 號,開始新一輪的選舉
- 投票方伺服器
- 每個 candidate 只為自己投票
- 每一個伺服器最多對一個 term 號投出一張票,按照先來先服務的規則
RequestVote RPC
請求中包含了 candidate 的日誌資訊,投票方伺服器會拒絕日誌沒有自己新的投票請求- 如果兩份日誌最後 entry 的 term 號不同,則 term 號大的日誌更新
- 如果兩份日誌最後 entry 的 term 號相同,則比較長的日誌更新
3.2 split vote 問題:
- 上文有提到,若同時有多個 candidate ,則它們可能都無法獲得超過半數的選票。此時它們會全部超時,並增加 term 號,開始新一輪的選舉。由於它們會同時超時,於是 split vote 問題可能會一直重複 。
- 為解決此問題,使用
隨機選舉超時時間
。這樣可以把伺服器超時時間點分散開,一個 candidate 超時後,它可以贏得選舉並在其他伺服器超時之前傳送心跳包 。即使出現了一個 split vote 情況,多個 candidate 會重置隨機選舉超時時間,可以避免下一次選舉也出現 split vote 問題 。 - 每次重置選舉計時器的時候,要選擇一個不同的新的隨機數,不能在伺服器第一次建立的時候確定一個隨機數,並在未來的選舉中重複使用該數字。否則可能有兩個伺服器選擇相同的隨機值 。
3.3 日誌條目完整性保證
( 保證之前 term 中已提交的 entry 在選舉時都出現在新的 leader 的日誌中 ):
- 因為 leader 提交一個 entry 需要此 entry 存在於大多數伺服器
- 又因為 candidate 為贏得選舉需要獲得叢集中大多數伺服器的投票
- 可知每一個已提交的 entry 肯定存在於 為 candidate 投票的伺服器 中的至少一個
- 因為投票方伺服器會拒絕日誌沒有自己新的投票請求,即新 leader 包含的日誌至少和所有投票方伺服器的日誌都一樣新
- 則新的 leader 一定包含了過去所有 term 已提交的所有 entry
3.4 時間和可用性
-
Raft 要求 安全性不能依賴時間:整個系統不能因為某些事件執行的比預期快一點或慢一點就產生錯誤的結果
-
為選舉並維持一個穩定的領導人,系統需滿足:
廣播時間(broadcastTime) << 選舉超時時間(electionTimeout) << 平均故障時間(MTBF)
- 廣播時間 指從一個伺服器並行傳送 RPC 給叢集中其他伺服器並接收響應的平均時間(0.5~20ms)
- 選舉超時時間 即上文介紹的選舉的超時時間限制(10~500ms)
- 平均故障間隔時間 指對於一臺伺服器而言,兩次故障之間的平均時間(幾個月甚至更長)
- 廣播時間遠小於選舉超時時間,是為了使 leader 能夠傳送穩定的心跳包維持管理
- 選舉超時時間遠小於平均故障時間,是為了使整個系統穩定執行( leader 崩潰後,系統不可用時間為選舉超時時間,這個時間應在合理範圍內儘量小 )
4. 日誌複製
4.1 日誌
每個 log entry 儲存一個 狀態機命令 和從 leader 收到這條命令的 term ,每一條 log entry 都有一個整數的 log index 來表明它在日誌中的位置 。
committed log entry
指可以安全應用到狀態機的命令。當由 leader 建立的 log entry 複製到大多數的伺服器上時,log entry 就會被提交 。同時,leader 日誌中之前的 log entry 也被提交 (包含其他 leader 建立的 log entry )。
日誌的作用:
- 使得 follower 以相同的順序執行與 leader 相同的命令
- 使得 leader 確認 follower 與自己的日誌是一致的
- 可以保留 leader 需要重新傳送給 follower 的命令
- 伺服器重啟後可以通過日誌中的命令重放
4.2 複製流程
-
leader 接收到來自 client 的請求
-
leader 將請求中的命令作為一個 log entry 寫入本伺服器的日誌
-
leader 並行地發起
AppendEntries RPC
請求給其他伺服器,讓它們複製這條 log entry -
當這條 log entry 被複制到叢集中的大多數伺服器( 即成功提交 ),leader 將這條 log entry 應用到狀態機( 即執行對應命令 )
-
leader 執行命令後響應 client
-
leader 會記錄最後提交的 log entry 的 index ,並在後續
AppendEntries RPC
請求( 包含心跳包 )中包含該 index ,follower 將此 index 指向的 log entry 應用到狀態機 -
若 follower 崩潰或執行緩慢或有網路丟包,leader 會不斷重複嘗試
AppendEntries RPC
,直到所有 follower 都最終儲存了所有 log entry -
若 leader 在某條 log entry 提交前崩潰,則 leader 不會對 client 響應,所以 client 會重新傳送包含此命令的請求
4.3 一致性保證
Raft 使用 日誌機制
來維護不同伺服器之間的一致性,它維護以下的特性:
- 如果在不同的日誌中的兩個 entry 擁有相同的 index 和 term,那麼他們儲存了相同的命令
- 如果在不同的日誌中的兩個 entry 擁有相同的 index 和 term,那麼他們之前的所有 log entry 也全部相同
對於 特性一:leader 最多在一個 term 裡,在指定的一個 index 位置建立 log entry ,同時 log entry 在日誌中的位置不會改變 。
對於 特性二: AppendEntries RPC
包含了 一致性檢驗
。傳送 AppendEntries RPC
時,leader 會將新的 log entry 以及之前的一條 log entry 的 index 和 term 包含進去,若 follower 在它的日誌中找不到相同的 index 和 term ,則拒絕此 RPC 請求 。所以,每當 AppendEntries RPC
返回成功時,leader 就知道 follower 的日誌與自己的相同了 。
4.4 衝突解決
上圖體現了一些 leader 和 follower 不一致的情況 。a~b 為 follower 有缺失的 log entry ,c~d 為 follower 有多出的未提交的 log entry ,e~f 為兩種問題並存的 。
Raft 演算法中,leader 處理不一致是通過:強制 follower 直接複製自己的日誌 。
- leader 對每一個 follower 都維護了一個
nextIndex
,表示下一個需要傳送給 follower 的 log entry 的 index - leader 剛上任後,會將所有 follower 的 nextIndex 初始化為自己的最後一條 log entry 的 index 加一( 上圖中的 11 )
- 如果一個
AppendEntries RPC
被 follower 拒絕後( leader 和 follower 不一致 ),leader 減小 nextIndex 值重試( prevLogIndex 和 prevLogTerm 也會對應改變 ) - 最終 nextIndex 會在某個位置使得 leader 和 follower 達成一致,此時,
AppendEntries RPC
成功,將 follower 中的衝突 log entry 刪除並加上 leader 的 log entiry
衝突解決示例:
10 11 12 13 S1 3 S2 3 3 4 S3 3 3 5
- S3 被選為 leader ,term 為 6,S3 要新增新的 entry 13 給 S1 和 S2
- S3 傳送
AppendEntries RPC
,包含 entry 13 、nextIndex[S2]=13、prevLogIndex=12 、preLogTerm=5- S2 和 S3 在 index=12 上不匹配,S2 返回 false
- S3 減小 nextIndex[S2] 到 12
- S3 重新傳送
AppendEntries RPC
,包含 entry 12+13 、nextIndex[S2]=12、preLogIndex=11、preLogTerm=3- S2 和 S3 在 index=11 上匹配,S2 刪除 entry 12 ,增加 leader 的 entry 12 和 entry 13
- S1 流程同理
一種 優化方法:
當 AppendEntries RPC
被拒絕時返回衝突 log entry 的 term 和 屬於該 term 的 log entry 的最早 index 。leader 重新傳送請求時減小 nextIndex 越過那個 term 衝突的所有 log entry 。這樣就變成了每個 term 需要一次 RPC 請求,而非每個 log entry 一次 。
減小 nextIndex 跨過 term 的不同情況:
follower 返回失敗時包含:
- XTerm:衝突的 log entry 所在的 term
- XIndex:衝突 term 的第一個 log entry 的 index
- XLen:follower 日誌的長度
follower 返回失敗時的情況:
其中 S2 是 leader ,它向 S1 傳送新的 entry ,
AppendEntries RPC
中 prevLogTerm=6對於 Case 1:nextIndex = XIndex
對於 Case 2:nextIndex = leader 在 XTerm 的最後一個 index
對於 Case 3:nextIndex = XLen
( 論文中對此處未作詳述,只要滿足要求的設計均可 )
5. follower 和 candidate 崩潰
如果 follower 和 candidate 崩潰了,那麼後續傳送給它們的 RPC 就會失敗 。
Raft 處理這種失敗就是無限地重試;如果機器重啟了,那麼這些 RPC 就會成功 。
如果一個伺服器完成一個 RPC 請求,在響應前崩潰,則它重啟後會收到同樣的請求 。這種重試不會產生什麼問題,因為 Raft 的 RPC 都具有冪等性 。( 如:一個 follower 如果收到附加日誌請求但是它已經包含了這一日誌,那麼它就會直接忽略這個新的請求 )
6. 日誌壓縮
日誌不斷增長帶來了兩個問題:空間佔用太大、伺服器重啟時重放日誌會花費太多時間 。
使用 快照
來解決這個問題,在快照系統中,整個系統狀態都以快照的形式寫入到持久化儲存中,然後到那個時間點之前的日誌全部丟棄 。
每個伺服器獨立地建立快照,快照中只包括被提交的日誌 。當日志大小達到一個固定大小時就建立一次快照 。使用 寫時複製 技術來提高效率 。
上圖為快照內容示例,可見快照中包含了:
- 狀態機狀態(以資料庫舉例,則快照記錄的是資料庫中的表)
- 最後被包含的 index :被快照取代的最後一個 log entry 的 index
- 最後被包含的 term:被快照取代的最後一個 log entry 的 term
儲存 last included index 和 last included term 是為了支援快照後複製第一個 log entry 的 AppendEntries RPC
的一致性檢查 。
為支援叢集成員更新,快照也將最後一次配置作為最後一個條目存下來 。
通常由每個伺服器獨立地建立快照,但 leader 會偶爾傳送快照給一些落後的 follower 。這通常發生在當 leader 已經丟棄了下一條需要傳送給 follower 的 log entry 時 。
leader 使用 InstallSnapShot RPC
來傳送快照給落後的 follower 。
7. 客戶端互動
客戶端互動過程:
-
client 啟動時,會隨機挑選一個伺服器進行通訊
-
若該伺服器是 follower ,則拒絕請求並提供它最近接收到的 leader 的資訊給 client
-
client 傳送請求給 leader
-
若 leader 已崩潰,client 請求會超時,之後再隨機挑選伺服器進行通訊
Raft 可能接收到一條命令多次:client 對於每一條命令都賦予一個唯一序列號,狀態機追蹤每條命令的序列號以及相應的響應,若序列號已被執行,則立即返回響應結果,不再重複執行。
只讀的操作可以直接處理而無需記錄日誌 ,但是為防止髒讀( leader 響應客戶端時可能已被作廢 ),需要有兩個保證措施:
- leader 在任期開始時提交一個空的 log entry 以確定日誌中的已有資料是已提交的( 根據領導人完全特性 )
- leader 在處理只讀的請求前檢查自己是否已被作廢
- 可以在處理只讀請求前傳送心跳包檢測自己是否已被作廢
- 也可以使用 lease 機制,在 leader 收到
AppendEntries RPC
的多數回覆後的一段時間內,它可以響應 client 的只讀請求
8. 叢集成員變化
8.1 共同一致
叢集的 配置 可以被改變,如替換當機的機器、加入新機器或改變複製級別 。
配置轉換期間,整個叢集會被分為兩個獨立的群體,在同一個時間點,兩個不同的 leader 可能在同一個 term 被選舉成功,一個通過舊的配置,一個通過新的配置 。
Raft 引入了一種過渡的配置 —— 共同一致 ,它允許伺服器在不同時間轉換配置、可以讓叢集在轉換配置過程中依然響應客戶端請求 。
共同一致是新配置和老配置的結合:
- log entry 被複制給叢集中新、老配置的所有伺服器
- 新、舊配置的伺服器都可以成為 leader
- 達成一致( 針對選舉和提交 )需要分別在兩種配置上獲得大多數的支援
8.2 配置轉換過程
保證配置變化安全性的關鍵 是:防止 \(C_{old}\) 和 \(C_{new}\) 同時做出決定( 即同時選舉出各自的 leader )。
叢集配置在複製日誌中以特殊的 log entry 來儲存 。下文中的 某配置做單方面決定 指的是在該配置內部選出 leader 。
- leader 接收到改變配置從 \(C_{old}\) 到 \(C_{new}\) 的請求,初始只允許 \(C_{old}\) 做單方面決定
- leader 將 \(C_{o,n}\) log entry 寫入日誌當中,並向其他伺服器中複製這個 log entry ( 伺服器總是使用最新的配置,無論對應 entry 是否已提交 )
- \(C_{o,n}\) log entry 提交以前,若 leader 崩潰重新選舉,新 leader 可能是 \(C_{old}\) 也可能是 \(C_{o,n}\)(取決於新 leader 是否含有此 $C_{o,n} $ log entry ) 。這一階段, \(C_{new}\) 不被允許做單方面的決定。
- \(C_{o,n}\) log entry 提交之後( \(C_{old}\) 和 \(C_{new}\) 中大多數都有該 entry ),若重新選舉,只有 \(C_{o,n}\) 的伺服器可被選為 leader 。——這一階段,因為 \(C_{old}\) 和 \(C_{new}\) 各自的大多數都處於 \(C_{o,n}\),所以 \(C_{old}\) 和 \(C_{new}\) 都無法做單方面的決定 。
- 此時,leader 可以開始在日誌中寫入 \(C_{new}\) log entry 並將其複製到 \(C_{new}\) 的伺服器。
- \(C_{new}\) log entry 提交之前,若重新選舉,新 leader 可能是 \(C_{new}\) 也可能是 \(C_{o,n}\) 。——這一階段,\(C_{new}\) 可以做出單方面決定 。
- \(C_{new}\) log entry 提交之後,配置改變完成。——這一階段,\(C_{new}\) 可以做出單方面決定 。
可以看到,整個過程中,\(C_{old}\) 和 \(C_{new}\) 都不會在同時做出決定(即在同時選出各自的 leader ),因此配置轉換過程是安全的 。
示例:
若 \(C_{old}\) 為 S1 、S2 、S3 ,\(C_{new}\) 為 S4 、S5 、S6
- 開始只允許 \(C_{old}\) 中有 leader
- S1 、S2 、S4 、S5 被寫入 \(C_{o,n}\) log entry 後, \(C_{o,n}\) log entry 變為已提交,此時 \(C_{old}\) 和 \(C_{new}\) 中的大多數都已是 \(C_{o,n}\) ,它們無法各自選出 leader
- 此後 leader 將 \(C_{new}\) log entry 開始複製到 S4 、S5 、S6
- \(C_{new}\) log entry 提交後,S1 、S2 、S3 退出叢集,若 leader 是三者之一,則退位,在 S4 、S5 、S6 中重新選舉
- 配置轉換完成
事實上,實際 Raft 的配置轉換實現一般都採用
Single Cluser MemberShip Change
機制,這種機制在 Diego 的 博士論文 中有介紹 。關於論文中敘述的使用共同一致的成員變更方法,Diego 建議僅在學術中討論它,實際實現使用Single Cluster MemberShip Change
更合適。至於該機制的內容,之後抽空看了再補充吧 /(ㄒoㄒ)/~~