Raft 實現日誌複製同步

本篇文章以 John Ousterhout(斯坦福大學教授)Diego Ongaro(斯坦福大學獲得博士學位,Raft算法發明人)Youtube 上的講解視頻PPT 爲藍本,深入分析 Raft 的內部機制,並以日誌複製同步(Replicated Logs)爲背景,詳細介紹使用 Raft 協議實現日誌複製的共識性問題。

目標:日誌複製同步

raft_02

Raft 的目標是將日誌完整地複製到集羣內的所有服務器,這些複製的日誌會被狀態機所使用。假設我們希望程序或應用能可靠地執行,能夠實現的一種方式是保證集羣中所有服務器內的狀態機都能按照相同的方式執行命令,這就是狀態機複製同步的目的,這裏的狀態機通常指的是一個輸入輸出程序或應用。日誌可以保證狀態機執行相同的命令。下面介紹它的運作機制。

如果系統的客戶端將要執行的命令傳遞給集羣中的一臺服務器,假設命令是 X ,那麼它會被該臺服務器記錄,然後命令會被髮送到其他服務器,並被其他服務器上的日誌所記錄。一旦命令被安全的複製到日誌中,那麼它們就能被髮送到狀態機供執行。當其中的一臺狀態機完成了命令的執行,結果會被返回給客戶端。可以注意到只要各個服務器上的日誌是相同的,各個服務器上的狀態機就能以相同的順序執行相同的命令,這樣它們執行的結果也都是一樣的。所以共識性模塊的任務就是管理這些日誌,並保證它們正確的在集羣內複製並且決定何時將命令傳送給狀態機纔是安全的。

我們將這一過程稱爲共識性方法的原因是我們不需要所有的服務器在任何時候都處於運行狀態,實際上,系統只要在大多數服務器存活的狀態下能繼續正常運行和相互通信就可以。所以例如可能有 3 臺服務器,那麼我們就可以接受其中 1 臺服務器宕機,只要有兩臺服務器是存活的即可;當服務器有 5 臺時,我們就可以接受其中的 2 臺服務器宕機,只要其中三臺是正常運行的。

現在我們來簡短地介紹希望系統能夠處理的失敗的情況。我們允許服務器崩潰,不過我們希望它們是 “失敗-停止(fail-stop)” 的方式。也就是說,它們只是停止工作,或者在停止後又恢復,不過要求只要它們是處於運行狀態的,它們的行爲就必須正確。這個協議要求服務器不能有 拜占庭行爲 做一些錯誤的操作。我們還允許網絡的通信可以被打斷,消息可以出現延遲或丟失的狀態,甚至出現消息到達處於無序的狀態。網絡也有可能出現隔離的情況,然後又恢復正常。

達成共識性的方式

raft_03

想要實現共識性算法主要有兩種方式:第一種方式稱爲對稱式或無主式,在這種方式下,所有的服務器都有相同的角色,它們有同等的權力,它們任何時候的行爲幾乎都是一樣的,客戶端可以與任何一臺服務器進行通信。第二種方式稱爲非對稱式或基於領導者(leader),服務器在任何時候都不是對等的,只有其中的一臺服務器是領導者(leader),領導者負責集羣的所有操作,其他的服務器只是簡單地服從領導者發出的指令,在這種系統下,客戶端永遠與領導者通信,只有領導者才與其他的服務器發送通信。

Raft 就是使用上面第二種方式。它將共識性算法的問題分解成兩類不同的問題,一種是在領導者正常運行下,進行的普通操作;另一種是在領導者崩潰時,需要對領導者進行重新選舉,這種方式有其優勢,它讓普通的操作變得非常簡單,不需要關心是否有多個領導者相互發生衝突,或同時發出指令,只要有一個領導者控制全局,就可以完全按照它的指令來運行。Raft 算法的複雜之處在於領導者發生變化時,因爲當領導者崩潰時,會使系統處於不一致的狀態,後續被選舉的領導者需要對這些不一致狀態進行清理。總體上說,基於領導者的方式要比無領導者的方式簡單,因爲無須擔心不同服務器間會出現衝突,只須關心領導者發生變化的情況。

Raft 概覽

raft_04

Raft 算法共分成 6 個部分,首先我們要介紹的就是領導者的選舉。

  1. 如何從所有的服務器中選擇領導者?如何在當作爲領導者的服務器崩潰時能檢測到故障並挑選另一個領導者來替代它?

  2. 會介紹當領導者接收到客戶端請求時,系統是如何處理正常操作的。這是 Raft 算法中最簡單的部分。

  3. 會討論領導者發生改變的情況,這部分是 Raft 中最複雜的,也是保證整個系統行爲最重要的部分。首先,會討論什麼叫做安全,如何保證安全?其次,領導者是如何識別日誌的一致性的,從而可以將系統恢復到處於一致狀態下。

  4. 會討論領導者發生改變時的另一個問題。如何讓曾經崩潰死機的老領導者,重新迴歸到集羣后集羣的狀態仍然能保持一致。

  5. 會談論客戶端是如何與集羣交互的。關鍵點在於客戶端是如何處理服務器崩潰,如何保證客戶端發送的命令是線性的,即操作執行也僅執行一次。

  6. 最後會討論如何處理配置變更的情況,即如何對集羣增加或移除服務器。

