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 本人在
“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:
問題描述
分散式系統中的節點通訊存在兩種模型:共享記憶體(Shared memory)和訊息傳遞(Messages passing)。基於訊息傳遞通訊模型的分散式系統,不可避免地會發生以下錯誤:程序可能會慢、垮、重啟,訊息可能會延遲、丟失、重複(不考慮“
一個典型的場景是:在一個分散式資料庫系統中,如果各節點的初始狀態一致,每個節點都執行相同的操作序列,那麼它們最後能得到一個一致的狀態。為保證每個節點執行相同的命令序列,需要在每一條指令上執行一個「一致性演算法」以保證每個節點看到的指令一致。一個通用的一致性演算法可以應用在許多場景中,是分散式計算中的重要問題。從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機制,當配置發生變更時可以實時通知註冊客戶端,但是如何保證該通知的可靠送達呢,系統中是否可能同時存在新舊兩份配置?煩請有相關經驗的高人私下交流~