共識協議的技術變遷 - 既要“高”容錯,又要“易”定序,還要“好”理解

There is no compression algorithm for experience. -- Andy Jassy, CEO of AWS

一、緣起

共識協議,對於從事分佈式系統研發的同學們來說真可謂是最熟悉的陌生人。一方面,共識協議面向有狀態分佈式系統的數據一致性與服務容錯性這兩大難題提供了近乎完美的解決方案,絕大部分同學或多或少聽說過/研究過/使用過/實現過Paxos/Raft等經典共識協議;另一方面,共識協議的確很複雜,事實上,學習並弄懂共識協議原理倒是沒有那麼難,但是要在實際系統中用好用正確共識協議絕非易事,共識領域裏面的“雙主腦裂”,“幽靈復現”等傳說,也讓很多同學望而生畏。這篇文章與讀者朋友們好好聊一聊共識這個技術領域,期望能夠讓大夥兒對共識協議的前世今生以及這些年的技術演進有個大體瞭解。雖說經驗這種東西沒有壓縮算法,得自身一點一點實踐過才真正算數,但是認知學習是可以加速的,所謂:今日格一物,明日又格一物,豁然貫通,終知天理。

我們知道,分佈式系統最樸實的目標是把一堆普通機器的計算/存儲等能力整合到一起,然後整體上能夠像一臺超級機器一樣對外提供可擴展的讀寫服務,設計原則上要實現即使其中的部分機器發生故障,系統整體服務不受影響。對於有狀態的分佈式系統而言,實現上述容錯性目標的有效手段繞不開大名鼎鼎的複製狀態機。該模型的原理是爲一個服務器集合裏的每臺服務器都維護着相同機制的確定有限狀態機(DFSM,Determinate Finite State Machine),如果能夠保障每個狀態機輸入的是完全一致的命令序列,那麼這個集合中的每個狀態機最終都可以以相同狀態對外提供服務,同時也具備了容忍部分狀態機故障的能力。

圖1. 讓分佈式系統像“單機”一樣工作的複製狀態機模型

圖1展示了典型的複製狀態機模型,在實際應用中,上文提到的狀態機的命令序列通常會使用「只讀可追加日誌」來實現,即每個狀態機上都維護着一個日誌,日誌裏面保存着本狀態機待執行的命令序列。在圖1的例子中,每臺狀態機看到的日誌序列都必須是嚴格一致的「x<-3」,「y<-1」,「y<-9」,...,每個狀態機執行相同的命令序列,對外呈現的最終狀態均爲:x的值是3,y的值是9。顯然,在複製狀態機模型中,如何保證狀態機之間數據一致的問題轉換成了如何保證各臺機器上日誌序列一致的問題,這個成爲了複製狀態機模型最核心的挑戰,而這,也正是共識協議神聖的職責(不容易呀,話題總算引回來了 )。

分佈式共識協議的鼻祖,通常認爲是Leslie Lamport提出的Paxos。這裏有個有趣的典故,事實上Lamport在1990年的時候就向TOCS(ACM Transactions on Computer Systems)投遞了《The Part-Time Parliament》這篇大作,通篇以考古希臘Paxos小島爲背景,描述了古代該小島上議會制訂法令的流程,期望讀者能夠從中領悟出共識協議的精髓,並且能夠理解到大神的「幽默感」。奈何這種描述方式太過隱晦,編輯們沒看出來這層含義,幽默感當然也沒get到,結果即便這是大神投稿的文章也落得石沉大海的結局。時間來到了1998年,編輯部在整理舊的文獻資料的時候,從某個文件櫃中發現了這篇舊文並試着重新閱讀了一下,這次終於認識到了這裏面蘊含着的共識問題的武學奧義。最終這篇扛鼎之作得以重見天日!不過,實話實說,這個版本論文描述的的確是晦澀難懂,今天廣爲流傳的講解Paxos原理的論文其實是2001年Lamport再發表的《Paxos Made Simple》。話說這篇Paxos論文的摘要挺節約版面費的,就一句話:

The Paxos algorithm, when presented in plain English, is very simple.

說實話,從今天的角度來閱讀此文,Basic Paxos的思路的確是比較好懂。我們可以簡單回顧一下,如圖2中所展示的,Basic Paxos中一共有三類角色:其一提議者Proposer,其二接收者Acceptor,其三學習者Learner。基於實現就某個值達成共識這個具體目標,Basic Paxos全異步地經歷如下三個階段:

  • PREPARE階段,Proposer會生成全局唯一單調遞增的Proposal ID,向所有的Acceptor發送PREPARE提案請求,這裏無需攜帶內容,只攜帶提案的Proposal ID即可;
    • 相應的,Acceptor收到PREPARE提案消息後,如果提案的編號(不妨即爲 N )大於它已經回覆的所有PREPARE消息,則Acceptor將自己上次接受的決議內容回覆給Proposer,承諾有二:不再回復(或者明確拒絕)提案編號小於等於 N(這裏是<=)的PREPARE提案;不再回復(或者明確拒絕)提案編號小於N (這裏是<)的ACCEPT決議;
  • ACCEPT階段,如果Proposer收到了多數派Acceptors對PREPARE提案的肯定回覆,那麼進入決議階段。它要向所有Acceptors發送ACCEPT決議請求,這裏包括編號 N 以及根據PREPARE階段決定的 VALUE(如果PREPARE階段沒有收集到決議內容的 VALUE,那麼它可以自由決定 VALUE ;如果存在已經接受決議內容 的 VALUE,那麼Proposer就得繼續決議這個值);
    • 相應的,Acceptor收到ACCEPT決議請求後,按照之前的承諾,不回覆或者明確拒絕掉提案編號小於已經持久化的 N 的決議請求,如果不違背承諾,Acceptor接受這個決議請求;
  • COMMIT階段,如果Proposer收到了多數派Acceptors對於ACCEPT決議的肯定回覆,那麼該決議已經正式通過,異步地,Proposer把這個好消息廣播給所有的Learners;

圖2. 原始Basic Paxos的基本流程,其實真的還挺簡單

以上就是基礎版的Basic Paxos協議,完美地解決了分佈式系統中針對某一個值達成共識的問題,其實也挺好理解的,對不對?但是,實際的分佈式系統顯然不可能僅僅基於一個值的共識來運轉。按照圖1裏面的複製狀態機模型的要求,我們是要解決日誌序列一系列值的共識問題呀。咋辦?好辦!直觀地,我們可以反覆地一輪一輪執行Basic Paxos,爲日誌序列中每一條日誌達成共識,這個就是通常所謂的Multi Paxos。這裏的所謂一輪一輪的輪號就是我們常說的Log ID,而前者提到的Proposal ID,則是我們常說的Epoch ID。對於採用Multi Paxos的有狀態分佈式系統而言,日誌序列的Log ID必須是嚴格連續遞增並且無空洞地應用到狀態機,這個是Multi Paxos支持亂序提交所帶來的約束限制,否則我們無法保證各狀態機最終應用的命令序列是完全一致的。這裏需要課代表記下筆記,一句話重點就是:

Proposal ID(Epoch ID)的全局唯一且單調遞增的約束主要還是出於效率的考慮,Log ID的全局唯一且單調遞增的約束則是出於正確性的考慮。

分享到這,特別怕讀者朋友來這麼一句:報告,上面的內容我都看懂了,請問什麼時候可以上手開發共識協議?!其實,從Basic Paxos到Multi Paxos,這之間存在着巨大的鴻溝,實踐中有着太多的挑戰需要一一應對。這裏我們通過兩個經典的共識效率難題來感受一下:

  • LiveLock問題,其實活鎖問題源於Basic Paxos自身,如圖3所示,在併發度較高的場景下,如果某一輪有多個Proposer同時發起PREPARE提案,那麼有的Proposer會成功,譬如R1,有的會失敗,譬如R5,失敗的R5會提升Proposal ID重新發起PREPARE提案,如果這個足夠快,會導致開始PREPARE提案成功的R1繼續發起的ACCEPT決議會失敗,R1會提升Proposal ID重新發起PREPARE提案,. . . ,兩個副本就這樣循環往復,一直在爭搶提議權,形成了活鎖。解決方案也是有的,一者是控制各Proposer發起提案的頻率,二者是在提議失敗重試的時候增加超時退避。當然,這個只能降低活鎖的概率,畢竟根據著名的FLP理論,此時屬於放鬆系統liveness去追求共識協議立身之本linearzability,因此,理論上Paxos就是可能永遠也結束不了;
  • CatchUp問題,在Multi Paxos中,某個Proposer在收到客戶端請求後,首先要決定這條請求的Log ID,前面我們提到了,爲了保證複製狀態機的數據一致性,Log ID必須是全局嚴格連續遞增的,即這個值不能夠與已經持久化或者正在持久化中的日誌Log ID重複,否則Proposer會陷入發現這個Log ID已經被某個決議佔據、遞增本地維護的Max Log ID並嘗試重新提議、發現新的Log ID還是被某個決議佔據、繼續增加本地的Max Log ID並嘗試重新提議 . . . ,一直循環至發現了一個全新未啓用的Log ID。多累呀,是不是?解決方案也是有的,Proposer向所有Acceptors查詢它們本地已經寫盤的最大Log ID,收到了多數派的返回結果並選擇其中最大值加一作爲本次待提議的提案請求的Log ID。當然,這種方案的實現邏輯也不輕,並且依舊存在着爭搶失敗然後退避重試的概率;

