1. 程式人生 > >Raft 一致性演算法論文譯文

Raft 一致性演算法論文譯文

轉載:https://www.infoq.cn/article/raft-paper

Raft 一致性演算法論文譯文

本篇部落格為著名的 RAFT 一致性演算法論文的中文翻譯,論文名為《In search of an Understandable Consensus Algorithm (Extended Version)》(尋找一種易於理解的一致性演算法)。

Raft 是一種用來管理日誌複製的一致性演算法。它和 Paxos 的效能和功能是一樣的,但是它和 Paxos 的結構不一樣;這使得 Raft 更容易理解並且更易於建立實際的系統。為了提高理解性,Raft 將一致性演算法分為了幾個部分,例如領導選取(leader selection),日誌複製(log replication)和安全性(safety),同時它使用了更強的一致性來減少了必須需要考慮的狀態。從使用者學習的結果來看,Raft 比 Paxos 更容易學會。Raft 還包括了一種新的機制來使得動態改變叢集成員,它使用重疊大多數(overlapping majorities)來保證安全。

1. 引言

一致性演算法允許一組機器像一個整體一樣工作,即使其中的一些機器出了錯誤也能正常工作。正因為此,他們扮演著建立大規模可靠的軟體系統的關鍵角色。在過去的十年中 Paxos 一直都主導著有關一致性演算法的討論:大多數一致性演算法的實現都基於它或者受它影響,並且 Paxos 也成為了教學生關於一致性知識的主要工具。

不幸的是,儘管在降低它的複雜性方面做了許多努力,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 有幾個新的特性:

  • 強領導者(Strong Leader):Raft 使用一種比其他演算法更強的領導形式。例如,日誌條目只從領導者傳送向其他伺服器。這樣就簡化了對日誌複製的管理,使得 Raft 更易於理解。
  • 領導選取(Leader Selection):Raft 使用隨機定時器來選取領導者。這種方式僅僅是在所有演算法都需要實現的心跳機制上增加了一點變化,它使得在解決衝突時更簡單和快速。
  • 成員變化(Membership Change):Raft 為了調整叢集中成員關係使用了新的聯合一致性(joint consensus)的方法,這種方法中大多數不同配置的機器在轉換關係的時候會交迭(overlap)。這使得在配置改變的時候,叢集能夠繼續操作。

我們認為,Raft 在教學方面和實際實現方面比 Paxos 和其他演算法更出眾。它比其他演算法更簡單、更容易理解;它能滿足一個實際系統的需求;它擁有許多開源的實現並且被許多公司所使用;它的安全特性已經被證明;並且它的效率和其他演算法相比也具有競爭力。

這篇論文剩下的部分會講如下內容:複製狀態機(replicated state machine)問題(第 2 節),討論 Paxos 的優缺點(第 3 節),討論我們用的為了達到提升理解性的方法(第 4 節),陳述 Raft 一致性演算法(第 5~8 節),評價 Raft 演算法(第 9 節),對相關工作的討論(第 10 節)。

2. 複製狀態機(Replicated State Machine)

一致性演算法是在複製狀態機的背景下提出來的。在這個方法中,在一組伺服器的狀態機產生同樣的狀態的副本因此即使有一些伺服器崩潰了這組伺服器也還能繼續執行。複製狀態機在分散式系統中被用於解決許多有關容錯的問題。例如,GFS,HDFS 還有 RAMCloud 這些大規模的系統都是用一個單獨的叢集領導者,使用一個單獨的複製狀態機來進行領導選取和儲存配置資訊來應對領導者的崩潰。使用複製狀態機的例子有 Chubby 和 ZooKeeper。

圖 -1:複製狀態機的架構。一致性演算法管理來自客戶端狀態命令的複製日誌。狀態機處理的日誌中的命令的順序都是一致的,因此會得到相同的執行結果。

如圖 -1 所示,複製狀態機是通過複製日誌來實現的。每一臺伺服器儲存著一份日誌,日誌中包含一系列的命令,狀態機會按順序執行這些命令。因為每一臺計算機的狀態機都是確定的,所以每個狀態機的狀態都是相同的,執行的命令是相同的,最後的執行結果也就是一樣的了。

如何保證複製日誌一致就是一致性演算法的工作了。在一臺伺服器上,一致性模組接受客戶端的命令並且把命令加入到它的日誌中。它和其他伺服器上的一致性模組進行通訊來確保每一個日誌最終包含相同序列的請求,即使有一些伺服器宕機了。一旦這些命令被正確的複製了,每一個伺服器的狀態機都會按同樣的順序去執行它們,然後將結果返回給客戶端。最終,這些伺服器看起來就像一臺可靠的狀態機。

