1. 程式人生 > >Raft一致性演算法

Raft一致性演算法

Why Not Paxos

Paxos演算法是萊斯利·蘭伯特(LeslieLamport,就是 LaTeX 中的”La”,此人現在在微軟研究院)於1990年提出的一種基於訊息傳遞的一致性演算法。由於演算法難以理解起初並沒有引起人們的重視,使Lamport在八年後1998年重新發表到ACM Transactions on Computer Systems上(The Part-TimeParliament)。即便如此paxos演算法還是沒有得到重視,2001年Lamport 覺得同行無法接受他的幽默感,於是用容易接受的方法重新表述了一遍(Paxos MadeSimple)。可見Lamport對Paxos演算法情有獨鍾。近幾年Paxos演算法的普遍使用也證明它在分散式一致性演算法中的重要地位。2006年Google的三篇論文初現“雲”的端倪,其中的Chubby Lock服務使用Paxos作為Chubby Cell中的一致性演算法,Paxos的人氣從此一路狂飆。Lamport 本人在

他的blog 描寫了他用9年時間發表這個演算法的前前後後。

“There is only one consensus protocol, and that’sPaxos-all other approaches are just broken versions of Paxos.”Chubby authors

“The dirtylittle secret of the NSDI community is that at most five people really, trulyunderstand every part of Paxos ;-).” –NSDI reviewer

Notes:

回想當年,我不知翻閱了多少資料,才勉強弄明白“Basic Paxos”,由於缺乏實踐體會,至今對於“Multi-Paxos”仍如雲裡霧裡,不得要領。反觀本文的主角Raft,《InSearch of an Understandable Consensus Algorithm》,從它設計之初,作者就將Understandable作為最高準則,這在諸多決策選擇時均有體現。

問題描述

分散式系統中的節點通訊存在兩種模型:共享記憶體(Shared memory)和訊息傳遞(Messages passing)。基於訊息傳遞通訊模型的分散式系統,不可避免地會發生以下錯誤:程序可能會慢、垮、重啟,訊息可能會延遲、丟失、重複(不考慮“

Byzantinefailure”)。

一個典型的場景是:在一個分散式資料庫系統中,如果各節點的初始狀態一致,每個節點都執行相同的操作序列,那麼它們最後能得到一個一致的狀態。為保證每個節點執行相同的命令序列,需要在每一條指令上執行一個「一致性演算法」以保證每個節點看到的指令一致。一個通用的一致性演算法可以應用在許多場景中,是分散式計算中的重要問題。從20世紀80年代起對於一致性演算法的研究就沒有停止過。

圖 1Replicated State Machine Architecture

Raft演算法將這類問題抽象為“ReplicatedState Machine”,詳見上圖,每臺Server儲存使用者命令的日誌,供本地狀態機順序執行。顯而易見,為了保證“Replicated State Machine”的一致性,我們只需要保證“ReplicatedLog”的一致性。

演算法描述

通常來說,在分散式環境下,可以通過兩種手段達成一致:

1.       Symmetric, leader-less

所有Server都是對等的,Client可以和任意Server進行互動

2.       Asymmetric, leader-based

任意時刻,有且僅有1臺Server擁有決策權,Client僅和該Leader互動

Designing for understandability”的Raft演算法採用後者,基於以下考慮:

1.       問題分解:Normaloperation & Leader changes

2.       簡化操作:Noconflicts in normal operation

3.       更加高效:Moreefficient than leader-less approaches

基本概念

Server States

Raft演算法將Server劃分為3種角色:

1.       Leader

負責Client互動和log複製,同一時刻系統中最多存在1個

2.       Follower

被動響應請求RPC,從不主動發起請求RPC

3.       Candidate

由Follower向Leader轉換的中間狀態

圖 2Server States

Terms

眾所周知,在分散式環境中,“時間同步”本身是一個很大的難題,但是為了識別“過期資訊”,時間資訊又是必不可少的。Raft為了解決這個問題,將時間切分為一個個的Term,可以認為是一種“邏輯時間”。如下圖所示:

1.       每個Term至多存在1個Leader

2.       某些Term由於選舉失敗,不存在Leader

3.       每個Server本地維護currentTerm

圖 3Terms

Heartbeats and Timeouts

1.       所有的Server均以Follower角色啟動,並啟動選舉定時器

2.       Follower期望從Leader或者Candidate接收RPC

3.       Leader必須廣播Heartbeat重置Follower的選舉定時器

4.       如果Follower選舉定時器超時,則假定Leader已經crash,發起選舉

Leader election

自增currentTerm,由Follower轉換為Candidate,設定votedFor為自身,並行發起RequestVote RPC,不斷重試,直至滿足以下任一條件:

1.       獲得超過半數Server的投票,轉換為Leader,廣播Heartbeat

2.       接收到合法Leader的AppendEntries RPC,轉換為Follower

3.       選舉超時,沒有Server選舉成功,自增currentTerm,重新選舉

細節補充:

1.       Candidate在等待投票結果的過程中,可能會接收到來自其它Leader的AppendEntries RPC。如果該Leader的Term不小於本地的currentTerm,則認可該Leader身份的合法性,主動降級為Follower;反之,則維持Candidate身份,繼續等待投票結果

2.       Candidate既沒有選舉成功,也沒有收到其它Leader的RPC,這種情況一般出現在多個節點同時發起選舉(如圖Split Vote),最終每個Candidate都將超時。為了減少衝突,這裡採取“隨機退讓”策略,每個Candidate重啟選舉定時器(隨機值),大大降低了衝突概率

Log replication

圖 4Log Structure

正常操作流程:

1.       Client傳送command給Leader

2.       Leader追加command至本地log

3.       Leader廣播AppendEntriesRPC至Follower

4.       一旦日誌項committed成功:

1)     Leader應用對應的command至本地StateMachine,並返回結果至Client

2)     Leader通過後續AppendEntriesRPC將committed日誌項通知到Follower

3)     Follower收到committed日誌項後,將其應用至本地StateMachine

Safety

為了保證整個過程的正確性,Raft演算法保證以下屬性時刻為真:

1.       Election Safety

在任意指定Term內,最多選舉出一個Leader

2.       Leader Append-Only

Leader從不“重寫”或者“刪除”本地Log,僅僅“追加”本地Log

3.       Log Matching

如果兩個節點上的日誌項擁有相同的Index和Term,那麼這兩個節點[0, Index]範圍內的Log完全一致

4.       Leader Completeness

如果某個日誌項在某個Term被commit,那麼後續任意Term的Leader均擁有該日誌項

5.       State Machine Safety

一旦某個server將某個日誌項應用於本地狀態機,以後所有server對於該偏移都將應用相同日誌項

直觀解釋:

為了便於大家理解Raft演算法的正確性,這裡對於上述性質進行一些非嚴格證明。

“ElectionSafety”:反證法,假設某個Term同時選舉產生兩個LeaderA和LeaderB,根據選舉過程定義,A和B必須同時獲得超過半數節點的投票,至少存在節點N同時給予A和B投票,矛盾

LeaderAppend-Only: Raft演算法中Leader權威至高無上,當Follower和Leader產生分歧的時候,永遠是Leader去覆蓋修正Follower

LogMatching:分兩步走,首先證明具有相同Index和Term的日誌項相同,然後證明所有之前的日誌項均相同。第一步比較顯然,由Election Safety直接可得。第二步的證明藉助歸納法,初始狀態,所有節點均空,顯然滿足,後續每次AppendEntries RPC呼叫,Leader將包含上一個日誌項的Index和Term,如果Follower校驗發現不一致,則拒絕該AppendEntries請求,進入修復過程,因此每次AppendEntries呼叫成功,Leader可以確信Follower已經追上當前更新

