分散式共識演算法

尹瑞星發表於2022-03-22

背景

分散式共識演算法主要目的是為了保證同一份資料在多個節點上的一致性,以滿足CP要求。

共識(Consensus)與一致性(Consistency)的區別:一致性是指資料不同副本之間的差異,而共識是指達成一致性的方法與過程。由於翻譯的關係,很多中文資料把 Consensus 同樣翻譯為一致性,導致網路上大量的“二手中文資料”將這兩個概念混淆起來,如果你在網上看到“分散式一致性演算法”,應明白其指的其實是“Distributed Consensus Algorithm”。

如果你有一份很重要的資料,想要確保它不會丟失,你會怎麼做?

你可能會買幾塊硬碟或U盤,把資料複製上去。

而在分散式系統中,我們做法也是一樣的,通過備份,但是這種環境下,資料是動態的,各個備份系統之間的網路是不可靠的,也就是複製可能會失敗。所以引出了以下問題:

如果你有一份隨時變動的資料,要確保它正確地儲存在網路中的幾臺不同的機器上,你會怎麼做?

你可能會想到資料同步:每當有資料變化,把變化的資料在各個節點之間複製視作一種事務操作,所有機器都成功寫入硬碟後,才宣告資料同步完成。這種可以保證節點間的資料是絕對一致的,但可用性大大降低。這種以同步為代表的資料複製方法,稱為狀態轉移(State Transfer),通常要犧牲可用性。

如果你有一份隨時變動的資料,要確保它正確地儲存在網路中的幾臺不同的機器上,並且要儘可能保證資料隨時可用時,你會怎麼做?

為了緩解C和A之間的矛盾,在分散式系統裡主流的資料複製方法是以操作轉移(Operation Transfer)為基礎的。能夠使用確定的操作,促使狀態間產生確定的轉移結果的計算模型,在電腦科學中稱為狀態機:任何初始狀態一致的狀態機,如果執行命令序列一樣,最終的狀態也一樣。根據狀態機的特性,要讓多臺機器的最終狀態一致,只要確保它們的初始狀態是一致的,並且接收到的操作指令序列也是一致的即可。

考慮到分散式環境下網路分割槽現象是不可能消除的,甚至允許不再追求系統內所有節點在任何情況下的資料狀態都一致,而是採用“少數服從多數”的原則,一旦系統中過半數的節點中完成了狀態的轉換,就認為資料的變化已經被正確地儲存在系統當中,這樣就可以容忍少數(通常是不超過半數)的節點失聯,使得增加機器數量對系統整體的可用性變成是有益的,這種思想在分散式中被稱為Quorum 機制

拜占庭將軍問題

拜占庭將軍問題(The Byzantine Generals Problem)提供了對分散式共識問題的一種情景化描述,由Leslie Lamport等人在1982年首次發表。拜占庭將軍問題是分散式系統領域最複雜的容錯模型, 它描述瞭如何在存在惡意行為(如訊息篡改或偽造)的情況下使分散式系統達成一致。是我們理解分散式一致性協議和演算法的重要基礎。

拜占庭將軍問題描述了一個如下的場景,有一組將軍分別指揮一部分軍隊,每一個將軍都不知道其它將軍是否是可靠的,也不知道其他將軍傳遞的資訊是否可靠,但是它們需要通過投票選擇是否要進攻或者撤退(少數服從多數原則)。

當裡面存在叛徒時候,將會擾亂作戰計劃。圖展示了General C為叛徒的一種場景,他給General A和General B傳送了不同的訊息,在這種場景下General A通過投票得到進攻:撤退=1:2,最終將作出撤退的行動計劃;General B通過投票得到進攻:撤退=2:1,最終將作出進攻的行動計劃。結果只有General B發起了進攻並戰敗。

img

在分散式系統領域, 拜占庭將軍問題中的角色與計算機世界的對應關係如下:

將軍, 對應計算機節點;忠誠的將軍, 對應執行良好的計算機節點;叛變的將軍, 被非法控制的計算機節點;信使被殺, 通訊故障使得訊息丟失;信使被間諜替換, 通訊被攻擊, 攻擊者篡改或偽造資訊。

