1. 程式人生 > >再讀分散式一致性演算法Raft論文

再讀分散式一致性演算法Raft論文

Raft是一個管理日誌副本一致性的演算法。相比Paxos結果一樣,並且一樣高效,但是理解起來更加的容易。Raft將一致性的主要元素分離開來,比如leader選舉,log 複製,安全等。同時,也提供了一個新的機制實現cluster membership改變,其使用多數的原則來保證安全性。

一致性演算法

一致性演算法的意義就是保證一致性的一組機器能夠在其部分成員出現故障的時候依然能夠存活下來(提供服務)。在Raft之前所有的一致性演算法可以認為都是Paxos或者其變種的實現,但是Paxos難以理解,其工程實現往往都需要改變Paxos的架構。因此為了便於理解,以及工程上的實現,開發了Raft演算法,該演算法使用一些技術從而使其在保證正確性和效能的前提下,更加的容易理解和實現,這些技術主要有過程分解(領導選舉,日誌複製,安全)和狀態空間減少。

相比其他一致性演算法,Raft有幾個明顯的特點:
- strong leader: Raft使用很強的leadership,比所有的Log entries都是通過leader傳送到所有其他的伺服器。這樣簡化了管理也更加容易理解。
- leader election:採用隨機化的定時器去選舉leader,簡單快速的解決了leader選舉中的衝突。
- 成員變更: 使用一個joint consensus機制,保證在變更成員的時候依然能夠正常的操作。

Replicated state machines

(Replicated State Machine)狀態機:一致性group的節點的某個時刻的狀態(比如資料庫裡x=1,y=1是一個狀態)轉移可以看成自動機裡的一個狀態,所以叫狀態機。
Replicated Log

: 包含了來自客戶端的用於執行在狀態機上的命令(操作)。

replicate group裡的每一個節點都包含一個**相同序列的**log,也就是相同序列的操作,因此每一個節點的state machine都是執行相同序列的操作,所以其結果也是相同。

一致性演算法的主要目的就是保證每個節點上log的一致,其核心功能就是接收客戶端的命令,並新增到log裡,同時一致性模組與其他節點通訊,保證他們的log都是相同順序的序列,即使一些節點出現了故障,當log成功的複製到一定數量(通常是多數)的節點後,每個節點的狀態機就執行該條命令並將結果返回給客戶端。

總的來說一致性演算法有下面一下特性:
- 安全。這裡是指即使再機器出現故障(比如網路延遲,隔離,丟包,重複,重排等情況)的情況下絕不會返回錯誤的結果
- 可用性。在多數機器可用的情況下,整個叢集依然總是可以對外服務。出現故障的機器後續依然可以通過持久化的資料恢復並且重新加入叢集
- 不依賴時鐘。一致性演算法不依賴時鐘就能保證log的一致性
- Single-round RPC. 通常情況下,一個命令只要叢集中多數機器響應,就認為是成功了。即使一小部分機器出現了故障也不會影響整個系統的效能,而這個過程通常只需要一輪的RPC呼叫。

Paxos的缺陷

Paxos幾乎成為了分散式一致性演算法的代名詞,基本成為了一致性演算法的教學典範,以及工業實現的參考。Paxos分為單命令的(single decree)single-decree Paxos和多命令的multi-Paxos. Paxos的正確性和效能都已經得到理論上的證明。

但是Paxos有兩個主要的缺陷:
- 難以理解
- 工程實現困難

而且Paxos的架構實際系統的實現中並不實用,比如其收集日誌的一個集合然後將其重排成有序的日誌的做法並沒有任何好處,相反直接append到一個有序的log更加的高效。還有其實用同等地位的點對點,沒有leader的方式,對於一個確定一個決策沒有問題,但是對於一些序列決策就存在一定的問題,雖然其最後建議使用一個弱的leader,但是還是顯得很複雜。

Raft如何做到可理解性?

