1. 程式人生 > >分散式一致性演算法:Raft 演算法

分散式一致性演算法:Raft 演算法

文章出處:https://www.jianshu.com/p/2a2ba021f721

Raft 演算法是可以用來替代 Paxos 演算法的分散式一致性演算法,而且 raft 演算法比 Paxos 演算法更易懂且更容易實現。本文對 raft 論文進行翻譯,希望能有助於讀者更方便地理解 raft 的思想。如果對 Paxos 演算法感興趣,可以看我的另一篇文章:分散式系列文章——Paxos演算法原理與推導

摘要

Raft 是用來管理複製日誌(replicated log)的一致性協議。它跟 multi-Paxos 作用相同,效率也相當,但是它的組織結構跟 Paxos 不同。這使得 Raft 比 Paxos 更容易理解並且更容易在工程實踐中實現。為了使 Raft 協議更易懂,Raft將一致性的關鍵元素分開,如 leader 選舉、日誌複製和安全性,並且它實施更強的一致性以減少必須考慮的狀態的數量。使用者研究的結果表明,Raft 比 Paxos 更容易學習。 Raft 還包括一個用於變更叢集成員的新機制,它使用重疊的大多數(overlapping majorities)來保證安全性。

1 介紹

一致性演算法允許多臺機器作為一個叢集協同工作,並且在其中的某幾臺機器出故障時叢集仍然能正常工作。 正因為如此,一致性演算法在建立可靠的大規模軟體系統方面發揮了關鍵作用。 在過去十年中,Paxos [15,16] 主導了關於一致性演算法的討論:大多數一致性的實現都是基於 Paxos 或受其影響,Paxos 已成為用於教授學生一致性相關知識的主要工具。

不幸的是,Paxos 實在是太難以理解,儘管許多人一直在努力嘗試使其更易懂。 此外,其架構需要複雜的改變來支援實際系統。 結果是,系統開發者和學生都在與 Paxos 鬥爭。

在我們自己與 Paxos 鬥爭之後,我們開始著手尋找一個新的一致性演算法,可以為系統開發和教學提供更好的基礎。 我們的方法是不尋常的,因為我們的主要目標是可理解性:我們可以為實際系統定義一個一致性演算法,並以比 Paxos 更容易學習的方式描述它嗎?在該演算法的設計過程中,重要的不僅是如何讓該演算法起作用,還有清晰地知道該演算法為什麼會起作用。

這項工作的結果是一個稱為 Raft 的一致性演算法。 在設計 Raft 時,我們使用了特定的技術來提高可理解性,包括分解(Raft 分離 leader 選舉,日誌複製和安全)和狀態空間減少(相對於 Paxos ,Raft 減少了不確定性程度和伺服器之間彼此不一致的方式 )。 一項針對兩個大學的 43 名學生的使用者研究表明,Raft 比 Paxos 更容易理解:在學習兩種演算法後,其中 33 名學生能夠更好地回答關於 Raft 的問題。

Raft 在許多方面類似於現有的一致性演算法(尤其是 Oki 和 Liskov 的 Viewstamped Replication [29,22]),但它有幾個新特性:

  • Strong leader:在 Raft 中,日誌條目(log entries)只從 leader 流向其他伺服器。 這簡化了複製日誌的管理,使得 raft 更容易理解。
  • Leader 選舉:Raft 使用隨機計時器進行 leader 選舉。 這隻需在任何一致性演算法都需要的心跳(heartbeats)上增加少量機制,同時能夠簡單快速地解決衝突。
  • 成員變更:Raft 使用了一種新的聯合一致性方法,其中兩個不同配置的大多數在過渡期間重疊。 這允許叢集在配置更改期間繼續正常執行。

我們認為,Raft 優於 Paxos 和其他一致性演算法,不僅在教學方面,在工程實現方面也是。 它比其他演算法更簡單且更易於理解; 它被描述得十分詳細足以滿足實際系統的需要; 它有多個開源實現,並被多家公司使用; 它的安全性已被正式規定和驗證; 它的效率與其他演算法相當。

本文的剩餘部分介紹了複製狀態機問題(第 2 節),討論了 Paxos 的優點和缺點(第3節),描述了我們實現易理解性的方法(第 4 節),提出了Raft一致性演算法(第 5-8 節),評估Raft(第 9 節),並討論了相關工作(第 10 節)。

2 複製狀態機(Replicated state machines)

一致性演算法是在複製狀態機[37]的背景下產生的。 在這種方法中,一組伺服器上的狀態機計算相同狀態的相同副本,並且即使某些伺服器宕機,也可以繼續執行。

複製狀態機用於解決分散式系統中的各種容錯問題。 例如,具有單個 leader 的大規模系統,如 GFS [8],HDFS [38] 和 RAMCloud [33] ,通常使用單獨的複製狀態機來進行 leader 選舉和儲存 leader 崩潰後重新選舉需要的配置資訊。Chubby [2] 和 ZooKeeper [11] 都是複製狀態機。

複製狀態機通常使用複製日誌實現,如圖1所示。每個伺服器儲存一個包含一系列命令的日誌,其狀態機按順序執行日誌中的命令。 每個日誌中命令都相同並且順序也一樣,因此每個狀態機處理相同的命令序列。 這樣就能得到相同的狀態和相同的輸出序列。

圖1

一致性演算法的工作就是保證複製日誌的一致性。 每臺伺服器上的一致性模組接收來自客戶端的命令,並將它們新增到其日誌中。 它與其他伺服器上的一致性模組通訊,以確保每個日誌最終以相同的順序包含相同的命令,即使有一些伺服器失敗。 一旦命令被正確複製,每個伺服器上的狀態機按日誌順序處理它們,並將輸出返回給客戶端。 這樣就形成了高可用的複製狀態機。