服務器的狀態

raft_05

在對這六步進行詳細地介紹前,先來介紹一些總體信息。

任何時候,服務器都處於以下三種狀態中的一種:

  • 領導者(Leader):如前面已介紹的,領導者處理所有客戶端的交互以及日誌的複製同步,在任何時候只能有一個領導者。
  • 跟隨者(Follower):絕大多數的服務器在大多數時間下都處於跟隨者的狀態,這些服務器完全處於被動狀態,它們不會發起任何 RPC 調用,它們所做的只是對其他服務器發起的 RPC 調用做出響應。
  • 候選者(Candidate):它是處於領導者(Leader)與跟隨者(Follower)之間的一種狀態,它在只在選舉新領導者的過程中臨時出現,在系統處於普通狀態下,只會有一個領導者,其他的服務器都是跟隨者。

在上圖最下面展現了一個狀態圖,它展示了三種狀態,以及三種狀態在不同條件下發生轉變的情況。現在不會對此進行詳細解釋,但是在隨後對算法作詳細介紹時,就能發現它們之間的聯繫。

領導者任期

raft_06

時序被分割爲領導者任期,每段領導者任期都有一個序號,這些序號隨着任期數的增加會自動增長,不會被重複使用。每段任期都分爲兩個部分,首先,任期是由選舉開始的,這個過程會挑選任期內的領導者,如果選舉成功,被選擇的領導者會服務至本任期結束。在同一任期內,只有一臺服務器可以被選擇爲領導者。**不過也會存在某些任期沒有任何領導者,如果出現分票(Split Vote)就會出現這種情況,不存在獲得大多數投票的領導者,當發生這種狀況時,系統會即刻進入到下一個新的任期並嘗試重新選舉。**在 Raft 系統的所有服務器都保持着一個被稱爲當前任期的值,這個信息必須存於服務器的可靠媒介中(如硬盤)。這樣就能在服務器崩潰之後得以重啓並恢復。任期這個概念十分重要,它使 Raft 可以判斷過期的信息。例如,如果一臺服務器認爲當前的任期號是 2 與另一臺認爲當前任期號爲 3 的服務器進行通信,那麼我們就能知道來自於服務器 2 的信息是過期的,我們只會使用來自於最新任期的信息。所以我們將會看到在某些情況下,會使用到任期來檢查並消除過期的信息。

Raft 協議總覽

raft_07

上圖是 Raft 協議的完整概括,目前還不會對它們進行詳細的介紹,但是會簡單介紹一些它的特性。

首先分別描述 Raft 協議裏的三種角色:跟隨者(Followers)、候選者(Candidates)和領導者(Leaders)。

其次描述需要在服務器磁盤上進行持久化存儲的信息。

第三描述服務器是如何進行通信的,Raft 的所有通信都是基於遠程過程調用的(RPCs),這裏只有兩種類型的調用:一種被稱爲遠程過程調用投票(RequestVote RPC),它在選舉的過程中被用來挑選領導者;另外一種遠程過程調用是領導者用來執行正常操作,複製日誌記錄的。這是 Raft 系統使用的唯一兩種遠程過程調用的方式。這兩種調用都可以很好的處理日誌複製同步以及消息丟失等問題。

心跳檢測及超時處理

raft_08

現在讓我來一一講解 Raft 協議的六個組件。Raft 協議的第一個組件是選舉。Raft 必須保證在任何時候只能有一臺服務器作爲集羣的領導者。服務是以跟隨者角色啓動的,處於這種狀態時,它不會與其他的服務器進行通信,跟隨者完全是被動的,它只是簡單地對來自於其他服務器的遠程調用做出響應。不過,爲了讓跟隨者一直處於跟隨者的狀態,必須使它們相信集羣有一個活躍的領導者存在。 **唯一能實現的方式就是,如果它接收到來自於其他服務器的通信,無論是領導者或是候選者,**所以如果領導者想要保持它的領導地位,它就必須定期與集羣的其他服務器進行通信,如果它沒有與其他服務器進行主動通信的需要,那麼它也必須發送心跳檢測的消息,在 Raft 協議中,這些心跳檢查消息也只是一些不含任何數據信息的 AppendEntries 遠程調用。如果在一段時間內,跟隨者沒有接收到任何的遠程調用,那麼它會假定集羣內沒有可達或可用的領導者,所以它就會開始進行選舉,看它是否有必要成爲新的領導者。這段時間週期被稱爲選舉超時(electionTimeout),通常集羣將這個時間定爲 100ms 到 500ms 。所以當集羣啓動時,所有的服務器都是作爲跟隨者的,沒有領導者,所以它們都會等待這段超時,然後它們都會開始進行選舉。

選舉

raft_09

