分佈式協議之 Raft 算法

什麼是 Raft 算法?

Raft 算法屬於 Multi-Paxos 算法,它是在蘭伯特 Multi-Paxos 思想的基礎上,做了一些簡化和限制,比如增加了日誌必須是連續的,只支持領導者、跟隨者和候選人三種狀態。

Raft 算法是現在分佈式系統開發首選的共識算法。絕大多數選用 Paxos 算法的 系統(比如 Cubby、Spanner)都是在 Raft 算法發佈前開發的,當時沒得選;而全新的系統大多選擇了 Raft 算法(比如 Etcd、Consul)。

如果要用一句話概括 Raft 算法,我覺得是這樣的:從本質上說,Raft 算法是通過一切以領導者爲準的方式,實現一系列值的共識和各節點日誌的一致。

Raft 角色

  • 領導者:處理寫請求、管理日誌複製和不斷地發送心跳信息。
  • 候選者:候選人將向其他節點發送請求投票 RPC 消息,通知其他節點來投票,如果贏得了大多數選票,就晉升當領導者。
  • 跟隨者:默默地接收和處理來自領導者的消息,當等待領導者心跳信息超時的時候,就主動站出來,推薦自己當候選人。

Raft 通過保留複製的日誌來工作。該日誌是僅追加的數據結構,其中添加了新條目,並且只有一個服務器(領導者)負責管理此日誌。每個 write 請求都發送到領導者節點,並且該節點會將其分發給跟隨者節點,並確保僅在安全存儲數據後,客戶端纔會收到此寫入的確認。

共識問題分爲三個子問題:領導人選舉,複製和安全。

領導人選舉

服務器始終以跟隨者身份啓動,並且期望領導者發出心跳。跟隨者將等待此心跳一段時間(定義爲 election timeout),如果未收到該心跳 ,則將假定領導者死了並轉換爲候選狀態。進入此狀態後,它要做的第一件事是爲自己投票,然後向所有其他節點發送投票請求(此請求是一個稱爲的 RPC RequestVote)。如果它從該羣集中的大多數節點(例如 5 箇中的3個)收到對該請求的確認,則轉換爲 Leader 狀態。

在這裏插入圖片描述

1、超時爲 150ms 的節點,就增加自己的任期編號,並推舉自己爲候選人,先給自己投上一張選票,然後向其他節點發送請求投票 RPC 消息,請它們選舉自己爲領導者。

2、如果其他節點接收到候選人 A 的請求投票 RPC 消息,在編號爲 1 的這屆任期內,也還沒有進行過投票,那麼它將把選票投給節點 A,並增加自己的任期編號。

3、如果候選人在選舉超時時間內贏得了大多數的選票,那麼它就會成爲本屆任期內新的領導者。

4、節點 A 當選領導者後,它將週期性地發送心跳消息,通知其他服務器我是領導者,阻止跟隨者發起新的選舉。

存在的問題?

首先如果所有節點都同時啓動,那麼它們也都將同時超時,這意味着每個節點都將觸發該相同的RequestVote RPC,這使得單個節點很難獲得多數票。Raft 通過爲每個節點使用隨機選舉超時來緩解此問題,這意味着一個候選者通常會在其他候選者之前超時,有可能成爲新領導者。

即使有這個隨機超時,我們仍然可以有一個分裂表決的情況,其中沒有一個節點擁有多數表決權。例如,在一個由5 個節點組成的集羣中,當領導者去世時,我們將最終得到 4 個節點,並且如果其中兩個節點大致同時超時,那麼每個節點都將獲得 2 票,因此他們都無法成爲領導者。解決方案非常簡單:只需等待另一個超時,這很可能會解決問題。當發生此超時並且該週期沒有領導者時,將啓動一個新的週期,並且每個節點在下一次選舉中都將具有一個新的隨機超時值,這可能又不一樣了。因此,我們將對性能造成損失,但是此超時通常只有幾毫秒,並且需要進行分割表決這種情況應該很少見。

在 Raft 算法中約定,如果一個候選人或者領導者,發現自己的任期編號比其他節點小,那麼它會立即恢復成跟隨者狀態。

如果一個節點接收到一個包含較小的任期編號值的請求,那麼它會直接拒絕這個請求。

當任期編號相同時,日誌完整性高的跟隨者,拒絕投票給日誌完整性低的候選人

日誌複製

當我們選出一個負責人後,每個請求都將發送到該節點。如果跟隨者節點收到請求,則可以將其重定向到領導者,或者將錯誤返回給客戶端,指示哪個節點是領導者。

