摘要
Raft 是一種為了管理複製日誌的一致性演算法。它提供了和 Paxos 演算法相同的功能和效能,但是它的演算法結構和 Paxos 不同,使得 Raft 演算法更加容易理解並且更容易構建實際的系統。為了提升可理解性,Raft 將一致性演算法分解成了幾個關鍵模組,例如領導人選舉、日誌複製和安全性。同時它通過實施一個更強的一致性來減少需要考慮的狀態的數量。從一個使用者研究的結果可以證明,對於學生而言,Raft 演算法比 Paxos 演算法更加容易學習。Raft 演算法還包括一個新的機制來允許叢集成員的動態改變,它利用重疊的大多數來保證安全性。
1 介紹
一致性演算法允許一組機器像一個整體一樣工作,即使其中一些機器出現故障也能夠繼續工作下去。正因為如此,一致性演算法在構建可信賴的大規模軟體系統中扮演著重要的角色。在過去的 10 年裡,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 使用一種更強的領導能力形式。比如,日誌條目只從領導者傳送給其他的伺服器。這種方式簡化了對複製日誌的管理並且使得 Raft 演算法更加易於理解。
- 領導選舉:Raft 演算法使用一個隨機計時器來選舉領導者。這種方式只是在任何一致性演算法都必須實現的心跳機制上增加了一點機制。在解決衝突的時候會更加簡單快捷。
- 成員關係調整:Raft 使用一種共同一致的方法來處理叢集成員變換的問題,在這種方法下,處於調整過程中的兩種不同的配置叢集中大多數機器會有重疊,這就使得叢集在成員變換的時候依然可以繼續工作。
我們相信,Raft 演算法不論出於教學目的還是作為實踐專案的基礎都是要比 Paxos 或者其他一致性演算法要優異的。它比其他演算法更加簡單,更加容易理解;它的演算法描述足以實現一個現實的系統;它有好多開源的實現並且在很多公司裡使用;它的安全性已經被證明;它的效率和其他演算法比起來也不相上下。
接下來,這篇論文會介紹以下內容:複製狀態機問題(第 2 節),討論 Paxos 的優點和缺點(第 3 節),討論我們為了可理解性而採取的方法(第 4 節),闡述 Raft 一致性演算法(第 5-8 節),評價 Raft 演算法(第 9 節),以及一些相關的工作(第 10 節)。
2 複製狀態機
一致性演算法是從複製狀態機的背景下提出的(參考英文原文引用37)。在這種方法中,一組伺服器上的狀態機產生相同狀態的副本,並且在一些機器宕掉的情況下也可以繼續執行。複製狀態機在分散式系統中被用於解決很多容錯的問題。例如,大規模的系統中通常都有一個叢集領導者,像 GFS、HDFS 和 RAMCloud,典型應用就是一個獨立的的複製狀態機去管理領導選舉和儲存配置資訊並且在領導人當機的情況下也要存活下來。比如 Chubby 和 ZooKeeper。
圖 1 :複製狀態機的結構。一致性演算法管理著來自客戶端指令的複製日誌。狀態機從日誌中處理相同順序的相同指令,所以產生的結果也是相同的。
複製狀態機通常都是基於複製日誌實現的,如圖 1。每一個伺服器儲存一個包含一系列指令的日誌,並且按照日誌的順序進行執行。每一個日誌都按照相同的順序包含相同的指令,所以每一個伺服器都執行相同的指令序列。因為每個狀態機都是確定的,每一次執行操作都產生相同的狀態和同樣的序列。
保證複製日誌相同就是一致性演算法的工作了。在一臺伺服器上,一致性模組接收客戶端傳送來的指令然後增加到自己的日誌中去。它和其他伺服器上的一致性模組進行通訊來保證每一個伺服器上的日誌最終都以相同的順序包含相同的請求,儘管有些伺服器會當機。一旦指令被正確的複製,每一個伺服器的狀態機按照日誌順序處理他們,然後輸出結果被返回給客戶端。因此,伺服器叢集看起來形成一個高可靠的狀態機。
實際系統中使用的一致性演算法通常含有以下特性:
- 安全性保證(絕對不會返回一個錯誤的結果):在非拜占庭錯誤情況下,包括網路延遲、分割槽、丟包、冗餘和亂序等錯誤都可以保證正確。
- 可用性:叢集中只要有大多數的機器可執行並且能夠相互通訊、和客戶端通訊,就可以保證可用。因此,一個典型的包含 5 個節點的叢集可以容忍兩個節點的失敗。伺服器被停止就認為是失敗。他們當有穩定的儲存的時候可以從狀態中恢復回來並重新加入叢集。
- 不依賴時序來保證一致性:物理時鐘錯誤或者極端的訊息延遲只有在最壞情況下才會導致可用性問題。
- 通常情況下,一條指令可以儘可能快的在叢集中大多數節點響應一輪遠端過程呼叫時完成。小部分比較慢的節點不會影響系統整體的效能。
3 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演算法就是這次實驗的結果。
4 為了可理解性的設計
設計 Raft 演算法我們有幾個初衷:它必須提供一個完整的實際的系統實現基礎,這樣才能大大減少開發者的工作;它必須在任何情況下都是安全的並且在大多數的情況下都是可用的;並且它的大部分操作必須是高效的。但是我們最重要也是最大的挑戰是可理解性。它必須保證對於普遍的人群都可以十分容易的去理解。另外,它必須能夠讓人形成直觀的認識,這樣系統的構建者才能夠在現實中進行必然的擴充套件。
在設計 Raft 演算法的時候,有很多的點需要我們在各種備選方案中進行選擇。在這種情況下,我們評估備選方案基於可理解性原則:解釋各個備選方案有多大的難度(例如,Raft 的狀態空間有多複雜,是否有微妙的暗示)?對於一個讀者而言,完全理解這個方案和暗示是否容易?
我們意識到對這種可理解性分析上具有高度的主觀性;儘管如此,我們使用了兩種通常適用的技術來解決這個問題。第一個技術就是眾所周知的問題分解:只要有可能,我們就將問題分解成幾個相對獨立的,可被解決的、可解釋的和可理解的子問題。例如,Raft 演算法被我們分成領導人選舉,日誌複製,安全性和角色改變幾個部分。
我們使用的第二個方法是通過減少狀態的數量來簡化需要考慮的狀態空間,使得系統更加連貫並且在可能的時候消除不確定性。特別的,所有的日誌是不允許有空洞的,並且 Raft 限制了日誌之間變成不一致狀態的可能。儘管在大多數情況下我們都試圖去消除不確定性,但是也有一些情況下不確定性可以提升可理解性。尤其是,隨機化方法增加了不確定性,但是他們有利於減少狀態空間數量,通過處理所有可能選擇時使用相似的方法。我們使用隨機化去簡化 Raft 中領導人選舉演算法。
5 Raft 一致性演算法
Raft 是一種用來管理章節 2 中描述的複製日誌的演算法。圖 2 為了參考之用,總結這個演算法的簡略版本,圖 3 列舉了這個演算法的一些關鍵特性。圖中的這些元素會在剩下的章節逐一介紹。
Raft 通過選舉一個高貴的領導人,然後給予他全部的管理複製日誌的責任來實現一致性。領導人從客戶端接收日誌條目,把日誌條目複製到其他伺服器上,並且當保證安全性的時候告訴其他的伺服器應用日誌條目到他們的狀態機中。擁有一個領導人大大簡化了對複製日誌的管理。例如,領導人可以決定新的日誌條目需要放在日誌中的什麼位置而不需要和其他伺服器商議,並且資料都從領導人流向其他伺服器。一個領導人可以當機,可以和其他伺服器失去連線,這時一個新的領導人會被選舉出來。
通過領導人的方式,Raft 將一致性問題分解成了三個相對獨立的子問題,這些問題會在接下來的子章節中進行討論:
- 領導選舉:一個新的領導人需要被選舉出來,當現存的領導人當機的時候(章節 5.2)
- 日誌複製:領導人必須從客戶端接收日誌然後複製到叢集中的其他節點,並且強制要求其他節點的日誌保持和自己相同。
- 安全性:在 Raft 中安全性的關鍵是在圖 3 中展示的狀態機安全:如果有任何的伺服器節點已經應用了一個確定的日誌條目到它的狀態機中,那麼其他伺服器節點不能在同一個日誌索引位置應用一個不同的指令。章節 5.4 闡述了 Raft 演算法是如何保證這個特性的;這個解決方案涉及到一個額外的選舉機制(5.2 節)上的限制。
在展示一致性演算法之後,這一章節會討論可用性的一些問題和計時在系統的作用。
狀態:
狀態 | 所有伺服器上持久存在的 |
---|---|
currentTerm | 伺服器最後一次知道的任期號(初始化為 0,持續遞增) |
votedFor | 在當前獲得選票的候選人的 Id |
log[] | 日誌條目集;每一個條目包含一個使用者狀態機執行的指令,和收到時的任期號 |
狀態 | 所有伺服器上經常變的 |
---|---|
commitIndex | 已知的最大的已經被提交的日誌條目的索引值 |
lastApplied | 最後被應用到狀態機的日誌條目索引值(初始化為 0,持續遞增) |
狀態 | 在領導人裡經常改變的 (選舉後重新初始化) |
---|---|
nextIndex[] | 對於每一個伺服器,需要傳送給他的下一個日誌條目的索引值(初始化為領導人最後索引值加一) |
matchIndex[] | 對於每一個伺服器,已經複製給他的日誌的最高索引值 |
附加日誌 RPC:
由領導人負責呼叫來複制日誌指令;也會用作heartbeat
引數 | 解釋 |
---|---|
term | 領導人的任期號 |
leaderId | 領導人的 Id,以便於跟隨者重定向請求 |
prevLogIndex | 新的日誌條目緊隨之前的索引值 |
prevLogTerm | prevLogIndex 條目的任期號 |
entries[] | 準備儲存的日誌條目(表示心跳時為空;一次性傳送多個是為了提高效率) |
leaderCommit | 領導人已經提交的日誌的索引值 |
返回值 | 解釋 |
---|---|
term | 當前的任期號,用於領導人去更新自己 |
success | 跟隨者包含了匹配上 prevLogIndex 和 prevLogTerm 的日誌時為真 |
接收者實現:
- 如果
term < currentTerm
就返回 false (5.1 節) - 如果日誌在 prevLogIndex 位置處的日誌條目的任期號和 prevLogTerm 不匹配,則返回 false (5.3 節)
- 如果已經存在的日誌條目和新的產生衝突(索引值相同但是任期號不同),刪除這一條和之後所有的 (5.3 節)
- 附加日誌中尚未存在的任何新條目
- 如果
leaderCommit > commitIndex
,令 commitIndex 等於 leaderCommit 和 新日誌條目索引值中較小的一個
請求投票 RPC:
由候選人負責呼叫用來徵集選票(5.2 節)
引數 | 解釋 |
---|---|
term | 候選人的任期號 |
candidateId | 請求選票的候選人的 Id |
lastLogIndex | 候選人的最後日誌條目的索引值 |
lastLogTerm | 候選人最後日誌條目的任期號 |
返回值 | 解釋 |
---|---|
term | 當前任期號,以便於候選人去更新自己的任期號 |
voteGranted | 候選人贏得了此張選票時為真 |
接收者實現:
- 如果
term < currentTerm
返回 false (5.2 節) - 如果 votedFor 為空或者為 candidateId,並且候選人的日誌至少和自己一樣新,那麼就投票給他(5.2 節,5.4 節)
所有伺服器需遵守的規則:
所有伺服器:
- 如果
commitIndex > lastApplied
,那麼就 lastApplied 加一,並把log[lastApplied]
應用到狀態機中(5.3 節) - 如果接收到的 RPC 請求或響應中,任期號
T > currentTerm
,那麼就令 currentTerm 等於 T,並切換狀態為跟隨者(5.1 節)
跟隨者(5.2 節):
- 響應來自候選人和領導者的請求
- 如果在超過選舉超時時間的情況之前都沒有收到領導人的心跳,或者是候選人請求投票的,就自己變成候選人
候選人(5.2 節):
- 在轉變成候選人後就立即開始選舉過程
- 自增當前的任期號(currentTerm)
- 給自己投票
- 重置選舉超時計時器
- 傳送請求投票的 RPC 給其他所有伺服器
- 如果接收到大多數伺服器的選票,那麼就變成領導人
- 如果接收到來自新的領導人的附加日誌 RPC,轉變成跟隨者
- 如果選舉過程超時,再次發起一輪選舉
領導人:
- 一旦成為領導人:傳送空的附加日誌 RPC(心跳)給其他所有的伺服器;在一定的空餘時間之後不停的重複傳送,以阻止跟隨者超時(5.2 節)
- 如果接收到來自客戶端的請求:附加條目到本地日誌中,在條目被應用到狀態機後響應客戶端(5.3 節)
- 如果對於一個跟隨者,最後日誌條目的索引值大於等於 nextIndex,那麼:傳送從 nextIndex 開始的所有日誌條目:
- 如果成功:更新相應跟隨者的 nextIndex 和 matchIndex
- 如果因為日誌不一致而失敗,減少 nextIndex 重試
- 如果存在一個滿足
N > commitIndex
的 N,並且大多數的matchIndex[i] ≥ N
成立,並且log[N].term == currentTerm
成立,那麼令 commitIndex 等於這個 N (5.3 和 5.4 節)
圖 2:一個關於 Raft 一致性演算法的濃縮總結(不包括成員變換和日誌壓縮)。
特性 | 解釋 |
---|---|
選舉安全特性 | 對於一個給定的任期號,最多隻會有一個領導人被選舉出來(5.2 節) |
領導人只附加原則 | 領導人絕對不會刪除或者覆蓋自己的日誌,只會增加(5.3 節) |
日誌匹配原則 | 如果兩個日誌在相同的索引位置的日誌條目的任期號相同,那麼我們就認為這個日誌從頭到這個索引位置之間全部完全相同(5.3 節) |
領導人完全特性 | 如果某個日誌條目在某個任期號中已經被提交,那麼這個條目必然出現在更大任期號的所有領導人中(5.4 節) |
狀態機安全特性 | 如果一個領導人已經在給定的索引值位置的日誌條目應用到狀態機中,那麼其他任何的伺服器在這個索引位置不會提交一個不同的日誌(5.4.3 節) |
圖 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 投票了。
- 在領導人 U 選舉的時候一定沒有那條被提交的日誌條目(領導人從不會刪除或者覆蓋任何條目)。
- 領導人 T 複製這條日誌條目給叢集中的大多數節點,同時,領導人U 從叢集中的大多數節點贏得了選票。因此,至少有一個節點(投票者、選民)同時接受了來自領導人T 的日誌條目,並且給領導人U 投票了,如圖 9。這個投票者是產生這個矛盾的關鍵。
- 這個投票者必須在給領導人 U 投票之前先接受了從領導人 T 發來的已經被提交的日誌條目;否則他就會拒絕來自領導人 T 的附加日誌請求(因為此時他的任期號會比 T 大)。
- 投票者在給領導人 U 投票時依然儲存有這條日誌條目,因為任何中間的領導人都包含該日誌條目(根據上述的假設),領導人從不會刪除條目,並且跟隨者只有在和領導人衝突的時候才會刪除條目。
- 投票者把自己選票投給領導人 U 時,領導人 U 的日誌必須和投票者自己一樣新。這就導致了兩者矛盾之一。
- 首先,如果投票者和領導人 U 的最後一條日誌的任期號相同,那麼領導人 U 的日誌至少和投票者一樣長,所以領導人 U 的日誌一定包含所有投票者的日誌。這是另一處矛盾,因為投票者包含了那條已經被提交的日誌條目,但是在上述的假設裡,領導人 U 是不包含的。
- 除此之外,領導人 U 的最後一條日誌的任期號就必須比投票人大了。此外,他也比 T 大,因為投票人的最後一條日誌的任期號至少和 T 一樣大(他包含了來自任期 T 的已提交的日誌)。建立了領導人 U 最後一條日誌的之前領導人一定已經包含了那條被提交的日誌(根據上述假設,領導人 U 是第一個不包含該日誌條目的領導人)。所以,根據日誌匹配特性,領導人 U 一定也包含那條被提交的日誌,這裡產生矛盾。
- 這裡完成了矛盾。因此,所有比 T 大的領導人一定包含了所有來自 T 的已經被提交的日誌。
- 日誌匹配原則保證了未來的領導人也同時會包含被間接提交的條目,例如圖 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 毫秒之間。大多數的伺服器的平均故障間隔時間都在幾個月甚至更長,很容易滿足時間的需求。
本文經TopJohn授權轉自TopJohn's Blog
深入淺出區塊鏈 - 系統學習區塊鏈,打造最好的區塊鏈技術部落格。