Paxos Made Simple

silentteller發表於2024-06-08

1 Introduction

  Paxos演算法是萊斯利·蘭伯特(Leslie Lamport)於1990年提出的一種基於訊息傳遞且具有高度容錯特性的共識(consensus)演算法。《The Part-Time Parliament》最早發表於1998年,Paxos島上有一個議會,這個議會來決定島上的法律,而法律是由議會透過的一系列的法令定義的。當議會召開時,允許議員缺席,但法令仍然可以達成共識。《Paxos Made Simple》發表於2001年,用更清晰整潔的語言描述了Paxos演算法。本文主要基於《Paxos Made Simple》

2 The Consensus Algorithm

2.1 The Problem

共識問題

Suppose you have a collection of computers and want them all to agree on something. This is what consensus is about; consensus means agreement.

  共識在分散式系統設計中經常出現。我們需要它的原因有很多:同意誰可以訪問資源(互斥),同意誰負責(選舉),或者同意一組計算機之間的事件的共同順序(例如,下一步採取什麼行動,或狀態機複製)。
  共識問題可以用一種基本的、通用的方式來表述:一個或多個系統可能提出一些值。我們如何讓一組計算機恰好同意這些建議值中的一個呢?
  共識演算法就是為了解決共識問題,那麼假設有一組可以提出值的流程。共識演算法確保在建議值中選擇單個值。如果沒有建議值,則不應選擇任何值。如果選擇了一個值,那麼程序應該能夠學習所選擇的值。協商一致的安全要求如下。

Only a value that has been proposed may be chosen

Only a single value is chosen, and

A process never learns that a value has been chosen unless it actually has been

  我們的目標是確保最終選擇一些建議的值,如果一個值被選中,那麼程序最終可以學習這個值。三個角色proposers, acceptors, and learners,實際情況一個server或者說一個process可以扮演多個角色。
proposers:

  • 主動:提出要選擇的特定值(提案)
  • 處理客戶端的請求

acceptors:

  • 被動:回應proposer的資訊
  • 回應代表了形成共識的投票
  • 儲存選擇的值,決策過程的狀態
  • 想知道選擇了哪個值

learners:

  • 學習已經選擇的值

  我們模擬的環境是透過訊息傳送來實現程序之間的相互通訊。且是非同步的、非拜占庭的。

Agents operate at arbitrary speed, may fail by stopping, and may restart. Since all agents may fail after a value is chosen and then restart, a solution is impossible unless some information can be remembered by an agent that has failed and restarted.

Messages can take arbitrarily long to be delivered, can be duplicated,and can be lost, but they are not corrupted.

  這裡再簡單解釋下拜占庭將軍問題。

在分散式計算中,不同的計算機透過通訊交換資訊達成共識而按照同一套協作策略行動。但有時候,系統中的成員計算機可能出錯而傳送錯誤的資訊,用於傳遞資訊的通訊網路也可能導致資訊損壞,使得網路中不同的成員關於全體協作的策略得出不同結論,從而破壞系統一致性。拜占庭將軍問題被認為是容錯性問題中最難的問題型別之一。

2.2 Choosing a Value

  一個很容易想到的方案,如果我們的系統中,只存在一個acceptor,acceptor選擇accept收到的第一個提議,那麼共識就達成了,但是這個方案很失效,因為acceptor一旦出現任何的故障都會導致後續不會有任何進展,從而無法達成共識。
image
  為了避免上述的情況發生,我們採用多個acceptor,當一個提案產生時,proposer將提議傳送給一組acceptor,acceptor可以accept該提議,如果大多數的acceptor接受該提議,那麼這個提議是chosen 。 大多數如何定義?可以是2/3或者3/4,最起碼要超過半數,這樣兩個大多數的accptor組中必定是有重複的accptor,如果accptor只能接受至多一個提案,這是一個有效的方案!
image
  在沒有失敗或訊息丟失的情況下,我們希望選擇一個值 ,即使單個proposer只提出了一個值。這就提出了要求(約束):

P1. An acceptor must accept the first proposal that it receives.

  很明顯,當單個的proposer只提出了一個提案,那麼acceptor必須要接受它收到的第一個提案,否則很有可能是無法達成共識(該提案沒有被大多數acceptor接受)。
  但這又會引發一個問題,如果在同一時間範圍,多個proposer提出了不同的提案,每個acceptor都接受了一個提案,但沒有一個提案被大多數的acceptor所接受。即使只有兩個提案,每個提案都被近似一半的acceptor所接受,但只要有一個acceptor出現了故障(網路問題或當機等),都會使得無法達成共識。