當服務器開始進行選舉的時候,它所做的第一件事情就是增加當前的任期號,創建一個比之前使用過的任何值都要大的新任期號。隨後,服務器將它們自己從跟隨者狀態轉換到候選者狀態,在這種狀態下,它的目標就是要讓自己當選爲領導者,爲了這麼做,它需要接收來自於大多數服務器的投票。候選者要做的第一件事情就是給自己投票,然後它會給其他所有服務器發送投票請求的遠程調用(RequestVote),通常這些請求是並行發出的。如果它沒有獲得響應,它就會持續發送重試的請求,直到獲得響應爲止。

最終會出現三種情況中的其中一種:

第一,在大多數情況下,也是我們希望出現的情況就是候選者得到了多數票,然後它會將自己的狀態轉換爲領導者並立即向集羣其他服務器發送心跳檢測,這可以建立它的領導者地位,有效的標記領導者所管理的範圍。

第二,可能出現有其他的候選者也同時在運行,或許它們也有可能獲得多數票成爲領導者,在這個點上,如果候選者收到來自於有效領導者的 RPC 調用,那麼它會立即放棄成爲領導者的可能,隨即回到跟隨者的狀態。

第三,有可能沒有任何服務器得以獲勝,如果存在有多個服務器都同時成爲候選者,它們會導致分票,沒有服務器會獲得多數選票。爲了檢測到出現這種狀況的可能性,隨着時間的推移,當沒有出現以上第一、第二種情況時,它既沒有成爲領導者,也沒能獲得來自於其他領導者的響應,那麼它就會假定出現分票的情況。在這種情況下,只要簡單地增加任期號,重新選舉即可。

選舉的安全及可用

raft_10

選舉有兩個重要的屬性:安全(Safety)和可用(Liveness)

安全(Safety) 指的是必須最多隻有一個候選者可以在某一任期內贏得領導者地位。Raft 可以保證這件事。每臺服務器只給一個候選者投票,一旦它投出選票,它就會拒絕來自其他候選者的任何請求。服務器並不關心它的票到底投給了哪臺服務器。爲了實現這種機制,服務器需要保證將自己的投票信息存儲到磁盤,這樣就能在服務器崩潰之後也能恢復到之前的狀態。否則就會出現服務器已經作出投票,並在崩潰重啓後,在同一任期內將票又投給了另外一個不同服務器的情況。因爲每臺服務器只能進行一次投票,而且每個候選者都必須獲得多數票,也就可以發現,不可能出現兩個候選者同時獲勝的情況。

比方說有三臺服務器在某一任期內進行選舉,另外兩臺服務器顯然無法獲得多數票。不過後面會介紹不同任期間會出現不同候選者獲勝的情況,但在某一確定的任期內,只有一個候選者可以被選舉爲領導者。

可用(Liveness) 需要保證一定有獲勝者,這樣系統不會永遠處於沒有領導者的狀態。問題在於理論上,會反覆出現分票的情況,多個候選者在同一任期內同時開始進行選舉,這樣就會導致分票,在超時之後,又進行新一輪的選舉又再次出現分票,所以從理論上說這樣的狀態可以無限循環下去。Raft 需要分散出現超時的間隔,每臺服務器都會隨機的計算下次超時的間隔時間,這個時間間隔在 [T, 2T] 之間。 T 代表着選舉超時的時間,即服務器可能出現超時的最短時間。通過將超時時間分散,可以降低兩臺服務器同時開始選舉的機率,先啓動的那臺有足夠的時間向其他所有服務器發起請求,並在其他服務器參與競爭之前就完成選舉這個過程。當這個超時間隔時間遠大於廣播投票請求的時間時,這個策略會變得更爲有效。這裏的廣播時間指的是,一臺服務器與其他所有服務器通信所需的時間。

日誌的結構

raft_11

現在進入 Raft 協議的第二部分,即領導者用普通操作來處理日誌複製同步時使用的機制。

首先,讓我們說說日誌本身。每臺服務器無論是領導者還是跟隨者,都各自保存一個日誌副本。日誌本身被分成了多條記錄(Entries),記錄是由下標索引的位置來進行唯一標識的,在記錄內部有兩個主要信息:首先,每條記錄都包括供狀態機執行的一條命令,命令的格式可以是客戶端與狀態所達成一致的某種格式。其次,每條記錄都包括一個任期號,這個任期號是該條記錄創建時,領導者所處的任期,隨着日誌記錄的增多,這個任期號也會單調上升。每臺服務器都必須保證日誌能在崩潰後還可以恢復,所以日誌本身通常是存於磁盤或其他一些穩定的存儲介質中。無論服務器作何更新,它都需要在收到來自於其他服務器的響應之前,將內容寫入到磁盤。如果某條記錄已存儲於大多數服務器,例如上圖中的記錄 7 (Entry-7),那麼我們就稱該條記錄已提交(committed)。這是 Raft 協議裏非常重要的一個屬性。如果一條記錄是已提交的,那麼它就能安全被傳送給狀態機進行執行,Raft 可以保證該條記錄的耐久性。在上圖中記錄 7 是已提交的,所有先於記錄 7 的記錄也是已提交的狀態,但是記錄 8 還處於未提交狀態,因爲它只存儲於兩臺服務器上。

