本篇博客爲著名的 RAFT 一致性算法論文的中文翻譯,論文名爲《In search of an Understandable Consensus Algorithm (Extended Version)》(尋找一種易於理解的一致性算法)。git
Raft 是一種用來管理日誌複製的一致性算法。它和 Paxos 的性能和功能是同樣的,可是它和 Paxos 的結構不同;這使得 Raft 更容易理解而且更易於創建實際的系統。爲了提升理解性,Raft 將一致性算法分爲了幾個部分,例如領導選取(leader selection),日誌複製(log replication)和安全性(safety),同時它使用了更強的一致性來減小了必須須要考慮的狀態。從用戶學習的結果來看,Raft 比 Paxos 更容易學會。Raft 還包括了一種新的機制來使得動態改變集羣成員,它使用重疊大多數(overlapping majorities)來保證安全。github
一致性算法容許一組機器像一個總體同樣工做,即便其中的一些機器出了錯誤也能正常工做。正由於此,他們扮演着創建大規模可靠的軟件系統的關鍵角色。在過去的十年中 Paxos 一直都主導着有關一致性算法的討論:大多數一致性算法的實現都基於它或者受它影響,而且 Paxos 也成爲了教學生關於一致性知識的主要工具。web
不幸的是,儘管在下降它的複雜性方面作了許多努力,Paxos 依舊很難理解。而且,Paxos 須要通過複雜的修改才能應用於實際中。這些致使了系統構構建者和學生都十分頭疼。算法
在被 Paxos 折磨以後,咱們開始尋找一種在系統構建和教學上更好的新的一致性算法。咱們的首要目標是讓它易於理解:咱們能不能定義一種面向實際系統的一致性算法而且比 Paxos 更容易學習呢?而且,咱們但願這種算法能憑直覺就能明白,這對於一個系統構建者來講是十分必要的。對於一個算法,不只僅是讓它工做起來很重要,知道它是如何工做的更重要。安全
咱們工做的結果是一種新的一致性算法,叫作 Raft。在設計 Raft 的過程當中咱們應用了許多專門的技巧來提高理解性,包括算法分解(分爲領導選取(leader selection),日誌複製(log replication)和安全性(safety))和減小狀態(state space reduction)(相對於 Paxos,Raft 減小了非肯定性的程度和服務器互相不一致的方式)。在兩所學校的43個學生的研究中發現,Raft 比 Paxos 要更容易理解:在學習了兩種算法以後,其中的33個學生回答 Raft 的問題要比回答 Paxos 的問題要好。性能優化
Raft 算法和如今一些已經有的算法在一些地方很類似(主要是 Oki 和 Liskov 的 Viewstamped Replication。可是 Raft 有幾個新的特性:服務器
咱們認爲,Raft 在教學方面和實際實現方面比 Paxos 和其餘算法更出衆。它比其餘算法更簡單、更容易理解;它能知足一個實際系統的需求;它擁有許多開源的實現而且被許多公司所使用;它的安全特性已經被證實;而且它的效率和其餘算法相比也具備競爭力。網絡
這篇論文剩下的部分會講以下內容:複製狀態機(replicated state machine)問題(第2節),討論 Paxos 的優缺點(第3節),討論咱們用的爲了達到提高理解性的方法(第4節),陳述 Raft 一致性算法(第5~8節),評價 Raft 算法(第9節),對相關工做的討論(第10節)。數據結構
一致性算法是在複製狀態機的背景下提出來的。在這個方法中,在一組服務器的狀態機產生一樣的狀態的副本所以即便有一些服務器崩潰了這組服務器也還能繼續執行。複製狀態機在分佈式系統中被用於解決許多有關容錯的問題。例如,GFS,HDFS還有 RAMCloud 這些大規模的系統都是用一個單獨的集羣領導者,使用一個單獨的複製狀態機來進行領導選取和存儲配置信息來應對領導者的崩潰。使用複製狀態機的例子有 Chubby 和 ZooKeeper。架構
如圖-1所示,複製狀態機是經過複製日誌來實現的。每一臺服務器保存着一份日誌,日誌中包含一系列的命令,狀態機會按順序執行這些命令。由於每一臺計算機的狀態機都是肯定的,因此每一個狀態機的狀態都是相同的,執行的命令是相同的,最後的執行結果也就是同樣的了。
如何保證複製日誌一致就是一致性算法的工做了。在一臺服務器上,一致性模塊接受客戶端的命令而且把命令加入到它的日誌中。它和其餘服務器上的一致性模塊進行通訊來確保每個日誌最終包含相同序列的請求,即便有一些服務器宕機了。一旦這些命令被正確的複製了,每個服務器的狀態機都會按一樣的順序去執行它們,而後將結果返回給客戶端。最終,這些服務器看起來就像一臺可靠的狀態機。
應用於實際系統的一致性算法通常有如下特性:
確保安全性(歷來不會返回一個錯誤的結果),即便在全部的非拜占庭(Non-Byzantine)狀況下,包括網絡延遲、分區、丟包、冗餘和亂序的狀況下。
高可用性,只要集羣中的大部分機器都能運行,能夠互相通訊而且能夠和客戶端通訊,這個集羣就可用。所以,通常來講,一個擁有 5 臺機器的集羣能夠容忍其中的 2 臺的失敗(fail)。服務器中止工做了咱們就認爲它失敗(fail)了,沒準一會當它們擁有穩定的存儲時就能從中恢復過來,從新加入到集羣中。
不依賴時序保證一致性,時鐘錯誤和極端狀況下的消息延遲在最壞的狀況下才會引發可用性問題。
一般狀況下,一條命令可以儘量快的在大多數節點對一輪遠程調用做出相應時完成,一少部分慢的機器不會影響系統的總體性能。
在過去的10年中,Leslie Lamport 的 Paxos 算法幾乎已經成爲了一致性算法的代名詞:它是授課中最多見的算法,同時也是許多一致性算法實現的起點。Paxos 首先定義了一個可以達成單一決策一致的協議,例如一個單一複製日誌條目(single replicated log entry)。咱們把這個子集叫作單一決策 Paxos(single-decree Paxos)。以後 Paxos經過組合多個這種協議來完成一系列的決策,例如一個日誌(multi-Paxos)。Paxos 確保安全性和活躍性(liveness),而且它支持集羣成員的變動。它的正確性已經被證實,一般狀況下也很高效。
不幸的是,Paxos 有兩個致命的缺點。第一個是 Paxos 太難以理解。它的完整的解釋晦澀難懂;不多有人能徹底理解,只有少數人成功的讀懂了它。而且你們作了許多努力來用一些簡單的術語來描述它。儘管這些解釋都關注於單一決策子集問題,但仍具備挑戰性。在 NSDI 2012 會議上的一次非正式調查顯示,咱們發現你們對 Paxos 都感到不滿意,其中甚至包括一些有經驗的研究員。咱們本身也曾深陷其中,咱們在讀過幾篇簡化它的文章而且設計了咱們本身的算法以後才徹底理解了 Paxos,而整個過程花費了將近一年的時間。
咱們假定 Paxos 的晦澀來源於它將單決策子集做爲它的基礎。單決策(Single-decree)Paxos 是晦澀且微妙的:它被劃分爲兩個沒有簡單直觀解釋的階段,而且難以獨立理解。正由於如此,它不能很直觀的讓咱們知道爲何單一決策協議可以工做。爲多決策 Paxos 設計的規則又添加了額外的複雜性和精巧性。咱們相信多決策問題可以分解爲其它更直觀的方式。
Paxos 的第二個缺點是它難以在實際環境中實現。其中一個緣由是,對於多決策 Paxos (multi-Paxos) ,你們尚未一個一致贊成的算法。Lamport 的描述大部分都是有關於單決策 Paxos (single-decree Paxos);他僅僅描述了實現多決策的可能的方法,缺乏許多細節。有許多實現 Paxos 和優化 Paxos 的嘗試,可是他們都和 Lamport 的描述有些出入。例如,Chubby 實現的是一個相似 Paxos 的算法,可是在許多狀況下的細節沒有公開。
另外,Paxos 的結構也是不容易在一個實際系統中進行實現的,這是單決策問題分解帶來的又一個問題。例如,從許多日誌條目中選出條目而後把它們融合到一個序列化的日誌中並無帶來什麼好處,它僅僅增長了複雜性。圍繞着日誌來設計一個系統是更簡單、更高效的:新日誌按照嚴格的順序添加到日誌中去。另外一個問題是,Paxos 使用對等的點對點的實現做爲它的核心(儘管它最終提出了一種弱領導者的形式來優化性能)。這種方法在只有一個決策被制定的狀況下才顯得有效,可是不多有現實中的系統使用它。若是要作許多的決策,選擇一個領導人,由領帶人來協調是更簡單有效的方法。
所以,在實際的系統應用中和 Paxos 算法都相差很大。全部開始於 Paxos 的實現都會遇到不少問題,而後由此衍生出了許多與 Paxos 有很大不一樣的架構。這是既費時又容易出錯的,而且理解 Paxos 的難度又很是大。Paxos 算法在它正確性的理論證實上是很好的,可是在實現上的價值就遠遠不足了。來自 Chubby 的實現的一條評論就可以說明:
Paxos 算法的描述與實際實現之間存在巨大的鴻溝…最終的系統每每創建在一個沒有被證實的算法之上。
正由於存在這些問題,咱們認爲 Paxos 不只對於系統的構建者來講不友好,同時也不利於教學。鑑於一致性算法對於大規模軟件系統的重要性,咱們決定試着來設計一種另外的比 Paxos 更好的一致性算法。Raft 就是這樣的一個算法。
設計 Raft 的目標有以下幾個:
它必須提供一個完整的、實際的基礎來進行系統構建,爲的是減小開發者的工做;
它必須在全部狀況下都能保證安全可用;
它對於常規操做必須高效;
最重要的目標是:易於理解,它必須使得大多數人可以很容易的理解;
另外,它必須能讓開發者有一個直觀的認識,這樣才能使系統構建者們去對它進行擴展。
在設計 Raft 的過程當中,咱們不得不在許多種方法中作出選擇。當面臨這種狀況時,咱們一般會權衡可理解性:每種方法的可理解性是如何的?(例如,它的狀態空間有多複雜?它是否是有很細微的含義?)它的可讀性如何?讀者能不能輕易地理解這個方法和它的含義?
咱們意識到對這種可理解性的分析具備高度的主觀性;儘管如此,咱們使用了兩種適用的方式。第一種是衆所周知的問題分解:咱們儘量將問題分解成爲若干個可解決的、可被理解的小問題。例如,在 Raft 中,咱們把問題分解成爲了領導選取(leader election)、日誌複製(log replication)、安全(safety)和成員變化(membership changes)。
咱們採用的第二個方法是經過減小須要考慮的狀態的數量將狀態空間簡化,這可以使得整個系統更加一致而且儘量消除不肯定性。特別地,日誌之間不容許出現空洞,而且 Raft 限制了限制了日誌不一致的可能性。儘管在大多數狀況下,咱們都都在試圖消除不肯定性,可是有時候有些狀況下,不肯定性使得算法更易理解。尤爲是,隨機化方法使得不肯定性增長,可是它減小了狀態空間。咱們使用隨機化來簡化了 Raft 中的領導選取算法。
Raft 是一種用來管理第 2 章中提到的複製日誌的算法。表-2 爲了方便參考是一個算法的總結版本,表-3 列舉了算法中的關鍵性質;表格中的這些元素將會在這一章剩下的部分中分別進行討論。
狀態:
在全部服務器上持久存在的:(在響應遠程過程調用 RPC 以前穩定存儲的)
名稱 | 描述 |
currentTerm | 服務器最後知道的任期號(從0開始遞增) |
votedFor | 在當前任期內收到選票的候選人 id(若是沒有就爲 null) |
log[] | 日誌條目;每一個條目包含狀態機的要執行命令和從領導人處收到時的任期號 |
在全部服務器上不穩定存在的:
名稱 | 描述 |
commitIndex | 已知的被提交的最大日誌條目的索引值(從0開始遞增) |
lastApplied | 被狀態機執行的最大日誌條目的索引值(從0開始遞增) |
在領導人服務器上不穩定存在的:(在選舉以後初始化的)
名稱 | 描述 |
nextIndex[] | 對於每個服務器,記錄須要發給它的下一個日誌條目的索引(初始化爲領導人上一條日誌的索引值+1) |
matchIndex[] | 對於每個服務器,記錄已經複製到該服務器的日誌的最高索引值(從0開始遞增) |
附加日誌遠程過程調用 (AppendEntries RPC)
由領導人來調用複製日誌(5.3節);也會用做heartbeat
參數 | 描述 |
term | 領導人的任期號 |
leaderId | 領導人的 id,爲了其餘服務器能重定向到客戶端 |
prevLogIndex | 最新日誌以前的日誌的索引值 |
prevLogTerm | 最新日誌以前的日誌的領導人任期號 |
entries[] | 將要存儲的日誌條目(表示 heartbeat 時爲空,有時會爲了效率發送超過一條) |
leaderCommit | 領導人提交的日誌條目索引值 |
返回值 | 描述 |
term | 當前的任期號,用於領導人更新本身的任期號 |
success | 若是其它服務器包含可以匹配上 prevLogIndex 和 prevLogTerm 的日誌時爲真 |
接受者須要實現:
term < currentTerm
返回 false(5.1節)prevLogIndex
處的日誌的任期號與prevLogTerm
不匹配時,返回 false(5.3節)leaderCommit > commitIndex
,將commitIndex
設置爲leaderCommit
和最新日誌條目索引號中較小的一個
投票請求 RPC(RequestVote RPC)
由候選人發起收集選票(5.2節)
參數 | 描述 |
term | 候選人的任期號 |
candidateId | 請求投票的候選人 id |
lastLogIndex | 候選人最新日誌條目的索引值 |
lastLogTerm | 候選人最新日誌條目對應的任期號 |
返回值 | 描述 |
term | 目前的任期號,用於候選人更新本身 |
voteGranted | 若是候選人收到選票爲 true |
接受者須要實現:
term < currentTerm
返回 false(5.1節)votedFor
爲空或者與candidateId
相同,而且候選人的日誌和本身的日誌同樣新,則給該候選人投票(5.2節 和 5.4節)
服務器須要遵照的規則:
全部服務器:
commitIndex > lastApplied
,lastApplied
自增,將log[lastApplied]
應用到狀態機(5.3節)currentTerm
,則currentTerm
賦值爲 T,並切換狀態爲追隨者(Follower)(5.1節)追隨者(followers): 5.2節
AppendEntries RPC
或者沒有收到候選人的投票請求,則本身轉換狀態爲候選人候選人:5.2節
currentTerm
自增RequestVote RPC
AppendEntries RPC(heartbeat)
:轉換狀態爲追隨者領導人:
AppendEntries RPC(heartbeat)
;在空閒時間重複發送以防止選舉超時(5.2節)AppendEntries RPC
將 nextIndex 以後的全部日誌條目發送出去
nextIndex
和matchIndex
更新AppendEntries RPC
失敗:nextIndex
遞減而且從新發送(5.3節)N > commitIndex
和matchIndex[i] >= N
而且log[N].term == currentTerm
的 N,則將commitIndex
賦值爲 N
性質 | 描述 |
選舉安全原則(Election Safety) | 一個任期(term)內最多容許有一個領導人被選上(5.2節) |
領導人只增長原則(Leader Append-Only) | 領導人永遠不會覆蓋或者刪除本身的日誌,它只會增長條目 |
日誌匹配原則(Log Matching) | 若是兩個日誌在相同的索引位置上的日誌條目的任期號相同,那麼咱們就認爲這個日誌從頭到這個索引位置之間的條目徹底相同(5.3 節) |
領導人徹底原則(Leader Completeness) | 若是一個日誌條目在一個給定任期內被提交,那麼這個條目必定會出如今全部任期號更大的領導人中 |
狀態機安全原則(State Machine Safety) | 若是一個服務器已經將給定索引位置的日誌條目應用到狀態機中,則全部其餘服務器不會在該索引位置應用不一樣的條目(5.4.3節) |
Raft 經過首先選出一個領導人來實現一致性,而後給予領導人徹底管理複製日誌(replicated log)的責任。領導人接收來自客戶端的日誌條目,並把它們複製到其餘的服務器上,領帶人還要告訴服務器們何時將日誌條目應用到它們的狀態機是安全的。經過選出領導人可以簡化複製日誌的管理工做。例如,領導人可以決定將新的日誌條目放到哪,而並不須要和其餘的服務器商議,數據流被簡化成從領導人流向其餘服務器。若是領導人宕機或者和其餘服務器失去鏈接,就能夠選取下一個領導人。
經過選出領導人,Raft 將一致性問題分解成爲三個相對獨立的子問題:
在說明了一致性算法以後,本章會討論有關可用性(availability)的問題和系統中時序(timing)的問題。
一個 Raft 集羣包括若干服務器;對於一個典型的 5 服務器集羣,該集羣可以容忍 2 臺機器不能正常工做,而整個系統保持正常。在任意的時間,每個服務器必定會處於如下三種狀態中的一個:領導人、候選人、追隨者。在正常狀況下,只有一個服務器是領導人,剩下的服務器是追隨者。追隨者們是被動的:他們不會發送任何請求,只是響應來自領導人和候選人的請求。領導人來處理全部來自客戶端的請求(若是一個客戶端與追隨者進行通訊,追隨者會將信息發送給領導人)。候選人是用來選取一個新的領導人的,這一部分會在 5.2節 進行闡釋。圖-4 闡述了這些狀態,和它們之間的轉換;它們的轉換會在下邊進行討論。
如 圖-5 所示,Raft 算法將時間劃分紅爲任意不一樣長度的任期(term)。任期用連續的數字進行表示。每個任期的開始都是一次選舉(election),就像 5.2節 所描述的那樣,一個或多個候選人會試圖成爲領導人。若是一個候選人贏得了選舉,它就會在該任期的剩餘時間擔任領導人。在某些狀況下,選票會被瓜分,有可能沒有選出領導人,那麼,將會開始另外一個任期,而且馬上開始下一次選舉。Raft 算法保證在給定的一個任期最少要有一個領導人。
不一樣的服務器可能會在任期內觀察到屢次不一樣的狀態轉換,在某些狀況下,一臺服務器可能看不到一次選舉或者一個完整的任期。任期在 Raft 中充當邏輯時鐘的角色,而且它們容許服務器檢測過時的信息,好比過期的領導人。每一臺服務器都存儲着一個當前任期的數字,這個數字會單調的增長。當服務器之間進行通訊時,會互相交換當前任期號;若是一臺服務器的當前任期號比其它服務器的小,則更新爲較大的任期號。若是一個候選人或者領導人意識到它的任期號過期了,它會馬上轉換爲追隨者狀態。若是一臺服務器收到的請求的任期號是過期的,那麼它會拒絕這次請求。
Raft 中的服務器經過遠程過程調用(RPC)來通訊,基本的 Raft 一致性算法僅須要 2 種 RPC。RequestVote RPC 是候選人在選舉過程當中觸發的(5.2節),AppendEntries RPC 是領導人觸發的,爲的是複製日誌條目和提供一種心跳(heartbeat)機制(5.3節)。第7章加入了第三種 RPC 來在各個服務器之間傳輸快照(snapshot)。若是服務器沒有及時收到 RPC 的響應,它們會重試,而且它們可以並行的發出 RPC 來得到最好的性能。
Raft 使用一種心跳機制(heartbeat)來觸發領導人的選取。當服務器啓動時,它們會初始化爲追隨者。一太服務器會一直保持追隨者的狀態只要它們可以收到來自領導人或者候選人的有效 RPC。領導人會向全部追隨者週期性發送心跳(heartbeat,不帶有任何日誌條目的 AppendEntries RPC)來保證它們的領導人地位。若是一個追隨者在一個週期內沒有收到心跳信息,就叫作選舉超時(election timeout),而後它就會假定沒有可用的領導人而且開始一次選舉來選出一個新的領導人。
爲了開始選舉,一個追隨者會自增它的當前任期而且轉換狀態爲候選人。而後,它會給本身投票而且給集羣中的其餘服務器發送 RequestVote RPC。一個候選人會一直處於該狀態,直到下列三種情形之一發生:
這些情形會在下面的章節中分別討論。
一個候選人若是在一個任期內收到了來自集羣中大多數服務器的投票就會贏得選舉。在一個任期內,一臺服務器最多能給一個候選人投票,按照先到先服務原則(first-come-first-served)(注意:在 5.4節 針對投票添加了一個額外的限制)。大多數原則使得在一個任期內最多有一個候選人能贏得選舉(表-3 中提到的選舉安全原則)。一旦有一個候選人贏得了選舉,它就會成爲領導人。而後它會像其餘服務器發送心跳信息來創建本身的領導地位而且組織新的選舉。
當一個候選人等待別人的選票時,它有可能會收到來自其餘服務器發來的聲明其爲領導人的 AppendEntries RPC。若是這個領導人的任期(包含在它的 RPC 中)比當前候選人的當前任期要大,則候選人認爲該領導人合法,而且轉換本身的狀態爲追隨者。若是在這個 RPC 中的任期小於候選人的當前任期,則候選人會拒絕這次 RPC, 繼續保持候選人狀態。
第三種情形是一個候選人既沒有贏得選舉也沒有輸掉選舉:若是許多追隨者在同一時刻都成爲了候選人,選票會被分散,可能沒有候選人能得到大多數的選票。當這種情形發生時,每個候選人都會超時,而且經過自增任期號和發起另外一輪 RequestVote RPC 來開始新的選舉。然而,若是沒有其它手段來分配選票的話,這種情形可能會無限的重複下去。
Raft 使用隨機的選舉超時時間來確保第三種情形不多發生,而且可以快速解決。爲了防止在一開始是選票就被瓜分,選舉超時時間是在一個固定的間隔內隨機選出來的(例如,150~300ms)。這種機制使得在大多數狀況下只有一個服務器會率先超時,它會在其它服務器超時以前贏得選舉而且向其它服務器發送心跳信息。一樣的機制被用於選票一開始被瓜分的狀況下。每個候選人在開始一次選舉的時候會重置一個隨機的選舉超時時間,在超時進行下一次選舉以前一直等待。這可以減少在新的選舉中一開始選票就被瓜分的可能性。9.3節 展現了這種方法可以快速的選出一個領導人。
選舉是一個理解性引導咱們設計替代算法的一個例子。最開始時,咱們計劃使用一種排名系統:給每個候選人分配一個惟一的排名,用於在競爭的候選人之中選擇領導人。若是一個候選人發現了另外一個比它排名高的候選人,那麼它會回到追隨者的狀態,這樣排名高的候選人會很容易地贏得選舉。可是咱們發現這種方法在可用性方面有一點問題(一個低排名的服務器在高排名的服務器宕機後,須要等待超時才能再次成爲候選人,可是若是它這麼作的太快,它能重置選舉領帶人的過程)。咱們對這個算法作了屢次調整,可是每次調整後都會出現一些新的問題。最終咱們認爲隨機重試的方法是更明確而且更易於理解的。
一旦選出了領導人,它就開始接收客戶端的請求。每個客戶端請求都包含一條須要被複制狀態機(replicated state machine)執行的命令。領導人把這條命令做爲新的日誌條目加入到它的日誌中去,而後並行的向其餘服務器發起 AppendEntries RPC ,要求其它服務器複製這個條目。當這個條目被安全的複製以後(下面的部分會詳細闡述),領導人會將這個條目應用到它的狀態機中而且會向客戶端返回執行結果。若是追隨者崩潰了或者運行緩慢或者是網絡丟包了,領導人會無限的重試 AppendEntries RPC(甚至在它向客戶端響應以後)知道全部的追隨者最終存儲了全部的日誌條目。
日誌就像 圖-6 所示那樣組織的。每一個日誌條目存儲着一條被狀態機執行的命令和當這條日誌條目被領導人接收時的任期號。日誌條目中的任期號用來檢測在不一樣服務器上日誌的不一致性,而且能確保 圖-3 中的一些特性。每一個日誌條目也包含一個整數索引來表示它在日誌中的位置。
領導人決定何時將日誌條目應用到狀態機是安全的;這種條目被稱爲可被提交(commited)。Raft 保證可被提交(commited)的日誌條目是持久化的而且最終會被全部可用的狀態機執行。一旦被領導人建立的條目已經複製到了大多數的服務器上,這個條目就稱爲可被提交的(例如,圖-6中的7號條目)。領導人日誌中以前的條目都是可被提交的(commited),包括由以前的領導人建立的條目。5.4節將會討論當領導人更替以後這條規則的應用問題的細節,而且也討論了這種提交方式是安全的。領導人跟蹤記錄它所知道的被提交條目的最大索引值,而且這個索引值會包含在以後的 AppendEntries RPC 中(包括心跳 heartbeat 中),爲的是讓其餘服務器都知道這條條目已經提交。一旦一個追隨者知道了一個日誌條目已經被提交,它會將該條目應用至本地的狀態機(按照日誌順序)。
咱們設計了 Raft 日誌機制來保證不一樣服務器上日誌的一致性。這樣作不只簡化了系統的行爲使得它更可預測,而且也是保證安全性不可或缺的一部分。Raft 保證如下特性,而且也保證了 表-3 中的日誌匹配原則(Log Matching Property):
第一條特性源於領導人在一個任期裏在給定的一個日誌索引位置最多建立一條日誌條目,同時該條目在日誌中的位置也歷來不會改變。第二條特性源於 AppendEntries 的一個簡單的一致性檢查。當發送一個 AppendEntries RPC 時,領導人會把新日誌條目緊接着以前的條目的索引位置和任期號都包含在裏面。若是追隨者沒有在它的日誌中找到相同索引和任期號的日誌,它就會拒絕新的日誌條目。這個一致性檢查就像一個概括步驟:一開始空的日誌的狀態必定是知足日誌匹配原則的,一致性檢查保證了當日誌添加時的日誌匹配原則。所以,只要 AppendEntries 返回成功的時候,領導人就知道追隨者們的日誌和它的是一致的了。
在通常狀況下,領導人和追隨者們的日誌保持一致,所以 AppendEntries 一致性檢查一般不會失敗。然而,領導人的崩潰會致使日誌不一致(舊的領導人可能沒有徹底複製完日誌中的全部條目)。這些不一致會致使一系列領導人和追隨者崩潰。圖-7 闡述了一些追隨者可能和新的領導人日誌不一樣的狀況。一個追隨者可能會丟失掉領導人上的一些條目,也有可能包含一些領導人沒有的條目,也有可能二者都會發生。丟失的或者多出來的條目可能會持續多個任期。
在 Raft 算法中,領導人經過強制追隨者們複製它的日誌來處理日誌的不一致。這就意味着,在追隨者上的衝突日誌會被領導者的日誌覆蓋。5.4節會說明當添加了一個額外的限制以後這是安全的。
爲了使得追隨者的日誌同本身的一致,領導人須要找到追隨者同它的日誌一致的地方,而後刪除追隨者在該位置以後的條目,而後將本身在該位置以後的條目發送給追隨者。這些操做都在 AppendEntries RPC 進行一致性檢查時完成。領導人給每個追隨者維護了一個nextIndex
,它表示領導人將要發送給該追隨者的下一條日誌條目的索引。當一個領導人開始掌權時,它會將nextIndex
初始化爲它的最新的日誌條目索引數+1(圖-7 中的 11)。若是一個追隨者的日誌和領導者的不一致,AppendEntries 一致性檢查會在下一次 AppendEntries RPC 時返回失敗。在失敗以後,領導人會將nextIndex
遞減而後重試 AppendEntries RPC。最終nextIndex
會達到一個領導人和追隨者日誌一致的地方。這時,AppendEntries 會返回成功,追隨者中衝突的日誌條目都被移除了,而且添加所缺乏的上了領導人的日誌條目。一旦 AppendEntries 返回成功,追隨者和領導人的日誌就一致了,這樣的狀態會保持到該任期結束。
若是須要的話,算法還能夠進行優化來減小 AppendEntries RPC 失敗的次數。例如,當拒絕了一個 AppendEntries 請求,追隨者能夠記錄下衝突日誌條目的任期號和本身存儲那個任期的最先的索引。經過這些信息,領導人可以直接遞減nextIndex
跨過那個任期內全部的衝突條目;這樣的話,一個衝突的任期須要一次 AppendEntries RPC,而不是每個衝突條目須要一次 AppendEntries RPC。在實踐中,咱們懷疑這種優化是不是必要的,由於AppendEntries 一致性檢查不多失敗而且也不太可能出現大量的日誌條目不一致的狀況。
經過這種機制,一個領導人在掌權時不須要採起另外特殊的方式來恢復日誌的一致性。它只須要使用一些常規的操做,經過響應 AppendEntries 一致性檢查的失敗能使得日誌自動的趨於一致。一個領導人歷來不會覆蓋或者刪除本身的日誌(表-3 中的領導人只增長原則)。
這個日誌複製機制展現了在第2章中闡述的所但願的一致性特性:Raft 可以接受,複製而且應用新的日誌條目只要大部分的服務器是正常的。在一般狀況下,一條新的日誌條目能夠在一輪 RPC 內完成在集羣的大多數服務器上的複製;而且一個速度很慢的追隨者並不會影響總體的性能。
以前的章節中討論了 Raft 算法是如何進行領導選取和複製日誌的。然而,到目前爲止這個機制還不能保證每個狀態機能按照相同的順序執行一樣的指令。例如,當領導人提交了若干日誌條目的同時一個追隨者可能宕機了,以後它又被選爲了領導人而後用新的日誌條目覆蓋掉了舊的那些,最後,不一樣的狀態機可能執行不一樣的命令序列。
這一節經過在領帶人選取部分加入了一個限制來完善了 Raft 算法。這個限制可以保證對於固定的任期,任何的領導人都擁有以前任期提交的所有日誌條目(表-3 中的領導人徹底原則)。有了這一限制,日誌提交的規則就更清晰了。最後,咱們提出了對於領導人徹底原則的簡單證實而且展現了它是如何修正複製狀態機的行爲的。
在全部的以領導人爲基礎的一致性算法中,領導人最終必需要存儲所有已經提交的日誌條目。在一些一致性算法中,例如:Viewstamped Replication,即便一開始沒有包含所有已提交的條目也能夠被選爲領導人。這些算法都有一些另外的機制來保證找到丟失的條目並將它們傳輸給新的領導人,這個過程要麼在選舉過程當中完成,要麼在選舉以後當即開始。不幸的是,這種方式大大增長了複雜性。Raft 使用了一種更簡單的方式來保證在新的領導人開始選舉的時候在以前任期的全部已提交的日誌條目都會出如今上邊,而不須要將這些條目傳送給領導人。這就意味着日誌條目只有一個流向:從領導人流向追隨者。領導人永遠不會覆蓋已經存在的日誌條目。
Raft 使用投票的方式來阻止沒有包含所有日誌條目的服務器贏得選舉。一個候選人爲了贏得選舉必需要和集羣中的大多數進行通訊,這就意味着每一條已經提交的日誌條目最少在其中一臺服務器上出現。若是候選人的日誌至少和大多數服務器上的日誌同樣新(up-to-date,這個概念會在下邊有詳細介紹),那麼它必定包含有所有的已經提交的日誌條目。RequestVote RPC 實現了這個限制:這個 RPC(遠程過程調用)包括候選人的日誌信息,若是它本身的日誌比候選人的日誌要新,那麼它會拒絕候選人的投票請求。
Raft 經過比較日誌中最後一個條目的索引和任期號來決定兩個日誌哪個更新。若是兩個日誌的任期號不一樣,任期號大的更新;若是任期號相同,更長的日誌更新。
正如 5.3節 中描述的那樣,只要一個日誌條目被存在了在多數的服務器上,領導人就知道當前任期就能夠提交該條目了。若是領導人在提交以前就崩潰了,以後的領導人會試着繼續完成對日誌的複製。然而,領導人並不能判定存儲在大多數服務器上的日誌條目必定在以前的任期中被提交了。圖-8 說明了一種狀況,一條存儲在了大多數服務器上的日誌條目仍然被新上任的領導人覆蓋了。
爲了消除 圖-8 中描述的問題,Raft 歷來不會經過計算複製的數目來提交以前人氣的日誌條目。只有領導人當前任期的日誌條目才能經過計算數目來進行提交。一旦當前任期的日誌條目以這種方式被提交,那麼因爲日誌匹配原則(Log Matching Property),以前的日誌條目也都會被間接的提交。在某些狀況下,領導人能夠安全的知道一個老的日誌條目是否已經被提交(例如,經過觀察該條目是否存儲到全部服務器上),可是 Raft 爲了簡化問題使用了一種更加保守的方法。
由於當領導人從以前任期複製日誌條目時日誌條目保留了它們最開始的任期號,因此這使得 Raft 在提交規則中增長了額外的複雜性。在其餘的一致性算法中,若是一個新的領導人要從以前的任期中複製日誌條目,它必需要使用當前的新任期號。Raft 的方法使得判斷日誌更加容易,由於它們全程都保持着一樣的任期號。另外,和其它的一致性算法相比,Raft 算法中的新領導人會發送更少的以前任期的日誌條目(其餘算法必需要發送冗餘的日誌條目而且在它們被提交以前來從新排序)。
給出了完整的 Raft 算法,如今咱們可以更精確的論證領導人徹底原則(Leader Completeness)(這基於 9.2節 提出的安全性證實)。咱們假定領導人徹底原則是不成立的,而後推導出矛盾。假定任期 T 的領導人 leaderT在它的任期提交了一個日誌條目,可是這條日誌條目並無存儲在以後的任期中的領導人上。咱們設大於 T 的最小的任期 U 的領導人(leaderU) 沒有存儲這條日誌條目。
在 leaderU 選舉時必定沒有那條被提交的日誌條目(領導人歷來不會刪除或者覆蓋日誌條目)。
leaderT 複製了這個條目到集羣的大多數的服務器上。所以,只是有一臺服務器(投票者)即接收了來自 leaderT 的日誌條目而且給 leaderU 投票,就像 圖-9 中所示那樣。這個投票者是產生矛盾的關鍵。
投票者必須在給 leaderU 投票以前接收來自 leaderT 的日誌條目;不然它會拒絕來自 leaderT 的 AppendEntries 請求(它的當前任期會比 T 要大)。
投票者會在它給 leaderU 投票時存儲那個條目,由於任何中間的領導人都保有該條目(基於假設),領導人歷來不會移除這個條目,而且追隨者也只會在和領導人衝突時纔會移除日誌條目。
投票者給 leaderU 投票了,因此 leaderU 的日誌必須和投票者的同樣新。這就致使了一個矛盾。
首先,若是投票者和 leaderU 最後一條日誌條目的任期號相同,那麼 leaderU 的日誌必定和投票者的同樣長,所以它的日誌包含所有投票者的日誌條目。這是矛盾的,由於在假設中投票者和 leaderU 包含的已提交條目是不一樣的。
除此以外, leaderU 的最後一條日誌的任期號必定比投票者的大。另外,它也比 T 要大,由於投票者的最後一條日誌條目的任期號最小也要是 T(它包含了全部任期 T 提交的日誌條目)。建立 leaderU 最後一條日誌條目的上一任領導人必須包含已經提交的日誌條目(基於假設)。那麼,根據日誌匹配原則(Log Matching),leaderU 也必定包含那條提交的日誌條目,這也是矛盾的。
這時就完成了矛盾推導。所以,全部比任期 T 大的領導人必定包含全部在任期 T 提交的日誌條目。
日誌匹配原則(Log Matching)保證了將來的領導人也會包含被間接提交的日誌條目,就像 圖-8 中(d)時刻索引爲2的條目。
經過給出了 領導人徹底原則(Leader Completeness),咱們可以證實 表-3 中的狀態機安全原則(State Machine Safety),狀態機安全原則(State Machine Safety)講的是若是一臺服務器將給定索引上的日誌條目應用到了它本身的狀態機上,其它服務器的同一索引位置不可能應用的是其它條目。在一個服務器應用一條日誌條目到它本身的狀態機中時,它的日誌必須和領導人的日誌在該條目和以前的條目上相同,而且已經被提交。如今咱們來考慮在任何一個服務器應用一個指定索引位置的日誌的最小任期;日誌徹底特性(Log Completeness Property)保證擁有更高任期號的領導人會存儲相同的日誌條目,因此以後的任期裏應用某個索引位置的日誌條目也會是相同的值。所以,狀態機安全特性是成立的。
最後,Raft 算法須要服務器按照日誌中索引位置順序應用日誌條目。和狀態機安全特性結合起來看,這就意味着全部的服務器會應用相同的日誌序列集到本身的狀態機中,而且是按照相同的順序。
截止到目前,咱們只討論了領導人崩潰的問題。追隨者和候選人崩潰的問題解決起來要比領導人崩潰要簡單得多,這二者崩潰的處理方式是同樣的。若是一個追隨者或者候選人崩潰了,那麼以後的發送給它的 RequestVote RPC 和 AppendEntries RPC 會失敗。Raft 經過無限的重試來處理這些失敗;若是崩潰的服務器重啓了,RPC 就會成功完成。若是一個服務器在收到了 RPC 以後可是在響應以前崩潰了,那麼它會在重啓以後再次收到同一個 RPC。由於 Raft 中的 RPC 都是冪等的,所以不會有什麼問題。例如,若是一個追隨者收到了一個已經包含在它的日誌中的 AppendEntries 請求,它會忽視這個新的請求。
咱們對於 Raft 的要求之一就是安全性不依賴於時序(timing):系統不能僅僅由於一些事件發生的比預想的快一些或慢一些就產生錯誤。然而,可用性(系統能夠及時響應客戶端的特性)不可避免的要依賴時序。例如,若是消息交換在服務器崩潰時花費更多的時間,候選人不會等待太長的時間來贏得選舉;沒有一個穩定的領導人,Raft 將沒法工做。
領導人選取是 Raft 中對時序要求最關鍵的地方。Raft 會選出而且保持一個穩定的領導人只有系統知足下列時序要求(timing requirement):
在這個不等式中,broadcastTime
指的是一臺服務器並行的向集羣中的其餘服務器發送 RPC 而且收到它們的響應的平均時間;electionTimeout
指的就是在 5.2節 描述的選舉超時時間;MTBF
指的是單個服務器發生故障的間隔時間的平均數。broadcastTime
應該比electionTimeout
小一個數量級,爲的是使領導人可以持續發送心跳信息(heartbeat)來阻止追隨者們開始選舉;根據已經給出的隨機化選舉超時時間方法,這個不等式也使得瓜分選票的狀況變成不可能。electionTimeout
也要比MTBF
小几個數量級,爲的是使得系統穩定運行。當領導人崩潰時,整個大約會在electionTimeout
的時間內不可用;咱們但願這種狀況僅佔所有時間的很小的一部分。
broadcastTime
和MTBF
是由系統決定的性質,可是electionTimeout
是咱們必須作出選擇的。Raft 的 RPC 須要接收方將信息持久化的保存到穩定存儲中去,因此廣播時間大約是 0.5 毫秒到 20 毫秒,這取決於存儲的技術。所以,electionTimeout
通常在 10ms 到 500ms 之間。大多數的服務器的MTBF
都在幾個月甚至更長,很容易知足這個時序需求。
截止到目前,咱們都假定集羣的配置(加入到一致性算法的服務器集合)是固定的。在實際中,咱們會常常更改配置,例如,替換掉那些崩潰的機器或者更改複製級別。雖然經過關閉整個集羣,升級配置文件,而後重啓整個集羣也能夠解決這個問題,可是這回致使在更改配置的過程當中,整個集羣不可用。另外,若是存在須要手工操做,那麼就會有操做失誤的風險。爲了不這些問題,咱們決定採用自動改變配置而且把這部分加入到了 Raft 一致性算法中。
爲了讓配置修改機制可以安全,那麼在轉換的過程當中在任什麼時候間點兩個領導人不能再同一個任期被同時選爲領導人。不幸的是,服務器集羣從舊的配置直接升級到新的配置的任何方法都是不安全的,一次性自動的轉換全部服務器是不可能的,因此集羣能夠在轉換的過程當中劃分紅兩個單獨的組(如 圖-10 所示)。
爲了保證安全性,集羣配置的調整必須使用兩階段(two-phase)方法。有許多種實現兩階段方法的實現。例如,一些系統在第一個階段先把舊的配置設爲無效使得它沒法處理客戶端請求,而後在第二階段啓用新的配置。在 Raft 中,集羣先切換到一個過渡配置,咱們稱其爲共同一致(joint consensus);一旦共同一致被提交了,而後系統再切換到新的配置。共同一致是舊的配置和新的配置的組合:
日誌條目被複制給集羣中新、老配置的全部服務器。
新、老配置的服務器都能成爲領導人。
須要分別在兩種配置上得到大多數的支持才能達成一致(針對選舉和提交)
共同一致容許獨立的服務器在不影響安全性的前提下,在不一樣的時間進行配置轉換過程。此外,共同一致可讓集羣在配置轉換的過程當中依然可以響應服務器請求。
集羣配置在複製日誌中用特殊的日誌條目來存儲和通訊;圖-11 展現了配置變動的過程。當一個領導人接收到一個改變配置 Cold 爲 Cnew 的請求,它會爲了共同一致之前面描述的日誌條目和副本的形式將配置存儲起來(圖中的 Cold,new)。一旦一個服務器將新的配置日誌條目增長到它的日誌中,它就會用這個配置來作出將來全部的決定(服務器老是使用最新的配置,不管它是否已經被提交)。這意味着領導人要使用 Cold,new 的規則來決定日誌條目 Cold,new 何時須要被提交。若是領導人崩潰了,被選出來的新領導人多是使用 Cold 配置也多是 Cold,new 配置,這取決於贏得選舉的候選人是否已經接收到了 Cold,new 配置。在任何狀況下, Cnew 配置在這一時期都不會單方面的作出決定。
一旦 Cold,new 被提交,那麼不管是 Cold 仍是 Cnew,在沒有通過他人批准的狀況下都不可能作出決定,而且領導人徹底特性(Leader Completeness Property)保證了只有擁有 Cold,new 日誌條目的服務器纔有可能被選舉爲領導人。這個時候,領導人建立一條關於 Cnew 配置的日誌條目並複製給集羣就是安全的了。另外,每一個服務器在收到新的配置的時候就會當即生效。當新的配置在 Cnew的規則下被提交,舊的配置就變得可有可無,同時不使用新的配置的服務器就能夠被關閉了。如 圖-11,Cold 和 Cnew 沒有任何機會同時作出單方面的決定;這就保證了安全性。
針對從新配置提出了三個問題。第一個問題是一開始的時候新的服務器可能沒有任何日誌條目。若是它們在這個狀態下加入到集羣中,那麼它們須要一段時間來更新追趕,在這個階段它們還不能提交新的日誌條目。爲了不這種可用性的間隔時間,Raft 在配置更新的時候使用了一種額外的階段,在這個階段,新的服務器以沒有投票權的身份加入到集羣中來(領導人複製日誌給他們,可是不把它們考慮到大多數中)。一旦新的服務器追遇上了集羣中的其它機器,從新配置能夠像上面描述的同樣處理。
第二個問題是,集羣的領導人可能不是新配置的一員。在這種狀況下,領導人就會在提交了 Cnew日誌以後退位(回到跟隨者狀態)。這意味着有這樣的一段時間,領導人管理着集羣,可是不包括本身;它複製日誌可是不把它本身看做是大多數之一。當 Cnew 被提交時,會發生領導人過渡,由於這時是新的配置能夠獨立工做的最先的時間點(老是可以在 Cnew 配置下選出新的領導人)。在此以前,可能只能從 Cold 中選出領導人。
第三個問題是,移除不在 Cnew 中的服務器可能會擾亂集羣。這些服務器將不會再接收到心跳(heartbeat),因此當選舉超時時,它們就會進行新的選舉過程。它們會發送帶有新的任期號的 RequestVote RPC,這樣會致使當前的領導人回退成跟隨者狀態。新的領導人最終會被選出來,可是被移除的服務器將會再次超時,而後這個過程會再次重複,致使總體可用性大幅下降。
爲了不這個問題,當服務器確認當前領導人存在時,服務器會忽略 RequestVote RPC。特別的,當服務器在當前最小選舉超時時間內收到一個 RequestVote RPC,它不會更新當前的任期號或者投出選票。這不會影響正常的選舉,每一個服務器在開始一次選舉以前,至少等待一個最小選舉超時時間。然而,這有利於避免被移除的服務器擾亂:若是領導人可以發送心跳給集羣,那麼它就不會被更大的任期號廢除。
Raft 產生的日誌在持續的正常操做中不斷增加,可是在實際的系統中,它不會無限的增加下去。隨着日誌的不斷增加,它會佔據愈來愈多的空間而且花費更多的時間重置。若是沒有一個機制使得它可以廢棄在日誌中不斷累積的過期的信息就會引發可用性問題。
快照(snapshot)是最簡單的壓縮方式。在快照中,所有的當前系統狀態都被寫入到快照中,存儲到持久化的存儲中,而後在那個時刻以前的所有日誌均可以被丟棄。在 Chubby 和 ZooKeeper 中都使用了快照技術,這一章的剩下的部分會介紹 Raft 中使用的快照技術。
增量壓縮(incremental approaches)的方法,例如日誌清理(log cleaning)或者日誌結構合併樹(log-structured merge trees),都是可行的。這些方法每次只對一小部分數據進行操做,這樣就分散了壓縮的負載壓力。首先,他們先選擇一個已經積累的大量已經被刪除或者被覆蓋對象的區域,而後重寫那個區域還活躍的對象,以後釋放那個區域。和簡單操做整個數據集合的快照相比,須要增長複雜的機制來實現。狀態機能夠使用和快照相同的接口來實現 LSM tree ,可是日誌清除方法就須要修改 Raft 了。
圖-12 展現了 Raft 中快照的基礎思想。每一個服務器獨立的建立快照,只包括已經被提交的日誌。主要的工做包括將狀態機的狀態寫入到快照中。Raft 也將一些少許的元數據包含到快照中:最後被包含的索引(last included index)指的是被快照取代的最後的條目在日誌中的索引值(狀態機最後應用的日誌),最後被包含的任期(last included term)指的是該條目的任期號。保留這些數據是爲了支持快照前的第一個條目的附加日誌請求時的一致性檢查,由於這個條目須要最後的索引值和任期號。爲了支持集羣成員更新(第 6 章),快照中也將最後的一次配置做爲最後一個條目存下來。一旦服務器完成一次快照,他就能夠刪除最後索引位置以前的全部日誌和快照了。
儘管一般服務器都是獨立的建立快照,可是領導人必須偶爾的發送快照給一些落後的跟隨者。這一般發生在當領導人已經丟棄了下一條須要發送給跟隨者的日誌條目的時候。幸運的是這種狀況不是常規操做:一個與領導人保持同步的跟隨者一般都會有這個條目。然而一個運行很是緩慢的跟隨者或者新加入集羣的服務器(第 6 章)將不會有這個條目。這時讓這個跟隨者更新到最新的狀態的方式就是經過網絡把快照發送給它們。
* 安裝快照 RPC(InstallSnapshot RPC)*
在領導人發送快照給跟隨者時使用調用。領導人老是按順序發送。
參數 | 描述 |
---|---|
term | 領導人的任期 |
leaderId | 爲了追隨者能重定向到客戶端 |
lastIncludedIndex | 快照中包含的最後日誌條目的索引值 |
lastIncludedTerm | 快照中包含的最後日誌條目的任期號 |
offset | 分塊在快照中的偏移量 |
data[] | 快照塊的原始數據 |
done | 若是是最後一塊數據則爲真 |
返回值 | 描述 |
term | currentTerm,用於領導人更新本身 |
接受者須要實現:
若是term < currentTerm
馬上回復
若是是第一個分塊(offset 爲 0)則建立新的快照
在指定的偏移量寫入數據
若是 done
爲 false,則回覆並繼續等待以後的數據
保存快照文件,丟棄全部存在的或者部分有着更小索引號的快照
若是現存的日誌擁有相同的最後任期號和索引值,則後面的數據繼續保留而且回覆
丟棄所有日誌
可以使用快照來恢復狀態機(而且裝載快照中的集羣配置)
在這種狀況下領導人使用一種叫作安裝快照(InstallSnapshot)的新的 RPC 來發送快照給太落後的跟隨者;見 表-13。當跟隨者經過這種 RPC 接收到快照時,它必須本身決定對於已經存在的日誌該如何處理。一般快照會包含沒有在接收者日誌中存在的信息。在這種狀況下,跟隨者直接丟棄它全部的日誌;這些會被快照所取代,可是可能會和沒有提交的日誌產生衝突。若是接收到的快照是本身日誌的前面部分(因爲網絡重傳或者錯誤),那麼被快照包含的條目將會被所有刪除,可是快照以後的條目必須是正確的和而且被保留下來。
這種快照的方式背離了 Raft 的強領導人原則(strong leader principle),由於跟隨者能夠在不知道領導人狀況下建立快照。可是咱們認爲這種背離是值得的。領導人的存在,是爲了解決在達成一致性的時候的衝突,可是在建立快照的時候,一致性已經達成,這時不存在衝突了,因此沒有領導人也是能夠的。數據依然是從領導人傳給跟隨者,只是跟隨者能夠從新組織它們的數據了。
咱們考慮過一種替代的基於領導人的快照方案,即只有領導人建立快照,而後發送給全部的跟隨者。可是這樣作有兩個缺點。第一,發送快照會浪費網絡帶寬而且延緩了快照處理的時間。每一個跟隨者都已經擁有了全部產生快照須要的信息,並且很顯然,本身從本地的狀態中建立快照比經過網絡接收別人發來的要經濟。第二,領導人的實現會更加複雜。例如,領導人須要發送快照的同時並行的將新的日誌條目發送給跟隨者,這樣纔不會阻塞新的客戶端請求。
還有兩個問題影響了快照的性能。首先,服務器必須決定何時應該建立快照。若是快照建立的過於頻繁,那麼就會浪費大量的磁盤帶寬和其餘資源;若是建立快照頻率過低,它就要承受耗盡存儲容量的風險,同時也增長了從日誌重建的時間。一個簡單的策略就是當日志大小達到一個固定大小的時候就建立一次快照。若是這個閾值設置的顯著大於指望的快照的大小,那麼快照對磁盤壓力的影響就會很小了。
第二個影響性能的問題就是寫入快照須要花費顯著的一段時間,而且咱們還不但願影響到正常操做。解決方案是經過寫時複製(copy-on-write)的技術,這樣新的更新就能夠被接收而不影響到快照。例如,具備函數式數據結構的狀態機自然支持這樣的功能。另外,操做系統的寫時複製技術的支持(如 Linux 上的 fork)能夠被用來建立完整的狀態機的內存快照(咱們的實現就是這樣的)。
這一節將介紹客戶端是如何和 Raft 進行交互的,包括客戶端是如何發現領導人的和 Raft 是如何支持線性化語義(linearizable semantics)的。這些問題對於全部基於一致性的系統都存在,而且 Raft 的解決方案和其餘的也差很少。
Raft 中的客戶端將全部請求發送給領導人。當客戶端啓動的時候,它會隨機挑選一個服務器進行通訊。若是客戶端第一次挑選的服務器不是領導人,那麼那個服務器會拒絕客戶端的請求而且提供它最近接收到的領導人的信息(附加條目請求包含了領導人的網絡地址)。若是領導人已經崩潰了,那麼客戶端的請求就會超時;客戶端以後會再次重試隨機挑選服務器的過程。
咱們 Raft 的目標是要實現線性化語義(linearizable semantics)(每一次操做當即執行,在它調用和收到回覆之間只執行一次)。可是,如上述所說,Raft 是能夠屢次執行同一條命令的:例如,若是領導人在提交了這條日誌以後,可是在響應客戶端以前崩潰了,那麼客戶端會和新的領導人重試這條指令,致使這條命令就被再次執行了。解決方案就是客戶端對於每一條指令都賦予一個惟一的序列號。而後,狀態機跟蹤每條指令最新的序列號和相應的響應。若是接收到一條指令,它的序列號已經被執行了,那麼就當即返回結果,而不從新執行指令。
只讀(read-only)的操做能夠直接處理而不須要記錄日誌。可是,在不增長任何限制的狀況下,這麼作可能會冒着返回過時數據(stale data)的風險,由於領導人響應客戶端請求時可能已經被新的領導人做廢了,可是它還不知道。線性化的讀操做必須不能返回過時數據,Raft 須要使用兩個額外的措施在不使用日誌的狀況下保證這一點。首先,領導人必須有關於被提交日誌的最新信息。領導人徹底原則(Leader Completeness Property)保證了領導人必定擁有全部已經被提交的日誌條目,可是在它任期開始的時候,它可能不知道哪些是已經被提交的。爲了知道這些信息,它須要在它的任期裏提交一條日誌條目。Raft 中經過領導人在任期開始的時候提交一個空白的沒有任何操做的日誌條目到日誌中去來進行實現。第二,領導人在處理只讀的請求以前必須檢查本身是否已經被廢除了(若是一個更新的領導人被選舉出來,它本身的信息就已通過期了)。Raft 中經過讓領導人在響應只讀請求以前,先和集羣中的大多數節點交換一次心跳(heartbeat)信息來處理這個問題。另外,領導人能夠依賴心跳機制來實現一種租約的機制,可是這種方法依賴時序來保證安全性(它假設時間偏差是有界的)。
咱們已經爲 RAMCloud 實現了 Raft 算法做爲存儲配置信息的複製狀態機的一部分,而且幫助 RAMCloud 協調故障轉移。這個 Raft 實現包含大約 2000 行 C++ 代碼,其中不包括測試、註釋和空行。這些代碼是開源的。同時也有大約 25 個其餘獨立的第三方的基於這篇論文草稿的開源實現,針對不一樣的開發場景。同時,不少公司已經部署了基於 Raft 的系統。
這一章會從三個方面來評估 Raft 算法:可理解性、正確性和性能。
爲了比較 Paxos 和 Raft 算法的可理解性,咱們針對高層次的本科生和研究生,在斯坦福大學的高級操做系統課程和加州大學伯克利分校的分佈式計算課程上,進行了一次學習的實驗。咱們分別拍了針對 Raft 和 Paxos 的視頻課程,並準備了相應的小測驗。Raft 的視頻講課覆蓋了這篇論文除了日誌壓縮以外的全部內容;Paxos 課程包含了足夠的資料來建立一個等價的複製狀態機,包括單決策 Paxos,多決策 Paxos,從新配置和一些實際系統須要的性能優化(例如領導人選舉)。小測驗測試一些對算法的基本理解和解釋一些示例。每一個學生都是看完第一個視頻,回答相應的測試,再看第二個視頻,回答相應的測試。大約有一半的學生先進行 Paxos 部分,而後另外一半先進行 Raft 部分,這是爲了說明二者獨立的區別從第一個算法處學來的經驗。咱們計算參加人員的每個小測驗的得分來看參與者是否對 Raft 的理解更好。
因素 | 消除偏見的手段 | 複習材料 |
---|---|---|
相同的講課質量 | 使用相同的講師。Paxos 的講義是基於以前在幾所大學中使用的材料的而且作了改進。Paxos 的講義要長 14% | 視頻 |
相同的測試難度 | 用難度給問題分組,在測試中成對出現 | 測驗 |
公平的打分 | 使用紅字標題。隨機順序打分,兩個測驗交替進行。 | 紅字標題 |
咱們儘量的使得 Paxos 和 Raft 的比較更加公平。這個實驗偏心 Paxos 表如今兩個方面:43 個參加者中有 15 我的在以前有一些 Paxos 的經驗,而且 Paxos 的視頻要長 14%。如表-1 總結的那樣,咱們採起了一些措施來減輕這種潛在的偏見。咱們全部的材料均可供審查。
參加者平均在 Raft 的測驗中比 Paxos 高 4.9 分(總分 60,那麼 Raft 的平均得分是 25.7,而 Paxos 是 20.8);圖-14 展現了每一個參與者的得分。一對 t -測試代表,擁有 95% 的可信度,真實的 Raft 分數分佈至少比 Paxos 高 2.5 分。
咱們也創建了一個線性迴歸模型來預測一個新的學生的測驗成績,基於如下三個因素:他們使用的是哪一個小測驗,以前對 Paxos 的經驗,和學習算法的順序。模型顯示,對小測驗的選擇會產生 12.5 分的差異在對 Raft 的好感度上。這顯著的高於以前的 4.9 分,由於不少學生在以前都已經有了對於 Paxos 的經驗,這至關明顯的幫助 Paxos,對 Raft 就沒什麼太大影響了。可是奇怪的是,模型預測對於先進行 Paxos 小測驗的人而言,Raft 的小測驗得分會比 Paxos 低 6.3 分;咱們不知道爲何,但這在統計學上是這樣的。
咱們同時也在測驗以後調查了參與者,他們認爲哪一個算法更加容易實現和解釋;這個的結果在圖-15 上。壓倒性的結果代表 Raft 算法更加容易實現和解釋(41 人中的 33個)。可是,這種本身報告的結果不如參與者的成績更加可信,而且參與者可能由於咱們的 Raft 更加易於理解的假說而產生偏見。
關於 Raft 用戶學習有一個更加詳細的討論,詳見http://ramcloud.stanford.edu/ ̃ongaro/thesis.pdf
在第5章,咱們已經進行了一個正式的說明,和對一致性機制的安全性證實。這個正式說明經過 TLA+ 讓 表-2 中的信息很是清晰。它大約有 400 行而且充當了證實的主題。同時對於任何想實現的人也是十分有用的。咱們很是機械的經過 TLA 證實系統證實了日誌徹底特性(Log Completeness Property)。然而,這個證實依賴的約束前提尚未被機械證實(例如,咱們尚未證實這個說明中的類型安全 type safety)。並且,咱們已經寫了一個非正式的證實關於狀態機安全性質是完備的,而且是至關清晰的(大約 3500 個詞)。
Raft 和其餘一致性算法例如 Paxos 有着差很少的性能。在性能方面,最重要的關注點是,當領導人被選舉成功時,何時複製新的日誌條目。Raft 經過不多數量的消息包(一輪從領導人到集羣大多數機器的消息)就達成了這個目的。同時,進一步提高 Raft 的性能也是可行的。例如,很容易經過支持批量操做和管道操做來提升吞吐量和下降延遲。對於其餘一致性算法已經提出過不少性能優化方案;其中有不少也能夠應用到 Raft 中來,可是咱們暫時把這個問題放到將來的工做中去。
咱們使用咱們本身的 Raft 實現來衡量 Raft 領導人選舉的性能而且回答如下兩個問題。首先,領導人選舉的過程收斂是否快速?第二,在領導人宕機以後,最小的系統宕機時間是多久?
爲了衡量領導人選舉,咱們反覆的使一個擁有五個節點的服務器集羣的領導人宕機,並計算須要多久才能發現領導人已經宕機並選出一個新的領導人(見圖-16)。爲了構建一個最壞的場景,在每一的嘗試裏,服務器都有不一樣長度的日誌,意味着有些候選人是沒有成爲領導人的資格的。另外,爲了促成選票瓜分的狀況,咱們的測試腳本在終止領導人以前同步的發送了一次心跳廣播(這大約和領導人在崩潰前複製一個新的日誌給其餘機器很像)。領導人均勻的隨機的在心跳間隔裏宕機,也就是最小選舉超時時間的一半。所以,最小宕機時間大約就是最小選舉超時時間的一半。
圖-16 上面的圖表代表,只須要在選舉超時時間上使用不多的隨機化就能夠大大避免選票被瓜分的狀況。在沒有隨機化的狀況下,在咱們的測試裏,選舉過程因爲太多的選票瓜分的狀況每每都須要花費超過 10 秒鐘。僅僅增長 5 毫秒的隨機化時間,就大大的改善了選舉過程,如今平均的宕機時間只有 287 毫秒。增長更多的隨機化時間能夠大大改善最壞狀況:經過增長 50 毫秒的隨機化時間,最壞的完成狀況(1000 次嘗試)只要 513 毫秒。
圖-16 中下面的圖顯示,經過減小選舉超時時間能夠減小系統的宕機時間。在選舉超時時間爲 12-24 毫秒的狀況下,只須要平均 35 毫秒就能夠選舉出新的領導人(最長的一次花費了 152 毫秒)。然而,進一步下降選舉超時時間的話就會違反 Raft 的時間不等式需求:在選舉新領導人以前,領導人就很難發送完心跳包。這會致使沒有意義的領導人改變並下降了系統總體的可用性。咱們建議使用更爲保守的選舉超時時間,好比 150-300 毫秒;這樣的時間不大可能致使沒有意義的領導人改變,並且依然提供不錯的可用性。
已經有不少關於一致性算法的工做被髮表出來,其中不少均可以歸到下面的類別中:
Raft 和 Paxos 最大的不一樣之處就在於 Raft 的強領導特性:Raft 使用領導人選舉做爲一致性協議裏必不可少的部分,而且將盡量多的功能集中到了領導人身上。這樣就能夠使得算法更加容易理解。例如,在 Paxos 中,領導人選舉和基本的一致性協議是正交的:領導人選舉僅僅是性能優化的手段,並且不是一致性所必需要求的。可是,這樣就增長了多餘的機制:Paxos 同時包含了針對基本一致性要求的兩階段提交協議和針對領導人選舉的獨立的機制。相比較而言,Raft 就直接將領導人選舉歸入到一致性算法中,並做爲兩階段一致性的第一步。這樣就減小了不少機制。
像 Raft 同樣,VR 和 ZooKeeper 也是基於領導人的,所以他們也擁有一些 Raft 的優勢。可是,Raft 比 VR 和 ZooKeeper 擁有更少的機制由於 Raft 儘量的減小了非領導人的功能。例如,Raft 中日誌條目都遵循着從領導人發送給其餘人這一個方向:附加條目 RPC 是向外發送的。在 VR 中,日誌條目的流動是雙向的(領導人能夠在選舉過程當中接收日誌);這就致使了額外的機制和複雜性。根據 ZooKeeper 公開的資料看,它的日誌條目也是雙向傳輸的,可是它的實現更像 Raft。
和上述咱們說起的其餘基於一致性的日誌複製算法中,Raft 的消息類型更少。例如,咱們數了一下 VR 和 ZooKeeper 使用的用來基本一致性須要和成員改變的消息數(排除了日誌壓縮和客戶端交互,由於這些都比較獨立且和算法關係不大)。VR 和 ZooKeeper 都分別定義了 10 中不一樣的消息類型,相對的,Raft 只有 4 中消息類型(兩種 RPC 請求和對應的響應)。Raft 的消息都稍微比其餘算法的要信息量大,可是都很簡單。另外,VR 和 ZooKeeper 都在領導人改變時傳輸了整個日誌;因此爲了可以實踐中使用,額外的消息類型就很必要了。
Raft 的強領導人模型簡化了整個算法,可是同時也排斥了一些性能優化的方法。例如,平等主義 Paxos (EPaxos)在某些沒有領導人的狀況下能夠達到很高的性能。平等主義 Paxos 充分發揮了在狀態機指令中的交換性。任何服務器均可以在一輪通訊下就提交指令,除非其餘指令同時被提出了。然而,若是指令都是併發的被提出,而且互相之間不通訊溝通,那麼 EPaxos 就須要額外的一輪通訊。由於任何服務器均可以提交指令,因此 EPaxos 在服務器之間的負載均衡作的很好,而且很容易在 WAN 網絡環境下得到很低的延遲。可是,他在 Paxos 上增長了很是明顯的複雜性。
一些集羣成員變換的方法已經被提出或者在其餘的工做中被實現,包括 Lamport 的原始的討論,VR 和 SMART。咱們選擇使用共同一致(joint consensus)的方法由於它對一致性協議的其餘部分影響很小,這樣咱們只須要不多的一些機制就能夠實現成員變換。Raft 沒有采用 Lamport 的基於 α 的方法是由於它假設在沒有領導人的狀況下也能夠達到一致性。和 VR 和 SMART 相比較,Raft 的從新配置算法能夠在不限制正常請求處理的狀況下進行;相比較而言,VR 須要中止全部的處理過程,SMART 引入了一個和 α 相似的方法,限制了請求處理的數量。和 VR、SMART 比較而言,Raft 的方法同時須要更少的額外機制來實現。
算法的設計一般會把正確性,效率或者簡潔做爲主要的目標。儘管這些都是頗有意義的目標,可是咱們相信,可理解性也是同樣的重要。在開發者把算法應用到實際的系統中以前,這些目標沒有一個會被實現,這些都會必然的偏離發表時的形式。除非開發人員對這個算法有着很深的理解而且有着直觀的感受,不然將會對他們而言很難在實現的時候保持原有指望的特性。
在這篇論文中,咱們嘗試解決分佈式一致性問題,可是一個廣爲接受可是十分使人費解的算法 Paxos 已經困擾了無數學生和開發者不少年了。咱們創造了一種新的算法 Raft,顯而易見的比 Paxos 要容易理解。咱們同時也相信,Raft 也能夠爲實際的實現提供堅實的基礎。把可理解性做爲設計的目標改變了咱們設計 Raft 的方式;這個過程是咱們發現咱們最終不多有技術上的重複,例如問題分解和簡化狀態空間。這些技術不只提高了 Raft 的可理解性,同時也使咱們堅信其正確性。
這項研究必須感謝如下人員的支持:Ali Ghodsi,David Mazie res,和伯克利 CS 294-91 課程、斯坦福 CS 240 課程的學生。Scott Klemmer 幫咱們設計了用戶調查,Nelson Ray 建議咱們進行統計學的分析。在用戶調查時使用的關於 Paxos 的幻燈片很大一部分是從 Lorenzo Alvisi 的幻燈片上借鑑過來的。特別的,很是感謝 DavidMazieres 和 Ezra Hoch,他們找到了 Raft 中一些難以發現的漏洞。許多人提供了關於這篇論文十分有用的反饋和用戶調查材料,包括 Ed Bugnion,Michael Chan,Hugues Evrard,Daniel Giffin,Arjun Gopalan,Jon Howell,Vimalkumar Jeyakumar,Ankita Kejriwal,Aleksandar Kracun,Amit Levy,Joel Martin,Satoshi Matsushita,Oleg Pesok,David Ramos,Robbert van Renesse,Mendel Rosenblum,Nicolas Schiper,Deian Stefan,Andrew Stone,Ryan Stutsman,David Terei,Stephen Yang,Matei Zaharia 以及 24 位匿名的會議審查人員(可能有重複),而且特別感謝咱們的領導人 Eddie Kohler。Werner Vogels 發了一條早期草稿連接的推特,給 Raft 帶來了極大的關注。咱們的工做由 Gigascale 系統研究中心和 Multiscale 系統研究中心給予支持,這兩個研究中心由關注中心研究程序資金支持,一個是半導體研究公司的程序,由 STARnet 支持,一個半導體研究公司的程序由 MARCO 和 DARPA 支持,在國家科學基金會的 0963859 號批准,而且得到了來自 Facebook,Google,Mellanox,NEC,NetApp,SAP 和 Samsung 的支持。Diego Ongaro 由 Junglee 公司,斯坦福的畢業團體支持。
本文的版權歸做者 羅遠航 全部,採用 Attribution-NonCommercial 3.0 License。任何人能夠進行轉載、分享,但不可在未經容許的狀況下用於商業用途;轉載請註明出處。感謝配合!