Raft 一致性算法論文中文譯文

Raft 是一種爲了管理複製日誌的一致性算法。它提供了和 Paxos 算法相同的功能和性能,但是它的算法結構和 Paxos 不同,使得 Raft 算法更加容易理解並且更容易構建實際的系統。爲了提升可理解性,Raft 將一致性算法分解成了幾個關鍵模塊,例如領導人選舉、日誌複製和安全性。同時它通過實施一個更強的一致性來減少需要考慮的狀態的數量。從一個用戶研究的結果可以證明,對於學生而言,Raft 算法比 Paxos 算法更加容易學習。Raft 算法還包括一個新的機制來允許集羣成員的動態改變,它利用重疊的大多數來保證安全性。

  1. 介紹

一致性算法允許一組機器像一個整體一樣工作,即使其中一些機器出現故障也能夠繼續工作下去。正因爲如此,一致性算法在構建可信賴的大規模軟件系統中扮演着重要的角色。在過去的 10 年裏,Paxos 算法統治着一致性算法這一領域:絕大多數的實現都是基於 Paxos 或者受其影響。同時 Paxos 也成爲了教學領域裏講解一致性問題時的示例。

但是不幸的是,儘管有很多工作都在嘗試降低它的複雜性,但是 Paxos 算法依然十分難以理解。並且,Paxos 自身的算法結構需要進行大幅的修改才能夠應用到實際的系統中。這些都導致了工業界和學術界都對 Paxos 算法感到十分頭疼。

和 Paxos 算法進行過努力之後,我們開始尋找一種新的一致性算法,可以爲構建實際的系統和教學提供更好的基礎。與 Paxos 不同,我們的首要目標是可理解性:我們是否可以在實際系統中定義一個一致性算法,並且比 Paxos 算法更容易學習。此外,我們希望該算法方便系統構建者的直覺的發展。重要的不僅僅是算法能夠工作,更重要的是能夠顯而易見的知道它爲什麼能工作。

Raft 一致性算法就是這些工作的結果。在設計 Raft 算法的時候,我們使用一些特別的技巧來提升它的可理解性,包括算法分解(Raft 主要被分成了領導人選舉,日誌複製和安全三個模塊)和減少狀態機的狀態(相對於 Paxos,Raft 減少了非確定性和服務器互相處於非一致性的方式)。一份針對兩所大學 43 個學生的研究表明 Raft 明顯比 Paxos 算法更加容易理解。在這些學生同時學習了這兩種算法之後,和 Paxos 比起來,其中 33 個學生能夠回答有關於 Raft 的問題。

Raft 算法在許多方面和現有的一致性算法都很相似(主要是 Oki 和 Liskov 的 Viewstamped Replication),但是它也有一些獨特的特性:

我們相信,Raft 算法不論出於教學目的還是作爲實踐項目的基礎都是要比 Paxos 或者其他一致性算法要優異的。它比其他算法更加簡單,更加容易理解;它的算法描述足以實現一個現實的系統;它有好多開源的實現並且在很多公司裏使用;它的安全性已經被證明;它的效率和其他算法比起來也不相上下。

接下來,這篇論文會介紹以下內容:複製狀態機問題(第 2 節),討論 Paxos 的優點和缺點(第 3 節),討論我們爲了可理解性而採取的方法(第 4 節),闡述 Raft 一致性算法(第 5-8 節),評價 Raft 算法(第 9 節),以及一些相關的工作(第 10 節)。

  1. 複製狀態機

一致性算法是從複製狀態機的背景下提出的(參考英文原文引用 37)。在這種方法中,一組服務器上的狀態機產生相同狀態的副本,並且在一些機器宕掉的情況下也可以繼續運行。複製狀態機在分佈式系統中被用於解決很多容錯的問題。例如,大規模的系統中通常都有一個集羣領導者,像 GFS、HDFS 和 RAMCloud,典型應用就是一個獨立的的複製狀態機去管理領導選舉和存儲配置信息並且在領導人宕機的情況下也要存活下來。比如 Chubby 和 ZooKeeper。

圖 1 :複製狀態機的結構。一致性算法管理着來自客戶端指令的複製日誌。狀態機從日誌中處理相同順序的相同指令,所以產生的結果也是相同的。

複製狀態機通常都是基於複製日誌實現的,如圖 1。每一個服務器存儲一個包含一系列指令的日誌,並且按照日誌的順序進行執行。每一個日誌都按照相同的順序包含相同的指令,所以每一個服務器都執行相同的指令序列。因爲每個狀態機都是確定的,每一次執行操作都產生相同的狀態和同樣的序列。

保證複製日誌相同就是一致性算法的工作了。在一臺服務器上,一致性模塊接收客戶端發送來的指令然後增加到自己的日誌中去。它和其他服務器上的一致性模塊進行通信來保證每一個服務器上的日誌最終都以相同的順序包含相同的請求,儘管有些服務器會宕機。一旦指令被正確的複製,每一個服務器的狀態機按照日誌順序處理他們,然後輸出結果被返回給客戶端。因此,服務器集羣看起來形成一個高可靠的狀態機。

實際系統中使用的一致性算法通常含有以下特性:

  1. Paxos 算法的問題

在過去的 10 年裏,Leslie Lamport 的 Paxos 算法幾乎已經成爲一致性的代名詞:Paxos 是在課程教學中最經常使用的算法,同時也是大多數一致性算法實現的起點。Paxos 首先定義了一個能夠達成單一決策一致的協議,比如單條的複製日誌項。我們把這一子集叫做單決策 Paxos。然後通過組合多個 Paxos 協議的實例來促進一系列決策的達成。Paxos 保證安全性和活性,同時也支持集羣成員關係的變更。Paxos 的正確性已經被證明,在通常情況下也很高效。

不幸的是,Paxos 有兩個明顯的缺點。第一個缺點是 Paxos 算法特別的難以理解。完整的解釋是出了名的不透明;通過極大的努力之後,也只有少數人成功理解了這個算法。因此,有了幾次用更簡單的術語來解釋 Paxos 的嘗試。儘管這些解釋都只關注了單決策的子集問題,但依然很具有挑戰性。在 2012 年 NSDI 的會議中的一次調查顯示,很少有人對 Paxos 算法感到滿意,甚至在經驗老道的研究者中也是如此。我們自己也嘗試去理解 Paxos;我們一直沒能理解 Paxos 直到我們讀了很多對 Paxos 的簡化解釋並且設計了我們自己的算法之後,這一過程花了近一年時間。

我們假設 Paxos 的不透明性來自它選擇單決策問題作爲它的基礎。單決策 Paxos 是晦澀微妙的,它被劃分成了兩種沒有簡單直觀解釋和無法獨立理解的情景。因此,這導致了很難建立起直觀的感受爲什麼單決策 Paxos 算法能夠工作。構成多決策 Paxos 增加了很多錯綜複雜的規則。我們相信,在多決策上達成一致性的問題(一份日誌而不是單一的日誌記錄)能夠被分解成其他的方式並且更加直接和明顯。

Paxos 算法的第二個問題就是它沒有提供一個足夠好的用來構建一個現實系統的基礎。一個原因是還沒有一種被廣泛認同的多決策問題的算法。Lamport 的描述基本上都是關於單決策 Paxos 的;他簡要描述了實施多決策 Paxos 的方法,但是缺乏很多細節。當然也有很多具體化 Paxos 的嘗試,但是他們都互相不一樣,和 Paxos 的概述也不同。例如 Chubby 這樣的系統實現了一個類似於 Paxos 的算法,但是大多數的細節並沒有被公開。

而且,Paxos 算法的結構也不是十分易於構建實踐的系統;單決策分解也會產生其他的結果。例如,獨立的選擇一組日誌條目然後合併成一個序列化的日誌並沒有帶來太多的好處,僅僅增加了不少複雜性。圍繞着日誌來設計一個系統是更加簡單高效的;新日誌條目以嚴格限制的順序增添到日誌中去。另一個問題是,Paxos 使用了一種對等的點對點的方式作爲它的核心(儘管它最終提議了一種弱領導人的方法來優化性能)。在只有一個決策會被制定的簡化世界中是很有意義的,但是很少有現實的系統使用這種方式。如果有一系列的決策需要被制定,首先選擇一個領導人,然後讓他去協調所有的決議,會更加簡單快速。