應用於實際系統的一致性演算法一般有以下特性:

  • 確保安全性(從來不會返回一個錯誤的結果),即使在所有的非拜占庭(Non-Byzantine)情況下,包括網路延遲、分割槽、丟包、冗餘和亂序的情況下。

  • 高可用性,只要叢集中的大部分機器都能執行,可以互相通訊並且可以和客戶端通訊,這個叢集就可用。因此,一般來說,一個擁有 5 臺機器的叢集可以容忍其中的 2 臺的失敗(fail)。伺服器停止工作了我們就認為它失敗(fail)了,沒準一會當它們擁有穩定的儲存時就能從中恢復過來,重新加入到叢集中。

  • 不依賴時序保證一致性,時鐘錯誤和極端情況下的訊息延遲在最壞的情況下才會引起可用性問題。

  • 通常情況下,一條命令能夠儘可能快的在大多數節點對一輪遠端呼叫作出相應時完成,一少部分慢的機器不會影響系統的整體效能。

3. Paxos 演算法的不足

在過去的 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 就是這樣的一個演算法。

4. 易於理解的設計

設計 Raft 的目標有如下幾個:

  • 它必須提供一個完整的、實際的基礎來進行系統構建,為的是減少開發者的工作;

  • 它必須在所有情況下都能保證安全可用;

  • 它對於常規操作必須高效;

  • 最重要的目標是:易於理解,它必須使得大多數人能夠很容易的理解;

  • 另外,它必須能讓開發者有一個直觀的認識,這樣才能使系統構建者們去對它進行擴充套件。

在設計 Raft 的過程中,我們不得不在許多種方法中做出選擇。當面臨這種情況時,我們通常會權衡可理解性:每種方法的可理解性是如何的?(例如,它的狀態空間有多複雜?它是不是有很細微的含義?)它的可讀性如何?讀者能不能輕易地理解這個方法和它的含義?

我們意識到對這種可理解性的分析具有高度的主觀性;儘管如此,我們使用了兩種適用的方式。第一種是眾所周知的問題分解:我們儘可能將問題分解成為若干個可解決的、可被理解的小問題。例如,在 Raft 中,我們把問題分解成為了領導選取(leader election)日誌複製(log replication)安全(safety)成員變化(membership changes)

我們採用的第二個方法是通過減少需要考慮的狀態的數量將狀態空間簡化,這能夠使得整個系統更加一致並且儘可能消除不確定性。特別地,日誌之間不允許出現空洞,並且 Raft 限制了日誌不一致的可能性。儘管在大多數情況下,我們都都在試圖消除不確定性,但是有時候有些情況下,不確定性使得演算法更易理解。尤其是,隨機化方法使得不確定性增加,但是它減少了狀態空間。我們使用隨機化來簡化了 Raft 中的領導選取演算法。

5. Raft 一致性演算法

Raft 是一種用來管理第 2 章中提到的複製日誌的演算法。表 -2 為了方便參考是一個演算法的總結版本,表 -3 列舉了演算法中的關鍵性質;表格中的這些元素將會在這一章剩下的部分中分別進行討論。

狀態:

在所有伺服器上持久存在的:(在響應遠端過程呼叫 RPC 之前穩定儲存的)

名稱 描述
currentTerm 伺服器最後知道的任期號(從 0 開始遞增)
votedFor 在當前任期內收到選票的候選人 id(如果沒有就為 null)
log[] 日誌條目;每個條目包含狀態機的要執行命令和從領導人處收到時的任期號

在所有伺服器上不穩定存在的

名稱 描述
commitIndex 已知的被提交的最大日誌條目的索引值(從 0 開始遞增)
lastApplied 被狀態機執行的最大日誌條目的索引值(從 0 開始遞增)

在領導人伺服器上不穩定存在的:(在選舉之後初始化的)

名稱 描述
nextIndex[] 對於每一個伺服器,記錄需要發給它的下一個日誌條目的索引(初始化為領導人上一條日誌的索引值 +1)
matchIndex[] 對於每一個伺服器,記錄已經複製到該伺服器的日誌的最高索引值(從 0 開始遞增)

表 -2-i

附加日誌遠端過程呼叫 (AppendEntries RPC)

由領導人來呼叫複製日誌(5.3 節);也會用作 heartbeat

引數 描述
term 領導人的任期號
leaderId 領導人的 id,為了其他伺服器能重定向到客戶端
prevLogIndex 最新日誌之前的日誌的索引值
prevLogTerm 最新日誌之前的日誌的領導人任期號
entries[] 將要儲存的日誌條目(表示 heartbeat 時為空,有時會為了效率傳送超過一條)
leaderCommit 領導人提交的日誌條目索引值
返回值 描述
term 當前的任期號,用於領導人更新自己的任期號
success 如果其它伺服器包含能夠匹配上 prevLogIndex 和 prevLogTerm 的日誌時為真

