分布式系統學習2-Raft算法分析與實現

Raft是一個分布式系統的一致性算法,它不像Paxos那么難懂,實現比Paxos簡單許多,性能與Paxos相當,在Etcd,Consul里面等都有廣泛運用。之前在容器服務化的時候用到Consul,順帶看了Raft算法的論文,然后為了練手Go語言做了mit6.824分布式系統課程的lab2。由于實驗里面隨機選舉時間和模擬的節點crash導致的異常可能在你運行上百次才會出現,實現后要測試多次以保證測試通過。我的Raft算法的實現代碼在這里 6.824-2017-raft,多有參考其他代碼,見README。6.824課程的lab1是完成一個簡化版的MapReduce,實現比較簡單,代碼見 6.824-2017-mapreduce。如有錯誤,懇請指正。

1 概述

分布式系統的一致性算法就是指一組機器協同工作,即便其中有某些機器宕機了,系統還能正常對外提供服務。以前通常都喜歡用Paxos來講解一致性算法,但是Paxos本身很復雜,代碼實現也很難,于是催生了Raft這個更加簡單易懂的一致性算法,難得的是,它的效果跟Paxos差不多。

為了易于理解,Raft采用了算法分解(分為leader選舉,日志復制以及安全性)和減少狀態的方式。與以往一致性算法不同的是,Raft有一些特別的地方:

  • 強Leader。Raft使用了一個強Leader特性,日志復制只能從Leader節點復制到其他節點。
  • Leader選舉。Raft使用了一個隨機超時來選舉Leader,以確保選舉不會失敗。
  • 成員變化。Raft使用了聯合一致性方法來實現成員配置變化時保證服務不受影響。

2 復制狀態機

一致性算法是在復制狀態機的背景下提出來的。在復制狀態機中,集群中的服務器從相同的狀態中生成同樣的副本,即使其中有些服務器宕機了,客戶端還是可以繼續執行操作,復制狀態機可以用來解決分布式系統中許多容錯問題。大型分布式系統中通常擁有一個集群leader,比如GFS,HDFS等通常使用一個單獨的復制狀態機管理leader選舉和配置信息以應對leader的崩潰。此外,使用復制狀態機的還有Chubby以及ZooKeeper等。

復制狀態機通過復制日志實現,如下圖所示。每個服務器都會存儲一份日志,日志存儲的是一系列命令,而服務器的狀態機會按順序執行這些日志中的命令。每份日志中以同樣的順序存儲了同樣的命令,而狀態機以同樣的順序執行這些相同的命令。每臺服務器的狀態機都是確定的,它們以同樣的順序執行同樣的命令,最終的狀態和輸出也必然是一樣的。

復制狀態機圖示

保持復制日志的一致性就是一致性算法的工作了。服務器上的一致性模塊接收客戶端命令并添加命令到它的日志中。它與其他服務器通信以保證每個日志最終都以相同的順序包含相同的命令,即便過程中有服務器宕機了。一旦客戶端命令正確的復制了,每個服務器的狀態機按照日志中順序處理這些命令,并將輸出返回給客戶端。最終,這些服務器看起來就像是一臺高可用的狀態機。

應用到實際系統中的一致性算法通常具備下面幾個特性:

  • 保證安全。在所有的非拜占庭將軍條件下保證安全,包括網絡延遲,分區,丟包,亂序等。
  • 高可用。只要集群中的服務器有大多數(超過一半)可用,系統即是可用的。比如5臺服務器的集群可用允許2臺服務器宕機而不影響服務。
  • 不依賴時序保證日志的一致性。錯誤的時鐘和極端的消息延遲在最壞情況下才會導致可用性問題。
  • 在通常情況下,只要集群中大部分服務器對過程調用做出響應,命令就可以完成,少數慢服務器不會影響整體系統性能。

3 Raft算法

Raft算法分為兩部分,領導選舉(Leader Election)和日志復制(Log Replication)。這里有個很形象的動畫說明Raft算法的實現,關于成員變更和日志快照這里有篇解釋很好的文章,見 深入淺出 Raft - Membership Change

3.1 領導選舉

首先了解下Raft中節點的幾個狀態:Follower,Candidate,Leader。狀態變遷如下圖所示。

