一文徹底搞懂Raft算法,看這篇就夠了!!!

最近需要設計一個分佈式系統,需要一箇中間件來存儲共享的信息,來保證多個系統之間的數據一致性,調研了兩個主流框架Zookeeper和ETCD,發現都能滿足我們的系統需求。其中ETCD是K8s中採用的分佈式存儲,而其底層採用了RAFT算法來保證一致性,所以隨便研究了下RAFT算法,這篇文章會從頭到尾分析Raft算法的方方面面。

什麼是分佈式一致性 ?

分佈式系統通常由異步網絡連接的多個節點構成,每個節點有獨立的計算和存儲,節點之間通過網絡通信進行協作。分佈式一致性指多個節點對某一變量的取值達成一致,一旦達成一致,則變量的本次取值即被確定。

在大量客戶端併發請求讀/寫的情況下,維護數據多副本的一致性無疑非常重要,且富有挑戰。因此,分佈式一致性在我們生產環境中顯得尤爲重要。

總結來講,分佈式一致性就是爲了解決以下兩個問題:

  • 數據不能存在單個節點(主機)上,否則可能出現單點故障。

  • 多個節點(主機)需要保證具有相同的數據。

常見分佈式一致性算法

常見的一致性算法包括Paxos算法,Raft算法,ZAB算法等,

  • Paxos算法是Lamport宗師提出的一種基於消息傳遞的分佈式一致性算法,使其獲得2013年圖靈獎。自Paxos問世以來就持續壟斷了分佈式一致性算法,Paxos這個名詞幾乎等同於分佈式一致性, 很多分佈式一致性算法都由Paxos演變而來#

  • Paxos是出了名的難懂,而Raft正是爲了探索一種更易於理解的一致性算法而產生的。它的首要設計目的就是易於理解,所以在選主的衝突處理等方式上它都選擇了非常簡單明瞭的解決方案。

  • ZAB 協議全稱:Zookeeper Atomic Broadcast(Zookeeper 原子廣播協議), 它應該是所有一致性協議中生產環境中應用最多的了。爲什麼呢?因爲他是爲 Zookeeper 設計的分佈式一致性協議!

本文我們主要介紹Raft算法,後續會對其他算法進行詳細介紹。

深入Raft算法

Raft算法和其他分佈式一致算法一樣,內部採用如下圖所示的複製狀態機模型,在這個模型中,會利用多臺服務器構成一個集羣,工作流程如下圖所示:

JzXWDq

整個工作流程可以歸納爲如下幾步:

  1. 用戶輸入設置指令,比如將設置y爲1,然後將y更改爲9.

  2. 集羣收到用戶指令之後,會將該指令同步到集羣中的多臺服務器上,這裏你可以認爲所有的變更操作都會寫入到每個服務器的Log文件中。

  3. 根據Log中的指令序列,集羣可以計算出每個變量對應的最新狀態,比如y的值爲9.

  4. 用戶可以通過算法提供的API來獲取到最新的變量狀態。

算法會保證變量的狀態在整個集羣內部是統一的,並且當集羣中的部分服務器宕機後,仍然能穩定的對外提供服務。

Raft算法在具體實現中,將分佈式一致性問題分解爲了Leader選舉日誌同步安全性保證三大子問題,接下來我會對這三方面進行仔細講解。

Raft算法基礎

Raft 正常工作時的流程如下圖,也就是正常情況下日誌複製的流程。Raft 中使用日誌來記錄所有操作,所有結點都有自己的日誌列表來記錄所有請求。算法將機器分成三種角色:LeaderFollowerCandidate。正常情況下只存在一個 Leader,其他均爲 Follower,所有客戶端都與 Leader 進行交互。

tcCXcV

所有操作採用類似兩階段提交的方式,Leader 在收到來自客戶端的請求後並不會執行,只是將其寫入自己的日誌列表中,然後將該操作發送給所有的 Follower。Follower 在收到請求後也只是寫入自己的日誌列表中然後回覆 Leader,當有超過半數的結點寫入後 Leader 纔會提交該操作並返回給客戶端,同時通知所有其他結點提交該操作。

通過這一流程保證了只要提交過後的操作一定在多數結點上留有記錄(在日誌列表中),從而保證了該數據不會丟失。

Raft 是一個非拜占庭的一致性算法,即所有通信是正確的而非僞造的。N個結點的情況下(N爲奇數)可以最多容忍(N-1)/2個結點故障。如果更多的節點故障,後續的Leader選舉和日誌同步將無法進行。