根據此問題,將分散式共識演算法分為兩類:

img

這裡主要介紹非拜占庭容錯演算法。

Paxos

Google Chubby的作者Mike Burrows說過:There is only one consensus protocol, and that's Paxos. All other approaches are just booked versions of Paxos. 所有的其他一致性演算法都是Paxos的不完整版。

這麼說是因為一方面它出現的很早,另一方面Paxos解決的其實是在分散式環境下所有服務達成一次某個值的共識的過程,而這一個過程,可以說每種共識演算法都是繞不開的。

Paxos演算法存在兩個很明顯的問題:

  • 特別複雜,難以理解
  • 缺失很多細節,難以實現

Paxos採取我們非常熟悉的達成共識的方法:少數服從多數,即Quorum機制,它將分散式系統中的節點分為三類,這些角色只是在不同時間下邏輯上的劃分。

  • Proposer:提案節點,提出對某個值進行設定操作的節點,就是接受客戶端寫操作的節點。請注意,Paxos 是典型的基於操作轉移模型而非狀態轉移模型來設計的演算法,這裡的“設定值”不要類比成程式中變數賦值操作,應該類比成日誌記錄操作。
  • Acceptor:決策節點,Acceptor 從含義上來說就是除了當前Proposer以外的其他機器,他們之間完全平等和獨立,Proposer需要爭取超過半數(N/2+1)的 Acceptor 批准後,其提案才能通過,它倡導的“value”操作才能被所有機器所接受。
  • Learner:記錄節點,不參與提案,也不參與決策,只是單純地從提案、決策節點中學習已經達成共識的提案,譬如少數派節點從網路分割槽中恢復時,將會進入這種狀態。

演算法流程

Paxos有兩個階段:

  • prepare請求階段:proposer告訴acceptor我有一個提案,詢問是否支援
  • accept請求階段:當proposer收到超過半數的acceptor回覆支援時,正式通知大家提案生效。

看起來挺簡單的,當有多個proposer時,每個proposer都收到了數量一致的acceptor時,演算法就會陷入死鎖狀態。此時Paxos加上一條規則:給每個提案加上一個編號,acceptor如果還沒有正式通過提案(即還沒有accept使操作生效),就可以接受編號更大的Prepare請求。為了提高效率:如果一個Prepare請求,到達Acceptor時,發現該Acceptor已經接受生效了另一個提案,那麼它除了回覆提案被拒絕外,還會帶上Acceptor已經通過的編號最大的那個提案的內容回到Proposer。Proposer收到帶內容的拒絕後,需要修改自己的提案為返回的內容。

所以需要保證編號全劇唯一且遞增。

一些異常情況及解決辦法:

  • 當某一個proposer進入accept階段時掛掉了,所有Acceptor就會陷入持續的等待,而其他的Proposer也會一直重試然後一直失敗。為了解決這個,Paxos決定允許proposer在拒絕時更新自己的提案編號重新發起prepare請求。

  • 每個Proposer都在被拒絕時,增大自己的編號,然後每個Proposer在新的編號下又爭取到了小於半數的Acceptor,都無法進入Accept,又重新加大編號發起提案,一直這樣往復迴圈,就成了活鎖。

    1、可以讓proposer引入一個隨機延遲去更新編號

    2、或者設定一個proposer的leader,全部由它來進行提案,這是共識演算法常見的套路。

  • 如果演算法進行中新增或下線了機器,此時一些Proposer知道機器數變了,一些Proposer不知道,那麼大家對半數的判斷就會不一致,導致演算法出錯。因此在實際執行中,機器節點數的變動,也需要作為一條要達成共識的請求提案,通過Paxos演算法本身,傳達到所有機器節點上。