因此,實際的系統中很少有和 Paxos 相似的實踐。每一種實現都是從 Paxos 開始研究,然後發現很多實現上的難題,再然後開發了一種和 Paxos 明顯不一樣的結構。這樣是非常費時和容易出錯的,並且理解 Paxos 的難度使得這個問題更加糟糕。Paxos 算法在理論上被證明是正確可行的,但是現實的系統和 Paxos 差別是如此的大,以至於這些證明沒有什麼太大的價值。下面來自 Chubby 實現非常典型:

在 Paxos 算法描述和實現現實系統中間有着巨大的鴻溝。最終的系統建立在一種沒有經過證明的算法之上。

由於以上問題,我們認爲 Paxos 算法既沒有提供一個良好的基礎給實踐的系統,也沒有給教學很好的幫助。基於一致性問題在大規模軟件系統中的重要性,我們決定看看我們是否可以設計一個擁有更好特性的替代 Paxos 的一致性算法。Raft 算法就是這次實驗的結果。

  1. 爲了可理解性的設計

設計 Raft 算法我們有幾個初衷:它必須提供一個完整的實際的系統實現基礎,這樣才能大大減少開發者的工作;它必須在任何情況下都是安全的並且在大多數的情況下都是可用的;並且它的大部分操作必須是高效的。但是我們最重要也是最大的挑戰是可理解性。它必須保證對於普遍的人羣都可以十分容易的去理解。另外,它必須能夠讓人形成直觀的認識,這樣系統的構建者才能夠在現實中進行必然的擴展。

在設計 Raft 算法的時候,有很多的點需要我們在各種備選方案中進行選擇。在這種情況下,我們評估備選方案基於可理解性原則:解釋各個備選方案有多大的難度(例如,Raft 的狀態空間有多複雜,是否有微妙的暗示)?對於一個讀者而言,完全理解這個方案和暗示是否容易?

我們意識到對這種可理解性分析上具有高度的主觀性;儘管如此,我們使用了兩種通常適用的技術來解決這個問題。第一個技術就是衆所周知的問題分解:只要有可能,我們就將問題分解成幾個相對獨立的,可被解決的、可解釋的和可理解的子問題。例如,Raft 算法被我們分成領導人選舉,日誌複製,安全性和角色改變幾個部分。

我們使用的第二個方法是通過減少狀態的數量來簡化需要考慮的狀態空間,使得系統更加連貫並且在可能的時候消除不確定性。特別的,所有的日誌是不允許有空洞的,並且 Raft 限制了日誌之間變成不一致狀態的可能。儘管在大多數情況下我們都試圖去消除不確定性,但是也有一些情況下不確定性可以提升可理解性。尤其是,隨機化方法增加了不確定性,但是他們有利於減少狀態空間數量,通過處理所有可能選擇時使用相似的方法。我們使用隨機化去簡化 Raft 中領導人選舉算法。

  1. Raft 一致性算法

Raft 是一種用來管理章節 2 中描述的複製日誌的算法。圖 2 爲了參考之用,總結這個算法的簡略版本,圖 3 列舉了這個算法的一些關鍵特性。圖中的這些元素會在剩下的章節逐一介紹。

Raft 通過選舉一個高貴的領導人,然後給予他全部的管理複製日誌的責任來實現一致性。領導人從客戶端接收日誌條目 (log entries),把日誌條目複製到其他服務器上,並且當保證安全性的時候告訴其他的服務器應用日誌條目到他們的狀態機中。擁有一個領導人大大簡化了對複製日誌的管理。例如,領導人可以決定新的日誌條目需要放在日誌中的什麼位置而不需要和其他服務器商議,並且數據都從領導人流向其他服務器。一個領導人可能會宕機,或者和其他服務器失去連接,在這種情況下一個新的領導人會被選舉出來。

通過領導人的方式,Raft 將一致性問題分解成了三個相對獨立的子問題,這些問題會在接下來的子章節中進行討論:

在展示一致性算法之後,這一章節會討論可用性的一些問題和計時在系統的作用。

狀態:

所有服務器上的持久性狀態(在響應 RPC 請求之前 已經更新到了穩定的存儲設備):

T0xdPW

所有服務器上的易失性狀態:

S8oUmm

領導者(服務器)上的易失性狀態(選舉後已經重新初始化):

8p6tgR

追加條目 RPC:

被領導者調用用於日誌條目的複製同時也被當做心跳使用。

參數:

zfqCJ1

結果:

q5deBF

接收者的實現:

  1. 返回假如果領導者的任期小於接收者的當前任期(譯者注:這裏的接收者是指跟隨者或者候選者)(5.1 節)

  2. 返回假如果接收者日誌中沒有包含這樣一個條目,即該條目的任期在 prevLogIndex 上能和 prevLogTerm 匹配上 (譯者注:在接收者日誌中 如果能找到一個和 prevLogIndex 以及 prevLogTerm 一樣的索引和任期的日誌條目則繼續執行下面的步驟,否則返回假)(5.3 節)

  3. 如果一個已經存在的條目和新條目(譯者注:即剛剛接收到的日誌條目)發生了衝突(因爲索引相同,任期不同),那麼就刪除這個已經存在的條目以及它之後的所有條目 (5.3 節)

  4. 追加日誌中尚未存在的任何新條目

  5. 如果領導者的已知已經提交的最高的日誌條目的索引 leaderCommit 大於接收者的已知已經提交的最高的日誌條目的索引 commitIndex,則把接收者的已知已經提交的最高的日誌條目的索引 commitIndex 重置爲領導者的已知已經提交的最高的日誌條目的索引 leaderCommit,或者是上一個新條目的索引取兩者的最小值

請求投票 RPC:

由候選人負責調用用來徵集選票(5.2 節):

2E2G2F

接收者實現:

  1. 如果 term < currentTerm 返回 false(5.2 節)

  2. 如果 votedFor 爲空或者爲 candidateId,並且候選人的日誌至少和自己一樣新,那麼就投票給他(5.2 節,5.4 節)

所有服務器需遵守的規則:

所有服務器:

跟隨者(5.2 節):

候選人(5.2 節):

領導人:

圖 2:一個關於 Raft 一致性算法的濃縮總結(不包括成員變換和日誌壓縮)。 Npqgja

圖 3:Raft 在任何時候都保證以上的各個特性。

5.1 Raft 基礎

一個 Raft 集羣包含若干個服務器節點;5 個服務器節點是一個典型的例子,這允許整個系統容忍 2 個節點失效。在任何時刻,每一個服務器節點都處於這三個狀態之一:領導人、跟隨者或者候選人。在通常情況下,系統中只有一個領導人並且其他的節點全部都是跟隨者。跟隨者都是被動的:他們不會發送任何請求,只是簡單的響應來自領導者或者候選人的請求。領導人處理所有的客戶端請求(如果一個客戶端和跟隨者聯繫,那麼跟隨者會把請求重定向給領導人)。第三種狀態,候選人,是用來在 5.2 節描述的選舉新領導人時使用。圖 4 展示了這些狀態和他們之間的轉換關係;這些轉換關係會在接下來進行討論。

圖 4:服務器狀態。跟隨者只響應來自其他服務器的請求。如果跟隨者接收不到消息,那麼他就會變成候選人併發起一次選舉。獲得集羣中大多數選票的候選人將成爲領導者。在一個任期內,領導人一直都會是領導人直到自己宕機了。

圖 5:時間被劃分成一個個的任期,每個任期開始都是一次選舉。在選舉成功後,領導人會管理整個集羣直到任期結束。有時候選舉會失敗,那麼這個任期就會沒有領導人而結束。任期之間的切換可以在不同的時間不同的服務器上觀察到。

