條分縷析 Raft 算法

本文整理自 Ongaro 在 Youtube 上的視頻。算法

目標

Raft 的目標(或者說是分佈式共識算法的目標)是:保證 log 徹底相同地複製到多臺服務器上安全

只要每臺服務器的日誌相同,那麼,在不一樣服務器上的狀態機以相同順序從日誌中執行相同的命令,將會產生相同的結果。性能優化

共識算法的工做就是管理這些日誌。服務器

系統模型

咱們假設:網絡

  • 服務器可能會宕機、會中止運行過段時間再恢復,可是非拜占庭的(即它的行爲是非惡意的,不會篡改數據等);
  • 網絡通訊會中斷,消息可能會丟失、延遲或亂序;可能會網絡分區;

Raft 是基於 Leader 的共識算法,故主要考慮:分佈式

  • Leader 正常運行
  • Leader 故障,必須選出新的 Leader

優勢:只有一個 Leader,簡單。性能

難點:Leader 發生改變時,可能會使系統處於不一致的狀態,所以,下一任 Leader 必須進行清理;優化

咱們將從 6 個部分解釋 Raft:spa

  1. Leader 選舉;
  2. 正常運行:日誌複製(最簡單的部分);
  3. Leader 變動時的安全性和一致性(最棘手、最關鍵的部分);
  4. 處理舊 Leader:舊的 Leader 並無真的下線怎麼辦?
  5. 客戶端交互:實現線性化語義(linearizable semantics);
  6. 配置變動:如何在集羣中增長或刪除節點;

開始以前

開始以前須要瞭解 Raft 的一些術語。3d

服務器狀態

服務器在任意時間只能處於如下三種狀態之一:

  • Leader:處理全部客戶端請求、日誌複製。同一時刻最多隻能有一個可行的 Leader;
  • Follower:徹底被動的(不發送 RPC,只響應收到的 RPC)——大多數服務器在大多數狀況下處於此狀態;
  • Candidate:用來選舉新的 Leader,處於 Leader 和 Follower 之間的暫時狀態;

系統正常運行時,只有一個 Leader,其他都是 Followers.

狀態轉換圖:

任期

時間被劃分紅一個個的任期(Term),每一個任期都由一個數字來表示任期號,任期號單調遞增而且永遠不會重複。

一個正常的任期至少有一個 Leader,一般分爲兩部分:

  • 任期開始時的選舉過程;
  • 正常運行的部分;

有些任期可能沒有選出 Leader(如圖 Term 3),這時候會當即進入下一個任期,再次嘗試選出一個 Leader。

每一個節點維護一個currentTerm變量,表示系統中當前任期。currentTerm必須持久化存儲,以便在服務器宕機重啓時將其恢復。

任期很是重要!任期可以幫助 Raft 識別過時的信息。例如:若是currentTerm = 2的節點與currentTerm = 3的節點通訊,咱們能夠知道第一個節點上的信息是過期的。

咱們只使用最新任期的信息。後面咱們會遇到各類狀況,去檢測和消除不是最新任期的信息。

兩個 RPC

Raft 中服務器之間全部類型的通訊經過兩個 RPC 調用:

  • RequestVote:用於選舉;
  • AppendEntries:用於複製 log 和發送心跳;

1. Leader 選舉

啓動

  • 節點啓動時,都是 Follower 狀態;
  • Follower 被動地接受 Leader 或 Candidate 的 RPC;
  • 因此,若是 Leader 想要保持權威,必須向集羣中的其它節點發送心跳包(空的AppendEntries RPC);
  • 等待選舉超時(electionTimeout,通常在 100~500ms)後,Follower 沒有收到任何 RPC:

    • Follower 認爲集羣中沒有 Leader
    • 開始新的一輪選舉

選舉