image
  P1和只有當一個提案被大多數acceptor接受時才被認為是chosen的,這意味著我們必須允許一個acceptor接受不止一個提案。
  當有多個提案被提出後,我們需要為每個提案分配一個編號(標識)來區分acceptor可能接受的不同提案,因此提案由提案編號和內容(value)組成。為了防止混淆,我們要求不同的提案有不同的編號。同時為了區分提案的先後順序,這個編號需要做到全域性遞增的。
  現在我們先假設一下。當具某個提案被大多數acceptor接受時,在這種情況下,這個提案是chosen的,或者說這個value是chosen的。
  我們允許多個提案是chosen的,但我們必須保證所有選擇的提案具有相同的value。透過對提案號的歸納,這需要:

P2. If a proposal with value v is chosen, then every higher-numbered proposal that is chosen has value v.

  因為數字是完全有序的,約束P2保證了關鍵的safety屬性,即只有一個value是chosen的。一個提案從提出到被選擇,經歷了proposer提出提案,acceptor接受提案,到大多數acceptor接受提案後,我們才認為這個提案是chosen的。
image
當認為一個提案是chosen的之前,一定存在acceptor接受了該提案,基於此,我們可以將約束P2再強化一下。

P2a . If a proposal with value v is chosen, then every higher-numbered proposal accepted by any acceptor has value v.

考慮一種情況,我們的系統中可能會存在多個proposer來提出提案,也可能會存在acceptor從未接受過提案,如果一個proposer提出了一個和現在大多數acceptor所接受的提案不同的提案,併傳送給c,由於約束P1的存在,c需要接受改提案,但這就違反了約束P2a,同時維護P1和P2a我們將約束P2a強化一下:

P2b . If a proposal with value v is chosen, then every higher-numbered proposal issued by any proposer has value v.

  與其限制acceptor,不如從源頭上來避免這種情況。如果當一個提案value=v是chosen的,那麼每一個更高編號的提案都攜帶value=v,這樣acceptor再接受的提案也都是v。不難發現滿足P2b的話,一定滿足P2a,也一定滿足P2。
  為了發現如何滿足P2b,這裡首先讓我們思考如何證明P2b。攜帶value是v的提案m已經是chosen的,那麼提案n(n>m)攜帶的value也是v。
  我們假設編號m..(n-1)的提案的value也都是v,而由P2b的if條件已知編號為m的提案其value=v已經是chosen的,那麼一定會存在一個大多數acceptor組成的集合C,其中C中的每一個acceptor都接受了該提案。
  到目前為止,實際上我們並沒有限制acceptor,也就是說acceptor並不會拒絕提案,而當提案m,提案m+1,一直到提案n-1,傳送給大多數acceptor時,acceptor會選擇接受,那麼實際上C中每一個的acceptor都接受過m~n-1中的提案,且任何被接受的提案的value都是v。
image

P2c. For any v and n, if a proposal with value v and number n is issued, then there is a set S consisting of a majority of acceptors such that either (a) no acceptor in S has accepted any proposal numbered less than n, or (b) v is the value of the highest-numbered proposal among all proposals numbered less than n accepted by the acceptors in S.

  P2c的提出,實際上要求proposer在釋出提案n時,需要學習小於n的最高的編號的提案資訊,這個提案很有可能已經被大多數acceptor所接受。

Learning about proposals already accepted is easy enough; predicting future acceptances is hard. Instead

  也就是說,當一個提案發布之後,很難去預測這個提案是否會被大多數的acceptor所接受,所以proposer不去預測未來,而是透過獲取acceptor的承諾來控制提案的接受與否。因為如果當前系統內不存在被chosen的提案,也就是說不存在大多數acceptor接受某個提案的集合。那麼此時有多個proposer提出自己的提案,那麼很多提案都是無效的,或者也會出現無法達成共識的情況出現。
  那麼此時我們也需要對acceptor提出要求。當proposer傳送提案給acceptor時,acceptor需要給予一定的承諾,同時將已經接受的提案資訊回應傳送給proposer以便proposer學習。
  當proposer選擇一個新的提案號n,並向大多數acceptor傳送請求,要求acceptor響應:

(a) A promise never again to accept a proposal numbered less than n, and
(b) The proposal with the highest number less than n that it has accepted, if any.

  這就是一個prepare階段的請求。如果proposer收到了大多數acceptor的回覆,那麼proposer可以釋出一個編號為n的提案,該提案攜帶value為v,其中v是acceptor回覆中提案最大的value內容,如果acceptor沒有回覆提案資訊,那麼v可以是一個任意的內容。
  proposer透過向一組acceptor傳送請求來發布提案,請求該提案被接受。(這不需要是響應prepare請求的同一組acceptor )我們稱其為accept請求。
  上面的prepare請求和prepare請求主要是針對proposer進行描述的。那麼當請求傳送到acceptor時,acceptor也需要做出相應的響應。acceptor是允許不去響應任何請求的,那麼根據上面的描述,我們可以得到P1的強化版本,即:

P1a . An acceptor can accept a proposal numbered n iff it has not responded to a prepare request having a number greater than n.

  假設一個acceptor收到一個編號為n的prepare請求,但它已經響應了一個編號大於n的prepare請求,因此 承諾不接受任何編號為n的新提案。那麼acceptor就沒有理由去響應新的prepare請求,因為它將不會去接受編號為n的提案,或者說任何小於它響應的prepare請求的編號的提案,它都不會去接受,這是acceptor給出的承諾。那麼我們可以讓acceptor忽略掉這樣的prepare請求,或者說當它已經接受了一個編號大於n的提案,任何小於該編號的prepare請求,也都是可以忽略的。
  基於此,我們要求acceptor只需要記住它曾經接受過的編號最高的提案和它響應過的編號最高的prepare請求的編號即可。
  對於proposer和acceptor我們都進行了描述,下面我們整理一下,看一下演算法如何在兩個階段執行的。
  prepare階段:

Phase 1. (a) A proposer selects a proposal number n and sends a prepare request with number n to a majority of acceptors.
(b) If an acceptor receives a prepare request with number n greater than that of any prepare request to which it has already responded, then it responds to the request with a promise not to accept any more proposals numbered less than n and with the highest-numbered proposal (if any) that it has accepted.

  accept階段:

Phase 2. (a) If the proposer receives a response to its prepare requests (numbered n) from a majority of acceptors, then it sends an accept request to each of those acceptors for a proposal numbered n with a value v, where v is the value of the highest-numbered proposal among the responses, or is any value if the responses reported no proposals.
(b) If an acceptor receives an accept request for a proposal numbered n, it accepts the proposal unless it has already responded to a prepare request having a number greater than n.

  proposer可以釋出多個提案,同時也可以在任何時刻放棄某個提案。如果某些proposer已經嘗試釋出更高編號的提案,那麼放棄提案顯然是一個好的主意。因此,如果一個acceptor因為已經收到一個編號更高的prepare請求而忽略了編號較低的prepare或accept請求,那麼它可能應該通知proposer,然後proposer應該放棄它的提議。這是一個效能最佳化,並不影響演算法的正確性。

2.3 Learning a Chosen Value

  當一個提案已經被選擇時,learner必須發現一個提案已經被大多數acceptor所接受。

  1. 一個簡單方法,對於每一個acceptor,無論何時只要接受了一個提案,就將該提案傳送給全部的learner。這種做法可以使learner儘快的學習到已被chosen的提案,但是這要求每一個acceptor與每一個learner都建立通訊,通訊的次數至少是acceptor的數量*learner的數量。
  2. 考慮到我們的環境是非拜占庭式的,那麼當一個learner學習到了已經被chosen的提案,其他的learner是很容易發現這個資訊的(訊息是不會被篡改的),我們可以讓acceptor用他們的接受提案傳送給一個distinguished learner(leader), 當一個提案被chosen時,由distinguished learner通知其他的learner。這種方法需要額外的一輪才能讓所有的learner學習到提案。同時由於distinguished learner可能發生故障,這個方法可能會失效。但是相比上面的方法,acceptor和learner之間通訊的次數大大減少。(acceptor的數量+learner的數量)。
  3. 方法一和二結合一下,考慮維護一個distinguished learner集合,也就是說當acceptor接受了某個提案後,傳送給集合中的所有distinguished learner,再由他們傳送給其他的learner。這在一定程度上提升了可靠性,但這樣同時也增加了通訊的複雜度。