現在需要注意的是,在稍後討論如何管理跨服務器日誌間的一致性的時候,我會對提交(commitment)這個概念的定義作些許修改。

普通操作

raft_12

普通操作比較簡單,客戶端將命令發送給領導者,領導者首先將命令寫入它自己的日誌中,然後向所有其他的跟隨者發送 AppendEntries 的遠程調用。通常這些調用的消息會被同時發送所有服務器,以並行的方式執行,並等待這些消息的響應。一旦領導者收到足夠多的響應,它可以認爲該條命令已經在多數服務器上處於已提交狀態時,那麼該條命令就可以被執行。領導者這時會將命令發送給狀態機,當執行結束後,它會將結果返回給客戶端。不僅如此,一旦服務器知道某個記錄已經處於提交狀態,它就會通過後續的 AppendEntries 遠程調用告知其他的服務器。所以最終,每個跟隨者都會知道該記錄已提交,並且將該命令發送至自己本地的狀態機執行。如果跟隨者崩潰了或處於慢響應狀態,領導者會反覆重試這個調用,直到跟隨者恢復後,領導者就能重試成功。但是領導者並不需要等待每個跟隨者的響應,它只需要等到足夠數量的響應,保證記錄已被大多數服務器存儲即可。所以這樣就能在一般情況下獲得很好的性能提升。也就是說,在通常情況下,只需要獲得大多數服務器的應答,領導者就可以立即執行命令,並將結果返回至客戶端。例如,如果某個服務器很慢,這並不能影響客戶端獲得響應的速度,因爲領導者並不需要一直等待該臺服務器。

日誌的一致性

raft_13

Raft 期望能將集羣日誌維持高水準的一致性。理想狀態下,這些日誌在任何時候都是相同的,甚至是服務器崩潰時也如此。Raft 會儘可能的保證在不同服務器上的日誌是一樣的。上圖的內容會列出一些重要的屬性,它們在任何時候都是有效的。

第一,日誌記錄的索引以及任期號的組合可以唯一標識一條日誌記錄。也就是說如果有兩條記錄的索引是一樣的,任期號也是一樣的,那麼就可以保證它們所存儲的命令也是相同的。除此之外,還能保證在這條記錄之前的所有記錄都能相互匹配。所以任期號和索引的組合可以唯一標識整個日誌的起始至該點的位置。如果某條記錄是已提交的,那麼其所有前序的記錄都應該處於已提交狀態。這也與之前介紹的規則一致,如果發現服務器存儲記錄(如上圖的記錄 5),因爲有了以上規則,它們存儲的前序記錄也必須相同。所以這些前序記錄也存在於集羣的大多數服務器上。

AppendEntries 一致性檢查

raft_14

這個屬性強制在 AppendEntries 遠程調用時進行檢查,當領導者向跟隨者發起 AppendEntries 調用時,除了新創建的新日誌記錄,它還包括兩個值。他包括當前新記錄前序記錄的下標位置索引以及任期號,跟隨者只會接受與它日誌匹配的遠程調用,如果跟隨者的日誌沒有相應的記錄,那麼它會拒絕這個遠程調用。

讓我們來看一個例子,假設領導者從客戶端接收到一個新命令 jmp ,它將這個命令以 AppendEntries 遠程調用的方式發送給跟隨者,包括它前序記錄的下標位置索引以及任期號,這裏下標位置索引是 Index-4 ,任期號是 Term-2 。這樣跟隨者會將此信息與它自己當前日誌的記錄匹配,然後接受創建新的記錄。如上圖下半部分,跟隨者的當前最新記錄與領導者的前序記錄的信息不匹配,這樣跟隨者會拒絕接受遠程調用的請求。

這個一致性檢查的過程非常重要。可以將這個過程看作一個歸納的步驟,從而保證前面一致性裏所講的內容。它要求前序每條記錄都能滿足此條件,所以這意味着如果一個跟隨者接受了來自領導者的新記錄,它的日誌記錄也與領導者的日誌記錄是完全匹配的。

以上就對普通操作的介紹告一段落。接下來介紹領導者變更的情況。

領導者變更

raft_15

當領導者發生變更時,新領導者面對的狀態不一定是乾淨的,因爲前一領導者可能在它完成複製同步之前就已經崩潰了,當 Raft 處理這個問題時,它在新的領導者被選出之前,不會有任何特別的操作,不會存在一個獨立清理過程,清理過程是在普通操作過程中發生的。原因是當新領導者被選出後,某些服務器可能還處於宕機的狀態,不可能立刻對它們的日誌進行清理,必須能有操作恢復它們,而且在這些機器重新加入集羣之前可能會要等待很長一段時間,所以就必須對系統進行設計,要求普通操作最終能讓所有的日誌達成一致狀態。爲了達成這個目標,Raft 始終會認爲領導者的日誌總是正確的,所以對於所有領導者,它們必須時刻的讓跟隨者的日誌與自己保持一致,但同時還是有可能出現在領導者未完成任務就崩潰的情況,所以就會出現一個又一個的新領導者。所以,在極端扭曲的狀態下,日誌記錄會無限堆積並出現混亂的狀態,就如上圖所示的那樣。