圖3. Basic Paxos在多個提議者同時提議的情況下面臨着活鎖問題

這麼一分析,應該都能感受到從Basic Paxos到Multi Paxos這般升級打怪的難度。即便數學家出身的Lamport大神此時也意識到需要給工程師們指引一個可行的工程化方向,否則他們勢必不會上船的 ...... 大神提供的解決方案很是簡單直接,大夥兒就是喜歡這種樸素:如果Basic Paxos中絕對公平意味着低效,那麼不妨在Multi Paxos中引入選舉併產生唯一Leader,只有當選爲Leader才能夠提議請求。這樣的好處很明顯,首先由唯一的Leader做Log ID分配,比較容易實現全局唯一且連續遞增,解決了CatchUp難題;其次,唯一的Leader提議不存在併發提議衝突問題,解決了LiveLock難題;進一步地,本來PREPARE就是爲了爭奪提議權,現在既然只有Leader能夠提議,就至少在Leader任職時間內就不必每個提議都要再走一遍PREPARE,這樣大幅提升了形成決議的效率。當然啦,引入了Leader之後,Multi Paxos也是要處理好Leader切換前後的數據一致性問題,並且新的Leader如何填補日誌空洞等難題也需要好好設計。問題還是不少,不過,這些都是後話,大方向不會錯,跟着大神衝吧!

二、野望

Lamport推出了Multi Paxos,明確了共識協議生產落地的大方向。但是,生產系統究竟應該怎麼正確、高效地使用Paxos,還是存在很多不確定性,工程師們一直在不斷探索、嘗試,但是並沒有形成理想的結果產出,業界整體上還是處於一種觀望狀態。一直等到2007年,來自Google的工程師們通過《Paxos Made Live - An Engineering Perspective》一文,詳細地介紹了他們在Chubby(GFS中提供分佈式鎖服務的關鍵組件,Apache ZooKeeper在很大程度上借鑑了Chubby的設計思想)中落地Multi Paxos的最佳實踐。這篇文章非常經典,即使十六年後的今天讀起來也完全不過時,建議有興趣瞭解學習Paxos/Raft協議的讀者朋友們一定要讀一讀。我們來總結一下這篇文章提到的Multi Paxos生產落地過程遇到的種種挑戰:

  • 磁盤故障(disk corruption):在Lamport的Paxos論文中特地提到過持久化的問題:
Stable storage, preserved during failures, is used to maintain the information that the acceptor must remember. An acceptor records its intended response in stable storage before actually sending the response.

但是現實系統中,用於數據持久化的設備,譬如磁盤,也會發生故障,一旦Acceptor機器上的磁盤發生了故障,那麼上述持久化的性質就被打破,這個是原則性問題,顯然此時的Acceptor就不再具備繼續應答提議或者決議的資格,怎麼辦?Google工程師們想到的解決方案是,爲這類發生了持久化設備故障的Acceptor機器引入一個重建窗口(這個期間不再參與提議或者決議應答),類似上面提到的Multi Paxos的CatchUp機制,通過持續的學習已有的提議,啥時候是個頭?一直等到重建窗口開始後發起的提議都已經被開始學習到了,那麼此時Acceptor很明確,自己可以安全地加回決議軍團了;

  • Master租約(master lease):Multi Paxos通過選主來有效提升提議效率,然後通過租約機制來維護當選Leader的主權。然而,基於租約的選主機制都會面臨“雙主”問題,即因爲網絡分區等緣故,舊的Leader依舊在工作,並不知道新的Leader已經產生並也已經對外提供服務。這種雙主問題,對於寫請求沒有影響(畢竟寫是走多數派決議的,舊的Leader的Epoch ID比較低,多數派Acceptors不會接受它的請求提議),可是對於讀請求,特別是強一致性讀,這就是個很大危害了,因爲從舊的Leader可能會讀到過期數據。Google工程師們想到的解決方案是,強化租約機制,即Follower收到Leader的租約更新後,嚴格承諾在租約有效期內不發起/接收新一輪選舉,包括與Leader斷連以及自身進程重啓等場景,這樣強化的租約機制能夠將不同代的Leader之間實現物理時間上的隔離,確保同一時刻最多隻有一個Leader提供內存態的強一致讀服務;
  • Master紀元(master epoch):這個其實就是我們所謂的“幽靈復現”問題,即舊的Leader處理某個提議請求超時了,沒有形成多數派決議,在幾輪重新選舉之後,這條決議可能會被重新提議並最終形成多數派。這個問題的最大危害在於讓客戶端無所適從:提議請求超時,客戶端並不知道後端系統實際處於什麼狀況,請求究竟是成功或失敗不清楚,所以客戶端做法通常是重試,這個要求後臺系統的業務邏輯需要支持冪等,不然重試會導致數據不一致。Google的工程師們想到的解決方案是,爲每個日誌都保存當前的Epoch ID,並且Leader在開始服務之前會寫一條StartWorking日誌,所以如果某條日誌的Epoch ID相對前一條日誌的Epoch ID變小了,那麼說明這是一條“幽靈復現”日誌,忽略這條日誌;
  • 共識組成員(group membership):實際系統中顯然是要面對成員變更這個需求。通過冷啓動方式自然是可以完成成員變更的,但是這種機制對於在線服務無法接受。能否在不停服的情況下進行成員變更?事實上,成員變更本身也是一個共識問題,需要所有節點對新成員列表達成一致。但是,成員變更又有其特殊性,因爲在成員變更的過程中,參與投票的成員會發生變化。所以這個功能該怎麼實現就非常有講究,如果新舊兩個成員組的多數派可以不相交,那麼就會出現腦裂,導致數據不一致的嚴重後果。Multi Paxos論文中提到了成員變更本身也要走共識協議,但是在實際系統中實現這個功能的時候,還是遇到了很多挑戰,譬如新配置什麼時候生效?譬如變更到一半,發生重新選舉,那麼後面究竟要不要繼續成員變更;
  • 鏡像快照(snapshots):Google的工程師們在Multi Paxos的實踐中,詳細描述了快照機制的引入,這裏的目的有二:其一加快節點重入的恢復,這樣就不用逐條日誌學習;其二有效控制日誌對磁盤空間量的佔用。需要特別注意的是(一定要牢記心裏),打快照完全是業務邏輯,底層一致性引擎可以通知上層業務狀態機何時打快照,至於快照怎麼打,保存到哪裏,如何加載快照,這些都是業務狀態機需要支持的。快照實現本身也面臨着諸多挑戰,譬如快照與日誌要配合且保證整體數據一致性;譬如打快照本身耗時耗力,儘可能要異步實現,不影響正常請求的決議流程;譬如打快照持久化後可能也會損壞,要增加多重備份以及必要校驗等等;

以上是Multi Paxos算法在Google Chubby實際落地過程遇到的諸多挑戰,我相信讀者朋友們應該都能夠感受到Paxos從理論到實踐的確是有很多技術點需要完善。其實就上面這些還是摟着說的,實際上Google的工程師們還探討了更多不確定性:如何保證正確地實現了Multi Paxos的真實語義?如何對一個基於共識協議的分佈式系統進行有效測試驗證?如何進行狀態機運行態正確性檢查,特別是版本升級等場景狀態機前後語義可能不兼容?

讀到這裏,我相信,讀者朋友會跟我有一樣的感嘆,Google的工程師們真的太強,着實讓人佩服,在2007年的時候,敢爲人先,把Paxos紮紮實實地應用到了生產,你看看人家上面提到的這些個問題,基本上做有狀態分佈式系統的研發同學在這些年的工作中或多或少都踩過類似的坑吧?想一想,早些年Google發表了號稱大數據領域三駕馬車的鉅作:GFS,BigTable,MapReduce,算是拉開了雲計算的大幕,近些年Google又開源了雲原生的Kubernetes,公開了大模型Transformer ...... 不得不感慨一句,Google的確是雲計算領域的弄潮兒 ......

圖4. 總結起來,這些年共識領域大體可以劃分爲六大演進方向