領導者收到請求後,首先將其追加到其日誌中,然後將請求發送給每個關注者,以便他們可以執行相同的操作。此RPC 稱爲 AppendEntries。儘管該消息已附加到日誌中,但尚未提交,並且客戶端未獲得確認操作成功的確認。領導者從大多數節點得到確認後,就可以真正提交消息,知道消息已安全存儲,然後響應客戶端。當候選者收到下一條心跳消息(只是一個空的 AppendEntries RPC)時,他們知道他們也可以提交此消息。

除了客戶端發送的命令外,每個日誌條目還具有周期編號和索引。該週期僅定義了一個時間單位,索引是日誌中的位置。

日誌的格式如下:
在這裏插入圖片描述

指令:一條由客戶端請求指定的、狀態機需要執行的指令。你可以將指令理解成客戶端指定的數據。

索引值:日誌項對應的整數索引值。它其實就是用來標識日誌項的,是一個連續的、單調遞增的整數號碼。

任期編號:創建這條日誌項的領導者的任期編號。

如何複製日誌?

在這裏插入圖片描述

1、接收到客戶端請求後,領導者基於客戶端請求中的指令,創建一個新日誌項,並附加到本地日誌中。

2、領導者通過日誌複製 RPC,將新的日誌項複製到其他的服務器。

3、當領導者將日誌項,成功複製到大多數的服務器上的時候,領導者會將這條日誌項提交到它的狀態機中。

4、領導者將執行的結果返回給客戶端。

5、當跟隨者接收到心跳信息,或者新的日誌複製 RPC 消息後,如果跟隨者發現領導者已經提交了某條日誌項,而它還沒提交,那麼跟隨者就將這條日誌項提交到本地的狀態機中。

安全

爲確保正確複製每個日誌並以相同的順序執行命令,某些安全機制是必需的。

日誌匹配屬性

Raft 維護 “日誌匹配屬性” 屬性,即如果兩個不同的日誌條目具有相同的術語號和相同的索引,則它們將:

  • 存儲完全相同的命令;
  • 在所有前面的條目中都相同。

由於領導者將永遠不會在同一週期中創建多個具有相同索引的條目,因此第一個屬性已實現。

第二個屬性是保證跟隨者在收到 AppendEntries RPC 時執行的一致性檢查,以確保前面的所有條目都是相同的。
它的工作方式如下:Leader 跟蹤其日誌中提交的最高索引,並在每個 AppendEntriesRPC(甚至是心跳)中發送該信息。如果跟隨者在其本地日誌中找不到帶有該索引的條目,它將拒絕該請求,因此,如果 AppendEntriesRPC成功返回,則領導者將知道其日誌與跟隨者的日誌相同。

當節點正常運行時,這些日誌將始終保持一致。但是,當領導者崩潰時,此日誌可能會不一致,這就是AppendEntries 一致性檢查將爲我們提供幫助的時候。想象一下這種情況:

  • 我們有三個節點,N1,N2 和 N3,N1 成爲領導者;
  • N1 複製信息 term=1; index=1; command=x,並 term=1; index=2; command=y 用 N2,但 N3 從來沒有得到這些消息;
  • 現在 N1 崩潰了,N2 成爲了新的領導者;
  • 如果 N2 嘗試將消息複製 term=2; index=3; command=z 到 N3,一致性檢查將拒絕此消息,因爲最高的提交索引(3)不存在於N3的日誌中。
  • N2 然後將返回日誌,並在中的最新條目之後傳輸所有條目 N3,使日誌再次一致。

選舉限制

此屬性保證如果候選人的日誌中沒有所有已提交的條目,則該候選人將永遠不會贏得領導人選舉。由於在大多數節點上都需要存在一個條目才能被視爲已提交,因此在進行選舉時,至少一個節點將具有最新的已提交條目。如果跟隨者節點 RequestVote 從日誌中後面的候選項(表示較小的期限編號,或相同的期限編號但索引較小)接收到RPC,則不會將投票授予該候選項。

在這裏插入圖片描述
在上面的示例中,我們有三個日誌,每個條目均以其創建時的週期編號表示。 在這種情況下,Node 1 是領導者,並且能夠提交到索引 5,在索引 5 中,它得到了大多數節點(自身和 Node 2)的確認。如果 Node 1 逝世並開始新的選舉,也許 Node 3 可以成爲第一個過渡到候選人並試圖成爲領導人的人。這將是一個問題,因爲其日誌沒有最新的提交條目(週期 3,索引 5)。當它向發送一個 RequestVote 時 Node 2,此節點將注意到其自己的日誌比的日誌更新 Node 3,因此不會授予其投票權,從而無法 Node 3 成爲領導者。