Raft節點狀態變遷圖
  • 處于Follower狀態的節點在一個隨機的超時時間(稱之為Election timeout,注意每次都要隨機選擇一個超時時間,這個超時時間通常為150-300毫秒,我在實驗中設置的是300+ms)內沒有收到投票或者日志復制和心跳包的RPC,則會變成Candidate狀態。

  • 處于Candidate狀態的節點會馬上開始選舉投票。它先投自己一票,然后向其他節點發送投票,這個請求稱之為Request Vote RPC。如果獲得了過半的節點投票,則轉成Leader狀態。如果投票給了其他節點或者發現了更新任期(Term)的指令(如更新任期的選舉投票和日志復制指令),則轉為Follower狀態。如果選舉超時沒有得到過半投票,則保持Candidate狀態開始一個新一輪選舉投票。

  • 處于Leader狀態的節點會定期發送(這個時間為HeartbeatTimeout,通常要遠小于選舉超時,實驗中我設置的位50ms)AppendEntries RPC請求給其他節點。如果發現了更新任期的指令,則轉為Follower狀態。

選舉投票需要兩個條件:

  • 條件一:請求投票的節點的任期必須大于等于本節點且本節點還沒有投過票給其他節點(包括投票給自己)。
  • 條件二:請求投票的節點的日志必須是包含了最新提交日志的節點,這是為了保證日志安全增加的限制條件。如何保證請求投票節點包含了最新提交日志呢?可以比較兩個節點最后一條日志的任期,如果任期不一樣,則任期大的日志更新;如果任期一樣,則日志更長的更新。

3.2 日志復制

Raft是強Leader機制,日志只能從Leader復制到其他節點。日志項LogEntry包括index,term,command三個元素。其中index為日志索引,term為任期,而command為具體的日志內容。日志格式如下圖所示:

Raft日志格式示意圖

通常的日志復制流程是這樣的:

  • 客戶端發送請求給Leader。
  • Leader接收客戶端請求,先將請求命令作為一個日志項(LogEntry)append到自己的log中。
  • Leader然后在最近的一個Heartbeat timeout時發送 Append Entries RPC給Follower節點。
  • 一旦日志提交成功:
    • 此時日志處于Uncommitted狀態,當過半節點添加log成功后,則Leader提交該日志給狀態機,返回給客戶端寫入成功。
    • 并在接下來的Append Entries RPC中通知其他節點提交該日志。
    • Follower節點提交日志到自己的狀態機中。
  • 如果Leader節點掛了,其他Follower節點會在超時后重新選舉新的Leader。而如果有宕機或者慢的Follower節點,則Leader會不斷重試直到成功。

即便出現網絡分割,集群中同時存在多個Leader時,也不會有問題。假定5個節點的集群分割成了3節點和2節點兩個大小集群,3節點大集群因為數目3過半,可成功提交日志,而節點數不夠的小集群沒法成功提交日志。當網絡恢復時,因為另外分割的一個大集群已經成功提交了日志,最終新的Leader會在大集群中產生(基于選舉投票的條件二保證)并同步到之前分割的小集群節點中。

關于日志復制的幾個要點:

  • 不同的服務器上面的提交的相同的索引和任期的日志項的command一定相同,而且這個日志項之前的所有日志項都相同。
  • 如果一個日志項被提交,則它之前索引的所有日志項也肯定已經提交。
  • Leader從來都不覆蓋自己的日志。其他狀態節點如果出現與當前Leader日志不一致,則需要更新日志,包括寫入新的日志和刪除不一致的日志。
  • Leader提交過的日志一定會出現將來新的Leader中。
  • Leader要保證安全的提交日志,必須滿足這兩個提交規則(見4.3中不安全的情況和4.4安全的情況):
    • 日志條目已經復制到大多數Follower節點。
    • Leader當前任期的新日志條目至少有一個復制到了大多數Follower節點。

時序和可用性:

Raft的一個特點就是安全性不依賴時序,系統不會因為時序問題而導致錯誤發生,但是系統的可用性不可避免的會對時序有所依賴。如果服務器崩潰會導致Candidate節點選舉不成功而不停的發起選舉,而Raft必須有一個穩定的Leader,否則無法工作。領導選舉是Raft中對時序要求最關鍵的地方,Raft能夠選舉出并保持一個穩定的Leader需要系統滿足如下時序要求:

broadcastTime << electionTimeout << MTBF