接受者需要實現:

  1. 如果 term < currentTerm返回 false(5.1 節)
  2. 如果在prevLogIndex處的日誌的任期號與prevLogTerm不匹配時,返回 false(5.3 節)
  3. 如果一條已經存在的日誌與新的衝突(index 相同但是任期號 term 不同),則刪除已經存在的日誌和它之後所有的日誌(5.3 節)
  4. 新增任何在已有的日誌中不存在的條目
  5. 如果leaderCommit > commitIndex,將commitIndex設定為leaderCommit最新日誌條目索引號中較小的一個

表 -2-ii

投票請求 RPC(RequestVote RPC)

由候選人發起收集選票(5.2 節)

引數 描述
term 候選人的任期號
candidateId 請求投票的候選人 id
lastLogIndex 候選人最新日誌條目的索引值
lastLogTerm 候選人最新日誌條目對應的任期號
返回值 描述
term 目前的任期號,用於候選人更新自己
voteGranted 如果候選人收到選票為 true

接受者需要實現:

  1. 如果term < currentTerm返回 false(5.1 節)
  2. 如果votedFor為空或者與candidateId相同,並且候選人的日誌和自己的日誌一樣新,則給該候選人投票(5.2 節 和 5.4 節)

表 -2-iii

伺服器需要遵守的規則:

所有伺服器:

  • 如果commitIndex > lastAppliedlastApplied自增,將log[lastApplied]應用到狀態機(5.3 節)
  • 如果 RPC 的請求或者響應中包含一個 term T 大於 currentTerm,則currentTerm賦值為 T,並切換狀態為追隨者(Follower)(5.1 節)

追隨者(followers): 5.2 節

  • 響應來自候選人和領導人的 RPC
  • 如果在超過選取領導人時間之前沒有收到來自當前領導人的AppendEntries RPC或者沒有收到候選人的投票請求,則自己轉換狀態為候選人

候選人:5.2 節

  • 轉變為選舉人之後開始選舉:
    • currentTerm自增
    • 給自己投票
    • 重置選舉計時器
    • 向其他伺服器傳送RequestVote RPC
  • 如果收到了來自大多數伺服器的投票:成為領導人
  • 如果收到了來自新領導人的AppendEntries RPC(heartbeat):轉換狀態為追隨者
  • 如果選舉超時:開始新一輪的選舉

領導人:

  • 一旦成為領導人:向其他所有伺服器傳送空的AppendEntries RPC(heartbeat); 在空閒時間重複傳送以防止選舉超時(5.2 節)
  • 如果收到來自客戶端的請求:向本地日誌增加條目,在該條目應用到狀態機後響應客戶端(5.3 節)
  • 對於一個追隨者來說,如果上一次收到的日誌索引大於將要收到的日誌索引(nextIndex):通過AppendEntries RPC將 nextIndex 之後的所有日誌條目傳送出去
    • 如果傳送成功:將該追隨者的 nextIndexmatchIndex更新
    • 如果由於日誌不一致導致AppendEntries RPC失敗:nextIndex遞減並且重新發送(5.3 節)
  • 如果存在一個滿足N > commitIndexmatchIndex[i] >= N並且log[N].term == currentTerm的 N,則將commitIndex賦值為 N

表 -2-iv

表 -2:Raft 一致性演算法的總結(不包括成員變化 membership changes 和日誌壓縮 log compaction)

性質 描述
選舉安全原則(Election Safety) 一個任期(term)內最多允許有一個領導人被選上(5.2 節)
領導人只增加原則(Leader Append-Only) 領導人永遠不會覆蓋或者刪除自己的日誌,它只會增加條目
日誌匹配原則(Log Matching) 如果兩個日誌在相同的索引位置上的日誌條目的任期號相同,那麼我們就認為這個日誌從頭到這個索引位置之間的條目完全相同(5.3 節)
領導人完全原則(Leader Completeness) 如果一個日誌條目在一個給定任期內被提交,那麼這個條目一定會出現在所有任期號更大的領導人中
狀態機安全原則(State Machine Safety) 如果一個伺服器已經將給定索引位置的日誌條目應用到狀態機中,則所有其他伺服器不會在該索引位置應用不同的條目(5.4.3 節)

表 -3:Raft 演算法保證這些特性任何時刻都成立

Raft 通過首先選出一個領導人來實現一致性,然後給予領導人完全管理複製日誌(replicated log)的責任。領導人接收來自客戶端的日誌條目,並把它們複製到其他的伺服器上,領導人還要告訴伺服器們什麼時候將日誌條目應用到它們的狀態機是安全的。通過選出領導人能夠簡化複製日誌的管理工作。例如,領導人能夠決定將新的日誌條目放到哪,而並不需要和其他的伺服器商議,資料流被簡化成從領導人流向其他伺服器。如果領導人宕機或者和其他伺服器失去連線,就可以選取下一個領導人。

