Raft是一種為了管理日志復制的一致性算法。它提供了和Paxos算法相同的功能和性能,但是它的算法結構和Paxos不同,使得Raft算法更加容易理解並且更容易構建實際的系統。為了提升可理解性,Raft將一致性算法分解成幾個關鍵的模塊,例如領導選舉,日志復制和安全性。同時它通過實施一個更強的一致性來減少需要考慮的狀態和數量。從一個用戶研究的結果可以證明,對於學生而言,Raft算法比Paxos算法更容易學。Raft算法還包括了新的機制來允許集群成員的動態改變,讓利用重疊的大多數來保證安全性。
Raft算法概述
Raft算法是從多副本狀態機的角度提出,用於管理多副本狀態機的日志復制,它一致性分解為多個子問題:領導選舉(Leader election),日志復制(Log replication),安全性(Safety),日志壓縮(Log compaction),成員變更(Menbership change)等。同時,Raft算法使用了更強的假設來減少需要考慮的狀態,使之變得更加容易理解。
Raft講系統中的角色分為領導者(Leader),跟從者(Follower)和候選人(Candidate):
Leader: 接收客戶端請求,並向Follower同步請求日志。當日志同步到大都數節點上后告告訴Follower提交日志。
Follower:接收並持久化Leader同步的日志,在Leader告訴它的日志提交之后,提交日志。
Candidate:Leader選舉過程中的臨時角色。
Raft要求系統在任意時刻最多只有一個Leader,正常工作期間Leader和Followers。
Raft算法角色狀態轉換如下:
跟隨者只響應來自其他服務器的請求。如果跟隨者接收不到消息,那么它就會變成候選人並發起一次選舉。獲得集群中大多數選票的候選人將成為領導者。在一個任期內,領導人直到自己宕機了。
Raft算法時間被划分成為一個個的任期(Term),每個任期(Term)開始都是一次Leader選舉。選舉成功后,領導人會管理整個集群直到任期(Term)結束。有時候會選舉失敗,那么任期(Term)就會沒有領導者,而結束。任期(Term)之間的切換可以在不同的時間不同的服務器上觀察到。
Raft算法中服務器之間節點通信使用遠程調用(RPCs).而在etcd的實現當中v2版本用的http,而v3版本采用的是grpc(本身是跨平台)。並且基本的一致性算法只是用了兩種類型的Rpcs。請求投票(RequestVote)RPCs由候選人發起。然后附加條目數(AppendEntries)由Leader發起。用來復制日志和提供一種心跳機制。為了服務器之間傳輸快照增加了第三種RPCs。當服務器沒有及時收到RPC的響應時,會進行重試,並且它們能夠並行發起RPCs來獲取最佳性能。
復制狀態機
一組服務器上的狀態機產生相同狀態的副本,並且在一些機器宕掉的情況下也可以繼續運行。復制狀態機在分布式系統中被用於解決很多容錯的問題。例如,大規模的系統中通常都有一個集群領導者,像 GFS、HDFS 和 RAMCloud,典型應用就是一個獨立的的復制狀態機去管理領導選舉和存儲配置信息並且在領導人宕機的情況下也要存活下來。比如 Chubby 和 ZooKeeper。
復制狀態機的結構。一致性算法管理着來自客戶端指令的復制日志。狀態機從日志中處理相同順序的相同指令,所以產生的結果也是相同的。
復制狀態機通常都是基於復制日志實現的,如圖 1。每一個服務器存儲一個包含一系列指令的日志,並且按照日志的順序進行執行。每一個日志都按照相同的順序包含相同的指令,所以每一個服務器都執行相同的指令序列。因為每個狀態機都是確定的,每一次執行操作都產生相同的狀態和同樣的序列。
保證復制日志相同就是一致性算法的工作了。在一台服務器上,一致性模塊接收客戶端發送來的指令然后增加到自己的日志中去。它和其他服務器上的一致性模塊進行通信來保證每一個服務器上的日志最終都以相同的順序包含相同的請求,盡管有些服務器會宕機。一旦指令被正確的復制,每一個服務器的狀態機按照日志順序處理他們,然后輸出結果被返回給客戶端。因此,服務器集群看起來形成一個高可靠的狀態機。
實際系統中使用的一致性算法通常含有以下特性:
安全性保證(絕對不會返回一個錯誤的結果):在非拜占庭錯誤情況下,包括網絡延遲、分區、丟包、冗余和亂序等錯誤都可以保證正確。
可用性:集群中只要有大多數的機器可運行並且能夠相互通信、和客戶端通信,就可以保證可用。因此,一個典型的包含 5 個節點的集群可以容忍兩個節點的失敗。服務器被停止就認為是失敗。他們當有穩定的存儲的時候可以從狀態中恢復回來並重新加入集群。
不依賴時序來保證一致性:物理時鍾錯誤或者極端的消息延遲在可能只有在最壞情況下才會導致可用性問題。
通常情況下,一條指令可以盡可能快的在集群中大多數節點響應一輪遠程過程調用時完成。小部分比較慢的節點不會影響系統整體的性能。
Leader選舉
Raft使用心跳(heartbeat)觸發Leader選舉。當服務器啟動時,初始化Follower。Leader向所有的Followers周期性的發送heartbeat。如果Follower在選舉超時時間內沒有收到Leader的heartbeat,就會等待一段隨機時間(150ms-300ms)發起一次選舉。
Follower先要增加自己的當前任期號,也就把當前的任期號加一並且轉換到候選人狀態。然后它們會並行的向集群中的其他服務器節點發起請求投票的RPCs來給自己投票。結果會有以下三種情況:
- 它自己贏得了這次選舉
- 其他的服務器成為了領導者
- 一段世家之后沒有人獲取勝利的人。
日志復制
Leader被選舉出來后.它就開始為客戶端提供服務,客戶端每個請求都包含一條被復制狀態機執行的命令。領導人將這條指令作為新的日志條目附加到日志中去。然后並行的發起附加條目RPCs給其他的服務器,讓它們復制這個日志條目,當這條日志條目被安全的復制。領導人會應用這條日志條目到它的狀態中然后把執行的結果返回客戶端。如果Follower崩潰或者運行緩慢,再或者網絡丟包,領導人會不斷的嘗試附加日志條目RPCs(盡管已經回復了客戶端)直到所有Follower都最終存儲了所有條目數。
日志由有序編號(log index)的日志組成條目。每個日志條目包含它被創建的任期號(term),和用於狀態機執行的命令。如果一個日志條目被復制到大多數服務器上,就被認為可以提交了(commit)了。
Raft維護着一下特征:
1.如果在不同的日志中的兩個條目擁有相同的索引和任期號,那么它們存儲了相同的指令。
2.如果在不同的日志中的兩個條目擁有相同的索引和任期號,那么他們的之前的所有日志條目也全部相同。
第一個特新來這樣的一個事實,領導人最多在一個任期里在指定的日志索引位置創建一條日志條目,同時日志條目在日志中的位置也從來不會改變。第二個特性由附加日志RPC的一個簡單一致性檢查保證。在發送附加日志RPC的時候,領導人會把新的日志條目緊接着之前的條目索引位置和任期號包含在里面。如果跟隨者在它的日志中找不到包含相同的日志索引位置和任期號的條目,那么他就會拒絕接收新的條目日志。一致性檢查就像一個歸納步驟:一開始空日志狀態肯定是滿足日志匹配特性的,然后一致性檢查保護了日志匹配特性當日志擴展的時候。因此,每當附加日志RPC返回成功時,領導人就知道跟隨着的日志時一樣的了。
當一個領導人成功當選時,跟隨者可能是任何情況(a-f)。每一個盒子表示是一個日志條目,里面的數字表示任期號。跟隨者可能缺少一些體制條目(a-b),可能會有一些未被提交的日志條目(c-d),或者兩種情況都存在的(e-f)。例如,場景f可能會發生,某些服務器在任期號2的時候是領導人,已附加了一些日志條目到自己的日志中,但在提交之前就就崩潰了,很快這個機器就被重啟了,在任期3重新被選為領導人,並且又增加了一些日志條目到自己的日志中,並且又增加了一些日志條目到自己的日志中,在任期2和任期3的日志被提交之前,這個服務器又宕機了,並且在接下來的幾個任期里一直處於宕機狀態。
要使得跟隨着的日志進入和自己一致的狀態,領導人必須找到最后兩者達成一致的地方,然后刪除那個點之后的所有日志條目,發送自己的日志給跟隨者。所有的這些日志操作都在進行附加日志RPCs的一致性檢查時完成。領導人針對沒一個維護者維護了一個nextIndex,這表示下一個發送給追隨者的日志條目的索引地址。當一個領導人剛獲得領導者的權利的時候,他初始化所有的nextIndex值作為自己的最后一條日志的index加1。如果一個跟隨者的日志和領導人不一致,那么下一次日志附RPC時的一致性檢查就會失敗。在被跟隨者拒絕之后,領導人就會減少nextIndex值並進行重試。最終nextIndex會在某個位置使得領導人和跟隨者的日志達成一致。當這種情況發生,附加日志RPC就會成功,這時就會把跟隨者沖突的日志條目全部刪除並且加上領導人的日志。一旦附加日志RPC成功,那么跟隨者的日志就會和領導人保持一直,並且在接下來的任期里一直繼續保持。
安全性
Raft增加了如下兩條限制以保證安全性:
1>擁有最新的已提交的log entry的Follower才有資格成為Leader。
這個保證是在RequestVote RPC中做的,Candidate在發送RequestVote RPC時。要帶上自己的最后一條日志的term和log Index。其他節點收到消息時,如果發現自己的日志請求中攜帶的更新,則拒絕投票。日志比較的原則是:如果本地的最后一條log entry的term更大,則term大更新,如果term一樣大,則log Index更大的更新。
2.Leader只能推進commit Index來提交當前term已經復制最到最大服務器上的日志,舊term日志的日志要等到提交當前的term的日志來間接提交(log Index 小於commit Index的日志被間接提交)
之所以要這樣,是因為可能會出現已提交的日志被覆蓋的情況:
如圖的時間序列展示了領導人無法決定對老任期號的日志條目進行提交。在(a)中,S1是Leader,部分的是復制了索引的位置2的條目數目。(b)是時期,S1崩潰了,然后S5在任期3里通過S3,S4和自己的選票贏得選舉,然后從客戶端接收了一條不一樣的日志條目放在了索引2處。然后到(c),S5崩潰了,S1重新啟動,選舉成功,開始日志復制。在這個時候,來自任期2的那條日志已經被復制到了集群的大多數機器上,但是還沒有被提交,如果S1在(d)時期中又崩潰了。S5可以重新被選舉成功(通過來自S2,S3,S4的選票),然后覆蓋了他門在索引2處的日志。反之,如果在崩潰之前,S1把自己主導的任期里產生的日志日條目復制到了大多數機器上,就如(e)中那樣。那么在后面任期里面這些新的日志條目會被提交(因為S5就不可能選舉成功)。這牙膏在同一時刻就同時保證了,之前的所有老的日志條目就會被提交。
時間和可用性
Raft的要求之一就是安全性不能依賴時間:整個系統不能因為某些事件運行的比預期快一點或者慢一點產生了錯誤的結果。但是,可用性(系統可以及時的響應客戶端)不可避免的要依賴時間。例如,如果消息交換比服務器故障間隔時間長,候選人沒有足夠長的時間來贏得選舉,沒有一個穩定的領導人,Raft將無法工作。
領導人選舉時Raft中對時間要求最為關鍵的方面。Raft可以選舉並維持一個穩定的領導人,只需要滿足下面的時間要求:
廣播時間(broadcastTime) << 選舉時間(election Timeout) << 平均故障時間(MTBF)
在這個不等式中,廣播時間指的時從一個服務器並行的發送RPCs給集群中的其他服務器並接收平均時間,選舉超時時間(150ms-300ms)選舉超時時間限制,然后平均故障時間就是對於一台服務器而言,兩次故障之間的平均時間。廣播時間必須比選舉超時時間小一個量級,這樣領導人才能發送穩定的心跳消息來阻止跟隨者開始進入選舉狀態,通過隨機化選舉超時時間的方法,整個不等式也使得選票瓜分的情況變成不肯能。選舉選舉超時時間要比平局故障時間間隔小上幾個數量級,這樣系統才能穩定的運行。當領導人崩潰后,整個系統會大約相當於超時時的時間里不可用。我們希望這種情況在系統中國運行很少出現。
廣播時間和平均故障間隔時間是由系統決定的,但是選舉超時時間是我們自己選擇的。Raft 的 RPCs 需要接收方將信息持久化的保存到穩定存儲中去,所以廣播時間大約是 0.5 毫秒到 20 毫秒,取決於存儲的技術。因此,選舉超時時間可能需要在 10 毫秒到 500 毫秒之間。大多數的服務器的平均故障間隔時間都在幾個月甚至更長,很容易滿足時間的需求。
成員變更
成員變更是在集群運行過程中副本發生變化,如增加/減少副本數,節點替換等。
成員變更也是一個分布式一致性的問題,既所有服務器對成員新成員達成一致。但是成員變更又有其特殊性,因為成員變更的一致性達成的過程中,參與投票的過程會發生變化。
如果將成員變更當成一般的一致性問題,直接向Leader發送成員變更請求,Leader復制成員變更日志,達成多數之后提交,各個服務器提交成員變更日志后從日志成員(Cold)切換到最新成員配置(Cnew的時刻不同.
成員變更不能影響服務的可用性,但是成員變更過程的某一時刻,可能出現Cold和Cnew中同時存在兩個不相交的多數派,進而可能選出兩個Leader,形成不同的決議,破壞安全性。
由於成員變更的這一特殊性,成員變更不能當成一般的一致性問題去解決。
為了解決這一問題.Raft提出了兩段的成員變更方法。集群先成舊成員配置Cold切換到一個過度的配置,稱為共同一致(joint consensus),共同一致時舊成員配置Cold和新成員配置Cnew的組合Cold U Cnew,一旦共同一致Cold U Cnew被提交,系統在切換到新成員配置Cnew。
一個配置切換的時間線。虛線表示已經被創建但是還沒有被提交的條目,實線表示最后被提交的日志條目。領導人首先創建了C-old
,new的配置條目在自己的日志中,並提交到C-old,new中(C-old的大多數和c-new的大多數)。然后他創建C-new條目並且提交到C-new的大多數。這樣就不存在C-new和C-old同時做出決定的時間點。
在關於重新配置還有三個問題需要提出,第一個問題是,新的服務器額能初始化沒有存儲任何的日志條目。當這些服務器以這種狀態加入到集群中,那么它們需要一段時間來更新追趕。這時還不能提交新的日志條目。為了避免這種可用性的間隔時間Raft在配置更新的時候用了一種額外的階段,在這種階段,新的服務器以沒有投票權的身份加入集群中來(領導人復制日志給它們。但是不考慮它們是大多數)。一旦新的服務器追趕上了集群中的集群,重新配置可以向上面描述一樣處理。
第二個問題,集群的領導人可能不是新配置的一員。在這種情況下,領導人就會在提交了C-new日志后退位(回到追隨者狀態)。這意味着有這樣一段時間,領導人管理着集群,但是不包括他自己,他復制日志但是不把他自己算作大多數之一。當C-new被提交時,會發生領導人過度。因為這時時最新的配置可以獨立工作時間點(將總是能夠在C-new配置下選出新的Leader)。再此之前,可能只從C-old中選出領導人。
第三個問題是:移除不再C-new中的服務器可能會擾亂集群。這些服務器將不會再接收心跳。當選舉超時時,它們就會進行新的選舉過程。它們會發送擁有新的任期號的請求投票RPCs,這樣會導致當前的領導人退回成跟隨者狀態。新的領導人最終被選出來,但是被移除的服務器將會再次超時,然后這種過程再次重復,導致整體可用性大幅度下降。
為了避免這個問題,當服務器確認當前領導人存在時,服務器會忽略投票RPCs。特別的,當服務器再當前最小選舉超時時間內收到一個請求投票的RPC。他不會更新當前的任期號和投票號。這不會影響正常的選舉,每個服務器在開始一次選舉之前,至少等待一個最小選舉超時時間。然后這有利於避免被移除的服務器的擾亂。如果領導人能夠發送心跳給集群,那么他就不會更大的任期號廢黜。
日志壓縮
在實際系統中,不能讓日志無限增長,否則系統重啟時需要花很長的時間回放,從而影響可用性。Raft采用對整個系統進行snapshot來解決,snapshot之前的日志都可以拋棄。
每個副本獨立的對自己系統狀態進行snapshot,並且只能對已經提交的日志進行snapshot。
Snapshot中包含以下內容:
1>日志元數據:最后提交的log entry的log index和term。這兩個值在snapshot之后的第一條log entry的AppendEntriesRPC的完整性檢查的時候會被用上。
2> 系統當前狀態。
當Leader要發給某個日志落后太多的Follower的log entry被丟棄,Leader會將snapshot發給Follower。或者新加入一台機器時,也會發送snapshot給它。發送snapshot使用InstalledSnapshot RPC。
一個服務器用新的快照替換了從1到5的條目數,快照存儲了當前的狀態。快照中包含了最后的索引位置和任期號
做snapshot不要做的太頻繁,否則消耗磁盤帶寬,也不要做的太平凡,否則一點節點重啟要回放大量日志,影響可用性。推薦當日組織達到某個固定的大小做一次snapshot。
做一次snapshot可能耗時過長,會影響正常日志同步。可以通過使用copy-on-write技術避免snapshot過程影響正常的日志同步過程。
一個關於 Raft 一致性算法的濃縮總結(不包括成員變換和日志壓縮)。
參考:
http://thesecretlivesofdata.com/raft/(Raft動畫)
https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md(Raft論文翻譯)