當一個節點開始競選:

  • 增長本身的currentTerm
  • 轉爲 Candidate 狀態,其目標是獲取超過半數節點的選票,讓本身成爲 Leader
  • 先給本身投一票
  • 並行地向集羣中其它節點發送RequestVote RPC索要選票,若是沒有收到指定節點的響應,它會反覆嘗試,直到發生如下三種狀況之一:
  1. 得到超過半數的選票:成爲 Leader,並向其它節點發送AppendEntries心跳;
  2. 收到來自 Leader 的 RPC:轉爲 Follower;
  3. 其它兩種狀況都沒發生,沒人可以獲勝(electionTimeout已過):增長currentTerm,開始新一輪選舉;

流程圖以下:

選舉安全性

選舉過程須要保證兩個特性:安全性(safety)活性(liveness)

安全性(safety):一個任期內只會有一個 Leader 被選舉出來。須要保證:

  • 每一個節點在同一任期內只能投一次票,它將投給第一個知足條件的投票請求,而後拒絕其它 Candidate 的請求。這須要持久化存儲投票信息votedFor,以便宕機重啓後恢復,不然重啓後votedFor丟失會致使投給別的節點;
  • 只有得到超過半數節點的選票才能成爲 Leader,也就是說,兩個不一樣的 Candidate 沒法在同一任期內都得到超過半數的票;

活性(liveness):確保最終能選出一個 Leader。

問題是:原則上咱們能夠無限重複分割選票,假如選舉同一時間開始,同一時間超時,同一時間再次選舉,如此循環。

