Raft算法解讀

.jpg

前言


以前的文章中我分享了我的對於Paxos算法的理解和看法,在文章的末尾引出了Raft算法,今天就來填完Raft算法這個坑。git

Raft算法的做者在論文中吐槽了Paxos算法難以以理解且難以實現,因此提出了一個以易於理解且方便構建的分佈式一致性算法,並且Raft算法提供了和Paxos算法相同的安全性及相差無幾的性能。Raft算法的提出能夠說是造福了咱們這些智商普通的下里巴人。github

我的認爲Raft算法最難能難得的地方就在於採用了一種工程化的思惟來設計算法,從而使得Raft算法可以普遍地應用在分佈式的領域之中。(etcd、SOFAJRaft、TiKV ...)算法

雖然Raft算法相比於Paxos算法更加容易理解,可是在閱讀原論文的時候,我仍是在很多地方踩了坑,本文就是疏理和分享我在這些關鍵點處的疑惑和我的看法。(本文討論範圍僅是Basic Raft算法)安全

Raft論文的中文翻譯版
Raft算法動畫服務器

1、Raft算法的本質


不管是Raft算法仍是Paxos算法,都依賴於複製狀態機的模型,複製狀態機的模型以下圖所示:網絡

raft-圖1.png

簡單的描述下這個模型就是:集羣的多個節點上都擁有相同的初始狀態(state),而經過執行一系列的操做日誌(log),最終仍是可以產生一致的狀態。在圖中的Consensus模塊就是Raft算法起做用的模塊,使得在複雜的分佈式環境下,仍可以實現複製狀態機的一致性。 圖中的四個操做相應以下:數據結構

  • ①客戶端請求集羣中的領導者,並要求執行操做日誌。
  • ②領導者向全部的追隨者發起寫日誌請求。追隨者接收到寫日誌請求後,將日誌放入日誌隊列中並回應領導者。
  • ③當大多數的追隨者響應成功,則領導者發起應用日誌請求,追隨者收到請求後則將日誌寫入各自的狀態機內
  • ④領導者回應客戶端寫入成功。

咱們在後面能夠看到Raft算法的所有內容都是圍繞着這個模型出發的,因此理解這個模型十分的重要,雖然看起來流程很簡單,可是仍是要考慮不少的邊界條件,這也是爲何Raft算法有不少補丁的緣故,好在Raft算法的做者都給出了相應的解決方案,業內也有不少公司也一直在作着對Raft算法進行優化的工做。併發

Raft算法和Paxos算法同樣,也提供瞭如下的分佈式算法的特性:app

  • 安全性保證(絕對不會返回一個錯誤的結果):在非拜占庭錯誤狀況下,包括網絡延遲、分區、丟包、冗餘和亂序等錯誤均可以保證正確。
  • 可用性:集羣中只要有大多數的機器可運行而且可以相互通訊、和客戶端通訊,就能夠保證可用。所以,一個典型的包含 5 個節點的集羣能夠容忍兩個節點的失敗。服務器被中止就認爲是失敗。他們當有穩定的存儲的時候能夠從狀態中恢復回來並從新加入集羣。
  • 不依賴時序來保證一致性:物理時鐘錯誤或者極端的消息延遲只有在最壞狀況下才會致使可用性問題。
  • 一般狀況下,一條指令能夠儘量快的在集羣中大多數節點響應一輪遠程過程調用時完成。小部分比較慢的節點不會影響系統總體的性能。

2、Raft爲何選擇隨機選舉超時時間的機制


相對於Paxos來講,Raft一個重要的簡化操做就是使用隨機選舉超時時間來代替原來Paxos複雜的兩階段提交的策略。使用隨機時間雖然會致使選舉沒有那麼高效,可是大大下降了複雜度,而犧牲的僅僅是在選主時的效率,在平常的使用過程當中,選主的時間僅僅只佔正常使用時間的不多一部分時間。咱們能夠從原論文的這張圖中看到:分佈式

raft-圖2.png

在正常使用過程當中,Raft算法和Paxos算法性能相差不大,只有在比較極端的狀況下,即Leader頻繁崩潰,Raft算法纔會在選舉時間上被Paxos算法甩開。