實際系統中的一致性演算法通常具有以下屬性:

  • 它們確保在所有非拜占庭條件下(包括網路延遲,分割槽和資料包丟失,重複和亂序)的安全性(不會返回不正確的結果)。

  • 只要任何大多數(過半)伺服器都可以執行,並且可以相互通訊和與客戶通訊,一致性演算法就可用。 因此,五臺伺服器的典型叢集可以容忍任何兩臺伺服器的故障。 假設伺服器突然宕機; 它們可以稍後從狀態恢復並重新加入群集。

  • 它們不依賴於時序來確保日誌的一致性:錯誤的時鐘和極端訊息延遲可能在最壞的情況下導致可用性問題。

  • 在通常情況下,只要叢集的大部分(過半伺服器)已經響應了單輪遠端過程呼叫,命令就可以完成; 少數(一半以下)慢伺服器不需要影響整個系統性能。

3 Paxos 存在的問題

在過去十年裡,Leslie Lamport 的 Paxos 協議[15]幾乎成為一致性的同義詞:它是課堂上教授最多的一致性協議,並且大多數一致性的實現也以它為起點。 Paxos 首先定義了能夠在單個決策(例如單個複製日誌條目)上達成一致的協議。 我們將這個子集稱為 single-decree Paxos。 然後 Paxos 組合該協議的多個例項以促進一系列決策,例如日誌(multi-Paxos)。 Paxos能夠確保安全性和活性,並且支援叢集成員的變更。它的正確性已被證明,並且在正常情況下是高效的。

不幸的是,Paxos 有兩個顯著的缺點。 第一個缺點是 Paxos 非常難以理解。 Paxos 的描述晦澀難懂,臭名昭著(譯者注:《The Part-time Parliament》比較晦澀難懂,但是《Paxos Made Simple》就比較容易理解); 很少有人成功地理解它,即使能理解也必須付出巨大的努力。 因此,已有幾個嘗試以更簡單的方式來描述 Paxos [16,20,21] 。 這些描述集中在 single-degree Paxos ,但它們仍然具有挑戰性。 在對 NSDI 2012 參會者的非正式調查中,我們發現很少有人喜歡 Paxos ,即使是經驗豐富的研究人員。 我們自己也跟 Paxos 進行了艱苦的鬥爭; 我們也無法完全理解整個協議,直到閱讀了幾個更簡單的描述和自己設計替代 Paxos 的協議,整個過程花了將近一年。

Paxos 晦澀難懂的原因是作者選擇了single-degree Paxos作為基礎。Single-decree Paxos 分成兩個階段,這兩個階段沒有簡單直觀的說明,並且不能被單獨理解。因此,很難理解為什麼該演算法能起作用。Multi-Paxos 的合成規則又增加了許多複雜性。我們相信,對多個決定(日誌而不是單個日誌條目)達成一致的總體問題可以用其他更直接和更明顯的方式進行分解。

Paxos的第二個問題是它不能為構建實際的實現提供良好的基礎。 一個原因是沒有針對 multi-Paxos 的廣泛同意的演算法。 Lamport的描述主要是關於 single-decree Paxos; 他描述了 multi-Paxos 的可能方法,但缺少許多細節。 已經有幾個嘗試來具體化和優化 Paxos ,例如[26],[39]和[13],但這些彼此各不相同並且跟 Lamport 描述的也不同。 像Chubby [4] 這樣的系統已經實現了類 Paxos(Paxos-like)演算法,但大多數情況下,它們的細節並沒有公佈。

此外,Paxos 的架構對於構建實際系統來說是一個糟糕的設計,這是 single-decree 分解的另一個結果。 例如,獨立地選擇日誌條目集合,然後再將它們合併到順序日誌中幾乎沒有任何好處,這隻會增加複雜性。 圍繞日誌設計系統是更簡單和有效的方法,新日誌條目按照約束順序地新增到日誌中。 Paxos 的做法適用於只需要做一次決策的情況,如果需要做一系列決策,更簡單和快速的方法是先選擇一個 leader ,然後讓該 leader 協調這些決策。

因此,實際的系統跟 Paxos 相差很大。幾乎所有的實現都是從 Paxos 開始,然後發現很多實現上的難題,接著就開發了一種和 Paxos 完全不一樣的架構。這樣既費時又容易出錯,而且 Paxos 本身晦澀難懂使得該問題更加嚴重。Paxos 的公式可能可以很好地證明它的正確性,但是現實的系統和 Paxos 差別是如此之大,以至於這些證明並沒有什麼太大的價值。下面來自 Chubby 作者的評論非常典型:

在Paxos演算法描述和實現現實系統中間有著巨大的鴻溝。最終的系統往往建立在一個還未被證明的協議之上。

由於以上問題,我們得出的結論是 Paxos 演算法沒有為系統實踐和教學提供一個良好的基礎。考慮到一致性問題在大規模軟體系統中的重要性,我們決定嘗試設計一個能夠替代 Paxos 並且具有更好特性的一致性演算法。Raft演算法就是這次實驗的結果。

4 為可理解性而設計

在設計 Raft 演算法過程中我們有幾個目標:它必須提供一個完整的實際的系統實現基礎,這樣才能大大減少開發者的工作;它必須在任何情況下都是安全的並且在典型的應用條件下是可用的;並且在正常情況下是高效的。但是我們最重要的目標也是最大的挑戰是可理解性。它必須保證能夠被大多數人容易地理解。另外,它必須能夠讓人形成直觀的認識,這樣系統的構建者才能夠在現實中進行擴充套件。

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

我們意識到這樣的分析具有高度的主觀性;但是我們使用了兩種通用的技術來解決這個問題。第一個技術就是眾所周知的問題分解:只要有可能,我們就將問題分解成幾個相對獨立的,可被解決的、可解釋的和可理解的子問題。例如,Raft 演算法被我們分成 leader 選舉,日誌複製,安全性和成員變更幾個部分。

我們使用的第二個方法是通過減少狀態的數量來簡化狀態空間,使得系統更加連貫並且儘可能消除不確定性。特別的,所有的日誌是不允許有空洞的,並且 Raft 限制了使日誌之間不一致的方式。儘管在大多數情況下我們都試圖去消除不確定性,但是在某些情況下不確定性可以提高可理解性。特別是,隨機化方法雖然引入了不確定性,但是他們往往能夠通過使用相近的方法處理可能的選擇來減少狀態空間。我們使用隨機化來簡化 Raft 中的 leader 選舉演算法。