解決辦法很簡單:

  • 節點隨機選擇超時時間,一般在 [T, 2T] 之間(T =electionTimeout
  • 這樣,節點不太可能再同時開始競選,先競選的節點有足夠的時間來索要其餘節點的選票
  • T >> broadcast time(T 遠大於廣播時間)時效果更佳

2. 日誌複製

日誌結構

每一個節點存儲本身的日誌副本(log[]),每條日誌記錄包含:

  • 索引:該記錄在日誌中的位置
  • 任期號:該記錄首次被建立時的任期號
  • 命令

日誌必須持久化存儲。一個節點必須先將記錄安全寫到磁盤,才能向系統中其餘節點返回響應。

若是一條日誌記錄被存儲在超過半數的節點上,咱們認爲該記錄已提交(committed)——這是 Raft 很是重要的特性!若是一條記錄已提交,意味着狀態機能夠安全地執行該記錄。

在上圖中,第 1-7 條記錄被提交,第 8 條還沒有提交。

提醒:多數派複製了日誌即已提交,這個定義並不精確,咱們會在後面稍做修改。

正常運行

  • 客戶端向 Leader 發送命令,但願該命令被全部狀態機執行;
  • Leader 先將該命令追加到本身的日誌中;
  • Leader 並行地向其它節點發送AppendEntries RPC,等待響應;
  • 收到超過半數節點的響應,則認爲新的日誌記錄是被提交的:

    • Leader 將命令傳給本身的狀態機,而後向客戶端返回響應
    • 此外,一旦 Leader 知道一條記錄被提交了,將在後續的AppendEntries RPC中通知已經提交記錄的 Followers
    • Follower 將已提交的命令傳給本身的狀態機
  • 若是 Follower 宕機/超時:Leader 將反覆嘗試發送 RPC;
  • 性能優化:Leader 沒必要等待每一個 Follower 作出響應,只須要超過半數的成功響應(確保日誌記錄已經存儲在超過半數的節點上)——一個很慢的節點不會使系統變慢,由於 Leader 沒必要等他;

日誌一致性

Raft 嘗試在集羣中保持日誌較高的一致性。

Raft 日誌的 index 和 term 惟一標示一條日誌記錄。(這很是重要!!!)

  1. 若是兩個節點的日誌在相同的索引位置上的任期號相同,則認爲他們具備同樣的命令;從頭到這個索引位置之間的日誌徹底相同
  2. 若是給定的記錄已提交,那麼全部前面的記錄也已提交

AppendEntries一致性檢查

Raft 經過AppendEntries RPC來檢測這兩個屬性。

  • 對於每一個AppendEntries RPC包含新日誌記錄以前那條記錄的索引(prevLogIndex)和任期(prevLogTerm);
  • Follower 檢查本身的 index 和 term 是否與prevLogIndexprevLogTerm匹配,匹配則接收該記錄;不然拒絕;

3. Leader 更替

當新的 Leader 上任後,日誌可能不會很是乾淨,由於前一任領導可能在完成日誌複製以前就宕機了。Raft 對此的處理方式是:無需採起任何特殊處理。

當新 Leader 上任後,他不會當即進行任何清理操做,他將會在正常運行期間進行清理。

緣由是當一個新的 Leader 上任時,每每意味着有機器故障了,那些機器可能宕機或網絡不通,因此沒有辦法當即清理他們的日誌。在機器恢復運行以前,咱們必須保證系統正常運行。

大前提是 Raft 假設了 Leader 的日誌始終是對的。因此 Leader 要作的是,隨着時間推移,讓全部 Follower 的日誌最終都與其匹配。

但與此同時,Leader 也可能在完成這項工做以前故障,日誌會在一段時間內堆積起來,從而形成看起來至關混亂的狀況,以下所示:

由於咱們已經知道 index 和 term 是日誌記錄的惟一標識符,這裏再也不顯示日誌包含的命令,下同。

如圖,這種狀況可能出如今 S4 和 S5 是任期 二、三、4 的 Leader,但不知何故,他們沒有複製本身的日誌記錄就崩潰了,系統分區了一段時間,S一、S二、S3 輪流成爲了任期 五、六、7 的 Leader,但沒法與 S四、S5 通訊以進行日誌清理——因此咱們看到的日誌很是混亂。

惟一重要的是,索引 1-3 之間的記錄是已提交的(已存在多數派節點),所以咱們必須確保留下它們

其它日誌都是未提交的,咱們尚未將這些命令傳遞給狀態機,也沒有客戶端會收到這些執行的結果,因此無論是保留仍是丟棄它們都可有可無。

安全性

一旦狀態機執行了一條日誌裏的命令,必須確保其它狀態機在一樣索引的位置不會執行不一樣的命令。

Raft 安全性(Safety):若是某條日誌記錄在某個任期號已提交,那麼這條記錄必然出如今更大任期號的將來 Leader 的日誌中。

這保證了安全性要求:

  • Leader 不會覆蓋日誌中的記錄;
  • 只有 Leader 的日誌中的記錄才能被提交;
  • 在應用到狀態機以前,日誌必須先被提交;

這決定咱們要修改選舉程序:

  • 若是節點的日誌中沒有正確的內容,須要避免其成爲 Leader;
  • 稍微修改 committed 的定義(_即前面提到的要稍做修改_):前面說多數派存儲便是已提交的,但在某些時候,咱們必須延遲提交日誌記錄,直到咱們知道這條記錄是安全的,所謂安全的,就是咱們認爲後續 Leader 也會有這條日誌

延遲提交,選出最佳 Leader

問題來了:咱們如何確保選出了一個很好地保存了全部已提交日誌的 Leader ?

這有點棘手,舉個例子:假設咱們要在下面的集羣中選出一個新 Leader,但此時第三臺服務器不可用。

這種狀況下,僅看前兩個節點的日誌咱們沒法確認是否達成多數派,故沒法確認第五條日誌是否已提交。

那怎麼辦呢?

經過比較日誌,在選舉期間,選擇最有可能包含全部已提交的日誌:

  • Candidate 在RequestVote RPCs中包含日誌信息(最後一條記錄的 index 和 term,記爲lastIndexlastTerm);
  • 收到此投票請求的服務器 V 將比較誰的日誌更完整:(lastTermV > lastTermC) ||
    (lastTermV == lastTermC) && (lastIndexV > lastIndexC)將拒絕投票;(即:V 的任期比 C 的任期新,或任期相同但 V 的日誌比 C 的日誌更完整);
  • 不管誰贏得選舉,能夠確保 Leader 和超過半數投票給它的節點中擁有最完整的日誌——最完整的意思就是 index 和 term 這對惟一標識是最大的

舉個例子

Case 1: Leader 決定提交日誌

任期 2 的 Leader S1 的 index = 4 日誌剛剛被複制到 S3,而且 Leader 能夠看到 index = 4 已複製到超過半數的服務器,那麼該日誌能夠提交,而且安全地應用到狀態機。

如今,這條記錄是安全的,下一任期的 Leader 必須包含此記錄,所以 S4 和 S5 都不可能從其它節點那裏得到選票:S5 任期太舊,S4 日誌過短。

只有前三臺中的一臺能夠成爲新的 Leader——S1 固然能夠,S二、S3 也能夠經過獲取 S4 和 S5 的選票成爲 Leader。

Case 2: Leader 試圖提交以前任期的日誌

如圖所示的狀況,在任期 2 時記錄僅寫在 S1 和 S2 兩個節點上,因爲某種緣由,任期 3 的 Leader S5 並不知道這些記錄,S5 建立了本身的三條記錄而後宕機了,而後任期 4 的 Leader S1 被選出,S1 試圖與其它服務器的日誌進行匹配。所以它複製了任期 2 的日誌到 S3。

此時 index=3 的記錄時是不安全的

由於 S1 可能在此時宕機,而後 S5 可能從 S二、S三、S4 得到選票成爲任期 5 的 Leader。一旦 S5 成爲新 Leader,它將覆蓋 index=3-5 的日誌,S1-S3 的這些記錄都將消失。

咱們還要須要一條新的規則,來處理這種狀況。

新的 Commit 規則

新的選舉不足以保證日誌安全,咱們還須要繼續修改 commit 規則。

Leader 要提交一條日誌:

  • 日誌必須存儲在超過半數的節點上;
  • Leader 必須看到:超過半數的節點上還必須存儲着至少一條本身任期內的日誌

如圖,回到上面的 Case 2: 當 index = 3 & term = 2 被複制到 S3 時,它還不能提交該記錄,必須等到 term = 4 的記錄存儲在超過半數的節點上,此時 index = 3 和 index = 4 能夠認爲是已提交。

此時 S5 沒法贏得選舉了,它沒法從 S1-S3 得到選票。

結合新的選舉規則和 commit 規則,咱們能夠保證 Raft 的安全性。

日誌不一致

Leader 變動可能致使日誌的不一致,這裏展現一種可能的狀況。

能夠從圖中看出,Raft 集羣中一般有兩種不一致的日誌:

  • 缺失的記錄(Missing Entries);
  • 多出來的記錄(Extraneous Entries);

咱們要作的就是清理這兩種日誌。

修復 Follower 日誌

新的 Leader 必須使 Follower 的日誌與本身的日誌保持一致,經過:

  • 刪除 Extraneous Entries;
  • 補齊 Missing Entries;

Leader 爲每一個 Follower 保存nextIndex

  • 下一個要發送給 Follower 的日誌索引;
  • 初始化爲: 1 + Leader 最後一條日誌的索引;

Leader 經過nextIndex來修復日誌。當AppendEntries RPC一致性檢查失敗,遞減nextIndex並重試。以下圖所示:

對於 a:

  • 一開始nextIndex= 11,帶上日誌 index = 10 & term = 6,檢查失敗;
  • nextIndex= 10,帶上日誌 index = 9 & term = 6,檢查失敗;
  • 如此反覆,直到nextIndex= 5,帶上日誌 index = 4 & term = 4,該日誌如今匹配,會在 a 中補齊 Leader 的日誌。如此往下補齊。

對於 b:
會一直檢查到nextIndex= 4 才匹配。值得注意的是,對於 b 這種狀況,當 Follower 覆蓋不一致的日誌時,它將刪除全部後續的日誌記錄(任何可有可無的記錄以後的記錄也都是可有可無的)。以下圖所示:

4. 處理舊 Leader

實際上,老的 Leader 可能不會立刻消失,例如:網絡分區將 Leader 與集羣的其他部分分隔,其他部分選舉出了一個新的 Leader。問題在於,若是老的 Leader 從新鏈接,也不知道新的 Leader 已經被選出來,它會嘗試做爲 Leader 繼續提交日誌。此時若是有客戶端向老 Leader 發送請求,老的 Leader 會嘗試存儲該命令並向其它節點複製日誌——咱們必須阻止這種狀況發生。

任期就是用來發現過期的 Leader(和 Candidates):

  • 每一個 RPC 都包含發送方的任期;
  • 若是發送方的任期太老,不管哪一個過程,RPC 都會被拒絕,發送方轉變到 Follower 並更新其任期;
  • 若是接收方的任期太老,接收方將轉爲 Follower,更新它的任期,而後正常的處理 RPC;

因爲新 Leader 的選舉會更新超過半數服務器的任期,舊的 Leader 不能提交新的日誌,由於它會聯繫至少一臺多數派集羣的節點,而後發現本身任期太老,會轉爲 Follower 繼續工做。

這裏不打算繼續討論別的極端狀況。

5. 客戶端協議

客戶端只將命令發送到 Leader:

  • 若是客戶端不知道 Leader 是誰,它會和任意一臺服務器通訊;
  • 若是通訊的節點不是 Leader,它會告訴客戶端 Leader 是誰;

Leader 直到將命令記錄、提交和執行到狀態機以前,不會作出響應。

這裏的問題是若是 Leader 宕機會致使請求超時:

  • 客戶端從新發出命令到其餘服務器上,最終重定向到新的 Leader
  • 用新的 Leader 重試請求,直到命令被執行

這留下了一個命令可能被執行兩次的風險——Leader 可能在執行命令以後但響應客戶端以前宕機,此時客戶端再去尋找下一個 Leader,同一個命令就會被執行兩次——這是不可接受的!

解決辦法是:客戶端發送給 Leader 的每一個命令都帶上一個惟一 id

  • Leader 將惟一 id 寫到日誌記錄中
  • 在 Leader 接受命令以前,先檢查其日誌中是否已經具備該 id
  • 若是 id 在日誌中,說明是重複的請求,則忽略新的命令,返回舊命令的響應

每一個命令只會被執行一次,這就是所謂的線性化的關鍵要素

6. 配置變動

隨着時間推移,會有機器故障須要咱們去替換它,或者修改節點數量,須要有一些機制來變動系統配置,而且是安全、自動的方式,無需中止系統。

系統配置是指:

  • 每臺服務器的 id 和地址
  • 系統配置信息是很是重要的,它決定了多數派的組成

首先要意識到,咱們不能直接從舊配置切換到新配置,這可能會致使矛盾的多數派。

如圖,系統以三臺服務器的配置運行着,此時咱們要添加兩臺服務器。若是咱們直接修改配置,他們可能沒法徹底在同一時間作到配置切換,這會致使 S1 和 S2 造成舊集羣的多數派,而同一時間 S3-S5 已經切換到新配置,這會產生兩個集羣。

這說明咱們必須使用一個兩階段(two-phase)協議。

若是有人告訴你,他能夠在分佈式系統中一個階段就作出決策,你應該很是認真地詢問他,由於他要麼錯了,要麼發現了世界上全部人都不知道的東西。

共同一致(Joint Consensus)

Raft 經過共同一致(Joint Consensus)來完成兩階段協議,即:新、舊兩種配置上都得到多數派選票。

第一階段:

  • Leader 收到 Cnew 的配置變動請求後,先寫入一條 Cold+new 的日誌,配置變動當即生效,而後將日誌經過AppendEntries RPC複製到 Follower 中,收到該 Cold+new 的節點當即應用該配置做爲當前節點的配置;
  • Cold+new 日誌複製到多數派節點上時,Cold+new 的日誌已提交;

Cold+new 日誌已提交保證了後續任何 Leader 必定有 Cold+new 日誌,Leader 選舉過程必須得到舊配置中的多數派和新配置中的多數派同時投票。

第二階段:

  • Cold+new 日誌已提交後,當即寫入一條 Cnew 的日誌,並將該日誌經過AppendEntries RPC複製到 Follower 中,收到 Cnew 的節點當即應用該配置做爲當前節點的配置;
  • Cnew 日誌複製到多數派節點上時,Cnew 的日誌已提交;在 Cnew 日誌提交之後,後續的配置都基於 Cnew 了;

相關文章
相關標籤/搜索