話說回來,我們有理由相信,人們對於Paxos共識協議的熱情被Google的這篇文章給重新點燃,越來越多的廠商、研究工作者投身其中,探索並持續改進共識協議,然後等到了2014年Raft的橫空出世 ...... 如圖4所示,我們梳理了這些年的共識領域的各類研究工作,把共識領域的演進工作分爲六大方向,接下來的篇幅,我們集中注意力,逐一說道說道共識領域的這六大門派:

  • 強主派:橫空出世的Raft,真正讓共識協議在工業界遍地開花,從前王謝堂前燕,由此飛入尋常百姓家;
  • 無主派:你有張良計,我有過牆梯。Generalized Paxos/Mencius/EPaxos等接力探索去除單點瓶頸依賴;
  • 標準派:Corfu/Delos爲共識協議引入模塊化層次設計,支持熱升級可插拔,實現標準化接入通用化移植;
  • 異步派:緣起EPaxos,Skyros/Tempo/Rabia推遲線性定序,主推異步共識,追求世間完美的一跳決議;
  • 靈活派:FPaxos/WPaxos腦洞大開,解耦共識的提議組與決議組的成員列表,靈活應對跨地域容災場景;
  • 軟硬一體派:NOPaxos/P4xos順應時代大潮,探索共識各模塊硬件卸載的可行性,軟硬一體化加速共識;
  • ......

三、進擊

我個人比較喜歡讀歷史類的書籍,其中《大秦帝國》這部著作反覆閱讀過幾遍。這個系列共計有六部,講述了從秦孝公變法圖強開始,直至秦末羣雄並起的這段恢弘曆史。我發覺《大秦帝國》六個篇章的鮮明標題(黑色裂變,國命縱橫,金戈鐵馬,陽謀春秋,鐵血文明,帝國烽煙)用來概括共識領域這些年六大演進方向還算是比較貼合,爲了增加文章閱讀的趣味性,斗膽引徵了過來,諸君請聽我細細道來。

3.1 黑色裂變

黑色裂變這個篇章,必須要留給Raft,並且爲了表示尊重,這個章節我們只放Raft這一個共識協議。正是Raft的橫空出世,解決了共識協議工業落地困難的問題,並且Raft在現代分佈式系統的狀態一致性保持這個關鍵性質上起到了巨大作用。事實上,今天我們在GitHub上搜索帶有Raft關鍵字的開源庫,可以查看有1300+個開源庫,其中stars破1000的也有30+,不乏etcd-io/etcd,tikv/tikv,baidu/braft,polardb/polardbx-sql,sofastack/sofa-jraft,hashicorp/raft等業界各大知名廠商的代表作。這裏,特別緻敬一下Raft的提出者,Diego Ongaro。

正如前文所言,Multi Paxos的工業落地並不順暢,如何解決活鎖,如何有效選舉,如何打快照,如何解決空洞等問題。關鍵這些問題的解決讓Multi Paxos變得異常複雜,我們怎麼保證實現正確呢?來自Standford的計算機博士Diego Ongaro沒有選擇跟Multi Paxos死磕,他的博士期間的研究關注點都放在了探尋一種可理解性以及易於生產應用這兩大維度上更優的全新共識協議,最終在2014年USENIX ATC展示了他「探尋」的答案,Raft。

我認爲Raft最出色的地方是在共識協議中引入了強主。是的,你沒有聽錯。讀者朋友們肯定有疑惑:你明明在剛纔介紹中提到了Multi Paxos同樣是引入Leader來解決提議效率問題的。這麼說吧,相較於Raft中的Leader被定義爲「強主」模式,我更願意把Multi Paxos中的Leader定義爲「代理」模式。先說Multi Paxos,事實上,每個Proposer都可以成爲那個唯一提議者,這裏僅僅是需要一個「代理」上位,省得七嘴八舌效率低。並且「代理」是可以隨時被更換的,大家沒有心理負擔;而到了Raft,Leader成爲了智者,是擁有最多信息的那個Proposer,Leader的標準就是整個分組的標準,不一致的地方統統以Leader爲準。事實上,其餘角色都指着Leader存活,成爲了Leader的依附,更換Leader也就成爲了一件非常慎重的事情,中間甚至會產生共識停服的窗口。「強主」模式體現了Raft的「簡單有效好實現」的設計理念,也直接支撐了其它如日誌複製、一致性保障等模塊的簡化。

具體的請看圖5,在Raft共識協議中,所有的日誌流均是嚴格按順序從Leader發送給Follower,這個共識組裏面並不存在其它通信通道。此外,Leader給Follower發送的每條日誌X都包含有Leader記錄的該Follower上一條消費的日誌Y,在Follower這邊收到Leader發送過來的日誌X並準備接受之前,會檢查本地記錄的上一條日誌是否是Y,如果不一致,那麼一切以Leader爲標準,Follower繼續往前追溯,找到與Leader分叉點,消除掉本地分叉並堅決學習Leader的日誌。通過這個強主邏輯的設計,日誌複製管理得到了質的簡化,而且服務器的狀態空間也被大爲簡化,是不是?

另外,不知道大家注意沒有,通過這種強主邏輯的設計,Raft事實上消除了日誌空洞的情形,這也將更加方便拉平兩個節點的最新日誌。回憶一下,Multi Paxos支持提議的亂序提交,允許日誌空洞存在,這個雖然是提供了協議的靈活性,併發性也更優,但是的確讓協議變得更加的複雜,阻礙了協議的工業落地。

圖5. 引入強主模式Leader,簡化了日誌複製管理,大大提升了協議可理解性

我們繼續來說一說Raft的選舉算法,它摒棄了Multi Paxos使用原生共識協議來進行選舉的複雜機制,而是解耦出來,通過引入簡單的“隨機超時+多數派”機制來進行有效選舉。圖6展示了Raft共識協議中角色狀態轉換圖,所有角色初始狀態都是Follower,有個隨機的選舉超時時間,如果在這個時間內沒有Leader主動聯絡過來,進入Candidata狀態,將選舉的Epoch加一,發起新一輪選舉:1)如果收到本輪多數派選舉投票,那麼當選Leader,通知其它同代的角色進入Follower狀態;2)如果發現了本輪Leader的存在,則自身轉換成Follower狀態;3)如果超時時間內沒有發生上述兩類事件,隨機超時後再次發起新一輪選舉。注意,在Raft選舉算法中,根據多數派原則,每一代(通過Epoch來表示)至多產生一個Leader。並且,爲了保證數據一致性,僅允許擁有最新日誌的節點當選爲Leader。這裏我要再次敲一敲小黑板,Multi Paxos中任意節點都可以當選Leader,好處是靈活,缺點是補全空洞等事宜讓Leader上任過程變得很複雜;Raft中只有擁有最新日誌的節點可以當選Leader,新官上任無需三把火。Raft的這個設計好,真好,怎麼那麼好呢?!

圖6. Raft着重簡化設計,單獨剝離出來的選舉算法主要依賴隨機超時機制

在Raft原生的選舉協議中,如果Leader發現了更大的Epoch,會主動退出角色。這個在實際落地時候會產生一個所謂StepDown問題:如果某個Follower與其他副本之間發生了網絡分區,那麼它會持續地選舉超時,持續地觸發新一輪選舉,然後它的Epoch會越來越大。此時,一旦網絡恢復,Leader會發現這個更大Epoch,直接退出當前角色,系統服務被中斷,顯然這個是系統可用性的隱患。根據多數派原則,每個Epoch至多隻有一個Leader當選,因此,每次發起選舉之前提升Epoch是必要的操作,那應該怎樣面對這個StepDown挑戰?通常的解決方案是,在正式選舉之前,即把Epoch正式提升併發起選舉之前,先發起一輪預選舉PreVote,只有PreVote確認自己能夠獲勝,那麼纔會發起正式選舉。注意,這裏的巧妙之處是,PreVote不會修改任何副本的選舉狀態。

最後我們聊一聊Raft的成員變更,這個就是Raft的特點,方方面面給你考慮的透透的,恨不得幫你把實現的代碼都寫了。Raft首次提出了Joint Consensus(聯合一致)的成員變更方法,將過程拆分成舊成員配置Cold生效,到聯合一致成員配置Cold,new生效,然後再到新成員配置Cnew生效這三個階段。如圖7所示,Leader收到成員變更請求後,先向Cold和Cnew同步一條Cold,new日誌,這條日誌比較特殊,需要在Cold和Cnew都達成多數派之後方可提交,並且這個之後其它的請求提議也都得在Cold和Cnew都達成多數派之後方可提交。在Leader認識到Cold,new日誌已經形成了決議,那麼繼續再向Cold和Cnew同步一條只包含Cnew的日誌,注意,這條日誌只需要Cnew的多數派確認即可提交,不在Cnew中的成員則自行下線,此時成員變更即算完成。在Cnew之後的其它的請求提議也都得在Cnew達到多數派即可。成員變更過程中如果發生failover,譬如老Leader宕機,那麼Cold,new中任意一個節點都可能成爲新Leader,如果新Leader上沒有Cold,new日誌,則繼續使用Cold,本次成員變更宣告失敗;如果新Leader上有Cold,new日誌,那麼它會繼續將未完成的成員變更流程走完直至Cnew生效。

Raft提出的Joint Consensus成員變更比較通用並且容易理解,之所以分爲兩個階段是爲了避免Cold和Cnew各自形成不相交的多數派而選出兩個Leader導致數據寫壞。實踐中,如果增強成員變更的限制,假設Cold與Cnew任意的多數派交集不爲空(這個並非強假設,日常我們的分佈式一致性系統做的成員變更幾乎都是加一/減一/換一,的確就是能夠滿足「Cold與Cnew無法獨自形成多數派」),則成員變更就可以進一步簡化爲一階段。