為了是一致性演算法更加容易理解,Raft主要使用了一下兩個技術:
- 問題分解(Decomposition)
將一致性問題分解成幾個容易理解和實現的子問題,Raft講演算法分為:領導選舉(Leader Election), 日誌複製(Log Replication),安全(Safety)和成員變更(Membership Change)四個部分
- 減少狀態空間
儘可能的減少需要考慮的狀態和不確定性,讓系統看起來更加的清晰。和Paxos一個很不同的地方就是Raft不允許log儲存漏洞(Holes),並且限制了不同節點之間log不一致的情況。通過減少狀態空間和一些限制,大大地增加了演算法的可理解性。除此之外,Raft使用隨機的方式簡化Raft的領導選舉,這樣雖然增加了演算法的不確定性,但是隨機化通過對於所有的狀態都是用同樣的處理方式可以簡化狀態空間。

Raft一致性演算法

Raft演算法的一個核心思路是在一組機器中間選出一個Leader, 然後又Leader去管理日誌,通過選舉一個leader大大地簡化了管理的複雜度,客戶端需要傳送新的命令,通過leader將log複製到所有其他節點,當大多數節點都複製了log後,leader通知所有節點,可以將日誌包含的命令應用在本地的狀態機。基於leader機制,Raft講演算法分為三個部分:
- leader選舉
當一個叢集中沒有leadr或者存在的leader出現故障時重新選舉一個leader
- 日誌複製
當前的leader接受客戶端傳送的日誌(命令),然後複製到叢集的其他節點。
- 安全(Safety)
安全主要指狀態機的安全屬性。

Raft基本概念

Role: 在一個Raft叢集中(通常五個節點,容忍兩個節點故障), 每個節點或者說(Raft例項)都有三種角色,Follwer, Candidate, Leader,每個例項初始化都是Follower狀態,設定一個定時器,當一定時間內沒有發現leader,則會發起leader選舉的請求,其狀態變為candidate,當其他節點投票滿足一定的條件後成為Leader, 其他所有的節點都轉變或者維持Follower狀態,具體的狀態裝換圖如下:
state transition.png

Trem: Raft演算法將時間分成不同長度的term,每個一個term都是從選舉一個leader開始,換句話說就是質只要出現新的leader選舉那麼就是進入了新的term,一旦選出leader後,該term後續的所有時間都有該leader負責與客戶端互動,管理日誌的複製。Term更像是一個邏輯時鐘,每個server都會維護一個currentTerm,這麼在server通訊的過程中便能識別出每個server的term是高於還是低於自己的term,從而進行狀態的轉換和term的更新。

Leader選舉

Raft使用心跳機制觸發leader選舉,所有伺服器啟動的時候都是follower,規定在一定時間間隔內如果能夠收到leader的心跳或者candidate的投票訊息,則一直保持follower,否則觸發leader選舉過程。

leader選舉有兩個階段:首先增加currentTerm,轉為candidate狀態,然後並行的向其他伺服器傳送投票RPC(RequestVote)。candidate狀態結束的條件有如下三個:
- 贏得了選舉,成為leader
如果candidate狀態的節點收到了多數節點的投票那麼就會贏的選舉,成為leader, 然後向其他節點發送心跳,告訴他們自己是leader。每一個節點在投票的時候,至多投一次,並且按照先到先得的原則,同時請求投票的節點的term必須大於等於自身的term。多數的規則能夠保證再一個選舉週期保證最多一個節點贏得選舉成為leader。
- 收到了自稱leader的節點的心跳
如果candidate狀態的節點在等待投票的過程中收到了某個節點的心跳自稱自己是leader,如果心跳裡包含的term大於等於自己的currentTerm,那麼就說明該leader是合法的,自己轉為follower,否則拒絕RPC,並返回自己的term.
- 既沒有贏得選舉,也沒有失敗(沒有其他leader產生)
如果同一個時刻有多個candidate狀態的機器,那麼就會產生votes split,這種情況就不會滿足多數的規則,所以不會產生leader,這個時候每個candidate增加當前term,重置election timeout,開始新一輪的選舉。這個時候為了防止每個candidate的election timeout相同導致無休止的選舉失敗,Raft採用了一個簡單但是非常有效的方法,隨機生成*election timeout,通常是一個範圍比如150ms~300ms。這個隨機化*也是Raft的一個重要的技術點,很多地方都用到了隨機化從而簡化了系統同時,也保證了正確性。