LeaderCompleteness:為了滿足該性質,Raft還引入了一些額外限制,比如,Candidate的RequestVote RPC請求攜帶本地日誌資訊,若Follower發現自己“更完整”,則拒絕該Candidate。所謂“更完整”,是指本地Term更大或者Term一致但是Index更大。有了這個限制,我們就可以利用反證法證明該性質了。假設在TermX成功commit某日誌項,考慮最小的TermY不包含該日誌項且滿足Y>X,那麼必然存在某個節點N既從LeaderX處接受了該日誌項,同時投票同意了LeaderY的選舉,後續矛盾就不言而喻了

StateMachine Safety:由於LeaderCompleteness性質存在,該性質不言而喻

Cluster membership changes

在實際系統中,由於硬體故障、負載變化等因素,機器動態增減是不可避免的。最簡單的做法是,運維人員將系統臨時下線,修改配置,重新上線。但是這種做法存在兩個缺點:

1.       系統臨時不可用

2.       人為操作易出錯

圖 5Online Switch Directly

失敗的嘗試:通過運維工具廣播系統配置變更,顯然,在分散式環境下,所有節點不可能在同一時刻切換至最新配置。由上圖不難看出,系統存在衝突的時間視窗,同時存在新舊兩份Majority。

兩階段方案:為了避免衝突,Raft引入Joint中間配置,採取了兩階段方案。當Leader接收到配置切換命令(Cold->Cnew)後,將Cold,new作為日誌項進行正常的複製,任何Server一旦將新的配置項新增至本地日誌,後續所有的決策必須基於最新的配置項(不管該配置項有沒有commit),當Leader確認Cold,new成功commit後,使用相同的策略提交Cnew。系統中配置切換過程如下圖所示,不難看出該方法杜絕了Cold和Cnew同時生效的衝突,保證了配置切換過程的一致性。

圖 6Joint Consensus

Log compaction

隨著系統的持續執行,操作日誌不斷膨脹,導致日誌重放時間增長,最終將導致系統可用性的下降。快照(Snapshot)應該是用於“日誌壓縮”最常見的手段,Raft也不例外。具體做法如下圖所示:

圖 7S基於“快照”的日誌壓縮

與Raft其它操作Leader-Based不同,snapshot是由各個節點獨立生成的。除了日誌壓縮這一個作用之外,snapshot還可以用於同步狀態:slow-follower以及new-server,Raft使用InstallSnapshot RPC完成該過程,不再贅述。

Client interaction

典型的使用者互動流程:

1.       Client傳送command給Leader

            若Leader未知,挑選任意節點,若該節點非Leader,則重定向至Leader

2.       Leader追加日誌項,等待commit,更新本地狀態機,最終響應Client

3.       若Client超時,則不斷重試,直至收到響應為止

細心的讀者可能已經發現這裡存在漏洞:Leader在響應Client之前crash,如果Client簡單重試,可能會導致command被執行多次。

Raft給出的方案:Client賦予每個command唯一標識,Leader在接收command之前首先檢查本地log,若標識已存在,則直接響應。如此,只要Client沒有crash,可以做到“Exactly Once”的語義保證。

個人建議:儘量保證操作的“冪等性”,簡化系統設計!

發展現狀

Raft演算法雖然誕生不久,但是在業界已經引起廣泛關注,強烈推薦大家瀏覽其官網http://raftconsensus.github.io,上面有豐富的學習資料,目前Raft演算法的開源實現已經涵蓋幾乎所有主流語言(C/C++/Java/Python/Javascript …),其流行程度可見一斑。由此可見,一項技術能否在工業界大行其道,有時“可理解性”、“可實現性”才是至關重要的。

應用場景

timyang在《Paxos在大型系統中常見的應用場景》一文中,列舉了一些Paxos常用的應用場合:

1.       Database replication, logreplication …

2.       Naming service

3.       配置管理

4.       使用者角色

5.       號碼分配

Note:對於分散式鎖、資料複製等場景,非常容易理解,但是對於“Naming Service”一類應用場景,具體如何實操,仍然表示困惑。翻閱一些資料發現,藉助Zookeeper的watch機制,當配置發生變更時可以實時通知註冊客戶端,但是如何保證該通知的可靠送達呢,系統中是否可能同時存在新舊兩份配置?煩請有相關經驗的高人私下交流~