5 Raft 一致性演算法

Raft 是一種用來管理第 2 節中描述的複製日誌的演算法。圖 2 是該演算法的濃縮,可用作參考,圖 3 列舉了該演算法的一些關鍵特性。圖中的這些內容將在剩下的章節中逐一介紹。

圖2 圖3

Raft 通過首先選舉一個 distinguished leader,然後讓它全權負責管理複製日誌來實現一致性。Leader 從客戶端接收日誌條目,把日誌條目複製到其他伺服器上,並且在保證安全性的時候通知其他伺服器將日誌條目應用到他們的狀態機中。擁有一個 leader 大大簡化了對複製日誌的管理。例如,領導人可以決定新的日誌條目需要放在日誌中的什麼位置而不需要和其他伺服器商議,並且資料都是從 leader 流向其他伺服器。leader 可能宕機,也可能和其他伺服器斷開連線,這時一個新的 leader 會被選舉出來。

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

  • Leader 選舉:當前的 leader 宕機時,一個新的 leader 必須被選舉出來。(章節 5.2)
  • 日誌複製:Leader 必須從客戶端接收日誌條目然後複製到叢集中的其他節點,並且強制要求其他節點的日誌和自己的保持一致。
  • 安全性:Raft 中安全性的關鍵是圖 3 中狀態機的安全性:如果有任何的伺服器節點已經應用了一個特定的日誌條目到它的狀態機中,那麼其他伺服器節點不能在同一個日誌索引位置應用一條不同的指令。章節 5.4 闡述了 Raft 演算法是如何保證這個特性的;該解決方案在選舉機制(5.2 節)上增加了額外的限制。

在展示一致性演算法之後,本章節將討論可用性的一些問題以及時序在系統中的作用。

5.1 Raft 基礎

一個 Raft 叢集包含若干個伺服器節點;通常是 5 個,這樣的系統可以容忍 2 個節點的失效。在任何時刻,每一個伺服器節點都處於這三個狀態之一:leader、follower 或者 candidate 。在正常情況下,叢集中只有一個 leader 並且其他的節點全部都是 follower 。Follower 都是被動的:他們不會發送任何請求,只是簡單的響應來自 leader 和 candidate 的請求。Leader 處理所有的客戶端請求(如果一個客戶端和 follower 通訊,follower 會將請求重定向給 leader)。第三種狀態,candidate ,是用來選舉一個新的 leader(章節 5.2)。圖 4 展示了這些狀態和他們之間的轉換關係;這些轉換關係在接下來會進行討論。

圖4

Raft 把時間分割成任意長度的任期(term),如圖 5 所示。任期用連續的整數標記。每一段任期從一次選舉開始,一個或者多個 candidate 嘗試成為 leader 。如果一個 candidate 贏得選舉,然後他就在該任期剩下的時間裡充當 leader 。在某些情況下,一次選舉無法選出 leader 。在這種情況下,這一任期會以沒有 leader 結束;一個新的任期(包含一次新的選舉)會很快重新開始。Raft 保證了在任意一個任期內,最多隻有一個 leader 。

圖5

不同的伺服器節點觀察到的任期轉換的次數可能不同,在某些情況下,一個伺服器節點可能沒有看到 leader 選舉過程或者甚至整個任期全程。任期在 Raft 演算法中充當邏輯時鐘的作用,這使得伺服器節點可以發現一些過期的資訊比如過時的 leader 。每一個伺服器節點儲存一個當前任期號,該編號隨著時間單調遞增。伺服器之間通訊的時候會交換當前任期號;如果一個伺服器的當前任期號比其他的小,該伺服器會將自己的任期號更新為較大的那個值。如果一個 candidate 或者 leader 發現自己的任期號過期了,它會立即回到 follower 狀態。如果一個節點接收到一個包含過期的任期號的請求,它會直接拒絕這個請求。

Raft 演算法中伺服器節點之間使用 RPC 進行通訊,並且基本的一致性演算法只需要兩種型別的 RPC。請求投票(RequestVote) RPC 由 candidate 在選舉期間發起(章節 5.2),追加條目(AppendEntries)RPC 由 leader 發起,用來複制日誌和提供一種心跳機制(章節 5.3)。第 7 節為了在伺服器之間傳輸快照增加了第三種 RPC。當伺服器沒有及時的收到 RPC 的響應時,會進行重試, 並且他們能夠並行的發起 RPC 來獲得最佳的效能。

5.2 Leader 選舉

Raft 使用一種心跳機制來觸發 leader 選舉。當伺服器程式啟動時,他們都是 follower 。一個伺服器節點只要能從 leader 或 candidate 處接收到有效的 RPC 就一直保持 follower 狀態。Leader 週期性地向所有 follower 傳送心跳(不包含日誌條目的 AppendEntries RPC)來維持自己的地位。如果一個 follower 在一段選舉超時時間內沒有接收到任何訊息,它就假設系統中沒有可用的 leader ,然後開始進行選舉以選出新的leader。

要開始一次選舉過程,follower 先增加自己的當前任期號並且轉換到 candidate 狀態。然後投票給自己並且並行地向叢集中的其他伺服器節點發送 RequestVote RPC(讓其他伺服器節點投票給它)。Candidate 會一直保持當前狀態直到以下三件事情之一發生:(a) 它自己贏得了這次的選舉(收到過半的投票),(b) 其他的伺服器節點成為 leader ,(c) 一段時間之後沒有任何獲勝者。這些結果會在下面的章節裡分別討論。

當一個 candidate 獲得叢集中過半伺服器節點針對同一個任期的投票,它就贏得了這次選舉併成為 leader 。對於同一個任期,每個伺服器節點只會投給一個 candidate ,按照先來先服務(first-come-first-served)的原則(注意:5.4 節在投票上增加了額外的限制)。要求獲得過半投票的規則確保了最多隻有一個 candidate 贏得此次選舉(圖 3 中的選舉安全性)。一旦 candidate 贏得選舉,就立即成為 leader 。然後它會向其他的伺服器節點發送心跳訊息來確定自己的地位並阻止新的選舉。