與隨機選舉超時時間相輔相成的是定時器和心跳包機制,追隨者經過經過定時器來競爭成爲候選者,而領導者經過心跳包來「壓制」追隨者,從而保持本身的領導地位,而經過隨機選舉超時時間可以儘可能避免在Paxos算法中討論的選票瓜分的狀況的發生。這樣的確是簡單易行的選主方法。可是仍是留有兩個漏洞須要解決:

  1. 隨機選舉超時是追隨者的行爲,而只有當領導者收到了來自更高任期的領導者發送的心跳包時,纔會意識到本身已經喪失了領導地位。因此這就會致使一箇舊的領導者和客戶端通訊,使得客戶端讀到過時的數據。爲了解決這個問題。Raft算法的做者指出,雖然是讀數據,可是領導者仍是須要在響應客戶端以前, 生成一條特殊的日誌(Read Index)並向全部的追隨者發送心跳包,若是大多數的追隨者都返回了正常的響應,那麼只要這條日誌被狀態機apply以後,就表明在此時此刻,領導者仍具備領導地位,此時再返回給客戶端的就是最新的數據了。如此一來,雖然保證了算法的安全性,可是讀操做的性能就變得和寫操做的性能相差不大了,這在讀多寫少的場景下仍是比較致命的。
  2. 上面那條說的是舊的領導者可能會致使的一些問題,相對的,追隨者也有可能會做妖。在下面兩種場景中,追隨者都會對集羣的穩定性形成影響。
    ①追隨者採用了錯誤的配置文件,不停向集羣中的其餘節點發送收集投票的請求,而實際上該追隨者並不包含在集羣內。
    ②追隨者只是單方面的和領導者失去了聯繫,而與集羣中其餘節點通訊無缺,因此追隨者不停向集羣中的其餘節點發送收集投票的請求,而實際上領導者並無崩潰。
    上面的這兩種狀況都會對集羣的網絡形成波動,那麼有什麼方案來避免呢?
    這個問題通常能夠經過在成爲候選者以前再加一層檢驗來解決,當候選人要發起選舉請求時,首先發送pre-vote,當收到大多數的節點的贊成後,再自增term併發起選舉請求。當節點收到pre-vote請求時,若是它近期和領導人有過通訊,則直接忽略掉pre-vote請求,從而避免了候選人無限的發起選舉請求。

3、prevLogIndex、prevLogIndex、lastLogIndex、lastLogTerm都有什麼用?


在Basic Raft算法中一共只有兩種RPC請求,分別是附加日誌RPC和請求投票RPC,具體的參數以下表所示:

附加日誌RPC:

參數 解釋
term 領導人的任期號
leaderId 領導人的 Id,以便於跟隨者重定向請求
prevLogIndex 新的日誌條目緊隨以前的索引值
prevLogTerm prevLogIndex 條目的任期號
entries[] 準備存儲的日誌條目(表示心跳時爲空;一次性發送多個是爲了提升效率)
leaderCommit 領導人已經提交的日誌的索引值

其餘的參數都比較好理解,可是第一次看prevLogIndex、prevLogIndex這兩個參數時,很很差理解。下面來詳細說明一下個兩個參數。
這兩個參數主要是爲了保證算法的一致性而存在的。(詳細的一致性檢查說明能夠去看下面的第四小節)
關於這個參數的說明以下:

若是日誌在 prevLogIndex 位置處的日誌條目的任期號和 prevLogTerm 不匹配,則返回 false

這句話即意爲當領導者和追隨者的日誌列表衝突時,追隨者會返回false給領導者,領導者得知後會發送以往的日誌,最終覆蓋衝突的日誌段。從而就可以保證Raft算法的日誌匹配特性:

若是兩個日誌在相同的索引位置的日誌條目的任期號相同,那麼咱們就認爲這個日誌從頭到這個索引位置之間所有徹底相同

請求投票RPC

參數 解釋
term 候選人的任期號
candidateId 請求選票的候選人的 Id
lastLogIndex 候選人的最後日誌條目的索引值
lastLogTerm 候選人最後日誌條目的任期號