圖7. Raft首創的Joint Consensus成員變更法,清晰指導工業實現

共識協議通常被用以解決分佈式系統中數據一致性這個難題,是個強需求強依賴。Raft協議的問世無疑大大降低了實現一個正確共識協議的門檻,業界有非常多不錯的實踐,很多也開源了相關的一致性庫,特別是國內的一衆廠商,如圖8所展示的。說實話,在正確性基礎之上,要實現一個高效穩定的一致性庫,保證分佈式系統能夠持續的平穩運行,還是要花費很多很多心思的。這裏我們介紹幾個業界比較知名的一致性庫:

  • 螞蟻的Oceanbase,OB應該是在2012年就開始搗鼓Paxos協議了,時間上在Raft問世之前,應該算是Google之外,另一個把Paxos整明白的團隊。網上很多經典的Paxos科普文也是OB團隊以前的一些成員分享的。事實上,在解決了高高的技術門檻之後,業務是可以充分享受Multi Paxos亂序提交日誌帶來的可用性和同步性能的提升。我記得陽振坤老師之前有提過,Raft的「要求嚴格按照順序決議事務」的侷限對數據庫的事務有着潛在性能和穩定性風險,讓不相關的事務產生了互相牽制,相較而言Multi Paxos就沒了這個約束。不過,總的來說,Multi Paxos還是有些曲高和寡,業界實際採用的屈指可數;
  • 騰訊的PhxPaxos,PhxPaxos是騰訊公司微信後臺團隊自主研發的一套基於Paxos協議的多機狀態拷貝類庫,2016年正式開源,比較忠實地遵照了Basic Paxos原本協議的工業級實現,宣稱在微信服務裏面經過一系列的工程驗證和大量惡劣環境下的測試,數據一致性有着很強健壯性保證,微信內部支持了PhxSQL,PaxosStore等存儲產品。不過,因爲PhxPaxos對於請求提議處理同樣做了串行化的處理,並且選舉出來的Leader必須有最新日誌,這些優化設定已經比較多地參考借鑑了Raft的設計;
  • 百度的Braft,這個可謂是開源裏面Raft協議的工業級實現。百度是在2018年2月初開源了其基於Brpc的Raft 一致性算法和可複製狀態機的工業級C++實現,Braft。這個項目最初是爲了解決百度各業務線上的狀態服務單點隱患,後來則幫助業務快速實現支持高負載和低延遲的分佈式系統。作爲一款開源基礎庫,除了在業界有龐大的擁躉羣體,Braft在百度內部應用十分廣泛,包括塊存儲、NewSQL存儲以及強一致性MYSQL等等,都是原生基於Braft構建的其可容錯的高效有狀態分佈式系統;

圖8. 國內各大廠商在共識開源這塊熱情很高,幾乎人手一個代表作

3.2 國命縱橫

看完Raft的橫空出世,是不是有種「物理學的大廈已基本建成」的感覺?那不能夠。無論是代理模式的Multi Paxos,抑或是強主模式的Raft,都是選擇引入Leader來解決提議衝突問題,這個事實上也就產生了性能、穩定性的單點瓶頸:一旦Leader掛掉,整個系統停止服務進入重新選舉階段,在新Leader產生之前這段時間即爲系統不可用窗口。那麼,是否存在能夠有效解決提議衝突,同時又能夠去Leader依賴的更優雅的方案呢?所謂“你有張良計,我有過牆梯”,正如戰國時代的合縱連橫一般,很多研究工作者在孜孜不倦地尋找、探索去Leader依賴或者是弱Leader依賴的共識協議。該方向比較有代表性的工作有Fast Paxos,Generalized Paxos, Multicoordinated Paxos,Mencius,EPaxos等等。這裏我們挑選Generalized Paxos,Mencius以及EPaxos給大夥兒介紹一下。

Generalized Paxos很值得一提,它同樣是來自Lamport大神的作品。這個共識協議源於對實際系統的觀察:對於複製狀態機而言,通過一輪一輪強共識敲定每一輪的值,然後業務狀態機拿到的日誌序列完全一樣,這樣當然沒什麼毛病,但是約束過於強了。如果某些併發提議請求之間並不相關,譬如KV存儲中修改不同KEY的請求,那麼這些併發請求的先後順序其實並不會影響狀態機的最終一致性,此時就沒有所謂提議衝突這一說,那麼這裏是不是就不需要引入Leader來做提議仲裁了?回顧一下,Multi Paxos定義了三類角色:Proposer,Acceptor,Learner,並引入Leader多協議仲裁。在Generalized Paxos協議中,Proposer直接發送提議請求(包括提議的值)給Acceptors,每個Acceptor會把收到的提議請求的序列發送給Learners。每個Learner獨立處理接收到的提議請求序列,根據請求衝突判定規則,如果能夠確認某些請求沒有仲裁的必要並且已經形成多數派,那麼Learner將這些提議請求直接應用於狀態機,整個過程2個RTT搞定;如果請求之間存在衝突需要仲裁,那麼Leader會主動參與進來,進一步給Learners再發一跳請求,指導它們行爲一致地定序衝突提議並應用至業務狀態機。整個過程起步得要3個RTT才能搞定。實際生產系統裏面,直接應用Generalized Paxos的很少。不過,我們認爲它更多提供了一個創新思路:通過挖掘生產系統中請求之間的不相關性,可以加速決議提交,弱化對Leader的強依賴。

接着來說一說Mencius,取自春秋戰國時期儒家大師孟子之名。這個共識協議的取名就很討喜,我們都記得正是孟子提出的「民爲貴,社稷次之,君爲輕」的思想,這個是不是跟弱化Leader依賴這個課題很契合?!該共識協議的想法也很樸實:如果單個Leader是性能以及穩定性的瓶頸,那麼就讓每個副本通過某種策略輪流成爲某些輪次的Leader。在同構的環境裏面,這種Leader輪轉策略可以最大程度分攤訪問壓力,提升系統整體吞吐,很好很強大,對不對?不過,這裏的問題也很明顯:在實際系統中,某些副本會變慢甚至掛掉,那麼這種輪轉機制就運行不下去了,怎麼辦?Mencius的解決方案是,找其它副本臨時替個班,其它副本能夠通過發送NO-OP請求的方式(這類請求在經典的Basic Paxos論文中就提到過,不影響業務狀態機的狀態,可以認爲是佔位某輪提議),爲出問題的副本跳過當值輪次的請求提議,等待出問題的副本恢復。總的來說,Mencius的輪轉值班依次提議可謂是弱化Leader依賴的一次很不錯的嘗試,並且很大程度上啓發了後來共識協議的演進。現在生產系統裏面用到的支持分區的Raft共識協議,靈感很大程度即來源於此處。

圖9. EPaxos引入二維序號空間解決了提議衝突,焦點轉移到多副本如何一致定序

上面這兩個共識協議,Generalized Paxos在挖掘請求之間的不相關性,Mencius則是在所有副本之間輪轉Leader,方案不一,都是爲了弱化單點瓶頸。不過,要說無主共識協議的集大成者,繞不開Egalitarian Paxos,簡稱EPaxos。EPaxos引入了二維序號空間,讓每個節點都可以獨立的、無競爭的提議請求。當然,分佈式領域有一句至理名言:複雜度不會消失,只會轉移。EPaxos面臨的挑戰轉換成了如何讓多個副本能夠一致地給提議請求的二維序號進行線性定序。換句話說,Paxos/Raft定義的共識是針對一個具體的值的,EPaxos重定義的共識是針對一系列請求的提議順序的,並且進一步巧妙地將這個過程拆分成兩階段:a)針對一個提議請求,以及其依賴項(在最終全局順序中必須要遵守的線性依賴)達成共識;b)異步地,針對一系列值以及依賴項,做個確定性的、全局一致的線性排序。

以圖10爲例,副本R1提議請求A,收集多數派情況沒有發現有依賴關係,直接發送COMMIT消息;而副本R5發送的提議B請求,多數派回覆收集回來發現有B依賴A的關係,那麼再將B->{A}這個依賴作爲ACCEPT請求廣播給其餘副本。在收到多數派相應之後(請求之間依賴關係是面向過去的,因此多數派反饋的依賴合集就可以作爲決議直接使用),廣播COMMIT給各個副本。進一步地,副本R1再提議請求C,多數派響應都是C->{B, A},那麼也是直接達成共識,廣播COMMIT給各個副本。

圖10. EPaxos需要一個額外排序過程得到確定性的、全局一致決議序列