在等待投票期間,candidate 可能會收到另一個聲稱自己是 leader 的伺服器節點發來的 AppendEntries RPC 。如果這個 leader 的任期號(包含在RPC中)不小於 candidate 當前的任期號,那麼 candidate 會承認該 leader 的合法地位並回到 follower 狀態。 如果 RPC 中的任期號比自己的小,那麼 candidate 就會拒絕這次的 RPC 並且繼續保持 candidate 狀態。

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

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

選舉的例子可以很好地展示可理解性是如何指導我們選擇設計方案的。起初我們打算使用一種等級系統(ranking system):每一個 candidate 都被賦予一個唯一的等級(rank),等級用來在競爭的 candidate 之間進行選擇。如果一個 candidate 發現另一個 candidate 擁有更高的等級,它就會回到 follower 狀態,這樣高等級的 candidate 能夠更加容易地贏得下一次選舉。但是我們發現這種方法在可用性方面會有一下小問題。我們對該演算法進行了多次調整,但是每次調整之後都會有新的小問題。最終我們認為隨機重試的方法更加顯然且易於理解。

5.3 日誌複製

Leader 一旦被選舉出來,就開始為客戶端請求提供服務。客戶端的每一個請求都包含一條將被複制狀態機執行的指令。Leader 把該指令作為一個新的條目追加到日誌中去,然後並行的發起 AppendEntries RPC 給其他的伺服器,讓它們複製該條目。當該條目被安全地複製(下面會介紹),leader 會應用該條目到它的狀態機中(狀態機執行該指令)然後把執行的結果返回給客戶端。如果 follower 崩潰或者執行緩慢,或者網路丟包,領導人會不斷地重試 AppendEntries RPC(即使已經回覆了客戶端)直到所有的 follower 最終都儲存了所有的日誌條目。

日誌以圖 6 展示的方式組織。每個日誌條目儲存一條狀態機指令和 leader 收到該指令時的任期號。任期號用來檢測多個日誌副本之間的不一致情況,同時也用來保證圖 3 中的某些性質。每個日誌條目都有一個整數索引值來表明它在日誌中的位置。

圖6

Leader 決定什麼時候把日誌條目應用到狀態機中是安全的;這種日誌條目被稱為已提交的。Raft 演算法保證所有已提交的日誌條目都是持久化的並且最終會被所有可用的狀態機執行。一旦建立該日誌條目的 leader 將它複製到過半的伺服器上,該日誌條目就會被提交(例如在圖 6 中的條目 7)。同時,leader 日誌中該日誌條目之前的所有日誌條目也都會被提交,包括由其他 leader 建立的條目。5.4 節討論在 leader 變更之後應用該規則的一些細節,並且證明了這種提交的規則是安全的。Leader 追蹤將會被提交的日誌條目的最大索引,未來的所有 AppendEntries RPC 都會包含該索引,這樣其他的伺服器才能最終知道哪些日誌條目需要被提交。Follower 一旦知道某個日誌條目已經被提交就會將該日誌條目應用到自己的本地狀態機中(按照日誌的順序)。

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

  • 如果不同日誌中的兩個條目擁有相同的索引和任期號,那麼他們儲存了相同的指令。
  • 如果不同日誌中的兩個條目擁有相同的索引和任期號,那麼他們之前的所有日誌條目也都相同。

Leader 在特定的任期號內的一個日誌索引處最多建立一個日誌條目,同時日誌條目在日誌中的位置也從來不會改變。該點保證了上面的第一條特性。第二個特性是由 AppendEntries RPC 執行一個簡單的一致性檢查所保證的。在傳送 AppendEntries RPC 的時候,leader 會將前一個日誌條目的索引位置和任期號包含在裡面。如果 follower 在它的日誌中找不到包含相同索引位置和任期號的條目,那麼他就會拒絕該新的日誌條目。一致性檢查就像一個歸納步驟:一開始空的日誌狀態肯定是滿足 Log Matching Property(日誌匹配特性) 的,然後一致性檢查保證了日誌擴充套件時的日誌匹配特性。因此,每當 AppendEntries RPC 返回成功時,leader 就知道 follower 的日誌一定和自己相同(從第一個日誌條目到最新條目)。

正常操作期間,leader 和 follower 的日誌保持一致,所以 AppendEntries RPC 的一致性檢查從來不會失敗。然而,leader 崩潰的情況會使日誌處於不一致的狀態(老的 leader 可能還沒有完全複製它日誌裡的所有條目)。這種不一致會在一系列的 leader 和 follower 崩潰的情況下加劇。圖 7 展示了在什麼情況下 follower 的日誌可能和新的 leader 的日誌不同。Follower 可能缺少一些在新 leader 中有的日誌條目,也可能擁有一些新 leader 沒有的日誌條目,或者同時發生。缺失或多出日誌條目的情況可能會涉及到多個任期。

圖7

圖 7:當一個 leader 成功當選時(最上面那條日誌),follower 可能是(a-f)中的任何情況。每一個盒子表示一個日誌條目;裡面的數字表示任期號。Follower 可能會缺少一些日誌條目(a-b),可能會有一些未被提交的日誌條目(c-d),或者兩種情況都存在(e-f)。例如,場景 f 可能這樣發生,f 對應的伺服器在任期 2 的時候是 leader ,追加了一些日誌條目到自己的日誌中,一條都還沒提交(commit)就崩潰了;該伺服器很快重啟,在任期 3 重新被選為 leader,又追加了一些日誌條目到自己的日誌中;在這些任期 2 和任期 3 中的日誌都還沒被提交之前,該伺服器又宕機了,並且在接下來的幾個任期裡一直處於宕機狀態。

在 Raft 演算法中,leader 通過強制 follower 複製它的日誌來解決不一致的問題。這意味著 follower 中跟 leader 衝突的日誌條目會被 leader 的日誌條目覆蓋。5.4 節會證明通過增加一個限制可以保證安全性。