2.4 Progress

  上面詳細的描述了Paxos中三種角色的作用。通常情況下,一個提案在執行完Paxos例項後是會被選擇的。但也容易構建這樣一個場景:兩個proposer各自不斷髮出一系列編號不斷增加的提案,但沒有一個提案被選中。
image
  proposer p發出了提案編號n1,完成了prepare階段的工作,然後另一個proposer q發出了提案編號n2(n2>n1),同樣完成了prepare階段的工作。那麼proposer p在accept階段發出的提案n1將會被acceptors忽略,因為acceptors已經承諾不會accept任何編號小於n2的提案了。然後proposer p開始發出提案編號n3,完成prepare階段的工作,那麼proposer q在accept階段發出的提案n2將會被acceptors忽略,這樣反覆下去沒有提案會被大多數acceptor所接受。
  為了保證流程正常運作,可以選出一個distinguished proposer作為唯一嘗試發出提案的proposer,如果distinguished proposer能夠與大多數acceptor成功地通訊,並且如果它使用的提案的編號大於任何已經接受的提案,那麼它將成功地發出一個被接受的提案。

2.5 The Implementation

  Paxos演算法假設一個程序網路。在其共識演算法中,每個程序扮演proposer、acceptor和learner。演算法選擇了一個leader扮演distinguished proposer和distinguished learner。上面描述的Paxos演算法,其中請求和響應作為普通訊息傳送(響應訊息應該帶有相應的提案編號,以防止混淆)。acceptor需要支援持久化儲存對應的prepare和accept階段所響應的提案資訊。
  剩下要做的就是描述一種機制,以保證不會有兩個提案的編號相同。不同的提議者從不相交的數字集合中選擇他們的數字,因此兩個不同的提議者永遠不會發出相同數字的提案。每個提議者都記住 (穩定儲存中)它嘗試發出的最高編號的提案 ,並以比它已經使用過的更高的提案號開始prepare階段。

3 Implementing a State Machine

狀態機同步
  一個確定的狀態機,以某種順序執行命令。狀態機具有當前狀態,它透過當前狀態和收到的命令來決定下一個狀態。例如,銀行系統可以描述為一個狀態機,而狀態機狀態由所有使用者的帳戶餘額組成。金額提現將透過執行狀態機命令來執行,當且僅當餘額大於提現金額時,該命令會減少帳戶餘額,併產生新舊餘額作為輸出。
  如果只有一臺伺服器提供服務,那麼當這臺機器故障時,我們無法對外提供有效的服務。因此,我們需要使用一組伺服器,每個伺服器都獨立的實現狀態機。因為狀態機是確定性的,所有的伺服器都執行相同的命令序列,它們將產生相同的狀態序列和輸出。
  如果伺服器失敗,那麼使用單箇中心伺服器的實現就會失敗。因此,我們轉而使用一組伺服器,每個伺服器都獨立地 實現狀態機。因為狀態機是確定性的, 如果所有伺服器都執行相同的命令序列,那麼它們將產生相同的狀態序列和輸出。那麼,使用者或者說客戶端可以向任何伺服器發出指令來獲取輸出結果。
  那麼為了保證所有伺服器都執行相同的狀態機命令序列,我們在伺服器上實現並且執行Paxos共識演算法例項,第i個例項選擇的value是狀態機命令序列中的第i個命令(這裡我們稱每一次paxos執行為一次例項)。每個伺服器在演算法的每個例項中扮演所有角色(proposer,acceptor,and learner)。現在,我們假設伺服器集是固定的,因此共識演算法的所有例項都使用相同的代理集。
  在通常的操作中,一個伺服器被選為leader,它在所有演算法例項中充當distinguishedproposer,也就是唯一嘗試發出提議的proposer。客戶端將命令傳送給leader,然後它來決定每個命令應該出現的順序。如果leader決定某個客戶端命令應該是第135個命令,那麼它會嘗試將該命令選為共識演算法的第135個例項的value。通常情況下它會成功,當然也可能因為某些問題而失敗 ,亦或者因為另一個伺服器也認為自己是leader,並且對第135條命令是什麼有不同的想法。但是共識演算法保證最多可以選擇一個命令作為第135個命令。
  這種方法效率的關鍵在於,在Paxos共識演算法中,要提出的value直到階段2,也就是accept階段才被選擇。回想一下,當proposer在完成prepare階段之後,要麼確定要提議的value ,要麼proposer可以自由地提出任何value。
  現在先描述一下在正常操作期間Paxos狀態機是如何工作的。然後我們討論可能出現的問題。比如當前leader剛剛出現了故障,新leader已經被選中時會發生什麼。
  新的leader,同樣作為共識演算法中所有例項的learner, 應該知道大多數已經被選擇的命令。假設它知道命令1-134、138和139,即在一致性演算法的例項1-134、138和139中選擇的value。然後,它執行例項135-137和所有大於139的例項的prepare階段。假設這些執行的結果決定了例項135和140中提議的value(prepare階段中acceptor響應返回了value),而其他例項中值沒有受到約束(對應例項的prepare階段,acceptor沒有響應value)。然後,leader對例項135和140執行accept階段的演算法,從而第135和第140個命令選擇了。