為了防止不包含之前leader已經commit的log entry,在投票的時候有一個限制,candidate在傳送RequestVote RPC的時候會附帶其最後一個log的index和term,只有candidate的term大於等於(等於時候比較index)自身的term才投票給candidate,否則拒絕投票。這樣能夠保證按照這種規則投票 選出的leader包含了所有之前leader已經committed的log entry。

Log Replication

當一個raft例項當選為leader後,該例項就開始負責與客戶端互動,並將客戶端的請求作為一個新的log entry儲存到本地logs內,然後並行的發給其他的follower,如果收到多數follower儲存該entry成功的相應,那麼就commit該log entry,並apply到本地的state machine, 其他follower也會在收到commit的請求後或者在後續的一致性檢查的時候apply改entry到本地的state machine,最終實現所有例項的一致性。

正常的情況下,按照上述的流程是不會出錯的。但是往往follower, candidate, leader都有可能出現宕機或者其他故障導致log的不一致,如下圖所示,leader和follower都有可能缺少log或者有多餘的Log.
Log inconsistent.png
最上面的log是當前的leader,a和b兩個follower的log屬於丟失log,c,d, e和f都含有沒有提交的log,f相對於現在的leader少了index 4之後所有的log entry,但是多了term 2和term3的log, 這種情況可能就是由於f在term2的時候是leader,生成了三個log,但是在沒有提交的的時候就掛了,迅速重啟後又稱為了term 3的leader,同樣生成了5個log entry,但是也是沒提交就掛了直到term 8的時候才啟動起來。

由於leader選舉時候的限制,當前的leader包含了之前leader已經commit的所有log, 對於follower缺失log的情況,leader在當選為leader的時候初始化每個follower已經匹配到的index為其log的長度,也就是最有一個log entry的下一個index,然後在appendEntries的時候,在follower檢視其log對應的該index時候和leader對應的index對應的log相同,如果相同則刪除其後所有的log,強制用leader的log覆蓋,否則index減一向前查詢,知道找到第一個匹配的log,然後刪除後面的用leader的log覆蓋,這樣就達到了follower的log和leader的一致。對於leader缺失的log,由於選舉時候的限制,說明該確實的log是沒有commit的,所以可以忽略,直接覆蓋即可。

那麼如果leader在replicate log entry的時候,如果leader掛了,然後重新選出的leader可能不是之前的例項,那麼對於之前的leader已經commit的log entry,或者可能沒有commit的log entry怎麼處理呢?leader出現故障可能發生在下面三個階段:
- log entry沒有replicate在多數的follower,然後crash
- log entry已經replicate在多數的follower,沒有commit,然後crash
- log entry已經replicate在多數的follower,並且已經commit,然後crash