要使得 follower 的日誌跟自己一致,leader 必須找到兩者達成一致的最大的日誌條目(索引最大),刪除 follower 日誌中從那個點之後的所有日誌條目,並且將自己從那個點之後的所有日誌條目傳送給 follower 。所有的這些操作都發生在對 AppendEntries RPCs 中一致性檢查的回覆中。Leader 針對每一個 follower 都維護了一個 nextIndex ,表示 leader 要傳送給 follower 的下一個日誌條目的索引。當選出一個新 leader 時,該 leader 將所有 nextIndex 的值都初始化為自己最後一個日誌條目的 index 加1(圖 7 中的 11)。如果 follower 的日誌和 leader 的不一致,那麼下一次 AppendEntries RPC 中的一致性檢查就會失敗。在被 follower 拒絕之後,leaer 就會減小 nextIndex 值並重試 AppendEntries RPC 。最終 nextIndex 會在某個位置使得 leader 和 follower 的日誌達成一致。此時,AppendEntries RPC 就會成功,將 follower 中跟 leader 衝突的日誌條目全部刪除然後追加 leader 中的日誌條目(如果有需要追加的日誌條目的話)。一旦 AppendEntries RPC 成功,follower 的日誌就和 leader 一致,並且在該任期接下來的時間裡保持一致。

如果想要的話,該協議可以被優化來減少被拒絕的 AppendEntries RPC 的個數。例如,當拒絕一個 AppendEntries RPC 的請求的時候,follower 可以包含衝突條目的任期號和自己儲存的那個任期的第一個 index 。藉助這些資訊,leader 可以跳過那個任期內所有衝突的日誌條目來減小 nextIndex;這樣就變成每個有衝突日誌條目的任期需要一個 AppendEntries RPC 而不是每個條目一次。在實踐中,我們認為這種優化是沒有必要的,因為失敗不經常發生並且也不可能有很多不一致的日誌條目。

通過這種機制,leader 在當權之後就不需要任何特殊的操作來使日誌恢復到一致狀態。Leader 只需要進行正常的操作,然後日誌就能在回覆 AppendEntries 一致性檢查失敗的時候自動趨於一致。Leader 從來不會覆蓋或者刪除自己的日誌條目(圖 3 的 Leader Append-Only 屬性)。

這樣的日誌複製機制展示了第 2 節中描述的一致性特性:只要過半的伺服器能正常執行,Raft 就能夠接受,複製並應用新的日誌條目;在正常情況下,新的日誌條目可以在一個 RPC 來回中被複制給叢集中的過半機器;並且單個執行慢的 follower 不會影響整體的效能。

5.4 安全性

前面的章節裡描述了 Raft 演算法是如何進行 leader 選舉和日誌複製的。然而,到目前為止描述的機制並不能充分地保證每一個狀態機會按照相同的順序執行相同的指令。例如,一個 follower 可能會進入不可用狀態,在此期間,leader 可能提交了若干的日誌條目,然後這個 follower 可能會被選舉為 leader 並且用新的日誌條目覆蓋這些日誌條目;結果,不同的狀態機可能會執行不同的指令序列。

這節通過對 leader 選舉增加一個限制來完善 Raft 演算法。這一限制保證了對於給定的任意任期號, leader 都包含了之前各個任期所有被提交的日誌條目(圖 3 中的 Leader Completeness 性質)。有了這一 leader 選舉的限制,我們也使得提交規則更加清晰。最後,我們展示了對於 Leader Completeness 性質的簡要證明並且說明該性質是如何領導複製狀態機執行正確的行為的。

5.4.1 選舉限制

在任何基於 leader 的一致性演算法中,leader 最終都必須儲存所有已經提交的日誌條目。在某些一致性演算法中,例如 Viewstamped Replication[22],一開始並沒有包含所有已經提交的日誌條目的伺服器也可能被選為 leader 。這種演算法包含一些額外的機制來識別丟失的日誌條目並將它們傳送給新的 leader ,要麼是在選舉階段要麼在之後很快進行。不幸的是,這種方法會導致相當大的額外的機制和複雜性。Raft 使用了一種更加簡單的方法,它可以保證新 leader 在當選時就包含了之前所有任期號中已經提交的日誌條目,不需要再傳送這些日誌條目給新 leader 。這意味著日誌條目的傳送是單向的,只從 leader 到 follower,並且 leader 從不會覆蓋本地日誌中已經存在的條目。

Raft 使用投票的方式來阻止 candidate 贏得選舉除非該 candidate 包含了所有已經提交的日誌條目。候選人為了贏得選舉必須與叢集中的過半節點通訊,這意味著至少其中一個伺服器節點包含了所有已提交的日誌條目。如果 candidate 的日誌至少和過半的伺服器節點一樣新(接下來會精確地定義“新”),那麼他一定包含了所有已經提交的日誌條目。RequestVote RPC 執行了這樣的限制: RPC 中包含了 candidate 的日誌資訊,如果投票者自己的日誌比 candidate 的還新,它會拒絕掉該投票請求。

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

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

如同 5.3 節描述的那樣,一旦當前任期內的某個日誌條目已經儲存到過半的伺服器節點上,leader 就知道該日誌條目已經被提交了。如果某個 leader 在提交某個日誌條目之前崩潰了,以後的 leader 會試圖完成該日誌條目的複製。然而,如果是之前任期內的某個日誌條目已經儲存到過半的伺服器節點上,leader 也無法立即斷定該日誌條目已經被提交了。圖 8 展示了一種情況,一個已經被儲存到過半節點上的老日誌條目,仍然有可能會被未來的 leader 覆蓋掉。

圖8

圖 8:如圖的時間序列展示了為什麼 leader 無法判斷老的任期號內的日誌是否已經被提交。在 (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 就不可能選舉成功)。 在這種情況下,之前的所有日誌也被提交了。

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

Raft 會在提交規則上增加額外的複雜性是因為當 leader 複製之前任期內的日誌條目時,這些日誌條目都保留原來的任期號。在其他的一致性演算法中,如果一個新的 leader 要重新複製之前的任期裡的日誌時,它必須使用當前新的任期號。Raft 的做法使得更加容易推匯出(reason about)日誌條目,因為他們自始至終都使用同一個任期號。另外,和其他的演算法相比,Raft 中的新 leader 只需要傳送更少的日誌條目(其他演算法中必須在它們被提交之前傳送更多的冗餘日誌條目來給它們重新編號)。