image
  leader,以及任何其他學習了所有的命令leader知道,現在可以執行命令1-135。然而它沒有辦法執行第138-第140號命令,它也知道不能執行,因為第136和第137號命令還沒有選擇,leader可以將客戶端的下兩個請求命令作為第136和第137號命令。這裡也可以用特殊的命令“no-op”,也就是無操作,來填補目前缺失的第136和第137號命令,維持當前狀態機的狀態(透過執行例項136,137accept階段演算法來完成,value=“noop”),一旦noop命令被選擇了,第138-第140號命令也就可以執行了。
image
  此時命令1-140現在已經被選擇。leader還完成了所有大於140的共識演算法例項的prepare階段,並且它可以自由地在這些例項的accept階段中提出任何值,它將客戶端請求的下一個命令作為第141號命令,並將其作為共識演算法例項141的accept階段中的值。然後將接收到的下一個客戶機命令作為第142號命令,以此類推。
  leader可以在它學習到第141號命令被選擇之前就釋出第142號命令的提案。第141號命令的提案的訊息有可能全部丟失,並且在任何其他伺服器知道領導提案的第141號命令之前,第142號命令就已經被選擇了。(丟失or網路延遲)。
  當leader在例項141中沒有收到對其accept階段訊息的預期響應時,它將重傳這些訊息。如果一切順利,將選擇提案中的命令。但是,它可能會失敗,在所選命令的序列中留下空白。一般來說,假設leader可以提前獲得a個命令,也就是說,在命令1到i被選中之後,它可以提出命令i+1到i+a。這樣就會出現高達a-1個命令的缺口(第i+a被chosen,前面的全部失敗)。
  新當選的leader可以執行共識演算法的無限多個例項的prepare階段——在上面場景中,也就是例項135-137,以及所有大於139號的例項。對於所有例項使用相同的提議號,它可以透過向其他伺服器傳送一條合理的短訊息來實現這一點。在prepare階段中,只有當acceptor已經從某個proposer那裡收到了accept階段的訊息時,它才會響應不止一個簡單的OK(會響應value)。(在這個場景中,只有例項135和140是這種情況。)因此,伺服器(作為acceptor)可以響應所有例項,併傳送一條合理的短訊息。因此,執行階段1的無限多個例項 不會產生任何問題。
  由於leader的失敗和新leader的選舉應該是較為罕見的事件,因此執行狀態機命令的有效成本(即在命令/值上達成共識的成本)是僅執行共識演算法的accept階段的成本。可以看出,在存在故障的情況下, Paxos共識演算法的accept階段在所有演算法中達成一致的代價最小。因此,Paxos演算法 本質上是最優的。
  在系統正常執行時總會存在一個leader,除了當前leader故障和新leader選舉間的短暫時間。異常情況下,leader的選舉可能會失敗,如果沒有leader,也就不會有新的命令被提出。如果有多個伺服器認為它們是leader,那麼它們都可以在共識演算法的同一例項中發出提案, 這可能阻止任意值被選擇。(上面的case)。但這顯然是安全的,也就是說兩個不同的伺服器不會再第i個命令上產生分歧,選舉一個leader來進行顯然是最有效的。
  如果伺服器集合可以改變,那麼一定有辦法決定哪些服務實現共識演算法的哪些例項。實現這個目的最簡單的方法就是透過狀態機本身。當前的一組伺服器可以作為狀態的一部分,並可以使用普通的狀態機命令進行更改

相關文章