Raft 把時間分割成任意長度的任期,如圖 5。任期用連續的整數標記。每一段任期從一次選舉開始,就像章節 5.2 描述的一樣,一個或者多個候選人嘗試成爲領導者。如果一個候選人贏得選舉,然後他就在接下來的任期內充當領導人的職責。在某些情況下,一次選舉過程會造成選票的瓜分。在這種情況下,這一任期會以沒有領導人結束;一個新的任期(和一次新的選舉)會很快重新開始。Raft 保證了在一個給定的任期內,最多隻有一個領導者。

不同的服務器節點可能多次觀察到任期之間的轉換,但在某些情況下,一個節點也可能觀察不到任何一次選舉或者整個任期全程。任期在 Raft 算法中充當邏輯時鐘的作用,這會允許服務器節點查明一些過期的信息比如陳舊的領導者。每一個節點存儲一個當前任期號,這一編號在整個時期內單調的增長。當服務器之間通信的時候會交換當前任期號;如果一個服務器的當前任期號比其他人小,那麼他會更新自己的編號到較大的編號值。如果一個候選人或者領導者發現自己的任期號過期了,那麼他會立即恢復成跟隨者狀態。如果一個節點接收到一個包含過期的任期號的請求,那麼他會直接拒絕這個請求。

Raft 算法中服務器節點之間通信使用遠程過程調用(RPCs),並且基本的一致性算法只需要兩種類型的 RPCs。請求投票(RequestVote) RPCs 由候選人在選舉期間發起(章節 5.2),然後附加條目(AppendEntries)RPCs 由領導人發起,用來複制日誌和提供一種心跳機制(章節 5.3)。第 7 節爲了在服務器之間傳輸快照增加了第三種 RPC。當服務器沒有及時的收到 RPC 的響應時,會進行重試, 並且他們能夠並行的發起 RPCs 來獲得最佳的性能。

5.2 領導人選舉

Raft 使用一種心跳機制來觸發領導人選舉。當服務器程序啓動時,他們都是跟隨者身份。一個服務器節點繼續保持着跟隨者狀態只要他從領導人或者候選者處接收到有效的 RPCs。領導者週期性的向所有跟隨者發送心跳包(即不包含日誌項內容的附加日誌項 RPCs)來維持自己的權威。如果一個跟隨者在一段時間裏沒有接收到任何消息,也就是選舉超時,那麼他就會認爲系統中沒有可用的領導者,並且發起選舉以選出新的領導者。

要開始一次選舉過程,跟隨者先要增加自己的當前任期號並且轉換到候選人狀態。然後他會並行的向集羣中的其他服務器節點發送請求投票的 RPCs 來給自己投票。候選人會繼續保持着當前狀態直到以下三件事情之一發生:(a)他自己贏得了這次的選舉,(b)其他的服務器成爲領導者,(c)一段時間之後沒有任何一個獲勝的人。這些結果會分別的在下面的段落裏進行討論。

當一個候選人從整個集羣的大多數服務器節點獲得了針對同一個任期號的選票,那麼他就贏得了這次選舉併成爲領導人。每一個服務器最多會對一個任期號投出一張選票,按照先來先服務的原則(注意:5.4 節在投票上增加了一點額外的限制)。要求大多數選票的規則確保了最多隻會有一個候選人贏得此次選舉(圖 3 中的選舉安全性)。一旦候選人贏得選舉,他就立即成爲領導人。然後他會向其他的服務器發送心跳消息來建立自己的權威並且阻止新的領導人的產生。

在等待投票的時候,候選人可能會從其他的服務器接收到聲明它是領導人的附加日誌項 RPC。如果這個領導人的任期號(包含在此次的 RPC 中)不小於候選人當前的任期號,那麼候選人會承認領導人合法並回到跟隨者狀態。如果此次 RPC 中的任期號比自己小,那麼候選人就會拒絕這次的 RPC 並且繼續保持候選人狀態。

第三種可能的結果是候選人既沒有贏得選舉也沒有輸:如果有多個跟隨者同時成爲候選人,那麼選票可能會被瓜分以至於沒有候選人可以贏得大多數人的支持。當這種情況發生的時候,每一個候選人都會超時,然後通過增加當前任期號來開始一輪新的選舉。然而,沒有其他機制的話,選票可能會被無限的重複瓜分。

Raft 算法使用隨機選舉超時時間的方法來確保很少會發生選票瓜分的情況,就算髮生也能很快的解決。爲了阻止選票起初就被瓜分,選舉超時時間是從一個固定的區間(例如 150-300 毫秒)隨機選擇。這樣可以把服務器都分散開以至於在大多數情況下只有一個服務器會選舉超時;然後他贏得選舉並在其他服務器超時之前發送心跳包。同樣的機制被用在選票瓜分的情況下。每一個候選人在開始一次選舉的時候會重置一個隨機的選舉超時時間,然後在超時時間內等待投票的結果;這樣減少了在新的選舉中另外的選票瓜分的可能性。9.3 節展示了這種方案能夠快速的選出一個領導人。

領導人選舉這個例子,體現了可理解性原則是如何指導我們進行方案設計的。起初我們計劃使用一種排名系統:每一個候選人都被賦予一個唯一的排名,供候選人之間競爭時進行選擇。如果一個候選人發現另一個候選人擁有更高的排名,那麼他就會回到跟隨者狀態,這樣高排名的候選人能夠更加容易的贏得下一次選舉。但是我們發現這種方法在可用性方面會有一點問題(如果高排名的服務器宕機了,那麼低排名的服務器可能會超時並再次進入候選人狀態。而且如果這個行爲發生得足夠快,則可能會導致整個選舉過程都被重置掉)。我們針對算法進行了多次調整,但是每次調整之後都會有新的問題。最終我們認爲隨機重試的方法是更加明顯和易於理解的。

5.3 日誌複製

一旦一個領導人被選舉出來,他就開始爲客戶端提供服務。客戶端的每一個請求都包含一條被複制狀態機執行的指令。領導人把這條指令作爲一條新的日誌條目附加到日誌中去,然後並行的發起附加條目 RPCs 給其他的服務器,讓他們複製這條日誌條目。當這條日誌條目被安全的複製(下面會介紹),領導人會應用這條日誌條目到它的狀態機中然後把執行的結果返回給客戶端。如果跟隨者崩潰或者運行緩慢,再或者網絡丟包,領導人會不斷的重複嘗試附加日誌條目 RPCs (儘管已經回覆了客戶端)直到所有的跟隨者都最終存儲了所有的日誌條目。

圖 6:日誌由有序序號標記的條目組成。每個條目都包含創建時的任期號(圖中框中的數字),和一個狀態機需要執行的指令。一個條目當可以安全的被應用到狀態機中去的時候,就認爲是可以提交了。

日誌以圖 6 展示的方式組織。每一個日誌條目存儲一條狀態機指令和從領導人收到這條指令時的任期號。日誌中的任期號用來檢查是否出現不一致的情況,同時也用來保證圖 3 中的某些性質。每一條日誌條目同時也都有一個整數索引值來表明它在日誌中的位置。

領導人來決定什麼時候把日誌條目應用到狀態機中是安全的;這種日誌條目被稱爲已提交。Raft 算法保證所有已提交的日誌條目都是持久化的並且最終會被所有可用的狀態機執行。在領導人將創建的日誌條目複製到大多數的服務器上的時候,日誌條目就會被提交(例如在圖 6 中的條目 7)。同時,領導人的日誌中之前的所有日誌條目也都會被提交,包括由其他領導人創建的條目。5.4 節會討論某些當在領導人改變之後應用這條規則的隱晦內容,同時他也展示了這種提交的定義是安全的。領導人跟蹤了最大的將會被提交的日誌項的索引,並且索引值會被包含在未來的所有附加日誌 RPCs (包括心跳包),這樣其他的服務器才能最終知道領導人的提交位置。一旦跟隨者知道一條日誌條目已經被提交,那麼他也會將這個日誌條目應用到本地的狀態機中(按照日誌的順序)。

我們設計了 Raft 的日誌機制來維護一個不同服務器的日誌之間的高層次的一致性。這麼做不僅簡化了系統的行爲也使得更加可預計,同時他也是安全性保證的一個重要組件。Raft 維護着以下的特性,這些同時也組成了圖 3 中的日誌匹配特性:

第一個特性來自這樣的一個事實,領導人最多在一個任期裏在指定的一個日誌索引位置創建一條日誌條目,同時日誌條目在日誌中的位置也從來不會改變。第二個特性由附加日誌 RPC 的一個簡單的一致性檢查所保證。在發送附加日誌 RPC 的時候,領導人會把新的日誌條目緊接着之前的條目的索引位置和任期號包含在裏面。如果跟隨者在它的日誌中找不到包含相同索引位置和任期號的條目,那麼他就會拒絕接收新的日誌條目。一致性檢查就像一個歸納步驟:一開始空的日誌狀態肯定是滿足日誌匹配特性的,然後一致性檢查保護了日誌匹配特性當日志擴展的時候。因此,每當附加日誌 RPC 返回成功時,領導人就知道跟隨者的日誌一定是和自己相同的了。

在正常的操作中,領導人和跟隨者的日誌保持一致性,所以附加日誌 RPC 的一致性檢查從來不會失敗。然而,領導人崩潰的情況會使得日誌處於不一致的狀態(老的領導人可能還沒有完全複製所有的日誌條目)。這種不一致問題會在領導人和跟隨者的一系列崩潰下加劇。圖 7 展示了跟隨者的日誌可能和新的領導人不同的方式。跟隨者可能會丟失一些在新的領導人中有的日誌條目,他也可能擁有一些領導人沒有的日誌條目,或者兩者都發生。丟失或者多出日誌條目可能會持續多個任期。

圖 7:當一個領導人成功當選時,跟隨者可能是任何情況(a-f)。每一個盒子表示是一個日誌條目;裏面的數字表示任期號。跟隨者可能會缺少一些日誌條目(a-b),可能會有一些未被提交的日誌條目(c-d),或者兩種情況都存在(e-f)。例如,場景 f 可能會這樣發生,某服務器在任期 2 的時候是領導人,已附加了一些日誌條目到自己的日誌中,但在提交之前就崩潰了;很快這個機器就被重啓了,在任期 3 重新被選爲領導人,並且又增加了一些日誌條目到自己的日誌中;在任期 2 和任期 3 的日誌被提交之前,這個服務器又宕機了,並且在接下來的幾個任期裏一直處於宕機狀態。

在 Raft 算法中,領導人處理不一致是通過強制跟隨者直接複製自己的日誌來解決了。這意味着在跟隨者中的衝突的日誌條目會被領導人的日誌覆蓋。5.4 節會闡述如何通過增加一些限制來使得這樣的操作是安全的。

要使得跟隨者的日誌進入和自己一致的狀態,領導人必須找到最後兩者達成一致的地方,然後刪除從那個點之後的所有日誌條目,發送自己的日誌給跟隨者。所有的這些操作都在進行附加日誌 RPCs 的一致性檢查時完成。領導人針對每一個跟隨者維護了一個 nextIndex,這表示下一個需要發送給跟隨者的日誌條目的索引地址。當一個領導人剛獲得權力的時候,他初始化所有的 nextIndex 值爲自己的最後一條日誌的 index 加 1(圖 7 中的 11)。如果一個跟隨者的日誌和領導人不一致,那麼在下一次的附加日誌 RPC 時的一致性檢查就會失敗。在被跟隨者拒絕之後,領導人就會減小 nextIndex 值並進行重試。最終 nextIndex 會在某個位置使得領導人和跟隨者的日誌達成一致。當這種情況發生,附加日誌 RPC 就會成功,這時就會把跟隨者衝突的日誌條目全部刪除並且加上領導人的日誌。一旦附加日誌 RPC 成功,那麼跟隨者的日誌就會和領導人保持一致,並且在接下來的任期裏一直繼續保持。

如果需要的話,算法可以通過減少被拒絕的附加日誌 RPCs 的次數來優化。例如,當附加日誌 RPC 的請求被拒絕的時候,跟隨者可以包含衝突的條目的任期號和自己存儲的那個任期的最早的索引地址。藉助這些信息,領導人可以減小 nextIndex 越過所有那個任期衝突的所有日誌條目;這樣就變成每個任期需要一次附加條目 RPC 而不是每個條目一次。在實踐中,我們十分懷疑這種優化是否是必要的,因爲失敗是很少發生的並且也不大可能會有這麼多不一致的日誌。

通過這種機制,領導人在獲得權力的時候就不需要任何特殊的操作來恢復一致性。他只需要進行正常的操作,然後日誌就能自動的在回覆附加日誌 RPC 的一致性檢查失敗的時候自動趨於一致。領導人從來不會覆蓋或者刪除自己的日誌(圖 3 的領導人只附加特性)。

日誌複製機制展示出了第 2 節中形容的一致性特性:Raft 能夠接受,複製並應用新的日誌條目只要大部分的機器是工作的;在通常的情況下,新的日誌條目可以在一次 RPC 中被複制給集羣中的大多數機器;並且單個的緩慢的跟隨者不會影響整體的性能。

5.4 安全性

前面的章節裏描述了 Raft 算法是如何選舉和複製日誌的。然而,到目前爲止描述的機制並不能充分的保證每一個狀態機會按照相同的順序執行相同的指令。例如,一個跟隨者可能會進入不可用狀態同時領導人已經提交了若干的日誌條目,然後這個跟隨者可能會被選舉爲領導人並且覆蓋這些日誌條目;因此,不同的狀態機可能會執行不同的指令序列。

這一節通過在領導選舉的時候增加一些限制來完善 Raft 算法。這一限制保證了任何的領導人對於給定的任期號,都擁有了之前任期的所有被提交的日誌條目(圖 3 中的領導人完整特性)。增加這一選舉時的限制,我們對於提交時的規則也更加清晰。最終,我們將展示對於領導人完整特性的簡要證明,並且說明領導人完整性特性是如何引導複製狀態機做出正確行爲的。

5.4.1 選舉限制

在任何基於領導人的一致性算法中,領導人都必須存儲所有已經提交的日誌條目。在某些一致性算法中,例如 Viewstamped Replication,某個節點即使是一開始並沒有包含所有已經提交的日誌條目,它也能被選爲領導者。這些算法都包含一些額外的機制來識別丟失的日誌條目並把他們傳送給新的領導人,要麼是在選舉階段要麼在之後很快進行。不幸的是,這種方法會導致相當大的額外的機制和複雜性。Raft 使用了一種更加簡單的方法,它可以保證所有之前的任期號中已經提交的日誌條目在選舉的時候都會出現在新的領導人中,不需要傳送這些日誌條目給領導人。這意味着日誌條目的傳送是單向的,只從領導人傳給跟隨者,並且領導人從不會覆蓋自身本地日誌中已經存在的條目。

Raft 使用投票的方式來阻止一個候選人贏得選舉除非這個候選人包含了所有已經提交的日誌條目。候選人爲了贏得選舉必須聯繫集羣中的大部分節點,這意味着每一個已經提交的日誌條目在這些服務器節點中肯定存在於至少一個節點上。如果候選人的日誌至少和大多數的服務器節點一樣新(這個新的定義會在下面討論),那麼他一定持有了所有已經提交的日誌條目。請求投票 RPC 實現了這樣的限制:RPC 中包含了候選人的日誌信息,然後投票人會拒絕掉那些日誌沒有自己新的投票請求。

Raft 通過比較兩份日誌中最後一條日誌條目的索引值和任期號定義誰的日誌比較新。如果兩份日誌最後的條目的任期號不同,那麼任期號大的日誌更加新。如果兩份日誌最後的條目任期號相同,那麼日誌比較長的那個就更加新。

5.4.2 提交之前任期內的日誌條目

如同 5.3 節介紹的那樣,領導人知道一條當前任期內的日誌記錄是可以被提交的,只要它被存儲到了大多數的服務器上。如果一個領導人在提交日誌條目之前崩潰了,未來後續的領導人會繼續嘗試複製這條日誌記錄。然而,一個領導人不能斷定一個之前任期裏的日誌條目被保存到大多數服務器上的時候就一定已經提交了。圖 8 展示了一種情況,一條已經被存儲到大多數節點上的老日誌條目,也依然有可能會被未來的領導人覆蓋掉。