子問題1:Leader選舉

在瞭解基本的工作流程之後,首先看下Rsft算法的第一個問題,如何選舉Leader。

1. 首次選舉

Kb4kPA

如果定時器超時,說明一段時間內沒有收到 Leader 的消息,那麼就可以認爲 Leader 已死或者不存在,那麼該結點就會轉變成 Candidate,意思爲準備競爭成爲 Leader。

成爲 Candidate 後結點會向所有其他結點發送請求投票的請求(RequestVote),其他結點在收到請求後會判斷是否可以投給他並返回結果。Candidate 如果收到了半數以上的投票就可以成爲 Leader,成爲之後會立即並在任期內定期發送一個心跳信息通知其他所有結點新的 Leader 信息,並用來重置定時器,避免其他結點再次成爲 Candidate。

如果 Candidate 在一定時間內沒有獲得足夠的投票,那麼就會進行一輪新的選舉,直到其成爲 Leader,或者其他結點成爲了新的 Leader,自己變成 Follower。

2. 再次選舉

當Leader下線或者因爲網絡問題產生分區時,會導致再次選舉。

  • 情況1:Leader下線,此時所有其他節點的計時器不會被重置,直到一個節點成爲了 Candidate,和上述一樣開始一輪新的選舉選出一個新的 Leader。

w95KQJ

  • 情況2:某一 Follower 結點與 Leader 間通信發生問題,導致發生了分區,這時沒有 Leader 的那個分區就會進行一次選舉。這種情況下,因爲要求獲得多數的投票纔可以成爲 Leader,因此只有擁有多數結點的分區可以正常工作。而對於少數結點的分區,即使仍存在 Leader,但由於寫入日誌的結點數量不可能超過半數因此不可能提交操作。這也是爲何一開始我提到Raft算法必須要半數以上節點正常才能工作。

03bK8U

下圖總結了Raft算法中,每個節點的狀態之間的變化:

zbojsM

  • Leader:處理與客戶端的交互和與 follower 的日誌複製等,一般只有一個 Leader;
  • Follower:被動學習 Leader 的日誌同步,同時也會在 leader 超時後轉變爲 Candidate 參與競選;
  • Candidate:在競選期間參與競選;

3. 任期Term

Raft算法將時間分爲一個個的任期(term),每一個term的開始都是Leader選舉。

每一個任期以一次選舉作爲起點,所以當一個結點成爲 Candidate 並向其他結點請求投票時,會將自己的 Term 加 1,表明新一輪的開始以及舊 Leader 的任期結束。所有結點在收到比自己更新的 Term 之後就會更新自己的 Term 並轉成 Follower,而收到過時的消息則拒絕該請求。

在成功選舉Leader之後,Leader會在整個term內管理整個集羣。如果Leader選舉失敗,該term就會因爲沒有Leader而結束。

cKqa1y

4. 投票

在投票時候,所有服務器採用先來先得的原則,在一個任期內只可以投票給一個結點,得到超過半數的投票纔可成爲 Leader,從而保證了一個任期內只會有一個 Leader 產生(Election Safety)。

在 Raft 中日誌只有從 Leader 到 Follower 這一流向,所以需要保證 Leader 的日誌必須正確,即必須擁有所有已在多數節點上存在的日誌,這一步驟由投票來限制。

在介紹投票規則之前,先簡單介紹下日誌的格式,方便理解:

bng8ou

如上圖所示,日誌由有序編號(log index)的日誌條目組成。每個日誌條目包含它被創建時的任期號(term),和用於狀態機執行的命令。如果一個日誌條目被複制到大多數服務器上,就被認爲可以提交(commit)了。

投票由一個稱爲 RequestVote 的 RPC 調用進行,請求中除了有 Candidate自己的 term 和 id 之外,還要帶有自己最後一個日誌條目的 index 和 term。Candidate首先會給自己投票,然後再向其他節點收集投票信息,收到投票信息的節點,會利用如下規則判斷是否投票:

  • 首先會判斷請求的term是否更大,不是則說明是舊消息,拒絕該請求。

  • 如果任期Term相同,則比較index,index較大則爲更加新的日誌;如果任期Term不同,term更大的則爲更新的消息。如果是更新的消息,則給Candidate投票。

由於只有日誌在被多數結點複製之後纔會被提交併返回,所以如果一個 Candidate 並不擁有最新的已被複制的日誌,那麼他不可能獲得多數票,從而保證了 Leader 一定具有所有已被多數擁有的日誌(Leader Completeness),在後續同步時會將其同步給所有結點。