完善後的兩階段:

  • Prepare準備階段:Proposer會嘗試告訴所有acceptor一個提案,請求得到支援。Acceptor如果已經支援編號為N的提案,則會拒絕編號小雨N的提案,如果生效了編號為N的提案,回覆時還會告知當前已生效的提案編號和內容。

  • Accept提交階段:Proposer會根據上一階段的回覆來決定行為。

    如果收到超過半數的支援,則正式通知所有機器生效。如果沒有收到超過半數回覆,提案取消。

    如果收到它們已經接收了其他編號更大的提案,那麼proposer會更新一個更大的編號去重試(隨機延遲)

    如果回覆已經生效其他編號的提案,那麼proposer接受此提案,併成為使其生效proposer的幫手,告訴其他acceptor生效資訊。

    接受其他提案以及提案取消情況下,proposer直接告訴客戶端該次請求失敗,等待客戶端重試即可。

image-20220322194623478

Multi-Paxos

上面演算法過程每次只更新一個值,被稱為Basic-Paxos。每次更新多個值稱為Multi-Paxos,作者並沒有給出具體的實現細節。

這種情況下使用Basic-Paxos一遍遍去執行也是可以的,但是效率很低。Lamport給出的解法是:

先選擇一個Leader來擔當Proposer的角色,取消多Proposer,只有一個Leader來提交提案,這樣就沒有了競爭(也沒有了活鎖)。同時,由於無需協商判斷,有了Leader後就可以取消Prepare階段,兩階段變一階段,提高效率。對於每一次要確定的值/操作,使用唯一的一個標識來區分,保證其單調遞增即可。

對於選擇Leader的過程,簡單的做法很多,複雜的也只需要進行一次Basic-Paxos即可。選出Leader後,直到Leader掛掉或者到期,都可以保持由它來進行簡化的Paxos協議。

如果有多個機器節點都由於某些問題自認為自己是Leader,從而都提交了提案,也沒關係,可以令其退化成Basic-Paxos,也可以在發現後再次選擇Leader即可。

Raft

和Paxos演算法幾乎是一個純理論演算法不同,Raft演算法就是為了工程實踐而設計的,“可理解性”稱為Raft設計的首要目標。

Raft演算法給出了大量實現細節,基本上對照論文就能實現,程式碼量也不大,論文中只用了2k+行程式碼就實現了Raft演算法。

Raft協議需要選舉出Leader的,從這裡也能看到,共識演算法大都會走向選舉出一個Leader的方向,來提升效率和穩定性。(不在需要兩階段提交了)不同之處可能只在於選舉的方式,以及訊息同步的方式。

演算法流程

每個節點有三種狀態:Follower、Candidate、Leader。

每個節點都有一個倒數計時器(Election Timeout),時間在150ms到300ms之間。當收到選舉請求或收到Leader的Heartbeat時候會重設。

在 Raft 執行過程中,最主要進行兩個活動:

選主 Leader Election

  • 節點一開始狀態是Follower。

  • 當倒數計時結束或者沒有收到leader的心跳,就會成為candidate,並給其他節點傳送RequestVote節點選舉請求。

    成為candidate後,會重新開啟一個計時器,過期時會重新發起投票請求。

    當有多個follower同時timeout成為candidate時,並得到了相同的選票,此時就會僵持,但他們的倒數計時仍然在執行,先timeout的candidate會重新發起新一輪的投票請求,follower還沒有在新一輪投過票,就會返回ok。這個candidate就會成為leader。

  • 如果超過一半節點投支援票,該節點將會成為leader,並每隔一小段時間給follower傳送一個心跳以重設計時器,此後所有對系統的修改都經過leader來完成。

當leader出故障時候,新一輪的選舉就會進行,每輪選舉都是有記錄的,當之前的leader恢復後,會自覺的降級為follower。

選主的過程和Basic-Paxos很像。

複製日誌 Log Replication

  • 當客戶端傳送請求給leader更新資料時,leader現將資料操作記錄在本地日誌中,這時候資料是Uncommited狀態。
  • 然後給follower傳送AppendEntries請求,將資料操作寫在本地日誌中,返回ok。
  • leader收到超過半數ok回覆後,將資料改為committed狀態,Leader 再次通知 Follower 資料已經提交,收到請求後,Follower 將本地日誌裡 Uncommitted 資料改成 Committed並更新資料。