爲了簡單起見,上圖中只顯示了下標索引位置以及任期號,沒有顯示具體的命令信息。

當服務器 S4、S5 在任期 2、3、4 時是領導者,但是由於某些原因,它們無法完成對其他服務器(S1、S2、S3)上日誌的複製同步,然後它們崩潰了,系統在一段時間內處於分隔狀態,服務器 S1、S2、S3 在任期 5、6、7 內成爲領導者,但同時也無法與服務器 S4、S5 進行通信,要求它們進行相應的清理操作。這就會出現上圖中所示的狀態,日誌完全是混亂的。這裏的關鍵在於 S1、S2、S3 的索引 1-3 以及 S4、S5 的索引 1-2 區域。這些都是已提交狀態的記錄,所以我們必須保留它們,但其他的日誌記錄都是未提交的,所以到底是保留還是丟棄它們並不重要。我們還沒有將它們傳入狀態機,也沒有客戶端得到了這些命令的執行結果。所以它們都是可以丟棄的。

例如,假設服務器 S4 是任期 7 的領導者,而且它可以與其他所有服務器通信,那麼它最終會讓集羣裏其他服務器上的日誌與它自己的保持一致,並刪除那些與之衝突的記錄。在介紹領導者是如何讓其他服務器上日誌與之保持一致前,首先需要介紹兩個概念:正確性(Correctness)和安全性(Safety)。我們是如何知道系統的行爲是正確的?如何知道它們沒有丟失一些重要信息?因爲這裏可以看到,爲了讓集羣回到一致的狀態,有些日誌記錄會被丟棄。我們是如何安全地做到這點的?

安全性的要求

raft_16

幾乎所有的日誌複製同步系統都會對安全性有所要求,一旦某個狀態機接收了一條日誌記錄並執行,我們必須保證不存在其他的狀態機執行不同的命令。需要保證所有的狀態機,以相同的順序執行相同日誌記錄的命令。爲了達成總體的安全性要求,Raft 實現了一個安全屬性,一旦領導者決定某個特定記錄已提交,那麼 Raft 就需要保證該條記錄會出現在它所有未來領導者的日誌記錄中,並且也處於已提交狀態。 如果我們可以讓 Raft 遵從這個屬性,那麼它就自然可以保證以上的安全性要求。首先,領導者永遠不會覆蓋日誌記錄,它只會追加,正如我們所知,作爲領導者時,這些日誌記錄永遠不會被改變,其次,爲了到達已提交的狀態,記錄必須在領導者日誌中,這樣就不會有其他值會被提交,第三,日誌記錄必須在發送給狀態機執行之前被提交,所以將以上三點放在一起,我們就能使該屬性可以滿足安全性的要求。

目前爲止,我們對 Raft 的描述還不能保證這個屬性。下面我會來看看 Raft 是如何解決這個問題的。不過在此之前我們需要再看看,如果某條記錄是已提交的,那麼它在未來的領導者日誌記錄中也必須是已提交的。爲了滿足這個要求,我們會從兩個方面對 Raft 算法作出修改。首先,我們會修改選舉過程,將日誌記錄不正確的那些機器排除在選舉之外,其次,會對已提交的定義做略微的調整。有時在知道安全之前,我們會延遲一條記錄的提交。

下面會先介紹選舉相關的問題

挑選最好的領導者

raft_17

如何保證選擇的領導者有所有已提交的日誌記錄?首先,這有點微妙,事實上我們無法辨別哪些記錄是已提交的,假設有如上圖的三臺服務器,我們需要選擇一個新的領導者,但其中的一臺服務器不可用,那麼只要在這個過程中,查看可用的服務器,我們此時是無法分辨記錄 5 是否已提交,它依賴於不可用服務器上存儲的內容。在這個例子中,記錄 5 是已提交的,但在其他情況下,可能不是。可以肯定的是我們無法知道哪些記錄已被提交了。所以我們能做的就是找到一個候選者,這個候選者很有可能包括所有已提交的記錄,我先從直觀上嘗試解釋如何做到的,然後在用精確的方式加以證明,我們是能夠挑選到候選者存有所有已提交的記錄的。

我們通過比較日誌的方式來實現。當一個候選者發起投票請求,它會包括自身的日誌記錄信息,位置索引 index 以及最後一條日誌記錄的任期號 term 。當響應投票的服務器接收到請求,它會將候選者的日誌信息與自己的日誌信息進行比較,如果投票者的日誌更完整,那麼它會拒絕投票 (lastTermV > lastTermC) ||(lastTermV == lastTermC) && (lastIndexV > lastIndexC)。結果是贏得選舉的服務器可以保證比大多數投票者有更完整的日誌記錄。

讓我們看看實際到底是如何工作的。

在當前任期提交記錄

最有趣的情況恰好是在領導者決定剛決定日誌記錄是已提交的時候,會有兩種場景:

第一種:提交的記錄是在當前任期

raft_18