子問題2:日誌同步

工作流程

Leader選出後,就開始接收客戶端的請求。Leader把請求作爲日誌條目(Log entries)加入到它的日誌中,然後並行的向其他服務器發起 AppendEntries RPC複製日誌條目。當這條日誌被複制到大多數服務器上,Leader將這條日誌應用到它的狀態機並向客戶端返回執行結果。

f7VXgC

某些Followers可能沒有成功的複製日誌,Leader會無限的重試 AppendEntries RPC直到所有的Followers最終存儲了所有的日誌條目。

日誌由有序編號(log index)的日誌條目組成。每個日誌條目包含它被創建時的任期號(term),和用於狀態機執行的命令。如果一個日誌條目被複制到大多數服務器上,就被認爲可以提交(commit)了。

實際處理邏輯

Leader 會給每個 Follower 發送該 RPC 以追加日誌,請求中除了當前任期 term、Leader 的 id 和已提交的日誌 index,還有將要追加的日誌列表(空則成爲心跳包),前一個日誌的 index 和 term。具體可以參考官方論文中的描述:

Vlm2Ub

在接到該請求後,會進行如下判斷:

  1. 檢查term,如果請求的term比自己小,說明已經過期,直接拒絕請求。

  2. 如果步驟1通過,則對比先前日誌的index和term,如果一致,則就可以從此處更新日誌,把所有的日誌寫入自己的日誌列表中,否則返回false。

這裏對步驟2進行展開說明,每個Leader在開始工作時,會維護 nextIndex[] 和 matchIndex[] 兩個數組,分別記錄了每個 Follower 下一個將要發送的日誌 index 和已經匹配上的日誌 index。每次成爲 Leader 都會初始化這兩個數組,前者初始化爲 Leader 最後一條日誌的 index 加 1,後者初始化爲 0,每次發送 RPC 時會發送 nextIndex[i] 及之後的日誌。

在步驟2中,當Leader收到返回成功時,則更新兩個數組,否則說明follower上相同位置的數據和Leader不一致,這時候Leader會減小nextIndex[i]的值重試,一直找到follower上兩者一致的位置,然後從這個位置開始複製Leader的數據給follower,同時follower後續已有的數據會被清空。

這裏減少 nextIndex 的值有不同的策略,可以每次減一,也可以減一個較大的值,或者是跨任期減少,用於快速找到和該結點相匹配的日誌條目。

在複製的過程中,Raft會保證如下幾點:

  • Leader 絕不會覆蓋或刪除自己的日誌,只會追加 (Leader Append-Only),成爲 Leader 的結點裏的日誌一定擁有所有已被多數節點擁有的日誌條目,所以先前的日誌條目很可能已經被提交,因此不可以刪除之前的日誌。

  • 如果兩個日誌的 index 和 term 相同,那麼這兩個日誌相同 (Log Matching),第二點主要是因爲一個任期內只可能出現一個 Leader,而 Leader 只會爲一個 index 創建一個日誌條目,而且一旦寫入就不會修改,因此保證了日誌的唯一性。

  • 如果兩個日誌相同,那麼他們之前的日誌均相同,因爲在寫入日誌時會檢查前一個日誌是否一致,從而遞歸的保證了前面的所有日誌都一致。從而也保證了當一個日誌被提交之後,所有結點在該 index 上提交的內容是一樣的(State Machine Safety)。

子問題3:安全性保障(核心)

Raft算法中引入瞭如下兩條規則,來確保了

  • 已經commit的消息,一定會存在於後續的Leader節點上,並且絕對不會在後續操作中被刪除。

  • 對於並未commit的消息,可能會丟失。

多數投票規則

在上面投票環節也有介紹過,一個candidate必須獲得集羣中的多數投票,才能被選爲Leader;而對於每條commit過的消息,它必須是被複制到了集羣中的多數節點,也就是說成爲Leader的節點,至少有1個包含了commit消息的節點給它投了票。

而在投票的過程中每個節點都會與candidate比較日誌的最後index以及相應的term,如果要成爲Leader,必須有更大的index或者更新的term,所以Leader上肯定有commit過的消息。

提交規則