通過選出領導人,Raft 將一致性問題分解成為三個相對獨立的子問題:

  • 領導人選取(Leader election): 在一個領導人宕機之後必須要選取一個新的領導人(5.2 節)
  • 日誌複製(Log replication): 領導人必須從客戶端接收日誌然後複製到叢集中的其他伺服器,並且強制要求其他伺服器的日誌保持和自己相同
  • 安全性(Safety): Raft 的關鍵的安全特性是 表 -3 中提到的狀態機安全原則(State Machine Safety): 如果一個伺服器已經將給定索引位置的日誌條目應用到狀態機中,則所有其他伺服器不會在該索引位置應用不同的條目。5.4 節闡述了 Raft 是如何保證這條原則的,解決方案涉及到一個對於選舉機制另外的限制,這一部分會在 5.2 節 中說明。

在說明了一致性演算法之後,本章會討論有關可用性(availability)的問題和系統中時序(timing)的問題。

5.1 Raft 基礎

一個 Raft 叢集包括若干伺服器;對於一個典型的 5 伺服器叢集,該叢集能夠容忍 2 臺機器不能正常工作,而整個系統保持正常。在任意的時間,每一個伺服器一定會處於以下三種狀態中的一個:領導人候選人追隨者。在正常情況下,只有一個伺服器是領導人,剩下的伺服器是追隨者。追隨者們是被動的:他們不會發送任何請求,只是響應來自領導人和候選人的請求。領導人來處理所有來自客戶端的請求(如果一個客戶端與追隨者進行通訊,追隨者會將資訊傳送給領導人)。候選人是用來選取一個新的領導人的,這一部分會在 5.2 節 進行闡釋。圖 -4 闡述了這些狀態,和它們之間的轉換;它們的轉換會在下邊進行討論。

圖 -4:伺服器的狀態。追隨者只響應其他伺服器的請求。如果追隨者沒有收到任何訊息,它會成為一個候選人並且開始一次選舉。收到大多數伺服器投票的候選人會成為新的領導人。領導人在它們宕機之前會一直保持領導人的狀態。

圖 -5:時間被分為一個個的任期(term),每一個任期的開始都是領導人選舉。在成功選舉之後,一個領導人會在任期內管理整個叢集。如果選舉失敗,該任期就會因為沒有領導人而結束。這個轉變會在不同的時間的不同伺服器上觀察到。

如 圖 -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 來獲得最好的效能。

5.2 領導人選取

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 節 展示了這種方法能夠快速的選出一個領導人。

選舉是一個理解性引導我們設計替代演算法的一個例子。最開始時,我們計劃使用一種排名系統:給每一個候選人分配一個唯一的排名,用於在競爭的候選人之中選擇領導人。如果一個候選人發現了另一個比它排名高的候選人,那麼它會回到追隨者的狀態,這樣排名高的候選人會很容易地贏得選舉。但是我們發現這種方法在可用性方面有一點問題(一個低排名的伺服器在高排名的伺服器宕機後,需要等待超時才能再次成為候選人,但是如果它這麼做的太快,它能重置選舉領帶人的過程)。我們對這個演算法做了多次調整,但是每次調整後都會出現一些新的問題。最終我們認為隨機重試的方法是更明確並且更易於理解的。

5.3 日誌複製

一旦選出了領導人,它就開始接收客戶端的請求。每一個客戶端請求都包含一條需要被複制狀態機(replicated state machine)執行的命令。領導人把這條命令作為新的日誌條目加入到它的日誌中去,然後並行的向其他伺服器發起 AppendEntries RPC ,要求其它伺服器複製這個條目。當這個條目被安全的複製之後(下面的部分會詳細闡述),領導人會將這個條目應用到它的狀態機中並且會向客戶端返回執行結果。如果追隨者崩潰了或者執行緩慢或者是網路丟包了,領導人會無限的重試 AppendEntries RPC(甚至在它向客戶端響應之後)直到所有的追隨者最終儲存了所有的日誌條目。

圖 -6:日誌由有序編號的日誌條目組成。每個日誌條目包含它被建立時的任期號(每個方塊中的數字),並且包含用於狀態機執行的命令。如果一個條目能夠被狀態機安全執行,就被認為可以提交了。

日誌就像 圖 -6 所示那樣組織的。每個日誌條目儲存著一條被狀態機執行的命令和當這條日誌條目被領導人接收時的任期號。日誌條目中的任期號用來檢測在不同伺服器上日誌的不一致性,並且能確保 圖 -3 中的一些特性。每個日誌條目也包含一個整數索引來表示它在日誌中的位置。