這裏任期 2 以及領導者(S1)剛成功調用 AppendEntries 至 S3 ,此時它發現記錄已在大多數服務器上存儲,隨即標記該記錄是已提交的,並將其傳送給狀態機。此時這條記錄是安全的,下一任期的領導者必須認定該記錄的已提交狀態。正如之前介紹的規則,S5 是無法成爲下一任期的領導者,S4 也無法成爲領導者,所以只有 S1、S2、S3 可能被選舉成領導者,實際上,如果 S1 在它們中間,S1 一定可以保證贏得選舉,但 S2、S3 也可以通過獲得其他服務器(S4、S5)的投票,獲勝成爲領導者。但在任意一種情況下,下一任期的領導者都必須包含該日誌記錄。

第二種:提交的記錄是在前序任期

raft_19

在這種狀態下,領導者在任期 2 只複製了兩臺服務上的日誌記錄,隨後任期 3 的領導者(S5)於某些原因沒有關注到這些記錄,在它本地創建了一些記錄,然後崩潰了。然後在任期 4 上,領導者(S1)作爲試圖將其他服務器上的日誌內容與它自己的達成一致。所以它讓服務器 S3 複製了它自己 Term-2 記錄,在這個點上,該記錄已被領導者知道存於大多數服務器上,但該記錄並沒有安全的被提交。因爲此時 S1 可能出現崩潰,S5 成爲領導者,因爲它的前序任期值 3 較大,所以它可以獲得來自於 S2、S3、S4 的投票,如果它當選,那麼它會試圖將自己的日誌推到其他的服務器,這也就意味着從 S1 - S4 下標位置索引 3 開始的所有記錄都會被刪除。 所以此時我們還無法認定記錄 3 是否已經提交。

新提交規則

raft_20

在這種情況下,新的選舉規則並不足以保證安全性(Safety),我們還需要修改提交的規則。到目前爲止只要領導者發現記錄已存於大多數服務器,那麼它就認爲該記錄已被提交。但是爲了保證安全性,我們需要增加另一條規則。除了上述規則,領導者必須能看見至少有一條來自於它本任期內的記錄也存於大多數服務器。 回到之前的例子,如果領導者完成了記錄 3-2 的複製,它此時還無法提交該記錄並將其發送給狀態機,取而代之的是,它必須等待直到它當前任期內的第一條記錄(4-4)提交併存於大多數的服務器。至此,兩條記錄才能都發送給狀態機。 這麼做的原因在於,在這種狀態下,服務器 S5 是不可能被選舉爲下屆領導者的,因爲有更多的服務器處於更近的任期(任期 4),服務器 S5 只能從服務器 S4 處得到選票。此時,記錄 3 和 4 都是安全的。所以將新選舉規則與新提交規則相結合,我們就能保證 Raft 的安全屬性總是有效的。即一旦領導者決定記錄已提交,它就會對未來的所有領導者可見。這裏我們展示的例子只說明,已提交的記錄對下一任期的領導者可見,但也可以很容易就證明,每個未來的領導者也會有相同的日誌記錄。

日誌的不一致

raft_21

現在我們可以保證安全性,也明白了日誌是正確的。那麼我們如何讓所有跟隨者的日誌都與領導者保持一致呢?首先,讓我們來看看日誌不一致可以出現怎樣的情況。

  • 跟隨者可能會丟失記錄(如 (a)-10、(b)-5、(e)-8)
  • 跟隨者可能會有不同的記錄(如 (d)-11、(f)-4、©-11)

需要做的是剔除所有不同的日誌記錄,並將所有丟失的記錄根據領導者的日誌填充完整。

修復跟隨者的日誌

raft_22

要想恢復到一致狀態,領導者會爲每個跟隨者維護一個狀態變量,這個變量稱爲 nextIndex,這個變量存儲日誌的下一條記錄的下標位置索引,服務器會把這個位置發送給跟隨者(如上圖所示,nextIndex = 11)。**當一臺服務器成爲領導者後,它會將 nextIndex 值設置成當前日誌記錄的下一位置。**所以在上面的例子中,任期 7 的領導者的最後一條記錄的索引位置是 10 ,那麼它會將 nextIndex 設置成 11 。**領導者會根據 AppendEntries 調用發現一致性問題,因爲當跟隨者接收到 AppendEntries 調用時,都會進行檢查。**這個檢查就可以發現所有的問題。所以當下一次領導者想要與跟隨者進行通信時,它都會包括下標位置索引(10)以及任期號(6)作爲請求的參數。**當選爲領導者後,下一次請求也有可能是以心跳檢測的方式發送的,心跳檢測與 AppendEntries 調用的方式一樣,只是沒有新值創建,但還是包括一致性檢查的。**所以當消息到達跟隨者(a)後,它會將接收到的下標位置索引與任期與自己的日誌信息進行比較,並沒有匹配的記錄,所以它會拒絕 AppendEntries 請求,當領導者收到拒絕的響應之後,它的響應很簡單,它要做的只是將 nextIndex 減 1 ,所以這個值就變成了 10 。如此逐一減少,直到最終 nextIndex 爲 5 的時候,領導者再次發送請求的信息會包括下標位置索引(4)以及任期號(4),這時它與跟隨者(a)當前的日誌記錄信息是相匹配的,所以這時跟隨者會接受 AppendEntries 請求,並追加記錄 5-4 。直到領導者將跟隨者的日誌記錄填充完整。相似的過程也會在跟隨者(b)上出現。當 nextIndex 減少到 4 時,領導者會包括下標位置索引(3)以及任期號(1)作爲請求的參數,並修正跟隨者(b)上的日誌記錄。