圖 8:如圖的時間序列展示了爲什麼領導人無法決定對老任期號的日誌條目進行提交。在(a)中,S1 是領導者,部分的複製了索引位置 2 的日誌條目。在(b)中,S1 崩潰了,然後 S5 在任期 3 裏通過 S3、S4 和自己的選票贏得選舉,然後從客戶端接收了一條不一樣的日誌條目放在了索引 2 處。然後到(c),S5 又崩潰了;S1 重新啓動,選舉成功,開始複製日誌。在這時,來自任期 2 的那條日誌已經被複制到了集羣中的大多數機器上,但是還沒有被提交。如果 S1 在(d)中又崩潰了,S5 可以重新被選舉成功(通過來自 S2,S3 和 S4 的選票),然後覆蓋了他們在索引 2 處的日誌。反之,如果在崩潰之前,S1 把自己主導的新任期裏產生的日誌條目複製到了大多數機器上,就如(e)中那樣,那麼在後面任期裏面這些新的日誌條目就會被提交(因爲 S5 就不可能選舉成功)。這樣在同一時刻就同時保證了,之前的所有老的日誌條目就會被提交。

爲了消除圖 8 裏描述的情況,Raft 永遠不會通過計算副本數目的方式去提交一個之前任期內的日誌條目。只有領導人當前任期裏的日誌條目通過計算副本數目可以被提交;一旦當前任期的日誌條目以這種方式被提交,那麼由於日誌匹配特性,之前的日誌條目也都會被間接的提交。在某些情況下,領導人可以安全的知道一個老的日誌條目是否已經被提交(例如,該條目是否存儲到所有服務器上),但是 Raft 爲了簡化問題使用一種更加保守的方法。

當領導人複製之前任期裏的日誌時,Raft 會爲所有日誌保留原始的任期號, 這在提交規則上產生了額外的複雜性。在其他的一致性算法中,如果一個新的領導人要重新複製之前的任期裏的日誌時,它必須使用當前新的任期號。Raft 使用的方法更加容易辨別出日誌,因爲它可以隨着時間和日誌的變化對日誌維護着同一個任期編號。另外,和其他的算法相比,Raft 中的新領導人只需要發送更少日誌條目(其他算法中必須在他們被提交之前發送更多的冗餘日誌條目來爲他們重新編號)。

5.4.3 安全性論證

在給定了完整的 Raft 算法之後,我們現在可以更加精確的討論領導人完整性特性(這一討論基於 9.2 節的安全性證明)。我們假設領導人完全性特性是不存在的,然後我們推出矛盾來。假設任期 T 的領導人(領導人 T)在任期內提交了一條日誌條目,但是這條日誌條目沒有被存儲到未來某個任期的領導人的日誌中。設大於 T 的最小任期 U 的領導人 U 沒有這條日誌條目。

圖 9:如果 S1 (任期 T 的領導者)提交了一條新的日誌在它的任期裏,然後 S5 在之後的任期 U 裏被選舉爲領導人,然後至少會有一個機器,如 S3,既擁有來自 S1 的日誌,也給 S5 投票了。

  1. 在領導人 U 選舉的時候一定沒有那條被提交的日誌條目(領導人從不會刪除或者覆蓋任何條目)。

  2. 領導人 T 複製這條日誌條目給集羣中的大多數節點,同時,領導人 U 從集羣中的大多數節點贏得了選票。因此,至少有一個節點(投票者、選民)同時接受了來自領導人 T 的日誌條目,並且給領導人 U 投票了,如圖 9。這個投票者是產生這個矛盾的關鍵。

  3. 這個投票者必須在給領導人 U 投票之前先接受了從領導人 T 發來的已經被提交的日誌條目;否則他就會拒絕來自領導人 T 的附加日誌請求(因爲此時他的任期號會比 T 大)。

  4. 投票者在給領導人 U 投票時依然保存有這條日誌條目,因爲任何中間的領導人都包含該日誌條目(根據上述的假設),領導人從不會刪除條目,並且跟隨者只有在和領導人衝突的時候纔會刪除條目。

  5. 投票者把自己選票投給領導人 U 時,領導人 U 的日誌必須和投票者自己一樣新。這就導致了兩者矛盾之一。

  6. 首先,如果投票者和領導人 U 的最後一條日誌的任期號相同,那麼領導人 U 的日誌至少和投票者一樣長,所以領導人 U 的日誌一定包含所有投票者的日誌。這是另一處矛盾,因爲投票者包含了那條已經被提交的日誌條目,但是在上述的假設裏,領導人 U 是不包含的。

  7. 除此之外,領導人 U 的最後一條日誌的任期號就必須比投票人大了。此外,他也比 T 大,因爲投票人的最後一條日誌的任期號至少和 T 一樣大(他包含了來自任期 T 的已提交的日誌)。創建了領導人 U 最後一條日誌的之前領導人一定已經包含了那條被提交的日誌(根據上述假設,領導人 U 是第一個不包含該日誌條目的領導人)。所以,根據日誌匹配特性,領導人 U 一定也包含那條被提交的日誌,這裏產生矛盾。

  8. 這裏完成了矛盾。因此,所有比 T 大的領導人一定包含了所有來自 T 的已經被提交的日誌。

  9. 日誌匹配原則保證了未來的領導人也同時會包含被間接提交的條目,例如圖 8(d)中的索引 2。

通過領導人完全特性,我們就能證明圖 3 中的狀態機安全特性,即如果服務器已經在某個給定的索引值應用了日誌條目到自己的狀態機裏,那麼其他的服務器不會應用一個不一樣的日誌到同一個索引值上。在一個服務器應用一條日誌條目到他自己的狀態機中時,他的日誌必須和領導人的日誌,在該條目和之前的條目上相同,並且已經被提交。現在我們來考慮在任何一個服務器應用一個指定索引位置的日誌的最小任期;日誌完全特性保證擁有更高任期號的領導人會存儲相同的日誌條目,所以之後的任期裏應用某個索引位置的日誌條目也會是相同的值。因此,狀態機安全特性是成立的。

最後,Raft 要求服務器按照日誌中索引位置順序應用日誌條目。和狀態機安全特性結合起來看,這就意味着所有的服務器會應用相同的日誌序列集到自己的狀態機中,並且是按照相同的順序。

5.5 跟隨者和候選人崩潰

到目前爲止,我們都只關注了領導人崩潰的情況。跟隨者和候選人崩潰後的處理方式比領導人要簡單的多,並且他們的處理方式是相同的。如果跟隨者或者候選人崩潰了,那麼後續發送給他們的 RPCs 都會失敗。Raft 中處理這種失敗就是簡單的通過無限的重試;如果崩潰的機器重啓了,那麼這些 RPC 就會完整的成功。如果一個服務器在完成了一個 RPC,但是還沒有響應的時候崩潰了,那麼在他重新啓動之後就會再次收到同樣的請求。Raft 的 RPCs 都是冪等的,所以這樣重試不會造成任何問題。例如一個跟隨者如果收到附加日誌請求但是他已經包含了這一日誌,那麼他就會直接忽略這個新的請求。

5.6 時間和可用性

Raft 的要求之一就是安全性不能依賴時間:整個系統不能因爲某些事件運行的比預期快一點或者慢一點就產生了錯誤的結果。但是,可用性(系統可以及時的響應客戶端)不可避免的要依賴於時間。例如,如果消息交換比服務器故障間隔時間長,候選人將沒有足夠長的時間來贏得選舉;沒有一個穩定的領導人,Raft 將無法工作。

領導人選舉是 Raft 中對時間要求最爲關鍵的方面。Raft 可以選舉並維持一個穩定的領導人, 只要系統滿足下面的時間要求:

廣播時間(broadcastTime) << 選舉超時時間(electionTimeout) << 平均故障間隔時間(MTBF)