領導人決定什麼時候將日誌條目應用到狀態機是安全的;這種條目被稱為可被提交(commited)。Raft 保證可被提交(commited)的日誌條目是持久化的並且最終會被所有可用的狀態機執行。一旦被領導人建立的條目已經複製到了大多數的伺服器上,這個條目就稱為可被提交的(例如,圖 -6 中的 7 號條目)。領導人日誌中之前的條目都是可被提交的(commited),包括由之前的領導人建立的條目。5.4 節將會討論當領導人更替之後這條規則的應用問題的細節,並且也討論了這種提交方式是安全的。領導人跟蹤記錄它所知道的被提交條目的最大索引值,並且這個索引值會包含在之後的 AppendEntries RPC 中(包括心跳 heartbeat 中),為的是讓其他伺服器都知道這條條目已經提交。一旦一個追隨者知道了一個日誌條目已經被提交,它會將該條目應用至本地的狀態機(按照日誌順序)。

我們設計了 Raft 日誌機制來保證不同伺服器上日誌的一致性。這樣做不僅簡化了系統的行為使得它更可預測,並且也是保證安全性不可或缺的一部分。Raft 保證以下特性,並且也保證了 表 -3 中的日誌匹配原則(Log Matching Property):

  • 如果在不同日誌中的兩個條目有著相同的索引和任期號,則它們所儲存的命令是相同的。
  • 如果在不同日誌中的兩個條目有著相同的索引和任期號,則它們之間的所有條目都是完全一樣的。

第一條特性源於領導人在一個任期裡在給定的一個日誌索引位置最多建立一條日誌條目,同時該條目在日誌中的位置也從來不會改變。第二條特性源於 AppendEntries 的一個簡單的一致性檢查。當傳送一個 AppendEntries RPC 時,領導人會把新日誌條目緊接著之前的條目的索引位置和任期號都包含在裡面。如果追隨者沒有在它的日誌中找到相同索引和任期號的日誌,它就會拒絕新的日誌條目。這個一致性檢查就像一個歸納步驟:一開始空的日誌的狀態一定是滿足日誌匹配原則的,一致性檢查保證了當日誌新增時的日誌匹配原則。因此,只要 AppendEntries 返回成功的時候,領導人就知道追隨者們的日誌和它的是一致的了。

圖 -7:當最上邊的領導人掌權之後,追隨者日誌可能有以下情況(a~f)。一個格子表示一個日誌條目;格子中的數字是它的任期。一個追隨者可能會丟失一些條目(a, b);可能多出來一些未提交的條目(c, d);或者兩種情況都有(e, f)。例如,場景 f 在如下情況下就會發生:如果一臺伺服器在任期 2 時是領導人並且往它的日誌中添加了一些條目,然後在將它們提交之前就宕機了,之後它很快重啟了,成為了任期 3 的領導人,又往它的日誌中添加了一些條目,然後在任期 2 和任期 3 中的條目提交之前它又宕機了並且幾個任期內都一直處於宕機狀態。

在一般情況下,領導人和追隨者們的日誌保持一致,因此 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 內完成在叢集的大多數伺服器上的複製;並且一個速度很慢的追隨者並不會影響整體的效能。

5.4 安全性

之前的章節中討論了 Raft 演算法是如何進行領導選取和複製日誌的。然而,到目前為止這個機制還不能保證每一個狀態機能按照相同的順序執行同樣的指令。例如,當領導人提交了若干日誌條目的同時一個追隨者可能宕機了,之後它又被選為了領導人然後用新的日誌條目覆蓋掉了舊的那些,最後,不同的狀態機可能執行不同的命令序列。

這一節通過在領帶人選取部分加入了一個限制來完善了 Raft 演算法。這個限制能夠保證對於固定的任期,任何的領導人都擁有之前任期提交的全部日誌條目(表 -3 中的領導人完全原則)。有了這一限制,日誌提交的規則就更清晰了。最後,我們提出了對於領導人完全原則的簡單證明並且展示了它是如何修正複製狀態機的行為的。

5.4.1 選舉限制

在所有的以領導人為基礎的一致性演算法中,領導人最終必須要儲存全部已經提交的日誌條目。在一些一致性演算法中,例如:Viewstamped Replication,即使一開始沒有包含全部已提交的條目也可以被選為領導人。這些演算法都有一些另外的機制來保證找到丟失的條目並將它們傳輸給新的領導人,這個過程要麼在選舉過程中完成,要麼在選舉之後立即開始。不幸的是,這種方式大大增加了複雜性。Raft 使用了一種更簡單的方式來保證在新的領導人開始選舉的時候在之前任期的所有已提交的日誌條目都會出現在上邊,而不需要將這些條目傳送給領導人。這就意味著日誌條目只有一個流向:從領導人流向追隨者。領導人永遠不會覆蓋已經存在的日誌條目。