raft_23

這個過程還需要注意一點,當跟隨者接收來自於領導者的替換請求時,它會將後續的日誌記錄截斷並刪除後續的所有日誌記錄,在上述的例子中,如果領導者發送請求(4-4),nextIndex = 4 ,這時跟隨者的記錄爲 4-2 ,是不一致的,這時它不僅會將 4-2 覆蓋,同時還會刪除剩餘的所有記錄,因爲在不一致的記錄後也都是不一致的記錄。

現在對領導者發生變更的情況作個小結。總體上需要解決兩個問題:一個是需要保證系統的安全性,第二個是一旦新的領導者開始行使權利,它要做的事情就是使所有跟隨者上的日誌記錄與自身保持一致,AppendEntries 的一致性檢查會爲我們提供所有的信息。

平衡舊領導者(Neutralizing Old Leader)

raft_24

Raft 協議的第四步也是與領導者更替相關的。舊領導者有可能並不是真的死了。例如出現了網絡的隔離,將領導者與集羣內其他服務器分隔,那麼剩下的服務器會等待選舉超時,並選舉一個新領導者,那麼問題來了,如果舊領導者又重新恢復連接怎麼辦?這個舊領導者並不知道已經重新進行了選舉,也不知道新領導者的存在。所以這時它還會試圖以領導者的身份繼續運行,它還會與跟隨者進行通信,並試圖讓其他跟隨者與自己的日誌記錄保持一致,我們必須阻止這個事情的發生。

**可以使用任期來防止這種情況的出現。因爲每個 RPC 請求都包括髮送者的任期號,當 RPC 接收時,接受者會將其與自己的任期號相比較,如果不匹配,則會更新那些過期的記錄。**所以如果發送者的任期比接收者的要老,那麼就表示發送者是過時的,這時接收者會立即拒絕 RPC 請求,並將包括了接收者任期信息的響應發送回發送者,這樣當發送者接收到響應時就會意識到,它的任期號是過期的,此時它就會停下並作爲跟隨者繼續運行,同時它還會更新自己的任期號,並與其他服務器保持一致。反之,如果接收者的任期號更老,如果這時接收者不是跟隨者,那麼它也會停下,並作爲跟隨者,而且更新它自己的任期號。略微不同的是接收者不會拒絕 RPC ,它會接收 RPC 請求。

這裏比較有趣的是選舉過程會導致任期號的更新,即當候選者請求投票並與大多數服務器發生通信後,它會將自己的任期號隨着 RPC 請求發送出去,這樣所有的接收者都會更新自己的任期號,並與候選者保持一致,所以當新領導者被選出後,集羣裏的多數服務器都會更新到這個任期號。這也就意味着,一旦選舉完成,被罷免的領導者是無法提交新記錄的,因爲它需要與至少一臺服務器進行通信,這樣它就能發現自己的任期號更老,這時它就會停止領導者的行爲並作爲跟隨者繼續運行。

還有一些比較典型的場景,這裏不作更多的討論,但可以用任期號來處理所有類似的問題。

客戶端協議

raft_25

現在讓我們看看 Raft 協議的第五部分,即客戶端是如何與系統進行交互的。這點並不複雜,客戶端將命令發送給領導者,並獲得響應,如果客戶端不知道哪臺服務器是領導者也沒關係,它可以與集羣的任意一臺服務器進行通信,如果這臺服務器不是領導者,那麼它會告知客戶端,並將客戶端重定向到領導者,然後客戶端會再次發送請求。只有在領導者記錄下命令,並已經將其提交,然後發送給狀態機執行之後,纔會將結果返回給客戶端。這裏比較微妙的是,如果領導者發生崩潰或請求發生超時該怎麼辦?如果發生這種情況,客戶端會隨機挑選另一臺服務器並再次發送請求,最終它會將請求發送到新的領導者,新的領導這會執行該命令。這個可以保證命令最終總能被執行。

但這留有一個風險,即命令有可能被執行兩次。

raft_26

問題在於領導者會在執行完命令後響應客戶端之前發生崩潰,所以命令本身是無法知道自己是否被記錄或已被執行。這時客戶端就會再次發起請求,這樣命令就又被執行了一遍。這是不能被接受的,因爲我們要每條命令執行且僅被執行一次。Raft 解決這個問題的辦法是讓客戶端爲每條命令生成一個唯一的 ID ,並將其與命令一起發送給領導者,當領導者記錄該條命令時,也會包括這個唯一 ID ,但在領導者接受命令之前,它會進行檢查,看其他記錄中是否已存在相同的 ID ,如果存在相同的,那麼它就會知道該條命令請求是多餘的,所以它會找到該條記錄,並忽略這條新命令,並將老的執行結果返回給客戶端。