下面來詳細說明一下lastLogIndex和lastLogTerm這兩個參數。首先這兩個參數是爲了領導者選舉服務的,是爲了在領導者選舉時排除掉不包含最新日誌的節點,從而避免已提交日誌被覆蓋的狀況的發生。(可是並不能徹底避免,在下面第五節提出了這樣仍會有一個小漏洞

首先咱們來看一下候選者是在什麼狀況下才能得到選票。

  1. 追隨者的當前任期值小於請求投票RPC中的任期(即候選者自增後的任期)
  2. 當任期相等時,就比對lastLogTerm的大小,當候選者的lastLogTerm小於追隨者的時,追隨者不迴應候選者

再結合Raft算法的運行機制:

Raft 算法保證全部已提交的日誌條目都是持久化的而且最終會被全部可用的狀態機執行。在領導人將建立的日誌條目複製到大多數的服務器上的時候,日誌條目就會被提交。

由於候選者成爲新任領導者必需要得到大多數節點的選票,而若是該候選者沒有最新一條日誌的信息,必定不會獲得大多數節點的選票,這樣一來就保證了被選舉出來的新領導者必定會包含最新的一條日誌,從而保證了系統的安全性。

4、Raft算法如何確保全部狀態機的一致性


接下來繼續聊一聊Raft算法是怎麼保證一致性的。

  1. 數據不一致的斷定標準

    首先得有一個標準來判斷數據是否是一致的,Raft算法設計了這個樣一個規則:

    領導人最多在一個任期裏在指定的一個日誌索引位置建立一條日誌條目,同時日誌條目在日誌中的位置也歷來不會改變。

    有了這個規則,就很好判斷不一致了,若是須要附加的新日誌的上一條日誌的任期或索引和領導者所記錄的(領導者須要維護這樣一個元數據信息)不一致,那麼就表明發生了不一致的狀況。

  2. Raft算法有一個領導人只附加原則:

領導人絕對不會刪除或者覆蓋本身的日誌,只會增長。

這裏引伸出來的意思就是,當數據不一致的狀況發生時,一切以領導人的數據爲準,領導者會經過強制複製本身的日誌到數據不一致的追隨者,從而使得集羣中的全部節點的數據保持一致性。這裏會有一個小疑問:若是新領導者覆蓋了舊領導者的日誌呢?這種狀況是必定會發生的,可是咱們從上一小節得知,新任領導者必定會包含最新一條日誌,即新任領導者的數據和舊領導者的數據就算是不一致的,也僅僅是未提交的日誌,即客戶端還沒有獲得回覆,此時就算是新任領導者覆蓋舊領導者的數據,客戶端獲得回覆,持久化日誌失敗。從客戶端的角度來開,並無產生數據不一致的狀況。

  1. 日誌被應用到各個節點的狀態機須要通過兩個階段:

    1. 領導者發起附加日誌RPC
    2. 領導者判斷日誌是否可被提交(被大多數節點複製成功),若是可被提交則回覆客戶端,而後並行地再次發送附加日誌RPC,追隨者看到該日誌可被提交,則應用到各自的狀態機中。

    這個和兩階段提交協議差很少,區別是不用全部的節點都要複製成功,只要有一半多的節點正常響應,就能維持集羣的正常運行,對於那些暫時不可以正常響應的追隨者,領導者會持續不斷的發送RPC,直到全部的節點都能存儲一致的數據。

  2. 日誌不一致的處理策略

    當附加日誌RPC的一致性檢查失敗時,追隨者會拒絕這個請求。當領導者檢測到附加日誌的請求失敗後,會減少當前附加日誌的索引值,再次嘗試附加日誌,直至成功。爲了減小領導者的附加日誌RPC被拒絕的次數,能夠作一個小優化,當追隨者拒絕領導者的附加日誌請求時,追隨者能夠返回包含衝突的條目的任期號和本身存儲的那個任期的最先的索引地址,從而使得領導人和追隨者儘快找到最後二者達成一致的地方。在下面的第八小節還能夠看到,當追隨者和領導者之間的日誌相差過大時,領導者會直接發送快照來快速達到一致。

經過以上四條規則,咱們能夠看到領導者並不須要耗費不少的資源,就能夠管理全部節點的一致性,經過不斷髮送附加日誌RPC(或心跳包),集羣的節點就會自動趨於一致。固然從客戶端的角度來看,Raft算法提供的強一致性的特性。

5、Raft 提交以前任期內的日誌條目的正確方式


下面咱們來看看在第三小節提到的小漏洞,在原論文的5.4.2節中,給出了下面這個例子,演示了一條已經被提交的日誌在將來被其餘的領導人日誌覆蓋的狀況。

raft-圖3.png

如圖的時間序列展現了爲何領導人沒法決定對老任期號的日誌條目進行提交。在 (a) 中,S1 是領導者,部分的複製了索引位置 2 的日誌條目。在 (b) 中,S1 崩潰了,而後 S5 在任期 3 裏經過 S三、S4 和本身的選票贏得選舉,而後從客戶端接收了一條不同的日誌條目放在了索引 2 處。而後到 (c),S5 又崩潰了;S1 從新啓動,選舉成功,開始複製日誌。在這時,來自任期 2 的那條日誌已經被複制到了集羣中的大多數機器上,可是尚未被提交。若是 S1 在 (d) 中又崩潰了,S5 能夠從新被選舉成功(經過來自 S2,S3 和 S4 的選票),而後覆蓋了他們在索引 2 處的日誌。反之,若是在崩潰以前,S1 把本身主導的新任期裏產生的日誌條目複製到了大多數機器上,就如 (e) 中那樣,那麼在後面任期裏面這些新的日誌條目就會被提交(由於S5 就不可能選舉成功)。 這樣在同一時刻就同時保證了,以前的全部老的日誌條目就會被提交。

爲了不上面的狀況,這裏又引出了Raft算法的另外一個補丁:Raft 永遠不會經過計算副本數目的方式去提交一個以前任期內的日誌條目。這個補丁是什麼意思呢?注意在時間戳爲(c)的時刻,此時S1爲領導人,S1發現了在他的上個任期內提交的日誌(即任期爲2的第一個日誌)尚未被大多數的追隨者複製。因此S1將該日誌發送給S3,而S3檢查了該日誌發現知足條件:

  1. S3 currentTerm = 1 < 該日誌任期 = 2
  2. S3 在index上的日誌爲空,能夠接受該日誌。

因此S3複製該日誌到本地日誌中,修改S3 currentTerm = 2, 而後返回給領導人S1,領導人經過計算已複製的追隨者的數量超過了一半,遂發起提交,而後返回給客戶端(client):「我已經將該日誌保存好了,能夠放心使用了。」 然而在時間戳爲(d)的時刻,S1崩潰,而S5成爲候選人,開始收集選票,這時候來分析S5和關鍵人S3之間的關係:

  • S5的任期 = 5 > S3的任期 = 2

那麼若是S5向S3收集選票,儘管S5上並無任期號爲2的日誌,而S3上有S1在任期號爲4時傳播上個任期的日誌,可是按照Raft選舉的規則,S3仍是會給S5投一票。又由於在Raft算法中:

領導人處理不一致是經過強制跟隨者直接複製本身的日誌來解決了。

因此任期爲2的第一個日誌,會由於這個緣由被覆蓋掉了,那麼客戶端若是來訪問,就會發現訪問先後的數據是不一致的,這是不可以容忍的。那麼若是領導人想要提交之前任期的日誌呢?這個關鍵點就在於在時間戳爲(d)的時刻,不可以讓缺乏了關鍵日誌的S5成爲領導人。那麼只要在時間戳爲(c)的時刻,有任期號爲4的日誌可以在系統中提交,而上個任期的日誌就可以跟隨着這個任期號爲4的日誌一塊兒提交,如時間戳爲(d)時所示,即便緊接着領導人S1崩潰,由於Raft算法選舉的限制:

候選人爲了贏得選舉必須聯繫集羣中的大部分節點,這意味着每個已經提交的日誌條目在這些服務器節點中確定存在於至少一個節點上。

那麼就會使得即便在選舉出來的新的領導人中,也會有這個任期號爲2的這個日誌,從而避免了領導人日誌覆蓋提交日誌的狀況。

6、Raft怎樣在集羣成員變化時不中止處理客戶端請求

因爲Raft算法和集羣成員的數量的關係聯繫的十分緊密,因此在集羣擴容的時候,要十分的謹慎,若是不使用暫停整個集羣更換配置的方案,而莽撞的直接添加機器會致使種種問題,如在同一個時間內,在集羣中選舉出了兩個領導者,在原論文中給出了這樣的實例:

raft-圖4.png

直接從一種配置轉到新的配置是十分不安全的,由於各個機器可能在任何的時候進行轉換。在這個例子中,集羣配額從 3 臺機器變成了 5 臺。不幸的是,存在這樣的一個時間點,兩個不一樣的領導人在同一個任期裏均可以被選舉成功。一個是經過舊的配置,一個經過新的配置。

這個問題就衍生爲Raft算法在不暫停服務的狀況下,完成配置變動。所以Raft算法提出了一個稱之爲共同一致的方案,這個方案指出在配置變動過程當中,對安全性最有影響的是新老配置互相沒法感知,而配置更替也沒法一蹴而就。因此在配置更替前,將集羣引導入一個過渡階段,使得適用新的配置和老的配置的機器都不會獨立地處理日誌。

raft-圖5.png

上圖是原論文的圖示,咱們能夠看到在配置更替的中間階段,會寫入一個特殊的日誌C(old,new),這個日誌包含了新配置和老配置,當咱們向領導者提出要採用新配置時,領導者就會在集羣內傳播這個特殊日誌,當追隨者收到這個日誌的第一時間,就會應用該配置。當大多數的追隨者都應用了該配置後,領導者就會迴應咱們已經成功應用配置了。在此之後 雖然配置沒有徹底更替完畢,可是即便在領導者崩潰的狀況下,因爲Raft算法選舉限制,最終選出的新領導者也必定會包含新的配置。最終當全部追隨者的配置更新完畢,若是舊的領導者並不包含在新的集羣當中,舊的領導者會退位讓賢,主動放棄本身的領導。

7、新節點對於集羣的影響


在新節點加入集羣的時候,還有一個小補丁,咱們再來看看上面圖四的場景,這裏先排除掉會選出多個領導者的狀況,咱們假設此時有一個客服端發起寫入日誌的請求,領導者收到後經過附加日誌RPC發送給各個節點,由於此時4節點和5節點是新加入的節點,因此確定會返回附加日誌失敗給領導者,那麼此時若是日誌要提交成功,原來的一、二、3節點必須所有複製成功,這至關於在沒有機器宕機或網絡異常的狀況下,自行下降了系統的可用性。

爲了解決這個問題,又提出了一個新補丁,當有新的節點加入到集羣中時,只會被動地從領導者那裏接收日誌,而不會參與到日誌複製的決策當中,即沒有投票權。當新的節點追遇上了其餘的節點後,纔會擁有投票權。

8、Raft日誌快照的做用

Raft日誌快照主要有兩種用途:

  1. 壓縮日誌

    當系統中的日誌愈來愈多後,會佔用大量的空間,Raft算法採用了快照機制來壓縮龐大的日誌,在某個時間點,將整個系統的全部狀態穩定地寫入到可持久化存儲中,而後這個時間點後的全部日誌所有清除。

  2. 快照RPC

    當咱們擁有了快照以後,就能經過快照直接將領導者的狀態複製到那些過於落後追隨者上,從而使得追隨者和領導者的狀態可以快速到達一致。

咱們能夠看到Raft的快照機制和Redis的持久化存儲是很相像的。因此一些Redis的優化機制能夠有選擇地應用到Raft之中,如快照自動觸發、使用fork機制來下降建立快照的資源佔用、使用特殊的數據結構來保證快照的可檢測性等。

9、Raft客戶端和服務端之間的冪等操做


這部分的內容實際上不太屬於Raft算法,可是仍是十分的重要,我在這裏簡單的作一點介紹。

只要是經過網絡進行交互,就要考慮到容錯和重發,若是由於瞬間的網絡波動致使客戶端重發請求,而服務端若是沒有正確處理請求,就會致使數據混亂的狀況發生,因此這裏就要引入冪等性的這個概念,冪等性是指一個操做不管執行多少遍,都會產生相同的狀態,如絕對值操做就是屬於冪等操做,而加減法就不屬於冪等操做。

而不少請求都是不屬於冪等操做,因此咱們須要在這些請求外設置一些限制,從而達到冪等操做的效果。 就拿Raft算法爲例,咱們須要給每個客戶端分配一個全局惟一的ID,而領導者記錄每一個客戶端已處理的請求的最大的序號(這個序號須要在客戶端組範圍內是遞增的),當領導者收到一個序號小於或等於已記錄最大序號的請求時,就要拒絕請求並返回異常給客戶端,客戶端捕獲異常後,自行處理。

如此一來,就能夠從入口和內部兩方面都可以保證數據一致性和安全性。

10、總結


這篇文章是我我的的一些對於Raft算法的理解,在寫文章的過程當中,發現了一些大牛的文章,這些國內的大牛(知乎上的我作分佈式系統、TiKV的唐劉....)是真滴強,在我還在研究Basic Raft的時候,他們已經在Raft算法優化的路上走了很遠了。看了這麼多資料,就愈是以爲本身菜,因此寫的就愈是艱難。若是個人理解有什麼紕漏,還請不吝賜教。


邀舞卡:王老魔的代碼備忘錄

相關文章
相關標籤/搜索