Raft 使用投票的方式來阻止沒有包含全部日誌條目的伺服器贏得選舉一個候選人為了贏得選舉必須要和叢集中的大多數進行通訊,這就意味著每一條已經提交的日誌條目最少在其中一臺伺服器上出現。如果候選人的日誌至少和大多數伺服器上的日誌一樣新(up-to-date,這個概念會在下邊有詳細介紹),那麼它一定包含有全部的已經提交的日誌條目。RequestVote RPC 實現了這個限制:這個 RPC(遠端過程呼叫)包括候選人的日誌資訊,如果它自己的日誌比候選人的日誌要新,那麼它會拒絕候選人的投票請求。

Raft 通過比較日誌中最後一個條目的索引和任期號來決定兩個日誌哪一個更新如果兩個日誌的任期號不同,任期號大的更新;如果任期號相同,更長的日誌更新。

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

圖 -8:如圖的時間序列說明了為什麼領導人不能通過之前任期的日誌條目判斷它的提交狀態。(a)中的 S1 是領導人並且部分複製了索引 2 上的日誌條目。(b)中 S1 崩潰了;S5 通過 S3,S4 和自己的選票贏得了選舉,並且在索引 2 上接收了另一條日誌條目。(c)中 S5 崩潰了,S1 重啟了,通過 S2,S3 和自己的選票贏得了選舉,並且繼續索引 2 處的複製,這時任期 2 的日誌條目已經在大部分伺服器上完成了複製,但是還並沒有提交。如果在(d)時刻 S1 崩潰了,S5 會通過 S2,S3,S4 的選票成為領導人,然後用它自己在任期 3 的日誌條目覆蓋掉其他伺服器的日誌條目。然而,如果在崩潰之前,S1 在它的當前任期在大多數伺服器上覆制了一條日誌條目,就像在(e)中那樣,那麼這條條目就會被提交(S5 就不會贏得選舉)。在這時,之前的日誌條目就會正常被提交。

正如 5.3 節 中描述的那樣,只要一個日誌條目被存在了在多數的伺服器上,領導人就知道當前任期就可以提交該條目了。如果領導人在提交之前就崩潰了,之後的領導人會試著繼續完成對日誌的複製。然而,領導人並不能斷定儲存在大多數伺服器上的日誌條目一定在之前的任期中被提交了。圖 -8 說明了一種情況,一條儲存在了大多數伺服器上的日誌條目仍然被新上任的領導人覆蓋了。

為了消除 圖 -8 中描述的問題,Raft 從來不會通過計算複製的數目來提交之前人氣的日誌條目。只有領導人當前任期的日誌條目才能通過計算數目來進行提交。一旦當前任期的日誌條目以這種方式被提交,那麼由於日誌匹配原則(Log Matching Property),之前的日誌條目也都會被間接的提交。在某些情況下,領導人可以安全的知道一個老的日誌條目是否已經被提交(例如,通過觀察該條目是否儲存到所有伺服器上),但是 Raft 為了簡化問題使用了一種更加保守的方法。

因為當領導人從之前任期複製日誌條目時日誌條目保留了它們最開始的任期號,所以這使得 Raft 在提交規則中增加了額外的複雜性。在其他的一致性演算法中,如果一個新的領導人要從之前的任期中複製日誌條目,它必須要使用當前的新任期號。Raft 的方法使得判斷日誌更加容易,因為它們全程都保持著同樣的任期號。另外,和其它的一致性演算法相比,Raft 演算法中的新領導人會發送更少的之前任期的日誌條目(其他演算法必須要傳送冗餘的日誌條目並且在它們被提交之前來重新排序)

5.4.3 安全性論證

圖 -9:如果 S1(任期 T 的領導人)在它的任期提交了一條日誌條目,並且 S5 在之後的任期 U 成為了領導人,那麼最少會有一臺伺服器(S3)接收了這條日誌條目並且會給 S5 投票。