5.4.3 安全性論證

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

圖9

圖 9:如果 S1 (任期 T 的 leader)在它的任期裡提交了一個新的日誌條目,然後 S5 在之後的任期 U 裡被選舉為 leader ,那麼肯定至少會有一個節點,如 S3,既接收了來自 S1 的日誌條目,也給 S5 投票了。

  1. U 一定在剛成為 leader 的時候就沒有那條被提交的日誌條目了(leader 從不會刪除或者覆蓋任何條目)。

  2. Leader T 複製該日誌條目給叢集中的過半節點,同時,leader U 從叢集中的過半節點贏得了選票。因此,至少有一個節點(投票者)同時接受了來自 leader T 的日誌條目和給 leader U 投票了,如圖 9。該投票者是產生矛盾的關鍵。

  3. 該投票者必須在給 leader U 投票之前先接受了從 leader T 發來的已經被提交的日誌條目;否則它就會拒絕來自 leader T 的 AppendEntries 請求(因為此時它的任期號會比 T 大)。

  4. 該投票者在給 leader U 投票時依然保有這該日誌條目,因為任何 U 、T 之間的 leader 都包含該日誌條目(根據上述的假設),leader 從不會刪除條目,並且 follower 只有跟 leader 衝突的時候才會刪除條目。

  5. 該投票者把自己選票投給 leader U 時,leader U 的日誌必須至少和投票者的一樣新。這就導致了以下兩個矛盾之一。

  6. 首先,如果該投票者和 leader U 的最後一個日誌條目的任期號相同,那麼 leader U 的日誌至少和該投票者的一樣長,所以 leader U 的日誌一定包含該投票者日誌中的所有日誌條目。這是一個矛盾,因為該投票者包含了該已被提交的日誌條目,但是在上述的假設裡,leader U 是不包含的。

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

  8. 因此,所有比 T 大的任期的 leader 一定都包含了任期 T 中提交的所有日誌條目。

  9. 日誌匹配特性保證了未來的 leader 也會包含被間接提交的日誌條目,例如圖 8 (d) 中的索引 2。

通過 Leader Completeness 特性,我們就能證明圖 3 中的狀態機安全特性,即如果某個伺服器已經將某個給定的索引處的日誌條目應用到自己的狀態機裡了,那麼其他的伺服器就不會在相同的索引處應用一個不同的日誌條目。在一個伺服器應用一個日誌條目到自己的狀態機中時,它的日誌和 leader 的日誌從開始到該日誌條目都相同,並且該日誌條目必須被提交。現在考慮如下最小任期號:某伺服器在該任期號中某個特定的索引處應用了一個日誌條目;日誌完整性特性保證擁有更高任期號的 leader 會儲存相同的日誌條目,所以之後任期裡伺服器應用該索引處的日誌條目也會是相同的值。因此,狀態機安全特性是成立的。

最後,Raft 要求伺服器按照日誌索引順序應用日誌條目。再加上狀態機安全特性,這就意味著所有的伺服器都會按照相同的順序應用相同的日誌條目到自己的狀態機中。

5.5 Follower 和 candidate 崩潰

到目前為止,我們只關注了 leader 崩潰的情況。Follower 和 candidate 崩潰後的處理方式比 leader 崩潰要簡單的多,並且兩者的處理方式是相同的。如果 follower 或者 candidate 崩潰了,那麼後續傳送給他們的 RequestVote 和 AppendEntries RPCs 都會失敗。Raft 通過無限的重試來處理這種失敗;如果崩潰的機器重啟了,那麼這些 RPC 就會成功地完成。如果一個伺服器在完成了一個 RPC,但是還沒有響應的時候崩潰了,那麼在它重啟之後就會再次收到同樣的請求。Raft 的 RPCs 都是冪等的,所以這樣的重試不會造成任何傷害。例如,一個 follower 如果收到 AppendEntries 請求但是它的日誌中已經包含了這些日誌條目,它就會直接忽略這個新的請求中的這些日誌條目。

5.6 定時(timing)和可用性

Raft 的要求之一就是安全性不能依賴定時:整個系統不能因為某些事件執行得比預期快一點或者慢一點就產生錯誤的結果。但是,可用性(系統能夠及時響應客戶端)不可避免的要依賴於定時。例如,當有伺服器崩潰時,訊息交換的時間就會比正常情況下長,candidate 將不會等待太長的時間來贏得選舉;沒有一個穩定的 leader ,Raft 將無法工作。

Leader 選舉是 Raft 中定時最為關鍵的方面。 只要整個系統滿足下面的時間要求,Raft 就可以選舉出並維持一個穩定的 leader:

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

在這個不等式中,廣播時間指的是一個伺服器並行地傳送 RPCs 給叢集中所有的其他伺服器並接收到響應的平均時間;選舉超時時間就是在 5.2 節中介紹的選舉超時時間;平均故障間隔時間就是對於一臺伺服器而言,兩次故障間隔時間的平均值。廣播時間必須比選舉超時時間小一個量級,這樣 leader 才能夠可靠地傳送心跳訊息來阻止 follower 開始進入選舉狀態;再加上隨機化選舉超時時間的方法,這個不等式也使得選票瓜分的情況變得不可能。選舉超時時間需要比平均故障間隔時間小上幾個數量級,這樣整個系統才能穩定地執行。當 leader 崩潰後,整個系統會有大約選舉超時時間不可用;我們希望該情況在整個時間裡只佔一小部分。

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

6 叢集成員變更

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

為了使配置變更機制能夠安全,在轉換的過程中不能夠存在任何時間點使得同一個任期裡可能選出兩個 leader 。不幸的是,任何伺服器直接從舊的配置轉換到新的配置的方案都是不安全的。一次性自動地轉換所有伺服器是不可能的,所以在轉換期間整個叢集可能劃分成兩個獨立的大多數(見圖 10)。

圖10