所以只要客戶端不崩潰,結果最多隻會被執行一次。這也是我們希望系統應該具備的線性一致性。

接下來要介紹 Raft 協議的第六部分,也是最後一部分。

配置變更

raft_27

我們已經有了應對配置發生變更的處理機制。當我們提到配置,指的是集羣服務器的信息,包括每臺服務器的 ID 、網絡地址等。這些信息都非常重要,因爲我們需要用它們來決定多數票的具體數量,從而進行領導者選舉或用來提交日誌記錄。我們要支持這些變更的原因在於,比如當服務器出現故障的情況,它們可以被新的機器替換,或者集權管理員希望能更改副本數量,我們希望所有的這些事情都能在安全自動的條件下完成,不要因爲配置的變更導致系統出現故障或停機的情況。

raft_28

必須要意識到,我們無法直接從舊配置切換到新配置。我們來看個例子。假設系統集羣有三臺服務器正在運行,這時我們希望再增加兩臺服務器,所以最終集羣內會有五臺服務器。如果我們只是要求每臺服務器從舊配置切到新配置,問題是這個切換不能無法同時完成,時間上總會有先有後。而這可能會導致衝突的大多數。因爲 S1、S2 可以在某個時候形成舊集羣的大多數,並決定領導者。而與此同時,另外三臺服務器 S3、S4、S5 已經切至新的配置,它們也形成了該配置狀態下的大多數。所以它們也可以決定領導者,確認提交狀態。這樣就會與 S1、S2 發生衝突。這樣,我們就需要使用兩段協議(two-phase protocol),無法在一段內達到目的。

這當然也是所有分佈式決策的所必須使用的方式。

聯合共識

raft_29

解決方案是使用兩段協議的方式來更改配置信息。

Raft首先切換到稱爲多邊共識(joint consensus)的中間階段。在這個階段中,集羣包括所有的服務器上新舊兩種配置,但是如選舉和提交的決策,需要在新舊兩個獨立的配置狀態下達成一致。

集羣配置以 Cold 開始,然後客戶端向領導者發送請求,當接收者收到請求(Leader收到從 Cold 切換成 Cnew 的成員變更請求)之後,會向日志裏新增一條記錄,要求記錄新配置 Cold+new ,配置與其他普通的命令記錄一樣,領導者會用 AppendEntries RPC 請求將其發送給集羣的其他服務器,**配置變更唯一的不同在於它們會立即生效,一旦服務器將新配置記錄到日誌中,那麼它就立刻生效,並不需要等待該日誌記錄變爲已提交狀態。所以此時在領導者上已經認爲 Cold+new 已生效,這意味着對於要提交的任何日誌條目,要求該條目分別在新舊配置服務器下同時都成爲大多數。**現在,在該條目被複制或到達提交點之前,可能會使用Cold或Cold+new作出決定。例如,如果領導者在記錄新配置記錄後就發生崩潰,有可能某些其他舊配置的機器仍然處於工作狀態,被選舉成領導者管理集羣。但在某個時間點,Cold+new 會變爲已提交的狀態,在此種狀態下,任何機器就無法只根據 Cold 來做出決策。爲了讓領導者被成功選舉,它必須保證所有的記錄都已提交,所以一旦 Cold+new 記錄已提交,它就能保證任意選舉的領導者都有該記錄,也就是說領導者已使用該配置。所以在這個時候,集羣是處於聯合共識下運行的,一旦聯合共識被提交確認,領導者就可以將配置變更 Cnew 寫入日誌記錄,併發送給集羣其他服務器。所以在這個時候,集羣下服務器配置可能在 Cnew 或 Cold+new 的狀態,因爲這時服務器也可能再次出現崩潰,另一服務器會替代成爲領導者,並使用聯合共識下的 Cold+new 配置。但最終新配置記錄 Cnew 會處於提交狀態,一旦出現這種情況,集羣所有未來的決策都將基於 Cnew 。所以關鍵在於,不存在 Cold 或 Cnew 在不進行相互協調的前提下就能做出決策的情況。Cold 可以獨立做出決策,Cnew 也可以獨立做出決策,但是兩者不會發生重疊。在這兩段時間之間,兩個配置需要相互協調,這就能保證,集羣不會兩個獨立的達成共識的羣體存在。

在這裏,兩段協議是一個基礎協議。任何共識性算法都需要使用兩段協議來對配置進行變更,實際上任何分佈式一致都需要兩段協議。

raft_30

這個協議還有些需要注意的地方。

在過度期間,有可能服務器來自於任何一種配置都能被選舉爲集羣領導者,這裏比較微妙的是如果當前的領導者不在新配置裏,那麼它最終會停下,並轉換爲跟隨者。在 Raft 裏,舊領導者在 Cnew 處於已提交狀態後立即停止並轉換成跟隨者。這時其他的跟隨者會超時,並選舉新的領導者,這時被選舉的領導者所使用的配置一定是 Cnew 。儘管如此,舊的領導者也還是會領導一小段時間。

總結

raft_31

參考

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