給出了完整的 Raft 演算法,現在我們能夠更精確的論證領導人完全原則(Leader Completeness)(這基於 9.2 節 提出的安全性證明)。我們假定領導人完全原則是不成立的,然後推匯出矛盾。假定任期 T 的領導人 leaderT在它的任期提交了一個日誌條目,但是這條日誌條目並沒有儲存在之後的任期中的領導人上。我們設大於 T 的最小的任期 U 的領導人(leaderU) 沒有儲存這條日誌條目。

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

  2. leaderT 複製了這個條目到叢集的大多數的伺服器上。因此,只是有一臺伺服器(投票者)即接收了來自 leaderT 的日誌條目並且給 leaderU 投票,就像 圖 -9 中所示那樣。這個投票者是產生矛盾的關鍵。

  3. 投票者必須在給 leaderU 投票之前接收來自 leaderT 的日誌條目;否則它會拒絕來自 leaderT 的 AppendEntries 請求(它的當前任期會比 T 要大)。

  4. 投票者會在它給 leaderU 投票時儲存那個條目,因為任何中間的領導人都保有該條目(基於假設),領導人從來不會移除這個條目,並且追隨者也只會在和領導人衝突時才會移除日誌條目。

  5. 投票者給 leaderU 投票了,所以 leaderU 的日誌必須和投票者的一樣新。這就導致了一個矛盾。

  6. 首先,如果投票者和 leaderU 最後一條日誌條目的任期號相同,那麼 leaderU 的日誌一定和投票者的一樣長,因此它的日誌包含全部投票者的日誌條目。這是矛盾的,因為在假設中投票者和 leaderU 包含的已提交條目是不同的

  7. 除此之外, leaderU 的最後一條日誌的任期號一定比投票者的大。另外,它也比 T 要大,因為投票者的最後一條日誌條目的任期號最小也要是 T(它包含了所有任期 T 提交的日誌條目)。建立 leaderU 最後一條日誌條目的上一任領導人必須包含已經提交的日誌條目(基於假設)。那麼,根據日誌匹配原則(Log Matching),leaderU 也一定包含那條提交的日誌條目,這也是矛盾的。

  8. 這時就完成了矛盾推導。因此,所有比任期 T 大的領導人一定包含所有在任期 T 提交的日誌條目。

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

通過給出了 領導人完全原則(Leader Completeness),我們能夠證明 表 -3 中的狀態機安全原則(State Machine Safety),狀態機安全原則(State Machine Safety)講的是如果一臺伺服器將給定索引上的日誌條目應用到了它自己的狀態機上,其它伺服器的同一索引位置不可能應用的是其它條目。在一個伺服器應用一條日誌條目到它自己的狀態機中時,它的日誌必須和領導人的日誌在該條目和之前的條目上相同,並且已經被提交。現在我們來考慮在任何一個伺服器應用一個指定索引位置的日誌的最小任期;日誌完全特性(Log Completeness Property)保證擁有更高任期號的領導人會儲存相同的日誌條目,所以之後的任期裡應用某個索引位置的日誌條目也會是相同的值。因此,狀態機安全特性是成立的。

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

5.5 追隨者和候選人崩潰

截止到目前,我們只討論了領導人崩潰的問題。追隨者和候選人崩潰的問題解決起來要比領導人崩潰要簡單得多,這兩者崩潰的處理方式是一樣的。如果一個追隨者或者候選人崩潰了,那麼之後的傳送給它的 RequestVote RPC 和 AppendEntries RPC 會失敗。Raft 通過無限的重試來處理這些失敗;如果崩潰的伺服器重啟了,RPC 就會成功完成。如果一個伺服器在收到了 RPC 之後但是在響應之前崩潰了,那麼它會在重啟之後再次收到同一個 RPC。因為 Raft 中的 RPC 都是冪等的,因此不會有什麼問題。例如,如果一個追隨者收到了一個已經包含在它的日誌中的 AppendEntries 請求,它會忽視這個新的請求。

5.6 時序和可用性

我們對於 Raft 的要求之一就是安全性不依賴於時序(timing):系統不能僅僅因為一些事件發生的比預想的快一些或慢一些就產生錯誤。然而,可用性(系統可以及時響應客戶端的特性)不可避免的要依賴時序。例如,如果訊息交換在伺服器崩潰時花費更多的時間,候選人不會等待太長的時間來贏得選舉;沒有一個穩定的領導人,Raft 將無法工作。

領導人選取是 Raft 中對時序要求最關鍵的地方。Raft 會選出並且保持一個穩定的領導人只有系統滿足下列時序要求(timing requirement):

broadcastTime << electionTimeout << MTBF

在這個不等式中,broadcastTime指的是一臺伺服器並行的向叢集中的其他伺服器傳送 RPC 並且收到它們的響應的平均時間;electionTimeout指的就是在 5.2 節 描述的選舉超時時間;MTBF指的是單個伺服器發生故障的間隔時間的平均數。broadcastTime應該比electionTimeout小一個數量級,為的是使領導人能夠持續傳送心跳資訊(heartbeat)來阻止追隨者們開始選舉;根據已經給出的隨機化選舉超時時間方法,這個不等式也使得瓜分選票的情況變成不可能。electionTimeout也要比MTBF小几個數量級,為的是使得系統穩定執行。當領導人崩潰時,整個大約會在electionTimeout的時間內不可用;我們希望這種情況僅佔全部時間的很小的一部分。