圖 10:直接從一種配置轉到另一種配置是不安全的,因為各個機器會在不同的時候進行轉換。在這個例子中,叢集從 3 臺機器變成了 5 臺。不幸的是,存在這樣的一個時間點,同一個任期裡兩個不同的 leader 會被選出。一個獲得舊配置裡過半機器的投票,一個獲得新配置裡過半機器的投票。

為了保證安全性,配置變更必須採用一種兩階段方法。目前有很多種兩階段的實現。例如,有些系統(比如,[22])在第一階段停掉舊的配置所以不能處理客戶端請求;然後在第二階段在啟用新的配置。在 Raft 中,叢集先切換到一個過渡的配置,我們稱之為聯合一致(joint consensus);一旦聯合一致已經被提交了,那麼系統就切換到新的配置上。聯合一致結合了老配置和新配置:

  • 日誌條目被複制給叢集中新、老配置的所有伺服器。
  • 新、舊配置的伺服器都可以成為 leader 。
  • 達成一致(針對選舉和提交)需要分別在兩種配置上獲得過半的支援。

聯合一致允許獨立的伺服器在不妥協安全性的前提下,在不同的時刻進行配置轉換過程。此外,聯合一致允許叢集在配置變更期間依然響應客戶端請求。

圖11

叢集配置在複製日誌中以特殊的日誌條目來儲存和通訊;圖 11 展示了配置變更過程。當一個 leader 接收到一個改變配置從 C-old 到 C-new 的請求,它就為聯合一致將該配置(圖中的 C-old,new)儲存為一個日誌條目,並以前面描述的方式複製該條目。一旦某個伺服器將該新配置日誌條目增加到自己的日誌中,它就會用該配置來做出未來所有的決策(伺服器總是使用它日誌中最新的配置,無論該配置日誌是否已經被提交)。這就意味著 leader 會使用 C-old,new 的規則來決定 C-old,new 的日誌條目是什麼時候被提交的。如果 leader 崩潰了,新 leader 可能是在 C-old 配置也可能是在 C-old,new 配置下選出來的,這取決於贏得選舉的 candidate 是否已經接收到了 C-old,new 配置。在任何情況下, C-new 在這一時期都不能做出單方面決定。

一旦 C-old,new 被提交,那麼 C-old 和 C-new 都不能在沒有得到對方認可的情況下做出決定,並且 leader 完整性特性保證了只有擁有 C-old,new 日誌條目的伺服器才能被選舉為 leader 。現在 leader 建立一個描述 C-new 配置的日誌條目並複製到叢集其他節點就是安全的了。此外,新的配置被伺服器收到後就會立即生效。當新的配置在 C-new 的規則下被提交,舊的配置就變得無關緊要,同時不使用新配置的伺服器就可以被關閉了。如圖 11 所示,任何時刻 C-old 和 C-new 都不能單方面做出決定;這保證了安全性。

在關於配置變更還有三個問題需要解決。第一個問題是,新的伺服器開始時可能沒有儲存任何的日誌條目。當這些伺服器以這種狀態加入到叢集中,它們需要一段時間來更新來趕上其他伺服器,這段它們無法提交新的日誌條目。為了避免因此而造成的系統短時間的不可用,Raft 在配置變更前引入了一個額外的階段,在該階段,新的伺服器以沒有投票權身份加入到叢集中來(leader 也複製日誌給它們,但是考慮過半的時候不用考慮它們)。一旦該新的伺服器追趕上了叢集中的其他機器,配置變更就可以按上面描述的方式進行。

第二個問題是,叢集的 leader 可能不是新配置中的一員。在這種情況下,leader 一旦提交了 C-new 日誌條目就會退位(回到 follower 狀態)。這意味著有這樣的一段時間(leader 提交 C-new 期間),leader 管理著一個不包括自己的叢集;它複製著日誌但不把自己算在過半里面。Leader 轉換髮生在 C-new 被提交的時候,因為這是新配置可以獨立運轉的最早時刻(將總是能夠在 C-new 配置下選出新的領導人)。在此之前,可能只能從 C-old 中選出領導人。

第三個問題是,那些被移除的伺服器(不在 C-new 中)可能會擾亂叢集。這些伺服器將不會再接收到心跳,所以當選舉超時,它們就會進行新的選舉過程。它們會發送帶有新任期號的 RequestVote RPCs ,這樣會導致當前的 leader 回到 follower 狀態。新的 leader 最終會被選出來,但是被移除的伺服器將會再次超時,然後這個過程會再次重複,導致系統可用性很差。

為了防止這種問題,當伺服器認為當前 leader 存在時,伺服器會忽略RequestVote RPCs 。特別的,當伺服器在最小選舉超時時間內收到一個 RequestVote RPC,它不會更新任期號或者投票。這不會影響正常的選舉,每個伺服器在開始一次選舉之前,至少等待最小選舉超時時間。相反,這有利於避免被移除的伺服器的擾亂:如果 leader 能夠傳送心跳給叢集,那它就不會被更大的任期號廢黜。

7 日誌壓縮

Raft 的日誌在正常操作中隨著包含更多的客戶端請求不斷地增長,但是在實際的系統中,日誌不能無限制地增長。隨著日誌越來越長,它會佔用越來越多的空間,並且需要花更多的時間來回放。如果沒有一定的機制來清除日誌中積累的過期的資訊,最終就會帶來可用性問題。

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

增量壓縮方法,例如日誌清理或者日誌結構合併樹(log-structured merge trees,LSM 樹),都是可行的。這些方法每次只對一小部分資料進行操作,這樣就分散了壓縮的負載壓力。首先,它們先選擇一個積累了大量已經被刪除或者被覆蓋的物件的資料區域,然後重寫該區域還活著的物件,之後釋放該區域。和快照技術相比,它們需要大量額外的機制和複雜性,快照技術通過操作整個資料集來簡化該問題。狀態機可以用和快照技術相同的介面來實現 LSM 樹,但是日誌清除方法就需要修改 Raft 了。

圖12

一臺伺服器用一個新快照替代了它日誌中已經提交了的條目(索引 1 到 5),該快照只儲存了當前的狀態(變數 x 和 y 的值)。快照的 last included index 和 last included term 被儲存來定位日誌中條目 6 之前的快照

