1. 程式人生 > >Raft論文《 In Search of an Understandable Consensus Algorithm (Extended Version) 》研讀

Raft論文《 In Search of an Understandable Consensus Algorithm (Extended Version) 》研讀

# Raft 論文研讀 **說明**:本文為論文 **《 In Search of an Understandable Consensus Algorithm (Extended Version) 》** 的個人理解,難免有理解不到位之處,歡迎交流與指正 。 **論文地址**:[Raft Paper](https://github.com/XutongLi/Learning-Notes/blob/master/Distributed_System/Paper_Reading/Raft/raft-extended.pdf) *** ## 1. 複製狀態機 `複製狀態機 (Replicated state machine)` 方法在分散式系統中被用於解決 **容錯問題** ,這種方法中,一個叢集中各伺服器有相同狀態的副本,並且在一些伺服器宕機的情況下也可以正常執行 。 ![](https://img2020.cnblogs.com/blog/2035097/202007/2035097-20200705201849803-1347728531.png) 如上圖所示,每臺伺服器都儲存一個包含一系列命令的 **日誌** ,並且按照日誌的順序執行。每臺伺服器都順序執行相同日誌上的命令,因此它們可以保證相同的狀態 。 **一致性演算法** 的工作就是保證複製日誌的相同 。一臺伺服器上,一致性模組接收 **client** 的請求命令並將其寫入到自己的日誌中,它和其他伺服器上一致性模組通訊來保證叢集中伺服器的日誌都相同 。命令被正確地複製後,每一個伺服器的狀態機按照日誌順序執行它們,最後將輸出結果返回給 **client** 。 因此,伺服器叢集看起來就是一個高可用的狀態機,只要叢集中大多數機器可以正常執行,就可以保證可用性 。 > 關於複製狀態機的更詳細內容,可以閱讀 **[VM-FT](https://blog.csdn.net/brianleelxt/article/details/106708202)** ,不過 **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* 。 ![](https://img2020.cnblogs.com/blog/2035097/202007/2035097-20200705201904341-1973789339.png) 一次選舉開始對應這一個新的 `term (任期)` ,*term* 使用連續的整數標記 ,一個 *term* 最多有一個 *leader* 。 *Raft* 會使用一種 `心跳機制` 來觸發領導人選舉,當 *leader* 在位時,它週期性地傳送心跳包(不含 *log entry* 的 `AppendEntries RPC` 請求) 給 *follower* ,若 *follower* 在一段時間內未接收到心跳包( `選舉超時` ),則認為系統中沒有 *leader* ,此時該 *follower* 會發起選舉 。 > 當系統啟動時,所有伺服器都是 *follower* ,第一個選舉超時的發起選舉 。 ### 3.1 選舉過程 ![](https://img2020.cnblogs.com/blog/2035097/202007/2035097-20200705201911133-1975077827.png) - **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 日誌 ![](https://img2020.cnblogs.com/blog/2035097/202007/2035097-20200705201921425-1227029911.png) 每個 *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* 使用 `日誌機制` 來維護不同伺服器之間的一致性,它維護以下的特性: 1. 如果在不同的日誌中的兩個 *entry* 擁有相同的 *index* 和 *term*,那麼他們儲存了相同的命令 2. 如果在不同的日誌中的兩個 *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 衝突解決 ![](https://img2020.cnblogs.com/blog/2035097/202007/2035097-20200705201930337-1141090768.png) 上圖體現了一些 *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* 返回失敗時的情況: > > ![](https://img2020.cnblogs.com/blog/2035097/202007/2035097-20200705201940978-1246351988.png) > > 其中 *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. 日誌壓縮 日誌不斷增長帶來了兩個問題:空間佔用太大、伺服器重啟時重放日誌會花費太多時間 。 使用 `快照` 來解決這個問題,在快照系統中,整個系統狀態都以快照的形式寫入到持久化儲存中,然後到那個時間點之前的日誌全部丟棄 。 每個伺服器獨立地建立快照,快照中只包括被提交的日誌 。當日志大小達到一個固定大小時就建立一次快照 。使用 **寫時複製** 技術來提高效率 。 ![](https://img2020.cnblogs.com/blog/2035097/202007/2035097-20200705201949696-281854979.png) 上圖為快照內容示例,可見快照中包含了: - 狀態機狀態(以資料庫舉例,則快照記錄的是資料庫中的表) - 最後被包含的 *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* 。 ![](https://img2020.cnblogs.com/blog/2035097/202007/2035097-20200705201956504-815465237.png) *** ## 7. 客戶端互動 **客戶端互動過程**: - *client* 啟動時,會隨機挑選一個伺服器進行通訊 - 若該伺服器是 *follower* ,則拒絕請求並提供它最近接收到的 *leader* 的資訊給 *client* - *client* 傳送請求給 *leader* - 若 *leader* 已崩潰,*client* 請求會超時,之後再隨機挑選伺服器進行通訊 **Raft 可能接收到一條命令多次**:*client* 對於每一條命令都賦予一個唯一序列號,狀態機追蹤每條命令的序列號以及相應的響應,若序列號已被執行,則立即返回響應結果,不再重複執行。 **只讀的操作可以直接處理而無需記錄日誌** ,但是為防止髒讀( *leader* 響應客戶端時可能已被作廢 ),需要有兩個保證措施: - *leader* 在任期開始時提交一個空的 *log entry* 以確定日誌中的已有資料是已提交的( 根據領導人完全特性 ) - *leader* 在處理只讀的請求前檢查自己是否已被作廢 - 可以在處理只讀請求前傳送心跳包檢測自己是否已被作廢 - 也可以使用 *lease* 機制,在 *leader* 收到 `AppendEntries RPC` 的多數回覆後的一段時間內,它可以響應 *client* 的只讀請求 *** ## 8. 叢集成員變化 ### 8.1 共同一致 叢集的 **配置** 可以被改變,如替換宕機的機器、加入新機器或改變複製級別 。 ![](https://img2020.cnblogs.com/blog/2035097/202007/2035097-20200705202014153-296693412.png) 配置轉換期間,整個叢集會被分為兩個獨立的群體,在同一個時間點,兩個不同的 *leader* 可能在同一個 *term* 被選舉成功,一個通過舊的配置,一個通過新的配置 。 *Raft* 引入了一種過渡的配置 —— **共同一致** ,它允許伺服器在不同時間轉換配置、可以讓叢集在轉換配置過程中依然響應客戶端請求 。 共同一致是新配置和老配置的結合: - *log entry* 被複制給叢集中新、老配置的所有伺服器 - 新、舊配置的伺服器都可以成為 *leader* - 達成一致( 針對選舉和提交 )需要分別在兩種配置上獲得大多數的支援 ### 8.2 配置轉換過程 **保證配置變化安全性的關鍵** 是:防止 $C_{old}$ 和 $C_{new}$ 同時做出決定( 即同時選舉出各自的 *leader* )。 叢集配置在複製日誌中以特殊的 *log entry* 來儲存 。下文中的 **某配置做單方面決定** 指的是在該配置內部選出 *leader* 。 ![](https://img2020.cnblogs.com/blog/2035097/202007/2035097-20200705202022853-1070271502.png) - *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ㄒ)/~~ *** ## 9. Raft 壓縮總結 ![](https://img2020.cnblogs.com/blog/2035097/202007/2035097-20200705202031580-227155218.png)