還是以論文中的例子看一下怎麼處理之前term的log entry:
![commit previous log.png](http://7sbpmg.com1.z0.glb.clouddn.com/blog/images/commit%20previous log.png?imageView/0/w/500/)
我們用(termID,indexID)表示圖中的term為termID,index是indexID的log entry.圖中(a)對應第一種情況,此時S1是leader,(2,2)對應的log entry只在S1,S2完成了複製,然後S1掛了,這個時候根據leader選舉的約束條件,S5可以當選為leader,然後接收了一個新的log entry,對應(3,2),這個時候還沒有在其他節點完成複製,就掛了,接著S1又成為了term 4階段的新的leader, 此時只有接收新的log entry,複製的到其他follower時候,完成之前沒有完成的複製,也就是圖中S3對應(2,2)的entry,這個時候完成多數的複製後,即圖中的(c)。這個時候(2,2)處於複製了多數但是沒有commit的狀態,(4,2)只有S1的logs裡有,這個時候(2,2)雖然是多數但是是不會提交的,原因後面會解釋。所以這個時候就有兩種情況,分別對應上面的第二種和第三種情況,對於第二種情況對應(d),S1中的(2,2)和(4,3)都沒有提交,然後掛了,這個時候S5當選為leader(term 5, S2, S3, S4,都可能投票給S5),然後強制用S5的(3,2)覆蓋S1,S2, S3, S4未提交的日誌。第三種情況對應(e), (2,2)和(4,3)已經提交,然後S1掛了,這個時候因為S1,S2,S3最後一個log的term是4,index是3,而S5最後一個log的term是3,不滿足leader選舉的限制條件,所以不能當選leader,新的leader只能從S1, S2,S3中選舉,所以之前提交的log都是沒有丟失。

對於(c)中的(2,2)為什麼雖然已經是多數,但是還是不會提交呢?

因為Raft為了簡化提交舊term的log entry的過程,Raft不根據舊的log entry已經完成的副本的數量進行commit,只有當前term的log entry根據多數原則提交,但是因為在完成當前term Log entry複製的過程中,會強制複製leader的log 到確實相應log entry的follower,所以當commit當前某個index對應的log的時候,會將leader該index之前的log都進行提交,而且存在多數的follower的log在改index之前的log entry是一致的,然後在appendEntrye(心跳也是這個方法)的時候會將leader已經commit的最大index傳送給follower, follower會根據該index提交本地的log。

會不會出現(b)中S1的(2,2)已經是多數,也就是S3也已經存在(2,2),然後已經提交然後S1掛了,這個時候S5當選為leader,強制覆蓋了S1,S2,S3裡的(2,2)?

這種情況是不會出現的,因為(2,2)是term 2階段生成的日誌,如果(2,2)已經提交,然後leader掛了,這個時候S5是不會當選為leader的,也就是說不會出現圖(b)中S5裡的(3,2)這個log entry.

Cluster Membership Changes

當叢集成員發生改變的時候,通常最直接的方法就是每個節點的配置直接更新成新的配置,但是這種方法會導致在切換的過程中,由於每個節點切換成功的時刻不一致,所以導致新舊配置產生兩個Leader.還有一種方法是,將切換過程分兩階段,首先讓叢集暫時不可用,然後切換成新的配置,最後使用新的配置啟用叢集,這樣顯然會造成叢集短暫的不可用。

為了確保在變更配置的時候,叢集依然能夠提供該服務,Raft提供了一種Joint Consesus的機制。其核心想法是講配置作為一個特殊的Log Entry,使用上面的replication演算法分發到每個節點。Joint Consesus將新舊配置組合到一起:
- Log Entries會複製到新舊配置中的所有節點
- 任何一個server,可能是新配置的也可能是舊配置的,都能夠當選為leader
- 選舉或者提交entry需要新配置節點的多數節點同意,也需要舊配置多數節點同意

下圖是配置變更的一個過程:
membership change.png
噹噹前叢集leader收到請求需要將配置從Cold變更到Cnew時,leader建立一個新的用於Joint Consesus的配置Cold,new,改配置作為一個log entry使用Raft的replicate演算法複製到新舊配置裡的所有節點,一旦節點接收到新的配置Log, 不管其是否已經提交都會使用該新的配置,也就是說當前叢集的leader會使用Cold,new來決策什麼時候提交該配置,如果這個時候leader宕機,那麼新選舉出來的leader可能是使用old配置的節點,也可能使用Cold,new的節點。一旦Cold,new提交,這個時候有多數的節點已經有Cold,new配置,這個時候如果leader宕機,只有有Cold,new的節點才會成為leader。所以這個時候leader就會建立一個Cnewlog進行replicate,一旦Cnew已經commit,那麼舊配置不相關的節點就可以關掉了。從圖中可以看出不存在一個時刻ColdCnew同時作用的時刻,這就能保證系統配置變更的正確性。

問: 新加入的節點沒有任何的log entry,將會導致其短期能不能commit 新的log entries,怎麼解決?

因為新加入的節點,其log可能需要一段時間才能接收完leader之前提交的log,所以會導致需要一段時間才能接受提交的新的log,Raft新增加了一個階段,對於新加入的節點,在其log複製完也就是跟上叢集內其他節點的日誌前,不參與多數節點的行列,只有接受完舊的日誌,才能夠正常參與表決(投票和commit表決)。

問: 如果leader不在新的配置裡怎麼辦?

這種情況下,leader在提交Cnew後再關掉,也就是說在提交新配置前,leader管理了一個不包含自己的叢集,這個時候replicate log的時候,計算多數的時候不算他自己在內。

問: 移除的節點在沒有關掉的情況下,因為這些節點收不到leader的心跳,所以會重新發起選舉,這個時候會像新叢集的節點發送rpc,這個時候可能影響新節點的狀態

Raft規定,如果新配置的叢集能夠收到leader的心跳,即使收到了選舉的RPC,也會拒絕掉,不會給他投票。

Log Compaction

隨著請求的增多,Raft每個例項的log不斷的增加,現實中是不可能任其無限增長的,因此Raft也提供了快照的方法來壓縮日誌。每個Raft例項都單獨執行快照演算法,當日志大小達到一定大小的時候觸發快照操作,儲存最後提交的日誌時刻狀態機的狀態,以及最後提交的一個日誌的index和term。

當一個節點的日誌遠落後於leader節點,比如新加入節點,這個時候就需要將leader的快照發送給該節點,因此Raft也提供了一個InstallSnapshot的RPC介面用來發送快照。當follower收到leader的快照時候,根據快照裡包含的LastIncludeIndex和LastIncludeTerm以及自身log的index和term來確定如果處理,如果當前節點包含一些沒有提交但是和快照衝突的日誌,那麼清楚有衝突的日誌,保留快照後面的日誌(正常情況不會出現,有可能快照是重傳或者產生了錯誤,所以收到一個自身日誌之前某個index前的快照,這個時候覆蓋該index之前的日誌保留後續日誌即可)。

Client Interaction

客戶端與Raft叢集進行互動主要存在兩個問題:第一,客戶端如何確定leader的位置?第二,如何保證Raft的線性一致性(Linearizable)?

客戶端如何確定leader的位置?當客戶端啟動的時候會隨機的選擇一個節點請求,如果這個節點不是leader,那麼會返回該節點最近知道的leader的位置(可能leader已經切換,該節點還沒感知)。如果leader宕機了,那麼客戶端隨機選擇一個節點請求。

第二個問題,Raft提供線性一致性,也就是說對於一個操作,Raft只執行一次,並且立即執行,後面的讀一定讀到最新的資料。但是如果不加任何的限制,目前描述的Raft演算法是可能出現同一個命令執行多次的情況,比如如果leader在提交了log entry後但是沒有返回給客戶端的時候宕機了,那麼客戶端超時後會重新請求改命令。這樣就可能提交兩次操作。針對這種情況,客戶端給每個操作都分配一個遞增的序列號,Raft叢集每個狀態機都記錄一下當前每個客戶端已經執行的操作的最新的序列號,如果客戶端請求了同一個序列號的操作,一點狀態機發現已經執行過了,就直接返回上次的結果給客戶端。

總結

Raft在保證正確性的前提下實現一個了容易理解和實現的一致性演算法,相比Paxos簡化了很多,與Paxos最大的不同就是Raft是一個強leader的協議,所以的操作都依賴於leader,操作流的方向也只能從Leader發往其他節點,所以Raft整個協議分為兩階段,每個term首先進行選主,然後後續leader掌權接受所有客戶端的請求,這樣大大的簡化了協議的複雜度,但是也存在leader負載過高的問題,不過通常實現的時候用的都是multi-raft,在接受請求前已經進行了負載的均衡,有時候還會使用旁路的監控,動態調整leader的位置達到更好的效能。

附錄
Raft正確性原則:
- Election Safety
一個term只能選出一個leader
- Leader Append-Only
leader絕不覆蓋或者刪除日誌,只會增加新的日誌
- Log Matching
如果兩個logs的包含一個entry其term和index都相同,那麼該entry之前所有的log entry都相同
- Leader Completeness
一個選舉出來的leader包含了當前term之前所有term已經提交的log entry
- State Machine Safety
如果一個節點的狀態機已經應用了某個index的操作,那麼其他節點的狀態機在改index也是執行同樣的操作,不會是其他操作。