上面說到,只要日誌在多數結點上存在,那麼 Leader 就可以提交該操作。但是Raft額外限制了 Leader只對自己任期內的日誌條目適用該規則,先前任期的條目只能由當前任期的提交而間接被提交。 也就是說,當前任期的Leader,不會去負責之前term的日誌提交,之前term的日誌提交,只會隨着當前term的日誌提交而間接提交。

這樣理解起來還是比較抽象,下面舉一個例子,該集羣中有S1到S5共5個節點,

HIAsDK

  • 初始狀態如 (a) 所示,之後 S1 下線;

  • (b) 中 S5 從 S3 和 S4 處獲得了投票成爲了 Leader 並收到了一條來自客戶端的消息,之後 S5 下線。

  • (c) 中 S1 恢復併成爲了 Leader,並且將日誌複製給了多數結點,之後進行了一個致命操作,將 index 爲 2 的日誌提交了,然後 S1 下線。

  • (d) 中 S5 恢復,並從 S2、S3、S4 處獲得了足夠投票,然後將已提交的 index 爲 2 的日誌覆蓋了。

這個例子中,在c狀態,由於Leader直接根據日誌在多數節點存在的這個規則,將之前term的日誌提交了,當該Term下線後,後續的Leader S5上線,就將之前已經commit的日誌清空了,導致commit過的日誌丟失了。

爲了避免這種已提交的日誌丟失,Raft只允許提交自己任期內的日誌,也就是不會允許c中這種操作。(c)中可能出現的情況有如下兩類:

  • (c)中S1有新的客戶端消息4,然後S1作爲Leader將4同步到S1、S2、S3節點,併成功提交後下線。此時在新一輪的Leader選舉中,S5不可能成爲新的Leader,保證了commit的消息2和4不會被覆蓋。

  • (c)中S1有新的消息,但是在S1將數據同步到其他節點並且commit之前下線,也就是說2和4都沒commit成功,這種情況下如果S5成爲了新Leader,則會出現(d)中的這種情況,2和4會被覆蓋,這也是符合Raft規則的,因爲2和4並未提交。

Raft相關擴展

1. 日誌壓縮

bFOq48

在實際的系統中,不能讓日誌無限增長,否則系統重啓時需要花很長的時間進行回放,從而影響可用性。Raft採用對整個系統進行snapshot來解決,snapshot之前的日誌都可以丟棄。

每個副本獨立的對自己的系統狀態進行snapshot,並且只能對已經提交的日誌記錄進行snapshot。

Snapshot中包含以下內容:

  • 日誌元數據。最後一條已提交的 log entry的 log index和term。這兩個值在snapshot之後的第一條log entry的AppendEntries RPC的完整性檢查的時候會被用上。

  • 系統當前狀態。上面的例子中,x爲0,y爲9.

當Leader要發給某個日誌落後太多的Follower的log entry被丟棄,Leader會將snapshot發給Follower。或者當新加進一臺機器時,也會發送snapshot給它。

做snapshot既不要做的太頻繁,否則消耗磁盤帶寬, 也不要做的太不頻繁,否則一旦節點重啓需要回放大量日誌,影響可用性。推薦當日志達到某個固定的大小做一次snapshot。

2. 集羣成員變更

很多時候,集羣需要對節點進行維護,這樣就會涉及到節點的添加和刪除。爲了在不停機的情況下, 動態更改集羣成員,Raft提供了下面兩種動態更改集羣成員的方式:

  • 單節點成員變更:One Server ConfChange

  • 多節點聯合共識:Joint Consensus

動態成員變更存在的問題

在Raft中有一個很重要的安全性保證就是隻有一個Leader,如果我們在不加任何限制的情況下,動態的向集羣中添加成員,那麼就可能導致同一個任期下存在多個Leader的情況,這是非常危險的。

如下圖所示,從Cold遷移到Cnew的過程中,因爲各個節點收到最新配置的實際不一樣,那麼肯能導致在同一任期下多個Leader同時存在。

比如圖中此時Server3宕機了,然後Server1和Server5同時超時發起選舉:

  • Server1:此時Server1中的配置還是Cold,只需要Server1和Server2就能夠組成集羣的Majority,因此可以被選舉爲Leader

  • Server5:已經收到Cnew的配置,使用Cnew的配置,此時只需要Server3,Server4,Server5就可以組成集羣的Majority,因爲可以被選舉爲Leader

也就是說,以Cold和Cnew作爲配置的節點在同一任期下可以分別選出Leader。

ysVN8U

基於此,Raft提供了兩種集羣變更的方式來解決上面可能出現的問題。

單節點成員變更