當請求提議以及它的依賴項達成共識之後,請求就算提交成功,可以返回客戶端了。但是,此時請求決議在全局請求序列中的順序還沒有最終確定,無法直接應用至業務狀態機,因此,EPaxos還需要一個額外的排序過程,對已提交的決議進行排序。當決議以及其依賴集合中所有決議都已經完成提交後,副本就可以開始排序過程。我們在圖10中給出了一個示例:將每個持久化的決議看作圖的節點,決議之間的依賴看作節點的出邊,因爲每個二維序號對應的決議和依賴項是達成全局共識的,即圖的節點和出邊在各副本之間已經達成了一致,因此各副本會看到相同依賴圖。接下來對決議的排序過程,類似於對圖進行確定性拓撲排序,結果即爲提議請求的線性順序。

需要注意的是,如圖10,請求決議之間的依賴可能會形成環,即圖中可能會有環路,因此這裏也不完全是拓撲排序。爲了處理循環依賴,EPaxos對請求決議排序的算法需要先尋找圖的強連通分量,環路都包含在了強連通分量中,此時如果把一個強連通分量整體看作圖的一個頂點,則所有強連通分量構成一個有向無環圖,然後對所有的強連通分量構成的有向無環圖再進行拓撲排序。尋找圖的強連通分量一般使用Tarjan算法,這是一個遞歸實現,在我們實踐過程中發現這裏的遞歸實現很容易爆棧,這點將給EPaxos的工程應用帶來一定挑戰。此外,隨着新的請求提議不斷產生,舊的提議可能依賴新的提議,新的提議又可能依賴更新的提議,如此下去可能導致依賴鏈不斷延伸,沒有終結,排序過程一直無法進行從而形成新的活鎖問題,這個是EPaxos工程應用的另一大挑戰。

總結一下,無主共識領域的探索方向工作還蠻多,有挖掘請求之間不相關性的,有副本間輪轉請求提議權的,還有引入二維序號空間將問題轉移成異步線性定序的 ...... 上述這些無主方向的技術探索,最終工業落地的不多,我們認爲這個跟該方向的協議普遍比較複雜有很大關係,有點高射炮打蚊子的味道。不過,必須要補充一句,業界完全是需要這類探索存在的。事實上,正是Generalized Paxos這類挖掘請求之間不相關性的工作,啓發了Paralle Raft這些工業界的嘗試;正是Mencius這類輪轉請求提議權的工作,啓發了多分區的Raft這些工業界的嘗試;EPaxos更是在很大程度上啓發了後面介紹的「異步共識」這個流派 ...... 所以怎麼說呢,每一次嘗試都是成功的序曲。理想還是要有的,萬一實現了呢?

3.3 金戈鐵馬

「金戈鐵馬」這個命名可以用來形容共識領域的「靈活派」。共識協議從提出之初,在AZ內的有狀態的分佈式系統中可謂大殺四方。隨着業務系統的快速發展,逐步演進出多AZ容災甚至是跨地域容災的需求。那麼,此時的共識協議還能應對自如嗎?畢竟印象中傳統共識協議的最佳應用場景還得是同構環境的,多AZ以及跨地域則是引入了異構場景,共識協議該如何征戰這片沙場?

「靈活派」的開山之作被稱爲Flexible Paxos,它的靈感來自一個非常簡單的觀察:假設共識協議中成員總數是N,PREPARE階段參與的成員數是Q1,ACCEPT階段參與的成員數是Q2,那麼Basic Paxos正確性保障最本質的述求是Q1與Q2兩個階段要有重合成員,以確保信息可以傳承。滿足上述要求最直觀的機制是Q1與Q2兩個階段都選擇多數派(majority)成員參與。可是,我們發現只要保證PREPARE與ACCEPT兩個階段參與的成員數超過成員總數,即Q1+Q2>N,那一樣能夠滿足Q1與Q2兩個階段要有重合成員的要求,並且這裏的參數選擇挺多,也很靈活,是不是?嗯,「靈活派」所有的故事就是從這裏開始講起的 ......

也許有人會說,是不是閒的慌,調整這兩參數有啥意義?別急,我們回顧一下這兩個階段,PREPARE階段事實上做的是Leader選舉,ACCEPT階段則是做日誌同步,前者可以認爲是管控依賴,後者是IO依賴,那麼我們是否可以把Q1調大,而Q2調小?這樣IO路徑依賴可以更加高效更加可靠。還是沒有心動的感覺??好吧,再看一個更加具體的例子,如圖11所示,我們非常熟悉的3 AZ場景,處於業務IO鏈路依賴的角色通常會選擇“3+3+3”部署模式,即Q1=Q2=5,實現「掛1個AZ+掛另一個AZ的1個節點」這樣高標準容災能力。3 AZ部署講求個對稱結構,如果“2+2+2”部署模式,對於傳統的Multi Paxos來說,相較於“2+2+1”不會增加額外容災能力,ACCEPT階段還得多同步一個節點,喫力不討好。而在“2+2+2”部署模式下,我們選用Flexible Paxos的話,Q1不妨設置爲4,Q2不妨設置爲3。那麼首先這種模式能夠抗住掛1個AZ,即使此時故障AZ內有Leader節點,剩餘AZ內的節點也能夠迅速選舉出新的Leader,並且在正常ACCEPT階段,該部署模式同樣具備「掛1個AZ+掛另一個AZ的1個節點」這樣高標準容災能力。感覺到Flexible Paxos的美了嗎?我們的觀點是,Flexible Paxos思想指導下的“2+2+2”模式是非常適合一些有狀態的管控類服務選用爲3 AZ部署方案的。

圖11. 面對越來越常見的3 AZ場景,逐步發現了Flexible Paxos的美

事實上Flexible Paxos還提供了一個思路,可以選擇性的確定參與PREPARE階段的成員列表與參與ACCEPT階段的成員列表,這裏連Q1+Q2>N這個條件都不需要,而放鬆至僅僅需要Q1與Q2有重合成員即可,這種成員列表確定思路也被稱爲GRID QUORUM模式,這種模式跟跨地域場景非常的搭。後續研究工作者相繼推出的DPaxos,WPaxos都是這個思路指導下的共識協議跨域容災方面的最佳實踐。這裏我們介紹一下WPaxos。該共識協議明確引入了AZ容災以及節點容災兩個維度的容災標準,假定參數fn是一個AZ可以容忍的掛的節點數,fz是可以容忍掛的AZ數目,我們來看看WPaxos原文《WPaxos: Wide Area Network Flexible Consensus》是怎麼進行參數選擇的。論文裏面也有詳細證明爲什麼這樣的選擇下,雖然Q1+Q2不大於N,但是Q1與Q2一定是有交集的,這裏證明我們就不展開介紹了,主要看看參數是如何選擇的:

In order to tolerate fn crash failures in every zone, WPaxos picks any fn + 1 nodes in a zone over l nodes, regardless of their row position. In addition, to tolerate fz zone failures within Z zones, q1 ∈ Q1 is selected from Z - fz zones, and q2 ∈ Q2 from fz + 1 zones.

繼續舉個具體的例子來生動地理解WPaxos的意義吧。譬如有些區域如果是2 AZ,怎麼做容災呢?我們可以在臨近一個區域選擇一個AZ,這樣也是一個3 AZ部署。如圖12所示,假如AZ1,AZ2是本地域的2 AZ,而AZ3是跨地域的AZ。按照WPaxos的協議約束,我們可以設定9節點部署,每個AZ部署3個節點。基於“掛1個AZ+掛另一個AZ的1個節點”這個容災能力的約束,參考上面的WPaxos的參數選擇機制,我們可以設定:fz = fn = 1,即Q1是“任意2 AZ,每個AZ任意2個節點”,Q2亦如此。這樣的設定真的跟跨地域容災很搭:無論是PREPARE階段,還是ACCEPT階段,提議者都是給所有的節點發送提議或者決議請求,因爲就近處理的延時比較低,很大概率我們會先迎回來AZ1/AZ2的請求迴應。因此,在常態情況下,我們的IO請求都會在本區域完成。當然,選舉也是可以在本區域即可完成。當發生AZ1或者AZ2掛掉的災難場景,那麼兩個階段的參與者會自動切換到AZ2/AZ3,或者是AZ2/AZ3,此時延遲會有所增加,但是服務可用性還在。

圖12. 進一步地,共識協議WPaxos在跨地域場景下的最佳容災實踐

AZ容災甚至是地域容災越來越多地出現了雲產品的建設要求裏面,我們相信,「靈活派」共識協議的靈活設計思路會在容災場景下發揮着越來越大的作用。

3.4 陽謀春秋