當存在網路分割槽情況時,raft也能保證資料的一致性。被分割出的非多數派叢集將無法達到共識,即腦裂。當叢集再次連通時,將只聽從最新任期Leader的指揮,舊Leader將退化為Follower,如圖中B節點的Leader(任期1)需要聽從D節點的Leader(任期2)的指揮,此時叢集重新達到一致性狀態

ZAB

ZAB全稱是Zookeeper Atomic Broadcast,也就是Zookeeper的原子廣播,顧名思義是用於Zookeeper的。ZAB也是對Multi Paxos演算法的改進,大部分和raft相同。

ZAB理解起來很簡單,在協議中有兩種角色:

  • Leader節點:有任期的領導節點,負責作為與客戶端的連線點接受讀寫操作,然後將其廣播到其他節點去。
  • Follower節點:主要是跟隨領導節點的廣播訊息進行同步,並關注領導節點的健康狀態,好隨時取而代之。

和raft演算法的主要區別:

  1. 對於Leader的任期,raft叫做term,而ZAB叫做epoch
  2. 在狀態複製的過程中,raft的心跳從Leader向Follower傳送,而ZAB則相反。

Gossip

Paxos、Raft、ZAB 等分散式演算法經常會被稱作是“強一致性”的分散式共識協議,其實這樣的描述摳細節概念的話是很彆扭的,會有語病嫌疑,但我們都明白它的意思其實是在說“儘管系統內部節點可以存在不一致的狀態,但從系統外部看來,不一致的情況並不會被觀察到,所以整體上看系統是強一致性的”。與它們相對的,還有另一類被冠以“最終一致性”的分散式共識協議,這表明系統中不一致的狀態有可能會在一定時間內被外部直接觀察到。在比特幣網路和許多重要分散式框架中都有應用的另一種具有代表性的“最終一致性”的分散式共識協議:Gossip 協議。

今天 Gossip 這個名字已經用得更為普遍了,除此以外,它還有“流言演算法”、“八卦演算法”、“瘟疫演算法”等別名,這些名字都是很形象化的描述,反應了 Gossip 的特點:要同步的資訊如同流言一般傳播、病毒一般擴散。

Gossip演算法每個節點都是對等的,即沒有角色之分。Gossip演算法中的每個節點都會將資料改動告訴其他節點(類似傳八卦)。有話說得好:"最多通過六個人你就能認識全世界任何一個陌生人",因此資料改動的訊息很快就會傳遍整個叢集。(被感染)

img

優點:

  • 我們很容易發現 Gossip 對網路節點的連通性和穩定性幾乎沒有任何要求,它一開始就將網路某些節點只能與一部分節點部分連通(Partially Connected Network)而不是以全連通網路(Fully Connected Network)作為前提
  • 能夠容忍網路上節點的隨意地增加或者減少,隨意地當機或者重啟,新增加或者重啟的節點的狀態最終會與其他節點同步達成一致。
  • Gossip 把網路上所有節點都視為平等而普通的一員,沒有任何中心化節點或者主節點的概念,這些特點使得 Gossip 具有極強的魯棒性,而且非常適合在公眾網際網路中應用。

同時我們也很容易找到 Gossip 的缺點:

  • 訊息最終是通過多個輪次的散播而到達全網的,因此它必然會存在全網各節點狀態不一致的情況
  • 而且由於是隨機選取傳送訊息的節點,所以儘管可以在整體上測算出統計學意義上的傳播速率,但對於個體訊息來說,無法準確地預計到需要多長時間才能達成全網一致。
  • 另外一個缺點是訊息的冗餘,同樣是由於隨機選取傳送訊息的節點,也就不可避免的存在訊息重複傳送給同一節點的情況,增加了網路的傳輸的壓力,也給訊息節點帶來額外的處理負載。

可以看到,共識演算法基本都需要解決兩個基本問題:

  1. 如何提出一個需要達成共識的提案(選舉Leader、隨機投票...)
  2. 如何讓多個節點對提案達成共識(廣播、複製、投票...)

在這兩個問題的處理方案上選擇不同,就會導致效能、可用性等指標的不同,所以其實,兵器各有利弊,還是要看使用場景和使用的人

相關文章