圖 12 展示了 Raft 中快照的基本思想。每個伺服器獨立地建立快照,快照只包括自己日誌中已經被提交的條目。主要的工作是狀態機將自己的狀態寫入快照中。Raft 快照中也包含了少量的元資料:the last included index 指的是最後一個被快照取代的日誌條目的索引值(狀態機最後應用的日誌條目),the last included term 是該條目的任期號。保留這些元資料是為了支援快照後第一個條目的 AppendEntries 一致性檢查,因為該條目需要之前的索引值和任期號。為了支援叢集成員變更(第 6 節),快照中也包括日誌中最新的配置作為 last included index 。一旦伺服器完成寫快照,他就可以刪除 last included index 之前的所有日誌條目,包括之前的快照。

儘管通常伺服器都是獨立地建立快照,但是 leader 必須偶爾傳送快照給一些落後的跟隨者。這通常發生在 leader 已經丟棄了需要傳送給 follower 的下一條日誌條目的時候。幸運的是這種情況在常規操作中是不可能的:一個與 leader 保持同步的 follower 通常都會有該日誌條目。然而一個例外的執行緩慢的 follower 或者新加入叢集的伺服器(第 6 節)將不會有這個條目。這時讓該 follower 更新到最新的狀態的方式就是通過網路把快照發送給它。

Leader 使用 InstallSnapshot RPC 來發送快照給太落後的 follower ;見圖 13。當 follower 收到帶有這種 RPC 的快照時,它必須決定如何處理已經存在的日誌條目。通常該快照會包含接收者日誌中沒有的資訊。在這種情況下,follower 丟棄它所有的日誌;這些會被該快照所取代,並且可能一些沒有提交的條目會和該快照產生衝突。如果接收到的快照是自己日誌的前面部分(由於網路重傳或者錯誤),那麼被快照包含的條目將會被全部刪除,但是快照之後的條目仍然有用並保留。

圖13

這種快照的方式違反了 Raft 的 strong leader 原則,因為 follower 可以在不知道 leader 狀態的情況下建立快照。但是我們認為這種違背是合乎情理的。Leader 的存在,是為了防止在達成一致性的時候的衝突,但是在建立快照的時候,一致性已經達成,因此沒有決策會衝突。資料依然只能從 leader 流到 follower ,只是 follower 可以重新組織它們的資料了。

我們考慮過一種可替代的基於 leader 的快照方案,在該方案中,只有leader 會建立快照,然後 leader 會發送它的快照給所有的 follower 。但是這樣做有兩個缺點。第一,傳送快照會浪費網路頻寬並且延緩了快照過程。每個 follower 都已經擁有了建立自己的快照所需要的資訊,而且很顯然,follower 從本地的狀態中建立快照遠比通過網路接收別人發來的要來得經濟。第二,leader 的實現會更加複雜。例如,leader 傳送快照給 follower 的同時也要並行地將新的日誌條目傳送給它們,這樣才不會阻塞新的客戶端請求。

還有兩個問題會影響快照的效能。首先,伺服器必須決定什麼時候建立快照。如果快照建立過於頻繁,那麼就會浪費大量的磁碟頻寬和其他資源;如果建立快照頻率太低,就要承擔耗盡儲存容量的風險,同時也增加了重啟時日誌回放的時間。一個簡單的策略就是當日志大小達到一個固定大小的時候就建立一次快照。如果這個閾值設定得顯著大於期望的快照的大小,那麼快照的磁碟頻寬負載就會很小。

第二個效能問題就是寫入快照需要花費一段時間,並且我們不希望它影響到正常的操作。解決方案是通過寫時複製的技術,這樣新的更新就可以在不影響正在寫的快照的情況下被接收。例如,具有泛函資料結構的狀態機天然支援這樣的功能。另外,作業系統對寫時複製技術的支援(如 Linux 上的 fork)可以被用來建立整個狀態機的記憶體快照(我們的實現用的就是這種方法)。

8 客戶端互動

本節介紹客戶端如何和 Raft 進行互動,包括客戶端如何找到 leader 和 Raft 是如何支援線性化語義的。這些問題對於所有基於一致性的系統都存在,並且 Raft 的解決方案和其他的也差不多。

Raft 的客戶端傳送所有的請求給 leader 。當客戶端第一次啟動的時候,它會隨機挑選一個伺服器進行通訊。如果客戶端第一次挑選的伺服器不是 leader ,那麼該伺服器會拒絕客戶端的請求並且提供關於它最近接收到的領導人的資訊(AppendEntries 請求包含了 leader 的網路地址)。如果 leader 已經崩潰了,客戶端請求就會超時;客戶端之後會再次隨機挑選伺服器進行重試。

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

只讀的操作可以直接處理而不需要記錄日誌。但是,如果不採取任何其他措施,這麼做可能會有返回過時資料(stale data)的風險,因為 leader 響應客戶端請求時可能已經被新的 leader 替代了,但是它還不知道自己已經不是最新的 leader 了。線性化的讀操作肯定不會返回過時資料,Raft 需要使用兩個額外的預防措施來在不使用日誌的情況下保證這一點。首先,leader 必須有關於哪些日誌條目被提交了的最新資訊。Leader 完整性特性保證了 leader 一定擁有所有已經被提交的日誌條目,但是在它任期開始的時候,它可能不知道哪些是已經被提交的。為了知道這些資訊,它需要在它的任期裡提交一個日誌條目。Raft 通過讓 leader 在任期開始的時候提交一個空的沒有任何操作的日誌條目到日誌中來處理該問題。第二,leader 在處理只讀請求之前必須檢查自己是否已經被替代了(如果一個更新的 leader 被選舉出來了,它的資訊就是過時的了)。Raft 通過讓 leader 在響應只讀請求之前,先和叢集中的過半節點交換一次心跳資訊來處理該問題。另一種可選的方案,leader 可以依賴心跳機制來實現一種租約的形式,但是這種方法依賴 timing 來保證安全性(假設時間誤差是有界的)。

Raft 網站

可進入我的部落格檢視原文。

歡迎關注公眾號: FullStackPlan 獲取更多幹貨