Raft共識算法在分佈式系統中是經常使用的共識算法之一,論文原文In Search of an Understandable Consensus Algorithm ,做者在論文中指出Poxas共識算法的兩大問題,其一是難懂,其二是應用到實際系統存在困難。針對Paxos存在的問題,做者的目的是提出一個易懂的共識算法,論文中有Designing for understandability單獨一小節,其中強調Raft必須是一個實用的、安全可用、有效易懂的共識算法。本文描述了Raft共識算法的細節,不少內容描述及引用圖片均摘自論文原文。git
咱們主要分如下三部分對Raft進行討論:github
正常工做過程當中,Raft分爲兩部分,首先是leader選舉過程,而後在選舉出來的leader基礎上進行正常操做,好比日誌複製操做等。算法
一個Raft集羣一般包含\(2N+1\)個服務器,容許系統有\(N\)個故障服務器。每一個服務器處於3個狀態之一:leader
、follower
或candidate
。正常操做狀態下,僅有一個leader,其餘的服務器均爲follower。follower是被動的,不會對自身發出的請求而是對來自leader和candidate的請求作出響應。leader處理全部的client請求(若client聯繫follower,則該follower將轉發給leader)。candidate狀態用來選舉leader。狀態轉換以下圖所示:
安全
爲了進行領導人選舉和日誌複製等,須要服務器節點存儲以下狀態信息:服務器
狀態 | 全部服務器上持久存在的 |
---|---|
currentTerm | 服務器最後一次知道的任期號(初始化爲 0,持續遞增) |
votedFor | 在當前得到選票的候選人的 Id |
log[] | 日誌條目集;每個條目包含一個用戶狀態機執行的指令,和收到時的任期號 |
狀態 | 全部服務器上常常變的 |
---|---|
commitIndex | 已知的最大的已經被提交的日誌條目的索引值 |
lastApplied | 最後被應用到狀態機的日誌條目索引值(初始化爲 0,持續遞增) |
狀態 | 在領導人裏常常改變的 (選舉後從新初始化) |
---|---|
nextIndex[] | 對於每個服務器,須要發送給他的下一個日誌條目的索引值(初始化爲領導人最後索引值加一) |
matchIndex[] | 對於每個服務器,已經複製給他的日誌的最高索引值 |
Raft在任什麼時候刻都知足以下特性:網絡
下面咱們詳細討論這幾部分。分佈式
一個節點初始狀態爲follower,當follower在選舉超時時間內未收到leader的心跳消息,則轉換爲candidate狀態。爲了不選舉衝突,這個超時時間是一個隨機數(通常爲150~300ms)。超時成爲candidate後,向其餘節點發出RequestVote
RPC請求,假設有\(2N+1\)個節點,收到\(N+1\)個節點以上的贊成迴應,即被選舉爲leader節點,開始下一階段的工做。若是在選舉期間接收到eader發來的心跳信息,則candidate轉爲follower狀態。動畫
在選舉期間,可能會出現多個candidate的狀況,可能在一輪選舉過程當中都沒有收到多數的贊成票,此時再次隨機超時,進入第二輪選舉過程,直至選出leader或着從新收到leader心跳信息,轉爲follower狀態。spa
正常狀態下,leader會不斷的廣播心跳信息,follower收到leader的心跳信息後會重置超時。當leader崩潰或者出現異常離線,此時網絡中follower節點接收不到心跳信息,超時再次進入選舉流程,選舉出一個leader。3d
這裏還有補充一些細節,每一個leader能夠理解爲都是有本身的任期(term)的,每一期起始於選舉階段,直到因節點失效等緣由任期結束。每一期選舉期間,每一個follower節點只能投票一次。圖中
t3
多是由於沒有得到超半數票等形成選舉失敗,須進行下一輪選舉,此時follower能夠再次對最早到達的candidate發出的RequestVote
請求投票(先到先得)。
對全部的請求(RequestVote、AppendEntry等請求),若是發現其Term小於當前節點,則拒絕請求,若是是candidate選舉期間,收到不小於當前節點任期的leader節點發來的AppendEntry
請求,則承認該leader,candidate轉換爲follower。
leader選舉成功後,將進入有效工做階段,即日誌複製階段,其中日誌複製過程會分記錄日誌和提交數據兩個階段。
整個過程以下:
能夠看到client每次提交command指令,服務節點都先將該指令entry追加記錄到日誌中,等leader確認大多數節點已追加記錄此條日誌後,在進行提交確認,更新節點狀態。若是還對這個過程有些模糊的話,能夠參考Raft動畫演示,較爲直觀的演示了領導人選舉及日誌複製的過程。
前面描述了Raft算法是如何選舉和複製日誌的。然而,到目前爲止描述的機制並不能充分的保證每個狀態機會按照相同的順序執行相同的指令。咱們須要再繼續深刻思考如下幾個問題:
針對第一個問題,以前並無細講,若是當前leader節點掛了,須要從新選舉一個新leader,此時follower節點的狀態多是不一樣的,有的follower可能狀態與剛剛掛掉的leader相同,狀態較新,有的follower可能記錄的當前index比原leader節點的少不少,狀態更新相對滯後,此時,從系統最優的角度看,選狀態最新的candidate爲佳,從正確性的角度看,要確保Leader Completeness,即若是在某一任期一條entry被提交成功了,那麼在更高任期的leader中這條entry必定存在,反過來說就是若是一個candidate的狀態舊於目前被committed的狀態,它必定不能被選爲leader。具體到投票規則:
1) 節點只投給擁有不比本身日誌狀態舊的節點;
2)每一個節點在一個term內只能投一次,在知足1的條件下,先到先得;
咱們看一下請求投票 RPC(由候選人負責調用用來徵集選票)的定義:
| 參數 | 解釋 |
| ----- | ----- |
| term| 候選人的任期號|
| candidateId| 請求選票的候選人的 Id |
| lastLogIndex| 候選人的最後日誌條目的索引值|
| lastLogTerm| 候選人最後日誌條目的任期號|
返回值 | 解釋 |
---|---|
term | 當前任期號,以便於候選人去更新本身的任期號 |
voteGranted | 候選人贏得了此張選票時爲真 |
接收者實現:
term < currentTerm
返回 false能夠看到RequestVote投票請求中包含了lastLogIndex和lastLogTerm用於比較日誌狀態。這樣,雖然不能保證最新狀態的candidate成爲leader,但可以保證被選爲leader的節點必定擁有最新被committed的狀態,但不能保證擁有最新uncommitted狀態entries。
領導人知道一條當前任期內的日誌記錄是能夠被提交的,只要它被存儲到了大多數的服務器上。可是以前任期的未提交的日誌條目,即便已經被存儲到大多數節點上,也依然有可能會被後續任期的領導人覆蓋掉。下圖說明了這種狀況:
如圖的時間序列展現了爲何領導人沒法決定對老任期號的日誌條目進行提交。在 (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 會爲全部日誌保留原始的任期號。
咱們先舉例說明:正常狀況下,follower節點應該向B節點同樣與leader節點日誌內容一致,但也會出現A、C等狀況,出現了不一致,以A、B節點爲例,當leader節點向follower節點發送AppendEntries<prevLogIndex=7,prevLogTerm=3,entries=[x<-4]>,leaderCommit=7
時,咱們分析一下發生了什麼,B節點日誌與prevLogIndex=7,prevLogTerm=3
相匹配,將index=7
(x<-5
)這條entry提交committed,並在日誌中新加入entryx<-4
,處於uncommitted狀態;A節點接收到時,當前日誌index<prevLogIndex
與prevLogIndex=7,prevLogTerm=3
不相匹配,拒接該請求,不會將x<-4
添加到日誌中,當leader知道A節點因日誌不一致拒接了該請求後,不斷遞減preLogIndex
從新發送請求,直到A節點index,term
與prevLogIndex,prevLogTerm
相匹配,將leader的entries複製到A節點中,達成日誌狀態一致。
咱們看一下附加日誌 RPC(由領導人負責調用複製日誌指令;也會用做heartbeat)的定義:
| 參數 | 解釋 |
| ------ | ------- |
|term| 領導人的任期號|
|leaderId| 領導人的 Id,以便於跟隨者重定向請求|
|prevLogIndex|新的日誌條目緊隨以前的索引值|
|prevLogTerm|prevLogIndex 條目的任期號|
|entries[]|準備存儲的日誌條目(表示心跳時爲空;一次性發送多個是爲了提升效率)|
|leaderCommit|領導人已經提交的日誌的索引值|
返回值 | 解釋 |
---|---|
term | 當前的任期號,用於領導人去更新本身 |
success | 跟隨者包含了匹配上 prevLogIndex 和 prevLogTerm 的日誌時爲真 |
接收者實現:
term < currentTerm
就返回 false;leaderCommit > commitIndex
,令 commitIndex 等於 leaderCommit 和 新日誌條目索引值中較小的一個;簡單總結一下,出現不一致時核心的處理原則是一切聽從leader。當leader向follower發送AppendEntry請求,follower對AppendEntry進行一致性檢查,若是經過,則更新狀態信息,若是發現不一致,則拒絕請求,leader發現follower拒絕請求,出現了不一致,此時將遞減nextIndex,並從新給該follower節點發送日誌複製請求,直到找到日誌一致的地方爲止。而後把follower節點的日誌覆蓋爲leader節點的日誌內容。
前面可能斷斷續續的提到這種狀況的處理方法,首要的就是選出新leader,選出新leader後,可能上一任期還有一些entries並無提交,處於uncommitted狀態,該怎麼辦呢?處理方法是新leader只處理提交新任期的entries,上一任期未提交的entries,若是在新leader選舉前已經被大多數節點記錄在日誌中,則新leader在提交最新entry時,以前處於未提交狀態的entries也被committed了,由於若是兩個日誌包含了一條具備相同index和term的entry,那麼這兩個日誌在這個index以前的全部entry都相同;若是在新leader選舉前沒有被大多數節點記錄在日誌中,則原有未提交的entries有可能被新leader的entries覆蓋掉。
分佈式系統中網絡分區的狀況基本沒法避免,出現網絡分區時,原有leader在分區的一側,此時若是客戶端發來指令,舊leader依舊在分區一測進行日誌複製的過程,但因收不到大多數節點的確認,客戶端所提交的指令entry只能記錄在日誌中,沒法進行提交確認,處於uncommitted狀態。而在分區的另外一側,此時收不到心跳信息,會進入選舉流程從新選舉一個leader,新leader負責分區零一側的請求,進行日誌複製等操做。由於新leader能夠收到大多數follower確認,客戶端的指令entry能夠被提交,並更新節點狀態,當網絡分區恢復時,此時兩個leader
會收到彼此廣播的心跳信息,此時,舊leader發現更大term的leader,舊leader轉爲follower,此時舊leader分區一側的全部操做都要回滾,接受新leader的更新。
參考文檔:
In Search of an Understandable Consensus Algorithm
The Raft Consensus Algorithm
深刻淺出RAFT共識算法
Raft共識算法及其實現