在這個不等式中,廣播時間指的是從一個服務器並行的發送 RPCs 給集羣中的其他服務器並接收響應的平均時間;選舉超時時間就是在 5.2 節中介紹的選舉的超時時間限制;然後平均故障間隔時間就是對於一臺服務器而言,兩次故障之間的平均時間。廣播時間必須比選舉超時時間小一個量級,這樣領導人才能夠發送穩定的心跳消息來阻止跟隨者開始進入選舉狀態;通過隨機化選舉超時時間的方法,這個不等式也使得選票瓜分的情況變得不可能。選舉超時時間應該要比平均故障間隔時間小上幾個數量級,這樣整個系統才能穩定的運行。當領導人崩潰後,整個系統會大約相當於選舉超時的時間裏不可用;我們希望這種情況在整個系統的運行中很少出現。

廣播時間和平均故障間隔時間是由系統決定的,但是選舉超時時間是我們自己選擇的。Raft 的 RPCs 需要接收方將信息持久化的保存到穩定存儲中去,所以廣播時間大約是 0.5 毫秒到 20 毫秒,取決於存儲的技術。因此,選舉超時時間可能需要在 10 毫秒到 500 毫秒之間。大多數的服務器的平均故障間隔時間都在幾個月甚至更長,很容易滿足時間的需求。

  1. 集羣成員變化

到目前爲止,我們都假設集羣的配置(加入到一致性算法的服務器集合)是固定不變的。但是在實踐中,偶爾是會改變集羣的配置的,例如替換那些宕機的機器或者改變複製級別。儘管可以通過暫停整個集羣,更新所有配置,然後重啓整個集羣的方式來實現,但是在更改的時候集羣會不可用。另外,如果存在手工操作步驟,那麼就會有操作失誤的風險。爲了避免這樣的問題,我們決定自動化配置改變並且將其納入到 Raft 一致性算法中來。

爲了讓配置修改機制能夠安全,那麼在轉換的過程中不能夠存在任何時間點使得兩個領導人同時被選舉成功在同一個任期裏。不幸的是,任何服務器直接從舊的配置直接轉換到新的配置的方案都是不安全的。一次性原子地轉換所有服務器是不可能的,所以在轉換期間整個集羣存在劃分成兩個獨立的大多數羣體的可能性(見圖 10)。

圖 10:直接從一種配置轉到新的配置是十分不安全的,因爲各個機器可能在任何的時候進行轉換。在這個例子中,集羣配額從 3 臺機器變成了 5 臺。不幸的是,存在這樣的一個時間點,兩個不同的領導人在同一個任期裏都可以被選舉成功。一個是通過舊的配置,一個通過新的配置。

爲了保證安全性,配置更改必須使用兩階段方法。目前有很多種兩階段的實現。例如,有些系統在第一階段停掉舊的配置所以集羣就不能處理客戶端請求;然後在第二階段在啓用新的配置。在 Raft 中,集羣先切換到一個過渡的配置,我們稱之爲共同一致;一旦共同一致已經被提交了,那麼系統就切換到新的配置上。共同一致是老配置和新配置的結合:

共同一致允許獨立的服務器在不影響安全性的前提下,在不同的時間進行配置轉換過程。此外,共同一致可以讓集羣在配置轉換的過程中依然響應客戶端的請求。

集羣配置在複製日誌中以特殊的日誌條目來存儲和通信;圖 11 展示了配置轉換的過程。當一個領導人接收到一個改變配置從 C-old 到 C-new 的請求,他會爲了共同一致存儲配置(圖中的 C-old,new),以前面描述的日誌條目和副本的形式。一旦一個服務器將新的配置日誌條目增加到它的日誌中,他就會用這個配置來做出未來所有的決定(服務器總是使用最新的配置,無論他是否已經被提交)。這意味着領導人要使用 C-old,new 的規則來決定日誌條目 C-old,new 什麼時候需要被提交。如果領導人崩潰了,被選出來的新領導人可能是使用 C-old 配置也可能是 C-old,new 配置,這取決於贏得選舉的候選人是否已經接收到了 C-old,new 配置。在任何情況下, C-new 配置在這一時期都不會單方面的做出決定。

一旦 C-old,new 被提交,那麼無論是 C-old 還是 C-new,在沒有經過他人批准的情況下都不可能做出決定,並且領導人完全特性保證了只有擁有 C-old,new 日誌條目的服務器纔有可能被選舉爲領導人。這個時候,領導人創建一條關於 C-new 配置的日誌條目並複製給集羣就是安全的了。再者,每個服務器在見到新的配置的時候就會立即生效。當新的配置在 C-new 的規則下被提交,舊的配置就變得無關緊要,同時不使用新的配置的服務器就可以被關閉了。如圖 11,C-old 和 C-new 沒有任何機會同時做出單方面的決定;這保證了安全性。

圖 11:一個配置切換的時間線。虛線表示已經被創建但是還沒有被提交的配置日誌條目,實線表示最後被提交的配置日誌條目。領導人首先創建了 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 配置下選出新的領導人)。在此之前,可能只能從 C-old 中選出領導人。

第三個問題是,移除不在 C-new 中的服務器可能會擾亂集羣。這些服務器將不會再接收到心跳,所以當選舉超時,他們就會進行新的選舉過程。他們會發送擁有新的任期號的請求投票 RPCs,這樣會導致當前的領導人回退成跟隨者狀態。新的領導人最終會被選出來,但是被移除的服務器將會再次超時,然後這個過程會再次重複,導致整體可用性大幅降低。

爲了避免這個問題,當服務器確認當前領導人存在時,服務器會忽略請求投票 RPCs。特別的,當服務器在當前最小選舉超時時間內收到一個請求投票 RPC,他不會更新當前的任期號或者投出選票。這不會影響正常的選舉,每個服務器在開始一次選舉之前,至少等待一個最小選舉超時時間。然而,這有利於避免被移除的服務器擾亂:如果領導人能夠發送心跳給集羣,那麼他就不會被更大的任期號廢黜。

  1. 日誌壓縮

Raft 的日誌在正常操作中不斷的增長,但是在實際的系統中,日誌不能無限制的增長。隨着日誌不斷增長,他會佔用越來越多的空間,花費越來越多的時間來重置。如果沒有一定的機制去清除日誌裏積累的陳舊的信息,那麼會帶來可用性問題。

快照是最簡單的壓縮方法。在快照系統中,整個系統的狀態都以快照的形式寫入到穩定的持久化存儲中,然後到那個時間點之前的日誌全部丟棄。快照技術被使用在 Chubby 和 ZooKeeper 中,接下來的章節會介紹 Raft 中的快照技術。

增量壓縮的方法,例如日誌清理或者日誌結構合併樹,都是可行的。這些方法每次只對一小部分數據進行操作,這樣就分散了壓縮的負載壓力。首先,他們先選擇一個已經積累的大量已經被刪除或者被覆蓋對象的區域,然後重寫那個區域還活躍的對象,之後釋放那個區域。和簡單操作整個數據集合的快照相比,需要增加複雜的機制來實現。狀態機可以實現 LSM tree 使用和快照相同的接口,但是日誌清除方法就需要修改 Raft 了。

圖 12:一個服務器用新的快照替換了從 1 到 5 的條目,快照值存儲了當前的狀態。快照中包含了最後的索引位置和任期號。

圖 12 展示了 Raft 中快照的基礎思想。每個服務器獨立的創建快照,只包括已經被提交的日誌。主要的工作包括將狀態機的狀態寫入到快照中。Raft 也包含一些少量的元數據到快照中:最後被包含索引指的是被快照取代的最後的條目在日誌中的索引值(狀態機最後應用的日誌),最後被包含的任期指的是該條目的任期號。保留這些數據是爲了支持快照後緊接着的第一個條目的附加日誌請求時的一致性檢查,因爲這個條目需要前一日誌條目的索引值和任期號。爲了支持集羣成員更新(第 6 節),快照中也將最後的一次配置作爲最後一個條目存下來。一旦服務器完成一次快照,他就可以刪除最後索引位置之前的所有日誌和快照了。