其中broadcastTime是指一臺服務器并行地向集群其他服務器發送RPC并接收到響應的平均時間,而electionTimeout是選舉超時時間,MTBF則是指單個服務器發生故障的平均間隔時間。broadcastTime遠小于electionTimeout可以保證Leader持續發送心跳包給Follower節點以防止Follower節點發起選舉,electionTimeout遠小于MTBF是為了保證系統的穩定運行。Leader崩潰后,理論上大約只有electionTimeout的時間內服務不可用。

根據存儲方式的不同,broadcastTime一般設置為0.5ms到20ms(實驗中設置心跳間隔略有不同,推薦是100ms左右,我設置的50ms),而electionTimeout一般是10-500ms(實驗設置的是300+ms)。通常服務器的MTBF一般是幾個月甚至幾年,很容易符合這個要求。

4 Raft日志復制狀態分析

4.1 前一條日志相同才能復制成功

復制狀態圖示1

4.2 Leader最新任期有日志已經復制到了大多數節點(安全)

復制狀態圖示2

如圖所示,S1-S3在任期2已復制成功了第4條LogEntry,這個時候Leader必須包括第4個LogEntry,因此重新選舉時S4和S5都不能選舉為Leader,第4條日志可以安全提交。

4.3 Leader試圖從一個較老的任期提交日志(不安全)

復制狀態圖示3

如上圖所示,這時候如果提交第3條LogEntry是不安全的,因為后續如果S5選舉為Leader的話會覆蓋S1,S2,S3的第3條日志。

4.4 Leader安全的提交日志

復制狀態圖示4

如上圖所示,此時Leader最新任期4的一個日志條目4已經復制到大多數節點S1-S3,此時S5不能選舉成功,日志條目3和4都是安全的。這就印證了前面提到的Leader當前任期的新日志條目至少有一個復制到了大多數Follower節點才能提交。

4.5 Leader變化導致日志不一致

Leader變化導致日志不一致

如上圖所示,Leader變化會導致各節點日志不一致,則需要做如下處理:

  • 新的Leader需要保證Follower的日志與其一致,Follower如果有不一致的多余日志要刪除,少了日志則要添加。如下面處理流程圖中的(a)是需要添加缺少的日志,(b)則是要刪除不一致的多余的日志再添加新的日志。
  • Leader會給每個Follower維護一個nextIndex列表,記錄要發送給對應Follower節點的下一個日志的索引。
  • 如果Follower復制日志失敗,Leader需要減小nextIndex并重試。
日志不一致處理流程示意

5 Raft實現需注意的幾個地方

Raft實現需要的數據結構在論文中已經很完整,如下圖:

Raft實現數據結構和流程要點
  • Leader的心跳和日志復制都可以作為Append Entries RPC請求發送,可以簡化代碼。與日志復制不同的是,心跳RPC的Entries參數為空。
  • 注意兩個超時。一個是選舉超時,一個是日志復制(心跳)間隔時間。選舉超時ElectionTimeout和日志復制間隔HeartbeatTimeout兩個超時時間的選擇,注意復制間隔必須遠小于選舉超時,即 HeartbeatTimeout << Electiontimeout。我的代碼設置的選舉超時隨機為(300+Rand(100))ms(原論文要求的是150-300ms,但是實驗里面的意思是要大于300ms比較好,不過設置為300+ms測試也能通過),注意選舉超時每次都要隨機,不然可能造成選舉不成功。復制間隔固定為50ms(論文里面要求是20ms以內,實驗里面是要求100ms左右,測試發現在選舉超時為300+ms的時候心跳間隔為50ms可以測試通過)。
  • 注意加鎖問題,多個協程共享的數據要加鎖訪問rf.mu.Lock(),記得釋放鎖,使用defer rf.mu.Unlock()是個不錯的方案。測試的時候也要記得加上data race的檢測, go test -race
  • 注意提交日志的時候applyLogs()函數里面的日志提交部分,commitIndex只要比lastApplied大的日志項都要提交,因為一次可能是提交多個日志的,否則會出錯。
  • 日志數組rf.log的第一項沒有使用,主要是為了和論文兼容,日志索引從1開始,注意,go語言的數組第一項如果是nil的話gob編碼解碼會有問題,所以要加個空的LogEntry進去填充。
  • 只要修改了本機要持久存儲的變量,就要調用rf.persist()進行持久化。每個節點都要持久存儲的變量有 currentTerm, voteFor, log
  • 對于優化Append Entries RPC次數的代碼,請參照參考資料3的說明。

6 參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容