《大秦帝國》的「陽謀春秋」這個篇章主要講述了六國統一者嬴政上位之前,秦國連續經歷的三次重大交接危機的這段恢弘曆史。共識領域裏「異步共識」發源點應該說是EPaxos (SOSP'13),前面章節也詳細介紹過,理想很美好,實際落地卻存在諸多挑戰。在EPaxos提出之後,領域裏面也一直陸陸續續有研究工作跟進,譬如來自OSDI'16的Janus。一直等到了2021年,這一年可謂是「異步共識」門派發展歷史上里程碑式的一年,該方向的研究成果在各大頂會遍地開花(有來自SOSP'21的Skyros,Rabia;有來自NSDI'21的EPaxos Revisited;以及來自EuroSys'21的Tempo)。正如陽謀春秋字面的意思,如果把傳統的Paxos/Raft等共識協議都視爲同步共識協議的話,那麼「異步共識」算是站到了對立面,並且的確找到了能夠最大發揮其優勢的典型應用場景。

我們首先來介紹下「異步共識」是如何開腦洞的。傳統的共識協議就像一個複雜精密的儀器,如圖13所示,一個請求從提交至返回,大體會經歷三個階段:1)請求會被複制給多個副本,這一個階段是爲了保證數據持久性(durability);2)每個請求都會被指定一個全局唯一,單調遞增的序號,這一個階段是爲了保證請求之間的線性順序(linearizability);3)請求會被最終在業務狀態機執行保證數據外顯性(externality)。注意,這個階段是根據前一階段確定的順序依次執行。這麼一個流程執行下來,一個請求至少要2 RTT才能返回,並且因爲併發請求線性定序天然會存在衝突導致的效率問題,傳統共識協議的最佳實踐就是要引入一個Leader來做高效定序,這個就反過來讓請求提議階段存在服務單點的問題。所以「異步共識」的陽謀就是:用戶提議的請求僅需經歷數據複製階段即返回,服務端異步地執行請求的線性定序以及決議執行階段。這樣解構的好處是可以專項優化數據複製階段的延時,甚至實現無主的高可用。當然,這裏的難點是,在數據複製階段,請求要留下足夠的痕跡,這些痕跡要足夠支持服務端僅根據大部分副本即可異步地恢復出請求之間正確的線性順序。異步共識最大的挑戰就在於此,目前業界複製留痕有三大思路:1)依賴請求落在每個副本上的偏序;2)依賴複製階段加鎖強定序;3)引入時間戳,給每個請求分配單調遞增的時間戳。

圖13. 根據請求類型是否需要「即時外顯」,我們可以把共識分爲同步與異步兩類

「異步共識」這裏其實蘊含有一個強假設,實際分佈式系統中,是否存在這樣的寫請求,它不需要即時執行狀態機,只要確保該請求被持久化了就可以返回。咋一聽還是有些反直覺的,不過事實上實際存儲系統中這類寫請求接口並不少見。最典型的是Key/Value存儲系統,譬如Put接口,並不需要即時APPLY,即返回客戶端的時候並不需要告知相應的KEY是否已經存在;譬如Delete接口,也並不需要即時APPLY,即返回客戶端的時候並不需要告知相應的KEY是否並不存在。業界類似的系統,包括有RocksDB,LevelDB等LSM-Tree結構的存儲系統,還有Memcached裏面的Set請求,ZippyDB裏面的Put/Delete/Merge等主要接口均是如此。認識到這些,相信讀者朋友們也會認同「異步共識」的出發點,也能夠看清「異步共識」想要打的靶子。

在正式介紹「異步共識」的各類協議之前,我們要重新學習一下何謂請求的線性序。在傳統共識協議的強主模式下,所謂線性序來的是理所當然,但是在異步共識之下,我們要嘗試恢復並保持請求之間的線性序,自然,我們要非常明確它的要求。下面這個摘自EPaxos對於線性序的定義:如果兩個請求γ和δ,如果δ是在γ被確認形成決議之後才發起的,那麼我們就稱δ是線性依賴的γ,每個副本狀態機都需要先執行γ,再執行δ。

Execution linearizability: If two interfering commands γ and δ are serialized by clients (i.e., δ is pro- posed only after γ is committed by any replica), then every replica will execute γ before δ.

這個章節以SOSP'21的Skyros爲例來介紹異步共識,它是屬於依賴偏序恢復線性序的流派。我們認爲這篇文章在提取異步共識的應用場景這方面做的非常出色。如下圖14所示,Skyros的思路很簡單,在同步複製階段引入一個Leader,客戶端直接寫包括Leader在內的super-majority的副本,一旦返回則請求完成。在正常情況下,後臺以Leader的請求順序作爲執行順序,依照該請求順序執行狀態機;在Leader節點掛掉的情況下,剩餘副本中選舉出新的Leader,收集存活副本中請求的偏序集合,嘗試恢復請求之間實際的線性序(這裏的做法很大程度上會參考EPaxos的圖排序算法),然後繼續履行Leader的職責。通過這樣的設計,Skyros實現了極致的一跳請求決議提交延遲,並且這個方案看起來非常簡單,是不是?

圖14. Skyros明確提出推遲定序以及執行過程,極致優化提議過程

絕大多數時候,現實沒有想象的那麼美好,當然,也沒有想象的那麼糟糕。Skyros美好的有些不真實。事實也如此,在Skyros的機制裏面,很大篇幅是在解決一旦Leader掛掉之後,新當選的Leader如何恢復出已經持久化的請求之間的線性順序,這裏實際新的Leader能夠利用的信息只有請求在每個副本上的偏序關係,但是我們發現這些信息並不足以支撐線性順序的恢復!圖15給出了一個我們用來證僞Skyros的簡單例子,有三個請求W1,W2,W3,有四個副本R1,R2,R3,R4,其中R4是Leader,從實際事實來看,可以確定是W1是先發生的,線性順序早於W2,而W3則是跟W1以及W2完全併發的一個請求。此時如果副本R4掛掉了,試問,此時根據什麼信息能夠推斷得出這個實際的線性序。畢竟,此時根據剩餘節點上請求的偏序關係,你得到了W1,W2,W3之間的循環依賴,並且,沒有更多的信息能夠支持破除這個循環依賴!

圖15. Skyros的依賴偏序對來推導線性序的做法有概率存在循環依賴,無法破環

來自EuroSys'21的Tempo找到了可能是解決異步共識裏線性定序難題的最佳方案:引入邏輯時間戳。我們即便從直觀上感受,也會覺得邏輯時間戳的方案的確更加高效,畢竟相較於EPaxos/Skyros動不動就給你上個圖排序,邏輯時間戳的直接比大小的線性定序方法要簡單很多。在Tempo中,每個提議請求會有指定的Coordinator來負責最終決定請求的邏輯時間戳,它收集各副本已經分配的邏輯序號情況,確定具體分配哪個時間戳序號給相關請求,並且這樣的分配要確保後來的請求不會被分配更小的邏輯序號。對於可能發生的提議衝突的情況,Tempo參考了Fast Paxos來解決,多走幾輪共識給有衝突的提議請求提供確定、線性的邏輯時間戳分配。思路大體如此。Tempo引入時間戳的做法很有新意,但是一方面引入單獨的Coordinator角色,另一方面依賴Fast Paxos仲裁提議衝突。整體的協議複雜度又上來了,不利於工業落地實現。

3.5 鐵血文明

共識協議對於有狀態的分佈式系統至關重要,現實的情況是,一個有狀態的分佈式系統可能花費了九牛二虎之力才把一個共識協議實現正確並且有效運維起來。但是接下來面臨的問題是,這個寶貴的積累很難直接複製到另外一個系統,並且這個系統也就綁定上了一開始選擇的共識協議,幾乎沒有可能繼續演進迭代至全新的共識協議。以Apache ZooKeeper爲例,其至今使用的共識協議還是十多年前的ZAB,並且該共識協議也沒有被ZooKeeper以外其它系統所採用。我們看看其它領域,像OS操作系統,像Network網絡系統等,這些系統的模塊化以及清晰的分層設計使得新的機制甚至是現有機制的迭代都能夠以可插拔的方式被快速引入,促進了這些技術領域的蓬勃發展。看着真是眼饞呀,我們不禁要問,共識協議能夠實現這樣可移植,可插拔,熱升級嗎?當然,這個其實也就是Corfu/Delos等共識協議發力的點,目標Replication-as-a-Service,打造出屬於共識協議的的鐵血文明!

首先要探討一個問題,我們能否以LIB形式提供通用一致性引擎庫?答曰,很難。不可行的原因是各種共識協議譬如Multi Paxos,Raft,ZAB以及EPaxos等等,語義差別還是很大的,很難給出一個通用的接口能夠兼容現有的這些共識協議。繼續來探討第二個問題,使用確定的某個共識協議的一致性引擎庫,成本高嗎?答曰,很高。雖然說像Braft等開源庫在接口設計上已經儘量簡潔了,但是作爲一個庫,自身沒有狀態,必然要暴露狀態管理的細節,譬如角色信息、成員信息、甚至鏡像信息等給到業務進程並與之耦合,包括狀態機的runtime consistency檢查等等,所以要用正確用好任何一個開源的一致性引擎庫,是需要業務長年累月地打磨投入的。最後再探討一個問題,作爲一致性引擎庫,能夠支持持續演進包括協議的更新換代嗎?答曰,至今沒見過 ......

圖16. Share Log對共識協議做了抽象,隱藏了實現細節,有利於模塊解耦獨立演進

