Raft 實現日誌複製同步

Raft 實現日誌複製同步

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

image

目標:日誌複製同步

image

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

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

咱們將這一過程稱爲共識性方法的緣由是咱們不須要全部的服務器在任什麼時候候都處於運行狀態,實際上,系統只要在大多數服務器存活的狀態下能繼續正常運行和相互通訊就能夠。因此例如可能有 3 臺服務器,那麼咱們就能夠接受其中 1 臺服務器宕機,只要有兩臺服務器是存活的便可;當服務器有 5 臺時,咱們就能夠接受其中的 2 臺服務器宕機,只要其中三臺是正常運行的。安全

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

達成共識性的方式

image

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

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

Raft 概覽

image

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

  1. 如何從全部的服務器中選擇領導者?如何在看成爲領導者的服務器崩潰時能檢測到故障並挑選另外一個領導者來替代它?分佈式

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

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

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

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

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

服務器的狀態

image

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

任什麼時候候,服務器都處於如下三種狀態中的一種:

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

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

領導者任期

image

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

Raft 協議總覽

image

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

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

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

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

心跳檢測及超時處理

image

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

選舉

如今讓咱們看看,選舉是如何工做的。

image

當服務器開始進行選舉的時候,它所作的第一件事情就是增長當前的任期號,建立一個比以前使用過的任何值都要大的新任期號。隨後,服務器將它們本身從跟隨者狀態轉換到候選者狀態,在這種狀態下,它的目標就是要讓本身當選爲領導者,爲了這麼作,它須要接收來自於大多數服務器的投票。候選者要作的第一件事情就是給本身投票,而後它會給其餘全部服務器發送投票請求的遠程調用(RequestVote),一般這些請求是並行發出的。若是它沒有得到響應,它就會持續發送重試的請求,直到得到響應爲止。

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

第一,在大多數狀況下,也是咱們但願出現的狀況就是候選者獲得了多數票,而後它會將本身的狀態轉換爲領導者並當即向集羣其餘服務器發送心跳檢測,這能夠創建它的領導者地位,有效的標記領導者所管理的範圍。

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

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

選舉的安全及可用

image

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

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

比方說有三臺服務器在某一任期內進行選舉,另外兩臺服務器顯然沒法得到多數票。不事後面會介紹不一樣任期間會出現不一樣候選者獲勝的狀況,但在某一肯定的任期內,只有一個候選者能夠被選舉爲領導者。

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

日誌的結構

image

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

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

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

普通操做

image

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

日誌的一致性

image

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

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

AppendEntries 一致性檢查

image

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

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

這個一致性檢查的過程很是重要。能夠將這個過程看做一個概括的步驟,從而保證前面一致性裏所講的內容。它要求前序每條記錄都能知足此條件,因此這意味着若是一個跟隨者接受了來自領導者的新記錄,它的日誌記錄也與領導者的日誌記錄是徹底匹配的。

以上就對普通操做的介紹告一段落。接下來介紹領導者變動的狀況。

領導者變動

image

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

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

當服務器 S四、S5 在任期 二、三、4 時是領導這,可是因爲某些緣由,它們沒法完成對其餘服務器(S一、S二、S3)上日誌的複製同步,而後它們崩潰了,系統在一段時間內處於分隔狀態,服務器 S一、S二、S3 在任期 五、六、7 內成爲領導者,但同時也沒法與服務器 S四、S5 進行通訊,要求它們進行相應的清理操做。這就會出現上圖中所示的狀態,日誌徹底是混亂的。這裏的關鍵在於 S一、S二、S3 的索引 1-3 以及 S四、S5 的索引 1-2 區域。這些都是已提交狀態的記錄,因此咱們必須保留它們,但其餘的日誌記錄都是未提交的,因此究竟是保留仍是丟棄它們並不重要。咱們尚未將它們傳入狀態機,也沒有客戶端獲得了這些命令的執行結果。因此它們都是能夠丟棄的。

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

安全性的要求

image

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

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

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

挑選最好的領導者

image

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

咱們經過比較日誌的方式來實現。當一個候選者發起投票請求,它會包括自身的日誌記錄信息,位置索引 index 以及該記錄的任期號 term 。當響應投票的服務器接收到請求,它會將候選者的日誌信息與本身的日誌信息進行比較,若是投票者的日誌更完整,那麼它會拒絕投票(lastTerm v > lastTerm c)|| (lastTerm v == lastTerm c) && (lastIndex v > lastTerm c)。結果是贏得選舉的服務器能夠保證比大多數投票者有更完整的日誌記錄。

讓咱們看看實際究竟是如何工做的。

在當前任期提交記錄

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

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

image

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

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

    image

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

新提交規則

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

日誌的不一致

image

如今咱們能夠保證安全性,也明白了日誌是正確的。那麼咱們如何讓全部跟隨者的日誌都與領導者保持一致呢?首先,讓咱們來看看日誌不一致能夠出現怎樣的狀況。

  • 跟隨者可能會丟失記錄(如 (a)-十、(b)-五、(e)-8)
  • 跟隨者可能會有不一樣的記錄(如 (d)-十一、(f)-四、(c)-6)

須要作的是剔除全部不一樣的日誌記錄,並將全部丟失的記錄根據領導者的日誌填充完整。

修復跟隨者的日誌

image

要想恢復到一致狀態,領導者會爲每一個跟隨者維護一個狀態變量,這個變量稱爲 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)上的日誌記錄。

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

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

平衡舊領導者(Neutralizing Old Leader)

image

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

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

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

還有一些比較典型的場景,這裏不做更多的討論,但能夠用任期號來處理全部相似的問題。

客戶端協議

image

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

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

image

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

因此只要客戶端不崩潰,結果最多隻會被執行一次。這也是咱們但願系統應該具有的線性一致性。

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

配置變動

image

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

image

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

這固然也是全部分佈式決策的所必須使用的方式。

聯合共識

image

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

Raft 將第一階段到中間階段稱爲多邊共識(joint consensus),在這個階段中,集羣包括全部的服務器上新舊兩種配置,可是如選舉和提交的決策,須要在新舊兩個獨立的配置狀態下達成一致。

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

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

image

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

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

image

參考

參考來源:

2013 Raft lecture, Diego Ongaro

2013 Raft user study

Wiki: Byzantine fault tolerance

結束

相關文章
相關標籤/搜索