說明:本文爲論文 《 In Search of an Understandable Consensus Algorithm (Extended Version) 》 的我的理解,不免有理解不到位之處,歡迎交流與指正 。git
論文地址:Raft Papergithub
複製狀態機 (Replicated state machine)
方法在分佈式系統中被用於解決 容錯問題 ,這種方法中,一個集羣中各服務器有相同狀態的副本,而且在一些服務器宕機的狀況下也能夠正常運行 。算法
如上圖所示,每臺服務器都存儲一個包含一系列命令的 日誌 ,而且按照日誌的順序執行。每臺服務器都順序執行相同日誌上的命令,所以它們能夠保證相同的狀態 。數據庫
一致性算法 的工做就是保證複製日誌的相同 。一臺服務器上,一致性模塊接收 client 的請求命令並將其寫入到本身的日誌中,它和其餘服務器上一致性模塊通訊來保證集羣中服務器的日誌都相同 。命令被正確地複製後,每個服務器的狀態機按照日誌順序執行它們,最後將輸出結果返回給 client 。安全
所以,服務器集羣看起來就是一個高可用的狀態機,只要集羣中大多數機器能夠正常運行,就能夠保證可用性 。服務器
關於複製狀態機的更詳細內容,能夠閱讀 VM-FT ,不過 Raft 應用到的複製狀態機通常是應用級複製,沒必要達到像 VM-FT 那樣的機器級複製 。網絡
可將複製狀態機應用於 MapReduce 的 master 、GFS 的 master 以及 VM-FT 的存儲服務器 。分佈式
Raft 是一種爲了管理複製日誌的一致性算法 。爲了提升 可理解性 :優化
Raft 算法在許多方面都和現有的一致性算法很類似,但也有獨特的特性:spa
一個 Raft 集羣必須包含奇數個服務器,若包含 2f+1 臺服務器,則能夠容忍 f 臺服務器的故障 。( 爲了保留多數服務器在線,以正常完成日誌提交和 leader 選舉 )
Raft 經過選舉一個 leader ,而後給予它所有的管理日誌的權限,以此來實現一致性 。
一個服務器處於三種狀態之一:leader
、follower
和 candidate
:
一次選舉開始對應這一個新的 term (任期)
,term 使用連續的整數標記 ,一個 term 最多有一個 leader 。
Raft 會使用一種 心跳機制
來觸發領導人選舉,當 leader 在位時,它週期性地發送心跳包(不含 log entry 的 AppendEntries RPC
請求) 給 follower ,若 follower 在一段時間內未接收到心跳包( 選舉超時
),則認爲系統中沒有 leader ,此時該 follower 會發起選舉 。
當系統啓動時,全部服務器都是 follower ,第一個選舉超時的發起選舉 。
RequestVote RPC
請求split vote
問題 ),則全部 candidate 都超時,並增長 term 號,開始新一輪的選舉RequestVote RPC
請求中包含了 candidate 的日誌信息,投票方服務器會拒絕日誌沒有本身新的投票請求
隨機選舉超時時間
。這樣能夠把服務器超時時間點分散開,一個 candidate 超時後,它能夠贏得選舉並在其餘服務器超時以前發送心跳包 。即便出現了一個 split vote 狀況,多個 candidate 會重置隨機選舉超時時間,能夠避免下一次選舉也出現 split vote 問題 。( 保證以前 term 中已提交的 entry 在選舉時都出如今新的 leader 的日誌中 ):
Raft 要求 安全性不能依賴時間:整個系統不能由於某些事件運行的比預期快一點或慢一點就產生錯誤的結果
爲選舉並維持一個穩定的領導人,系統需知足:
廣播時間(broadcastTime) << 選舉超時時間(electionTimeout) << 平均故障時間(MTBF)
每一個 log entry 存儲一個 狀態機命令 和從 leader 收到這條命令的 term ,每一條 log entry 都有一個整數的 log index 來代表它在日誌中的位置 。
committed log entry
指能夠安全應用到狀態機的命令。當由 leader 建立的 log entry 複製到大多數的服務器上時,log entry 就會被提交 。同時,leader 日誌中以前的 log entry 也被提交 (包含其餘 leader 建立的 log entry )。
日誌的做用:
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 會從新發送包含此命令的請求
Raft 使用 日誌機制
來維護不一樣服務器之間的一致性,它維護如下的特性:
對於 特性一: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 的日誌與本身的相同了 。
上圖體現了一些 leader 和 follower 不一致的狀況 。a~b 爲 follower 有缺失的 log entry ,c~d 爲 follower 有多出的未提交的 log entry ,e~f 爲兩種問題並存的 。
Raft 算法中,leader 處理不一致是經過:強制 follower 直接複製本身的日誌 。
nextIndex
,表示下一個須要發送給 follower 的 log entry 的 indexAppendEntries RPC
被 follower 拒絕後( leader 和 follower 不一致 ),leader 減少 nextIndex 值重試( prevLogIndex 和 prevLogTerm 也會對應改變 )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
( 論文中對此處未做詳述,只要知足要求的設計都可 )
若是 follower 和 candidate 崩潰了,那麼後續發送給它們的 RPC 就會失敗 。
Raft 處理這種失敗就是無限地重試;若是機器重啓了,那麼這些 RPC 就會成功 。
若是一個服務器完成一個 RPC 請求,在響應前崩潰,則它重啓後會收到一樣的請求 。這種重試不會產生什麼問題,由於 Raft 的 RPC 都具備冪等性 。( 如:一個 follower 若是收到附加日誌請求可是它已經包含了這一日誌,那麼它就會直接忽略這個新的請求 )
日誌不斷增加帶來了兩個問題:空間佔用太大、服務器重啓時重放日誌會花費太多時間 。
使用 快照
來解決這個問題,在快照系統中,整個系統狀態都以快照的形式寫入到持久化存儲中,而後到那個時間點以前的日誌所有丟棄 。
每一個服務器獨立地建立快照,快照中只包括被提交的日誌 。當日志大小達到一個固定大小時就建立一次快照 。使用 寫時複製 技術來提升效率 。
上圖爲快照內容示例,可見快照中包含了:
保存 last included index 和 last included term 是爲了支持快照後複製第一個 log entry 的 AppendEntries RPC
的一致性檢查 。
爲支持集羣成員更新,快照也將最後一次配置做爲最後一個條目存下來 。
一般由每一個服務器獨立地建立快照,但 leader 會偶爾發送快照給一些落後的 follower 。這一般發生在當 leader 已經丟棄了下一條須要發送給 follower 的 log entry 時 。
leader 使用 InstallSnapShot RPC
來發送快照給落後的 follower 。
客戶端交互過程:
client 啓動時,會隨機挑選一個服務器進行通訊
若該服務器是 follower ,則拒絕請求並提供它最近接收到的 leader 的信息給 client
client 發送請求給 leader
若 leader 已崩潰,client 請求會超時,以後再隨機挑選服務器進行通訊
Raft 可能接收到一條命令屢次:client 對於每一條命令都賦予一個惟一序列號,狀態機追蹤每條命令的序列號以及相應的響應,若序列號已被執行,則當即返回響應結果,再也不重複執行。
只讀的操做能夠直接處理而無需記錄日誌 ,可是爲防止髒讀( leader 響應客戶端時可能已被做廢 ),須要有兩個保證措施:
AppendEntries RPC
的多數回覆後的一段時間內,它能夠響應 client 的只讀請求集羣的 配置 能夠被改變,如替換宕機的機器、加入新機器或改變複製級別 。
配置轉換期間,整個集羣會被分爲兩個獨立的羣體,在同一個時間點,兩個不一樣的 leader 可能在同一個 term 被選舉成功,一個經過舊的配置,一個經過新的配置 。
Raft 引入了一種過渡的配置 —— 共同一致 ,它容許服務器在不一樣時間轉換配置、可讓集羣在轉換配置過程當中依然響應客戶端請求 。
共同一致是新配置和老配置的結合:
保證配置變化安全性的關鍵 是:防止 \(C_{old}\) 和 \(C_{new}\) 同時作出決定( 即同時選舉出各自的 leader )。
集羣配置在複製日誌中以特殊的 log entry 來存儲 。下文中的 某配置作單方面決定 指的是在該配置內部選出 leader 。
能夠看到,整個過程當中,\(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ㄒ)/~~