聊回這個章節的主角,Share Log,這個我認爲也是NSDI’12 Corfu這篇文章最爲出彩的貢獻,爲共識協議提供了一個簡單樸素而又精準的抽象(這裏面學習到一個道理:當我們對一個問題無法給出通用解釋的時候,那麼一定是抽象的還不夠),答案就是日誌序列,這個日誌序列是可容錯,強一致,支持線性定序的可追加日誌序列。如圖16所示,Share Log提供的APIs有如下四個:

  • O = append(V):追加日誌V,返回日誌所佔據的邏輯序號O;
  • V = read(O):讀取邏輯序號O對應的日誌;
  • trim(O):標記邏輯序號O(包括之前的序號)對應的日誌不再需要,可清理;
  • check():返回下一個可寫的邏輯序號O;

相當於,上層業務狀態機基於Share Log的接口來實現具體業務邏輯,無需關心持久化、線性保持、一致性等難點技術。以複製狀態機模型爲例,業務狀態機直接向底層的Share Log追加日誌,Share Log內部使用共識協議將日誌複製到其它節點,業務狀態機從Share Log中學習最新日誌,依次應用到狀態機。基於這層抽象,共識協議的細節被隱藏在Share Log內部,業務邏輯和共識協議可以獨立演進,互不影響,並且Share Log可以做成通用模塊,移植給不同的業務使用。事實上,Share Log這層抽象是那麼的簡單樸素並且準確,類似這種機制已經成爲了現代分佈式系統設計的基石,我們來看到Share Log廣泛應用的三大場景:

  • Pub/Sub:共享日誌其中一大應用場景是作爲可擴縮的異步消息傳遞服務,將生成消息的服務與處理這些消息的服務分離開來。這種解耦的設計大大簡化了分佈式應用開發,Pub/Sub在生產系統中被廣泛部署應用。各大雲廠商也都推出了消息隊列的重磅產品,譬如Alibaba MQ,Amazon Kinesis,Google Pub/Sub,IBM MQ,Microsoft Event Hubs以及Oracle Messaging Cloud Service。消息隊列對日誌需求是持久性(durability),唯一定序(uniquely ordered)以及線性定序(linearizability)。Share Log自然能夠滿足此類場景需求。分佈式存儲系統在做跨地域容災的時候,異步複製方案通常要求對IO鏈路的影響降到最低,同時儘可能地降低RPO,此時標準的技術選型就是基於消息隊列來拖Redo Log;
  • Distributed Journal:LSM-Tree在現代存儲系統中得到了廣泛應用,該這裏面關鍵的journaling機制很好地解決了寫放大問題,每次僅持久化增量的更改數據,而不需要全量數據持久化,同時通過嚴格順序寫入的設計大幅提升了寫入性能,對於寫多讀少的場景尤其的友好。在分佈式場景下,journaling也會升級爲一個append-only的分佈式日誌系統,類似GFS、HDFS等等。這正是Share Log主打場景。根據業務請求是否需要即時外顯,又細分出兩個場景:1)要求即時外顯,那麼這裏的append-only的分佈式日誌系統必須單寫,即同一時間某個文件最多隻允許一個客戶端操作,否則狀態機APPLY的請求序列與持久化的日誌序列可能不一樣,failover場景下數據一致性將被打破。此時分佈式日誌對日誌的需求是持久性(durability),外顯性(externality),唯一定序(uniquely ordered),以及線性定序(linearizability);2)不要求即時外顯,這裏的append-only的分佈式日誌系統需要支持多寫定序,最大程度提升訪問吞吐,降低請求延遲。此時分佈式日誌對日誌的需求是持久性(durability),可定序(orderable)以及線性定序(linearizability);
  • Replicated State Machie:如前文所言,複製狀態機模型是當前有狀態的分佈式系統廣泛採用的容錯機制。正如圖1裏面描述的,Share Log可以面向複製狀態機提供線性單調,強一致,可容錯的日誌序列,再合適不過了。複製狀態機對日誌的需求是持久性(durability),外顯性(externality),唯一定序(uniquely ordered),以及線性定序(linearizability)。事實上,傳統的消息中間件Apache Kafka,Apache Pulsar同樣能夠勝任此職責(所謂藝多不壓身,Kafka這類中間件已經打造的十八般武藝樣樣精通),業界很多數據庫公司也是基於消息隊列解決了同步複製的需求。該場景下,Share Log處於業務的直接IO依賴,事實上是跟業務耦合了,因此會產生新的挑戰。以基於分區調度架構的分佈式存儲場景爲例,其在依賴Share Log的時候,如何支持業務的分區變配(即保證變配前後日誌線性順序)就變得很棘手;

自從Corfu提出了Share Log這個概念之後,研究領域相繼推出Tango,Scalog以及Chariots等演進版本,工業界裏面CorfuDB,LogDevice,Aurora也都借鑑了類似Share Log的設計思想,實現了共識協議的平替。細心的讀者應該還記得我們的第三問,共識協議能夠支持可插拔,熱升級嗎?這個就要提到Corfu之後集大成者Delos了。事實上,共識協議自身也是存在控制與數據平面的,其中控制平面負責選主,元數據管理,成員變更,服務容錯等複雜能力,而數據平面僅僅需要定序以及數據持久化。類似Multi Paxos/Raft/ZAB等傳統共識協議耦合了控制與數據平面,事實上,Share Log對外展示的恰恰就是共識協議的數據平面,這個是可以解耦出來單獨演進的,而這也就是Delos裏面推出的所謂Virtual Log的思路。總結起來就是,讓傳統的共識協議負責控制平面的實現,而數據平面僅負責日誌定序及數據持久化,因此可以有各種更加高效的實現(有點套娃的意思,共識裏面套共識)。這個思路在業界並不新鮮,已經被廣泛採用,譬如Apache Kafka裏面即使用Apache ZooKeeper(新版本是自研的KRaft)來管理元數據的控制平面,使用ISR協議負責高吞吐靈活的數據平面;Apache BookKeeper(這個讀者可能會比較陌生,基於它長出來的EventBus,Pulsar,DistributedLog,Pravega等明星開源產品,你總聽說過一款吧)也是使用了Apache ZooKeeper來做控制平面管理,而自研的Quorum協議負責高吞吐的寫以及低延遲的讀;類似的例子還有很多,像Microsoft的PacificA也是採用管控數據平面分離的架構。

圖17. Facebook Delos的共識技術架構,在共識迭代這塊可插拔支持熱升級

當然,出身名門的Delos做了更多抽象,特別是創新性地在Share Log基礎API之上,引入SEAL能力。如圖17所示,Delos支持客戶端通過SEAL接口將前一個日誌序列禁寫(譬如原先是基於ZK共識協議實現的請求定序),進而開啓全新的可寫日誌序列(新的可以是基於Raft實現的請求定序),通過這樣的實現達到了Share Log底層的共識協議能夠熱切換熱升級的目的。這個是不是很好地解答了我們的第三問?

在這個章節的最後,我要特別推薦一下大名鼎鼎的Jay Kreps在2013年發表的大作《The Log: What every software engineer should know about real-time data's unifying abstraction》。這個文章真的非常不錯,值得大家耐着性子好好讀一讀。Jay Kreps把日誌這個概念講的非常清楚,以一個非常廣的視角揭示了日誌其實是很多分佈式系統最本質的東西,解釋了爲什麼日誌是更好的抽象(相對於共識協議本身)。如果用一段話來概括日誌對於分佈式系統的意義,那麼我認爲應該是文章裏的這段描述:

At this point you might be wondering why it is worth talking about something so simple? How is a append-only sequence of records in any way related to data systems? The answer is that  logs have a specific purpose: they record what happened and when. For distributed data systems this is, in many ways, the very heart of the problem.

事實上,作爲Kafka創始人之一,Jay Kreps在文中也預言了日誌最終應該會做成通用的產品組件,在分佈式系統中將被廣泛使用。有關日誌的深度思考,對於Jay Kreps自己也很重要,他個人命運的齒輪開始轉動 ......,很快推出了Kafka(這個應該不用多做一句介紹吧?),商業化也做的非常的成功(它的一個口號是:More than 80% of all Fortune 100 companies trust, and use Kafka ......,母公司Confluent的最新市值也已經達到了100億美金)。另外,有個有意思的小插曲,在發表這篇日誌博文的時候,Raft剛面世,Jay Kreps對這個協議非常的賞識,文中也強力推薦了Raft,真可謂英雄惜英雄也。

這個章節也聊了那麼多,總結起來是,Share Log對共識協議對了很好的抽象,標準化了共識協議對外的使用界面,實現了可插拔;進一步的抽象來自升級版的Virtual Log,通過控制平面與數據平面的分離,實現了共識協議熱升級的理想。如果說還能夠進一步再抽象的話,我們認爲那應該是以Kafka/Pulsar爲首的日誌複製服務所代表的Replication-as-a-Service(RaaS)的趨勢。隨着雲原生大潮的來臨,應用程序開發者一定需要學會借力開發,降低業務系統的複雜性,譬如可以使用RaaS解決狀態同步問題,可以採用OSS等元原生存儲解決狀態存儲問題,以及可以倚賴Etcd/ZooKeeper等組件解決狀態管理問題等等。