broadcastTimeMTBF是由系統決定的性質,但是electionTimeout是我們必須做出選擇的。Raft 的 RPC 需要接收方將資訊持久化的儲存到穩定儲存中去,所以廣播時間大約是 0.5 毫秒到 20 毫秒,這取決於儲存的技術因此,electionTimeout一般在 10ms 到 500ms 之間。大多數的伺服器的MTBF都在幾個月甚至更長,很容易滿足這個時序需求。

6. 叢集成員變化

截止到目前,我們都假定叢集的配置(加入到一致性演算法的伺服器集合)是固定的。在實際中,我們會經常更改配置,例如,替換掉那些崩潰的機器或者更改複製級別。雖然通過關閉整個叢集,升級配置檔案,然後重啟整個叢集也可以解決這個問題,但是這回導致在更改配置的過程中,整個叢集不可用。另外,如果存在需要手工操作,那麼就會有操作失誤的風險。為了避免這些問題,我們決定採用自動改變配置並且把這部分加入到了 Raft 一致性演算法中。

為了讓配置修改機制能夠安全,那麼在轉換的過程中在任何時間點兩個領導人不能在同一個任期被同時選為領導人。不幸的是,伺服器叢集從舊的配置直接升級到新的配置的任何方法都是不安全的,一次性自動的轉換所有伺服器是不可能的,所以叢集可以在轉換的過程中劃分成兩個單獨的組(如 圖 -10 所示)。

 

圖 -10:從一個配置切換到另一個配置是不安全的因為不同的伺服器會在不同的時間點進行切換。在這個例子中,叢集數量從三臺轉換成五臺。不幸的是,在一個時間點有兩個伺服器能被選舉成為領導人,一個是在使用舊的配置的機器中(Cold)選出的領導人,另一個領導人是通過新的配置(Cnew)選出來的。

為了保證安全性,叢集配置的調整必須使用兩階段(two-phase)方法。有許多種實現兩階段方法的實現。例如,一些系統在第一個階段先把舊的配置設為無效使得它無法處理客戶端請求,然後在第二階段啟用新的配置。在 Raft 中,叢集先切換到一個過渡配置,我們稱其為共同一致(joint consensus);一旦共同一致被提交了,然後系統再切換到新的配置。共同一致是舊的配置和新的配置的組合:

  • 日誌條目被複制給叢集中新、老配置的所有伺服器。

  • 新、老配置的伺服器都能成為領導人。

  • 需要分別在兩種配置上獲得大多數的支援才能達成一致(針對選舉和提交)

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

圖 -11:叢集配置變更的時間線。虛線表示的是已經被建立但是還沒提交的配置條目,實線表示的是最新提交的配置條目。領導人首先在它的日誌中建立 Cold,new配置條目並且將它提交到 Cold,new(使用舊配置的大部分伺服器和使用新配置的大部分伺服器)。然後建立 Cnew配置條目並且將它提交到使用新配置的大部分機器上。這樣就不存在 Cold和 Cnew能夠分別同時做出決定的時刻。

叢集配置在複製日誌中用特殊的日誌條目來儲存和通訊;圖 -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,它不會更新當前的任期號或者投出選票。這不會影響正常的選舉,每個伺服器在開始一次選舉之前,至少等待一個最小選舉超時時間。然而,這有利於避免被移除的伺服器擾亂:如果領導人能夠傳送心跳給叢集,那麼它就不會被更大的任期號廢除。

7. 日誌壓縮

Raft 產生的日誌在持續的正常操作中不斷增長,但是在實際的系統中,它不會無限的增長下去。隨著日誌的不斷增長,它會佔據越來越多的空間並且花費更多的時間重置。如果沒有一個機制使得它能夠廢棄在日誌中不斷累積的過時的資訊就會引起可用性問題。

快照(snapshot)是最簡單的壓縮方式。在快照中,全部的當前系統狀態都被寫入到快照中,儲存到持久化的儲存中,然後在那個時刻之前的全部日誌都可以被丟棄。在 Chubby 和 ZooKeeper 中都使用了快照技術,這一章的剩下的部分會介紹 Raft 中使用的快照技術。

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

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

圖 -12 展示了 Raft 中快照的基礎思想。每個伺服器獨立的建立快照,只包括已經被提交的日誌。主要的工作包括將狀態機的狀態寫入到快照中。Raft 也將一些少量的元資料包含到快照中:最後被包含的索引(last included index)指的是被快照取代的最後的條目在日誌中的索引值(狀態機最後應用的日誌)