集羣成員和共同共識

成員變更存在的問題:

在集羣中進行成員變更的最大風險是,可能會同時出現 2 個領導者。
在這裏插入圖片描述
在進行成員變更時,節點 A、B 和 C 之間發生了分區錯誤,節點 A、B 組成舊配置中的“大多數”,也就是變更前的 3 節點集羣中的“大多數”,那麼這時的領導者(節點 A)依舊是領導者。

另一方面,節點 C 和新節點 D、E 組成了新配置的“大多數”,也就是變更後的 5 節點集羣中的“大多數”,它們可能會選舉出新的領導者(比如節點 C)。那麼這時,就出現了同時存在 2 個領導者的情況。

最簡單的解決辦法:重啓

先將集羣關閉再啓動新集羣。也就是先把節點 A、B、C 組成的集羣關閉,然後再啓動節點 A、B、C、D、E 組成的新集羣。

這種辦法行不通,因爲你每次變更都要重啓集羣,意味着在集羣變更期間服務不可用,太影響用戶體驗了。

常用的解決辦法:單節點變更

通過一次變更一個節點實現成員變更。如果需要變更多個節點,那你需要執行多次單節點變更。比如將 3 節點集羣擴容爲 5 節點集羣,這時你需要執行 2 次單節點 變更,先將 3 節點集羣變更爲 4 節點集羣,然後再將 4 節點集羣變更爲 5 節點集羣,就像下圖的樣子:

在這裏插入圖片描述
具體步驟:

在這裏插入圖片描述

目前的集羣配置爲[A, B, C],我們先向集羣中加入節點 D,這意味着新配置爲[A, B, C, D]。成員變更,是通過這麼兩步實現的:

第一步,領導者(節點 A)向新節點(節點 D)同步數據;

第二步,領導者(節點 A)將新配置[A, B, C, D]作爲一個日誌項,複製到新配置中所有節點(節點 A、B、C、D)上,然後將新配置的日誌項提交到本地狀態機,完成單節點變更。

在這裏插入圖片描述

在變更完成後,現在的集羣配置就是[A, B, C, D],我們再向集羣中加入節點 E,也就是說,新配置爲[A, B, C, D, E]。成員變更的步驟和上面類似:

第一步,領導者(節點 A)向新節點(節點 E)同步數據;

第二步,領導者(節點 A)將新配置[A, B, C, D, E]作爲一個日誌項,複製到新配置中的所有節點(A、B、C、D、E)上,然後再將新配置的日誌項提交到本地狀態機,完成單節點變更。
在這裏插入圖片描述

這樣一來,我們就通過一次變更一個節點的方式,完成了成員變更,保證了集羣中始終只有一個領導者,而且集羣也在穩定運行,持續提供服務。

另外一種解決方案:聯合共識

當集羣中節點的狀態發生變化(集羣配置發生變化)時,系統容易受到可能導致系統故障的故障的影響。因此,爲防止這種情況,Raft 使用了一種稱爲兩階段的方法來更改羣集成員身份。因此,在這種方法中,羣集在實現新的羣集成員身份配置之前首先更改爲中間狀態(稱爲聯合共識)。聯合共識使系統即使在配置之間進行轉換時也可用於響應客戶端請求。因此,增加分佈式系統的可用性是主要目的。

Raft 和 Paxos

Paxos 和 Raft 採取了非常相似的方式來達成分佈式共識,只是在領導人選舉上的方式有所不同。最值得注意的是,Raft 僅允許具有最新日誌的服務器成爲領導者,而 Paxos 允許任何服務器成爲領導者,前提是它隨後更新其日誌以確保它是最新的。

考慮到其簡單性,Raft 的方法出奇的高效,因爲與 Paxos 不同,它不需要在領導人選舉期間交換日誌條目。

Raft 規範中一個特別有趣的組件是協調對集羣成員資格進行更改的機制。該協議採用一種新穎的方法,其中使用兩個重疊的多數(即由新舊集羣配置定義的法定人數)達成聯合共識,從而在不影響操作的情況下支持動態彈性。

Raft 的出現清楚地表明瞭軟件開發社區的積極擁護,這是近40 種使用各種不同語言的開源實現所證明的。儘管Paxos 在描述分佈式共識的本質上非常優雅,但是由於缺乏全面而規範的規範,Paxos 難以使用,並且難以在實際系統中實現。

參考資料

擴展閱讀

Raft 算法論文

參考資料

極客時間專欄:分佈式技術原理與算法解析

Raft: Consensus made simple®

Raft distributed consensus

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章