單節點成員變更,就是保證每次只往集羣中添加或者移除一個節點,這種方式可以很好的避免在變更過程中多個Leader的問題。

下面我們可以枚舉一下所有情況,原有集羣奇偶數節點情況下,分別添加和刪除一個節點。在下圖中可以看出,如果每次只增加和刪除一個節點,那麼Cold的Majority和Cnew的Majority之間一定存在交集,也就說是在同一個Term中,Cold和Cnew中交集的那一個節點只會進行一次投票,要麼投票給Cold,要麼投票給Cnew,這樣就避免了同一Term下出現兩個Leader。

nxpDam

變更的流程如下:

  1. 向Leader提交一個成員變更請求,請求的內容爲服務節點的是添加還是移除,以及服務節點的地址信息

  2. Leader在收到請求以後,迴向日誌中追加一條ConfChange的日誌,其中包含了Cnew,後續這些日誌會隨着AppendEntries的RPC同步所有的Follower節點中

  3. 當ConfChange的日誌被添加到日誌中是立即生效(注意:不是等到提交以後才生效)

  4. 當ConfChange的日誌被複制到Cnew的Majority服務器上時,那麼就可以對日誌進行提交了

以上就是整個單節點的變更流程,在日誌被提交以後,那麼就可以:

  1. 馬上響應客戶端,變更已經完成

  2. 如果變更過程中移除了服務器,那麼服務器可以關機了

  3. 可以開始下一輪的成員變更了,注意在上一次變更沒有結束之前,是不允許開始下一次變更的。

多節點聯合共識

對於同時變更多個節點的情況, Raft提供了多節點的聯合共識算法,這裏採用了兩階段提交的思想。

主要步驟分爲下面三步:

  1. Leader收到Cnew的成員變更請求,然後生成一個Cold,new的ConfChang日誌,馬上應用該日誌,然後將日誌通過AppendEntries請求複製到Follower中,收到該ConfChange的節點馬上應用該配置作爲當前節點的配置。

  2. 在將Cold,new日誌複製到大多數節點上時,那麼Cold,new的日誌就可以提交了,在Cold,new的ConfChange日誌被提交以後,馬上創建一個Cnew的ConfChange的日誌,並將該日誌通過AppendEntries請求複製到Follower中,收到該ConfChange的節點馬上應用該配置作爲當前節點的配置。

  3. 一旦Cnew的日誌複製到大多數節點上時,那麼Cnew的日誌就可以提交了,在Cnew日誌提交以後,就可以開始下一輪的成員變更了。

這裏有幾個概念比較拗口,這裏解釋一下:

  • Cold,new:這個配置是指Cold,和Cnew的聯合配置,其值爲Cold和Cnew的配置的交集,比如Cold爲[A, B, C], Cnew爲[B, C, D],那麼Cold,new就爲[A, B, C, D]

  • Cold,new的大多數:是指Cold中的大多數和Cnew中的大多數。

結合下面的圖,我們可以走一遍整個集羣的變更過程,在多點聯合共識的規則之下,每一個任期之中不會出現兩個Leader。

JNjEvv

  1. Cold,new日誌在提交之前,在這個階段,Cold,new中的所有節點有可能處於Cold的配置下,也有可能處於Cold,new的配置下,如果這個時候原Leader宕機了,無論是發起新一輪投票的節點當前的配置是Cold還是Cold,new,都需要Cold的節點同意投票,所以不會出現兩個Leader。也就是old節點不可能同時follow兩個leader。

  2. Cold,new提交之後,Cnew下發之前,此時所有Cold,new的配置已經在Cold和Cnew的大多數節點上,如果集羣中的節點超時,那麼肯定只有有Cold,new配置的節點才能成爲Leader,所以不會出現兩個Leader

  3. Cnew下發以後,Cnew提交之前,此時集羣中的節點可能有三種,Cold的節點(可能一直沒有收到請求), Cold,new的節點,Cnew的節點,其中Cold的節點因爲沒有最新的日誌的,集羣中的大多數節點是不會給他投票的,剩下的持有Cnew和Cold,new的節點,無論是誰發起選舉,都需要Cnew同意,那麼也是不會出現兩個Leader

  4. Cnew提交之後,這個時候集羣處於Cnew配置下運行,只有Cnew的節點纔可以成爲Leader,這個時候就可以開始下一輪的成員變更了。


參考:

歡迎關注公衆號【碼老思】,第一時間獲取通俗易懂的原創技術乾貨。

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