儘管通常服務器都是獨立的創建快照,但是領導人必須偶爾的發送快照給一些落後的跟隨者。這通常發生在當領導人已經丟棄了下一條需要發送給跟隨者的日誌條目的時候。幸運的是這種情況不是常規操作:一個與領導人保持同步的跟隨者通常都會有這個條目。然而一個運行非常緩慢的跟隨者或者新加入集羣的服務器(第 6 節)將不會有這個條目。這時讓這個跟隨者更新到最新的狀態的方式就是通過網絡把快照發送給他們。

安裝快照 RPC:

由領導人調用以將快照的分塊發送給跟隨者。領導者總是按順序發送分塊。

psSY2B

接收者實現:

  1. 如果 term < currentTerm 就立即回覆

  2. 如果是第一個分塊(offset 爲 0)就創建一個新的快照

  3. 在指定偏移量寫入數據

  4. 如果 done 是 false,則繼續等待更多的數據

  5. 保存快照文件,丟棄具有較小索引的任何現有或部分快照

  6. 如果現存的日誌條目與快照中最後包含的日誌條目具有相同的索引值和任期號,則保留其後的日誌條目並進行回覆

  7. 丟棄整個日誌

  8. 使用快照重置狀態機(並加載快照的集羣配置)

圖 13:一個關於安裝快照的簡要概述。爲了便於傳輸,快照都是被分成分塊的;每個分塊都給了跟隨者生命的跡象,所以跟隨者可以重置選舉超時計時器。

在這種情況下領導人使用一種叫做安裝快照的新的 RPC 來發送快照給太落後的跟隨者;見圖 13。當跟隨者通過這種 RPC 接收到快照時,他必須自己決定對於已經存在的日誌該如何處理。通常快照會包含沒有在接收者日誌中存在的信息。在這種情況下,跟隨者丟棄其整個日誌;它全部被快照取代,並且可能包含與快照衝突的未提交條目。如果接收到的快照是自己日誌的前面部分(由於網絡重傳或者錯誤),那麼被快照包含的條目將會被全部刪除,但是快照後面的條目仍然有效,必須保留。

這種快照的方式背離了 Raft 的強領導人原則,因爲跟隨者可以在不知道領導人情況下創建快照。但是我們認爲這種背離是值得的。領導人的存在,是爲了解決在達成一致性的時候的衝突,但是在創建快照的時候,一致性已經達成,這時不存在衝突了,所以沒有領導人也是可以的。數據依然是從領導人傳給跟隨者,只是跟隨者可以重新組織他們的數據了。

我們考慮過一種替代的基於領導人的快照方案,即只有領導人創建快照,然後發送給所有的跟隨者。但是這樣做有兩個缺點。第一,發送快照會浪費網絡帶寬並且延緩了快照處理的時間。每個跟隨者都已經擁有了所有產生快照需要的信息,而且很顯然,自己從本地的狀態中創建快照比通過網絡接收別人發來的要經濟。第二,領導人的實現會更加複雜。例如,領導人需要發送快照的同時並行的將新的日誌條目發送給跟隨者,這樣纔不會阻塞新的客戶端請求。

還有兩個問題影響了快照的性能。首先,服務器必須決定什麼時候應該創建快照。如果快照創建的過於頻繁,那麼就會浪費大量的磁盤帶寬和其他資源;如果創建快照頻率太低,他就要承受耗盡存儲容量的風險,同時也增加了從日誌重建的時間。一個簡單的策略就是當日志大小達到一個固定大小的時候就創建一次快照。如果這個閾值設置的顯著大於期望的快照的大小,那麼快照對磁盤壓力的影響就會很小了。

第二個影響性能的問題就是寫入快照需要花費顯著的一段時間,並且我們還不希望影響到正常操作。解決方案是通過寫時複製的技術,這樣新的更新就可以被接收而不影響到快照。例如,具有函數式數據結構的狀態機天然支持這樣的功能。另外,操作系統的寫時複製技術的支持(如 Linux 上的 fork)可以被用來創建完整的狀態機的內存快照(我們的實現就是這樣的)。

  1. 客戶端交互

這一節將介紹客戶端是如何和 Raft 進行交互的,包括客戶端如何發現領導人和 Raft 是如何支持線性化語義的。這些問題對於所有基於一致性的系統都存在,並且 Raft 的解決方案和其他的也差不多。

Raft 中的客戶端發送所有請求給領導人。當客戶端啓動的時候,他會隨機挑選一個服務器進行通信。如果客戶端第一次挑選的服務器不是領導人,那麼那個服務器會拒絕客戶端的請求並且提供他最近接收到的領導人的信息(附加條目請求包含了領導人的網絡地址)。如果領導人已經崩潰了,那麼客戶端的請求就會超時;客戶端之後會再次重試隨機挑選服務器的過程。

我們 Raft 的目標是要實現線性化語義(每一次操作立即執行,只執行一次,在他調用和收到回覆之間)。但是,如上述,Raft 是可以執行同一條命令多次的:例如,如果領導人在提交了這條日誌之後,但是在響應客戶端之前崩潰了,那麼客戶端會和新的領導人重試這條指令,導致這條命令就被再次執行了。解決方案就是客戶端對於每一條指令都賦予一個唯一的序列號。然後,狀態機跟蹤每條指令最新的序列號和相應的響應。如果接收到一條指令,它的序列號已經被執行了,那麼就立即返回結果,而不重新執行指令。

只讀的操作可以直接處理而不需要記錄日誌。但是,在不增加任何限制的情況下,這麼做可能會冒着返回髒數據的風險,因爲領導人響應客戶端請求時可能已經被新的領導人作廢了,但是他還不知道。線性化的讀操作必須不能返回髒數據,Raft 需要使用兩個額外的措施在不使用日誌的情況下保證這一點。首先,領導人必須有關於被提交日誌的最新信息。領導人完全特性保證了領導人一定擁有所有已經被提交的日誌條目,但是在他任期開始的時候,他可能不知道哪些是已經被提交的。爲了知道這些信息,他需要在他的任期裏提交一條日誌條目。Raft 中通過領導人在任期開始的時候提交一個空白的沒有任何操作的日誌條目到日誌中去來實現。第二,領導人在處理只讀的請求之前必須檢查自己是否已經被廢黜了(他自己的信息已經變髒瞭如果一個更新的領導人被選舉出來)。Raft 中通過讓領導人在響應只讀請求之前,先和集羣中的大多數節點交換一次心跳信息來處理這個問題。可選的,領導人可以依賴心跳機制來實現一種租約的機制,但是這種方法依賴時間來保證安全性(假設時間誤差是有界的)。

  1. 算法實現和評估

我們已經爲 RAMCloud 實現了 Raft 算法作爲存儲配置信息的複製狀態機的一部分,並且幫助 RAMCloud 協調故障轉移。這個 Raft 實現包含大約 2000 行 C++ 代碼,其中不包括測試、註釋和空行。這些代碼是開源的。同時也有大約 25 個其他獨立的第三方的基於這篇論文草稿的開源實現,針對不同的開發場景。同時,很多公司已經部署了基於 Raft 的系統。

這一節會從三個方面來評估 Raft 算法:可理解性、正確性和性能。

9.1 可理解性

爲了和 Paxos 比較 Raft 算法的可理解能力,我們針對高層次的本科生和研究生,在斯坦福大學的高級操作系統課程和加州大學伯克利分校的分佈式計算課程上,進行了一次學習的實驗。我們分別拍了針對 Raft 和 Paxos 的視頻課程,並準備了相應的小測驗。Raft 的視頻講課覆蓋了這篇論文的所有內容除了日誌壓縮;Paxos 講課包含了足夠的資料來創建一個等價的複製狀態機,包括單決策 Paxos,多決策 Paxos,重新配置和一些實際系統需要的性能優化(例如領導人選舉)。小測驗測試一些對算法的基本理解和解釋一些邊角的示例。每個學生都是看完第一個視頻,回答相應的測試,再看第二個視頻,回答相應的測試。大約有一半的學生先進行 Paxos 部分,然後另一半先進行 Raft 部分,這是爲了說明兩者從第一部分的算法學習中獲得的表現和經驗的差異。我們計算參加人員的每一個小測驗的得分來看參與者是否在 Raft 算法上更加容易理解。