3.6 帝國烽煙

哇嗚,能堅持看到現在的你們,的確是共識協議的真愛粉。我們能夠領略到,這麼些年研究工作者們在跟隨着Lamport大師指引的方向上,逐步建立起了龐大的共識帝國。不過,以上我們介紹的所有研究方向都是軟件算法層面的優化。而在這個章節,我們將講述的是全新的軟硬一體化視角下的共識協議研究工作。在智能網卡,RDMA,可編程交換機等數據中心的各類技術快速發展的當下,探索共識協議硬件卸載(offline)的各種可能性,很多時候能夠打破我們的既定認知,帶來降維打擊般的改進效果。所以,這個章節,我們選用“帝國烽煙”作爲標題來提醒傳統的共識協議的研究人員。話說這個標題起的呀,一個字,絕!

大體上,我們可以把迄今爲止軟硬一體化方向的共識協議的研究工作分成三類:1)隨着數據中心技術的快速迭代,預期網絡可以提供更多的承諾,譬如可靠傳輸不丟包,譬如可線性定序的傳輸。基於這樣的假定,共識協議圍繞着異步網絡(信息傳輸回丟失,亂序,超時等等)做的很多複雜設計可以拿掉,進而更簡單更高效地達成決議;2)可編程的交換機技術的快速發展,提供了將整個共識協議的三階段能力都卸載到交換機上的可能性,這將相當於把共識變成網絡提供的原語服務。從提議請求的處理吞吐角度而言,交換機的能力顯然是要遠勝於節點的CPU能力,那麼可以預期共識吞吐可以獲得幾個數量級的提升;3)異步共識很適合存儲領域的分佈式日誌場景,其中數據複製邏輯簡單,對穩定性要求高,吞吐延遲的指標也很敏感,比較適合將這部分邏輯卸載到智能網卡里面實現。而線性定序以及狀態機執行者兩個階段邏輯複雜,可以保留在軟件層面靈活控制,異步地執行。

圖18. 隨着對網絡依賴的強弱變化,所謂共識的實現難度會發生巨大變化

我們首先來看第一類研究工作,增加網絡能力的承諾。我們知道,Lamport提出共識協議時候對於網絡的假設是完全異步的,也就是說信息傳輸可能會丟失,可能會亂序,也可能發生延遲抖動甚至超時等等。基於這樣的假設,Paxos等共識從協議層面增加了各種設計,保證了一致性的性質,但是也增加了軟件複雜性並且存在執行效率的瓶頸。必須得說,Lamport的異步網絡假定對於Internet網絡是合理的人設設定。不過當前數據中心內的網絡狀態顯然要可控多了,能否提供一些可靠性的保證呢?如圖18所示,事實上,如果網絡側能夠做出更多可靠性承諾,共識達成的難度會降低很多。這個方向已經有很多相關研究成果,譬如NetPaxos,Speculative Paxos,NOPaxos,Hydra等。這裏我們着重介紹一下來自OSDI’16的NOPaxos(注意,這個取名是抖了個機靈,實際是指Network Ordering Paxos),它的假定即網絡可以提供可靠的定序保證,雖然可能會丟失一些鏈路的請求包。

圖19. NOPaxos定義的支持全局定序的請求廣播接口,相關功能卸載到交換機實現

具體來說,NOPaxos定義了一組名爲OUM(Ordered Unreliable Multicast)的網絡接口,通過可編程的交換機(例如P4)在交換機硬件上直接支持這樣的廣播接口,如圖19。在指定的網絡定序者(最佳選擇是基於可編程交換機實現)爲每個OUM組都維護了一個計數器,在每個轉發過來的OUM請求的包頭填充嚴格連續遞增的計數。最終達成的效果如圖20所示,包括Leader在內的各個副本收到的請求是嚴格有序的,正常情況下都不需要有協同動作,非常高效。每個副本自己會檢查是否發生丟包,如果發生了丟包,非Leader副本向Leader學習具體丟失的請求包;Leader副本則通過NO-OP請求走Basic Paxos來學習丟失的請求包。另外,客戶端一旦收到包括Leader在內的多數派副本的請求回覆,即可認爲請求完成,這裏僅僅需要一跳網絡延遲。

當然,NOPaxos這個工作存在一個設計上的硬傷,即作爲定序的交換機是個單點,這個就導致了系統在可擴展性、容錯性以及負載平衡等諸多方面存在限制。這個工作的延續是Hydra,進一步引入多臺交換機來做定序,緩解了上述的單點瓶頸。可能讀到這裏,讀者朋友們會有點驚訝,看了共識領域其他門派各種精巧複雜的協議設計,這個來自OSDI'16的NOPaxos共識協議是不是有點簡單過頭了?好吧,的確有點降維打擊的味道,事實上,如果網絡定序本身不是問題的話,複製的難度的確沒有那麼大。對該方向感興趣的讀者朋友們可以繼續研究下Hydra,Speculative Paxos等工作,感受下更可靠的網絡是如何有效提升共識的。

圖20. NOPaxos利用交換機的定序能力實現了高效一跳共識,容錯、成員變更等能力由軟件側保障

我們繼續看第二類研究工作,整個卸載掉共識的能力。這個方向的代表作是P4xos,看這個協議的名字就明白是把共識的能力直接給卸載到了P4可編程交換機。如圖21所示,P4xos的工作是在數據中心基於傳統的三層網絡架構來設計的,所有的請求會經過ToR(Top of Rack)交換機,Aggregate匯聚交換機,以及Spine核心交換機。基於這種網絡架構,我們看到傳統的Paxos共識協議從Proposer提議到Leader學到決議共經歷3/2 RTT。P4xos把三層網絡架構下的交換機都安排成共識協議中的角色:Spine核心交換機做Leader定序,Aggregate匯聚交換機做Accept接收決議,ToR交換機則做Learner學習決議。整個鏈路下來僅需要RTT/2。我想聊到現在,大家應該也有點感覺到傳統共識協議的瓶頸之所在:從延遲上來講,主要消耗在網絡傳輸;從整體吞吐上來講,主要瓶頸在單機CPU處理能力。P4xos通過將共識三階段能力整體卸載到P4交換機,相較於傳統共識協議,不僅僅是延遲得到了明顯改善,整體的共識吞吐能力有了質的飛躍(P4xos的論文中宣稱有4個數量級的提升)。當然,這裏選用Spine核心交換機做Leader定序同樣存在單點問題,需要引入傳統共識協議的重新選舉過程。

圖21. P4xos嘗試全面卸載Paxos的共識協議,實驗展示了驚人的優化效果

軟硬一體化方向的第三類研究工作,跟前文提到的異步共識的工作緊密相關。回顧一下,異步共識將複雜的線性定序以及狀態機執行等邏輯推遲處理,而用戶的請求僅僅在前面簡單的複製階段完成即可返回。這個思路跟硬件卸載的極簡主義的原則非常契合,實現了存儲系統的核心IO鏈路和周邊功能分離,並且有效防止故障擴散,在整個集羣管控都不可用的情況下,IO請求還是可以正常工作。所以試着YY一下,下一代存儲的技術選型會不會就長這樣?即將複製階段卸載到智能網卡,而將定序、轉儲的工作放在存儲軟件側,從而實現極致高吞吐,極致低延遲,穩定高可靠?

隨着時代的演進,軟硬一體化技術還在快速迭代,我們非常期待並也積極參與了其中。“莫道桑榆晚,爲霞尚滿天”,我們相信共識協議能夠搭上軟硬一體化這列時代的高鐵,繼續在未來有狀態分佈式系統的構建中發揮着中流砥柱的作用!

四、尾聲

寫到最後,我們也不得不感慨,共識真的很重要,共識領域也真的是研究紅海,但凡你能想到的方面都有一堆研究工作者在涉獵,這個恰恰也驗證了共識協議對於分佈式系統的至關重要性。寫這篇文章,很大一部分目的也在於,告訴更多的分佈式系統的從業者,共識領域的研究成果絕對不僅僅只有Paxos/Raft。事實上,隨着業務的蓬勃發展,分佈式領域的越來越豐富的場景都對共識協議提出了更多更新的要求,這裏也絕非依靠Paxos/Raft就可以一招鮮喫遍天的。這個世界很大,大到足以容納Paxos,Raft,EPaxos,FPaxos, Skyros,P4xos ......

圖22. 希臘羣島被各種類型的共識協議拿來命名,有點不夠用了 . . .

最後說一個有趣的現象,從早先年Leslie Lamport考古希臘羣島,並以其中的Paxos小島命名初始的共識協議開始,這個小島成爲了共識領域的聖地,至今圍繞着Paxos島的很多希臘小島也都被用來命名新的共識協議,譬如Corfu,Delos,Skyros,Hydra等等。所以,加油吧!希望共識領域保持着當前推陳出新的旺盛勢頭,儘早把希臘羣島的名字給用光了:)

作者|朱雲峯

點擊立即免費試用雲產品 開啓雲上實踐之旅!

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章