我們儘可能的使得 Paxos 和 Raft 的比較更加公平。這個實驗偏愛 Paxos 表現在兩個方面:43 個參加者中有 15 個人在之前有一些 Paxos 的經驗,並且 Paxos 的視頻要長 14%。如表格 1 總結的那樣,我們採取了一些措施來減輕這種潛在的偏見。我們所有的材料都可供審查。

L7FQXt

表 1:考慮到可能會存在的偏見,對於每種情況的解決方法,和相應的材料。

參加者平均在 Raft 的測驗中比 Paxos 高 4.9 分(總分 60,那麼 Raft 的平均得分是 25.7,而 Paxos 是 20.8);圖 14 展示了每個參與者的得分。配置 t - 檢驗(又稱 student‘s t-test)表明,在 95% 的可信度下,真實的 Raft 分數分佈至少比 Paxos 高 2.5 分。

圖 14:一個散點圖表示了 43 個學生在 Paxos 和 Raft 的小測驗中的成績。在對角線之上的點表示在 Raft 獲得了更高分數的學生。

我們也建立了一個線性迴歸模型來預測一個新的學生的測驗成績,基於以下三個因素:他們使用的是哪個小測驗,之前對 Paxos 的經驗,和學習算法的順序。模型預測,對小測驗的選擇會產生 12.5 分的差別。這顯著的高於之前的 4.9 分,因爲很多學生在之前都已經有了對於 Paxos 的經驗,這相當明顯的幫助 Paxos,對 Raft 就沒什麼太大影響了。但是奇怪的是,模型預測對於先進行 Paxos 小測驗的人而言,Raft 的得分低了 6.3 分; 雖然我們不知道爲什麼,這似乎在統計上是有意義的。

我們同時也在測驗之後調查了參與者,他們認爲哪個算法更加容易實現和解釋;這個的結果在圖 15 上。壓倒性的結果表明 Raft 算法更加容易實現和解釋(41 人中的 33 個)。但是,這種自己報告的結果不如參與者的成績更加可信,並且參與者可能因爲我們的 Raft 更加易於理解的假說而產生偏見。

圖 15:通過一個 5 分制的問題,參與者(左邊)被問哪個算法他們覺得在一個高效正確的系統裏更容易實現,右邊被問哪個更容易向學生解釋。

關於 Raft 用戶學習有一個更加詳細的討論。

9.2 正確性

在第 5 節,我們已經制定了正式的規範,和對一致性機制的安全性證明。這個正式規範使用 TLA+ 規範語言使圖 2 中總結的信息非常清晰。它長約 400 行,並作爲證明的主題。同時對於任何想實現 Raft 的人也是十分有用的。我們通過 TLA 證明系統非常機械的證明了日誌完全特性。然而,這個證明依賴的約束前提還沒有被機械證明(例如,我們還沒有證明規範的類型安全)。而且,我們已經寫了一個非正式的證明關於狀態機安全性是完備的,並且是相當清晰的(大約 3500 個詞)。

9.3 性能

Raft 和其他一致性算法例如 Paxos 有着差不多的性能。在性能方面,最重要的關注點是,當領導人被選舉成功時,什麼時候複製新的日誌條目。Raft 通過很少數量的消息包(一輪從領導人到集羣大多數機器的消息)就達成了這個目的。同時,進一步提升 Raft 的性能也是可行的。例如,很容易通過支持批量操作和管道操作來提高吞吐量和降低延遲。對於其他一致性算法已經提出過很多性能優化方案;其中有很多也可以應用到 Raft 中來,但是我們暫時把這個問題放到未來的工作中去。

我們使用我們自己的 Raft 實現來衡量 Raft 領導人選舉的性能並且回答兩個問題。首先,領導人選舉的過程收斂是否快速?第二,在領導人宕機之後,最小的系統宕機時間是多久?

圖 16:發現並替換一個已經崩潰的領導人的時間。上面的圖考察了在選舉超時時間上的隨機化程度,下面的圖考察了最小選舉超時時間。每條線代表了 1000 次實驗(除了 150-150 毫秒只試了 100 次),和相應的確定的選舉超時時間。例如,150-155 毫秒意思是,選舉超時時間從這個區間範圍內隨機選擇並確定下來。這個實驗在一個擁有 5 個節點的集羣上進行,其廣播時延大約是 15 毫秒。對於 9 個節點的集羣,結果也差不多。

爲了衡量領導人選舉,我們反覆的使一個擁有五個節點的服務器集羣的領導人宕機,並計算需要多久才能發現領導人已經宕機並選出一個新的領導人(見圖 16)。爲了構建一個最壞的場景,在每一的嘗試裏,服務器都有不同長度的日誌,意味着有些候選人是沒有成爲領導人的資格的。另外,爲了促成選票瓜分的情況,我們的測試腳本在終止領導人之前同步的發送了一次心跳廣播(這大約和領導人在崩潰前複製一個新的日誌給其他機器很像)。領導人均勻的隨機的在心跳間隔裏宕機,也就是最小選舉超時時間的一半。因此,最小宕機時間大約就是最小選舉超時時間的一半。

圖 16 中上面的圖表明,只需要在選舉超時時間上使用很少的隨機化就可以大大避免選票被瓜分的情況。在沒有隨機化的情況下,在我們的測試裏,選舉過程往往都需要花費超過 10 秒鐘由於太多的選票瓜分的情況。僅僅增加 5 毫秒的隨機化時間,就大大的改善了選舉過程,現在平均的宕機時間只有 287 毫秒。增加更多的隨機化時間可以大大改善最壞情況:通過增加 50 毫秒的隨機化時間,最壞的完成情況(1000 次嘗試)只要 513 毫秒。

圖 16 中下面的圖顯示,通過減少選舉超時時間可以減少系統的宕機時間。在選舉超時時間爲 12-24 毫秒的情況下,只需要平均 35 毫秒就可以選舉出新的領導人(最長的一次花費了 152 毫秒)。然而,進一步降低選舉超時時間的話就會違反 Raft 的時間不等式需求:在選舉新領導人之前,領導人就很難發送完心跳包。這會導致沒有意義的領導人改變並降低了系統整體的可用性。我們建議使用更爲保守的選舉超時時間,比如 150-300 毫秒;這樣的時間不大可能導致沒有意義的領導人改變,而且依然提供不錯的可用性。

  1. 相關工作

已經有很多關於一致性算法的工作被髮表出來,其中很多都可以歸到下面的類別中:

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。我們選擇使用共同一致的方法因爲他對一致性協議的其他部分影響很小,這樣我們只需要很少的一些機制就可以實現成員變換。Lamport 的基於 α 的方法之所以沒有被 Raft 選擇是因爲它假設在沒有領導人的情況下也可以達到一致性。和 VR 和 SMART 相比較,Raft 的重新配置算法可以在不限制正常請求處理的情況下進行;相比較的,VR 需要停止所有的處理過程,SMART 引入了一個和 α 類似的方法,限制了請求處理的數量。Raft 的方法同時也需要更少的額外機制來實現,和 VR、SMART 比較而言。

  1. 結論

算法的設計通常會把正確性,效率或者簡潔作爲主要的目標。儘管這些都是很有意義的目標,但是我們相信,可理解性也是一樣的重要。在開發者把算法應用到實際的系統中之前,這些目標沒有一個會被實現,這些都會必然的偏離發表時的形式。除非開發人員對這個算法有着很深的理解並且有着直觀的感覺,否則將會對他們而言很難在實現的時候保持原有期望的特性。

在這篇論文中,我們嘗試解決分佈式一致性問題,但是一個廣爲接受但是十分令人費解的算法 Paxos 已經困擾了無數學生和開發者很多年了。我們創造了一種新的算法 Raft,顯而易見的比 Paxos 要容易理解。我們同時也相信,Raft 也可以爲實際的實現提供堅實的基礎。把可理解性作爲設計的目標改變了我們設計 Raft 的方式;隨着設計的進展,我們發現自己重複使用了一些技術,比如分解問題和簡化狀態空間。這些技術不僅提升了 Raft 的可理解性,同時也使我們堅信其正確性。

譯文鏈接:https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/zMnqOCUVvRLQuJUwvM3QRA