1 Introduction
可能是因為之前的描述對大多數讀者來說太過Greek了,Paxos作為一種實現容錯的分布式系統的算法被認為是難以理解的。但事實上,它可能是最簡單,最顯而易見的分布式算法了。它的本質其實就是共識算法——the "synod" algorithm of。在下一節中我們將展示,該共識算法基本滿足了所有我們想要它滿足的特性。最后一節則展示了完整的Paxos算法,通過直接應用協商一致的狀態虛擬機來構建分布式系統——這種方法應該是廣為人知的,因為這可能是分布式系統理論中被引用最多的領域。
2 The Consensus Algorithm
2.1 The Problem
假設有一些進程可以提出value。共識算法保證了在所有提出的value里只有一個會被選中。如果沒有value被提出,那么也就沒有value會被選中。如果一個value被選中了,那么其他的進程應該能夠獲取該value。協商一致的要求如下:
- 只能選擇已經被提出的value
- 只能選擇一個value
- 進程只能獲取那些真正被選中的value
我們不會嘗試指定精確的要求。但是我們的目標是要確保總有一些被提出的value會被選中,如果一個value最終被選中了,那么其他進程最終要能夠獲取該value。
我們用三類agent來代表共識算法中的三類角色:proposers, acceptors和learners。在具體的實現中,一個進程可能扮演不止一類agent,但是從agent到進程的映射我們在這里並不關心。
假設agent之間可以通過發送message互相通信。我們使用customary asychronous,non-Byzantine model,其中:
- Agents以任意速度執行,可能發生故障,可能重啟。因為所有的agent都可能在一個value被選中之后故障並重啟,因此一般的方法是不可行的,除非agent能記住一些信息,即使發生了故障或重啟。
- 發送的message可以是任意長度的,可能重復,也可能丟失,但是它們不會被損壞
2.2 Choosing a Value
存在一個單一的acceptor agent是最簡單的選擇value的方式。proposer向acceptor發送提議,后者從中接收最早收到的那個。雖然很簡單,但是這種方法是不能滿足要求的因為acceptor的故障,因為acceptor的故障就將導致接下來的操作都無法進行。
因此我們需要嘗試另一種選擇值的方式。這次我們將有多個而不是一個acceptors。proposer將會向一個acceptor的集合發送value。acceptor可能會接受value。但是該value只有在足夠多的acceptor都接受它的情況下才算被選擇了。那么怎樣才算足夠大呢?為了確保只有一個value會被選中,我們可以認為一個足夠大的agent集合由任意的agent majority組成。因為任意兩個majority都至少有一個公共的agent,因此如果一個agent最多只能接收一個value,那么這種方法是可行的。
在沒有故障和message丟失的情況下,我們想要有一個value能被選中,即使只有一個proposer提出了一個value。這就需要滿足以下要求:
P1. acceptor必須接收第一個它收到的proposal
但是這個要求會引起這樣一個問題。不同的proposer可能會在幾乎同時提出好幾個不同的value,這會導致這樣一種情況:每個acceptor都接受了一個value,但是沒有一個value是被一個majority接受的。即使只提出了兩個value,而它們各自被一半的acceptor所接收,那么任意單個acceptor的故障都將讓我們無法獲取它選擇了哪個value。
P1以及value只有被majority個acceptor接受才被算被選中的要求就導致了我們的acceptor必須能接受超過多於一個的proposal。我們通過給每個proposal賦予一個編號來追蹤不同的proposal,所以一個proposal由一個proposal number和一個value組成。為了防止出現歧義,我們要求不同的proposal要有不同的number。這里我們僅僅只是做出這個假設,具體的實現可能有所不同。當一個proposal被一個acceptor的majority所接收時,我們就認為該value被選中了。這種情況下,我們說這個proposal(同時也包括它的value)被選中了。
我們可以允許多個proposal被選中,但是我們必須保證被選中的proposal有相同的value。通過歸納proposal number,足以保證:
P2.如果一個value為v的proposal被選中,那么所有被選中的higher-numbered proposal都具有value v
因為number都是有序的,條件P2保證了只有單一的value被選中這一重要特性。為了能夠被選中,proposal必須至少被一個acceptor所接受。所以我們可以通過滿足以下條件滿足P2:
P2a.如果一個value為v的proposal被選中,那么每一個被任何acceptor所接受的high-numbered proposal都有value v
我們依然需要滿足P1從而確保有proposal被選擇。因為交互是異步的,一個proposal可能被一些特定的,沒有接受過任何proposal的acceptor c選中。假設一個新的proposal "wake up"並且發送了一個帶有不同value的high-numbered proposal。P1要求c接受這個proposal,但是卻違背了P2a。為了同時滿足P1和P2a,需要加強P2a:
P2b.如果一個value為v的proposal被選中,那么之后每個proposer發出的high-numbered proposal都有value v
因為一個proposal 在被acceptor接受之前都要首先由proposer發出。因此滿足P2b就滿足了P2a,也就滿足了P2。
為了明白如何滿足P2b,我們先對它進行證明。我們假設有一些number為m,value為v的proposal已經被選中了,接下來我們證明任何的有number n>m的proposal的value都為v。我們可以通過歸納到n來簡化證明,首先假設每一個發出的number在m...(n-1)的proposal都有value為v,其中i...j代表范圍為i到j的所有number。既然有number為m的proposal被選中了,那么必然有這樣一個集合C,它由一個acceptor的majority組成,而C中的每個majority都接受它。結合歸納假設,從m被選中的假設可以推出:
C中的每一個acceptor都接受了number在m...(n-1)中的一個proposal,而每個被任意acceptor接受的number在m...(n-1)的proposal都有value為v。
因為任何集合由majority個acceptor組成的集合S必然和C存在公共的元素,我們可以通過滿足以下條件來確保標號為n的proposal有value為v:
P2c.對於任意的v和n,如果有一個value為v,number為n的proposal被發出了,那么就存在一個由majority個acceptor組成的集合S。要么(a)S中沒有一個acceptor接受過number小於n的proposal,要么(b)v是S中的acceptor接受過的number小於n的最高number的proposal的value
因此我們可以通過滿足P2c來滿足P2b。為了滿足P2c,如果proposer想要發出一個number為n的proposal,那么它必須要獲取已經或者將要被一個majority接受的最高number不大於n的proposal的value。獲取那些已經被接受的proposal是非常簡單的,但是對未來的接受情況進行預測就非常困難了。為了避免對未來進行預測,proposer通過獲得不會有這樣的接受情況的承諾來進行控制。換句話說,proposer請求acceptor不要接受任何number小於n的proposal。這就導出了以下發送proposal的算法:
1、一個proposer選擇了一個新的proposal number n並且給一些acceptor集合的每個成員發送了一個請求,並希望它們回復:
(1)、承諾再也不接受number小於n的proposal
(2)、如果已經接受了最高number小於n的proposal,返回
我們將這樣的請求稱為number為n的prepare request
2、如果proposer接受了來自一個majority對於請求的回復,那么它就可以發送一個number為n,value為v的proposal。其中v要么是返回的最高number的proposal的value,如果沒有proposal返回,那么proposer隨意選擇一個值。
proposer將proposal發送給一些acceptor的集合,請求它們接受。(這里的acceptor的集合不一定要和回復初始請求的acceptor的集合是同一個集合)我們將這樣的請求稱之為accept request。
這里描述的是proposer的算法。那么acceptor呢?它可以從proposer那里接受到兩種請求:prepare request和accept request。acceptor可以忽略任何沒有compromising safety的請求。因此我們只需要討論它允許回復請求的情況。它總允許對prepare request進行回復。它可以對一個accept request進行回復,代表接受一個proposal,如果它沒有承諾不那么做的話。換句話說:
P1a.acceptor可以接受number為n的proposal,如果它之前沒有回復任何number大於n的prepare request
可以發現P1a包含P1
現在我們已經為滿足安全性要求地選擇value提供了一個完整的算法——假設proposal number唯一的情況。最終的算法只是在這基礎之上做了一個小小的優化。
假設一個acceptor接受了一個number為n的prepare request,但是它已經回復了一個number大於n的prepare request。因此它承諾不會接受任何number為n的新的proposal。但是該acceptor是不會接受該proposer想要發出的number為n的proposal的,所以該acceptor沒有理由回復這個prepare request。因此,我們讓acceptor忽略這樣的prepare request。同樣,我們對於已經接受的proposal對應的prepare request也是忽略的。
經過這樣的優化以后,acceptor只需要記住它接受過的最高number的proposal以及它回復過的最高number的prepare request。即使發生了故障也要滿足P2c,因此即使發生了故障或者重啟了,acceptor也要記住這些信息。需要注意的是proposer總是可以放棄一個proposal並且將它忘得一干二凈——只要它不再發送另一個具有相同number的proposal。
將proposer和acceptor的行為結合在一起,我們可以看到算法分兩階段執行。
Phase 1.(a)proposer選擇一個proposal number n,然后向一個majority發送number為n的prepare request。
(b)如果一個acceptor接受了一個number為n的prepare request,並且n大於任何它已經回復的prepare request的number,那么它將承諾不再接受任何number小於n的proposal,並且回復已經接受的最高number的proposal(如果有的話)。
Phase 2.(a)如果proposer接受了來自majority對它的prepare request的回復,那么接下來它將給這些acceptor發送一個number為n,value為v的proposal作為accept request。其中v是收到的回復中最高number的proposal的value,或者如果回復中沒有proposal的話,就可以是它自己選的任意值。
(b)如果acceptor收到一個number為n的accept request,如果它沒有對number大於n的prepare request進行過回復,那么就接受該accept request。
一個proposer可以生成多個proposal,只要它能滿足算法的要求。它可以在協議的任意時刻放棄proposal。(正確性依然是得到滿足的,即使請求或者回復在對應的proposal被放棄了很久之后才到達目的地)當其他的proposer已經開始發出更高number的proposal時,最好放棄當前的proposal。因此。如果acceptor因為它已經接受了更高number的prepare request而忽略了其他的prepare或者accept request,那么它應該通知對應的proposer放棄該proposal。這是對性能優化,並不會影響正確性。
2.3 Learning a Chosen Value
為了獲取一個已經被選中的value,learner必須要確定已經有一個proposal被majority接受了。最顯而易見的算法就是讓每個acceptor在接受了一個proposal之后向所有的learner發送這個proposal。這能讓learner盡快地找到被選中的value,但這需要acceptor對每個learner進行回復——回復的數量為acceptor和learner數量的乘積。
non-Byzantine failures的假設允許我們讓一個learner可以從另一個learner處獲取已經被接受的value。我們可以讓acceptor把它們的接受情況都發送給一個distinguished learner,這個learner再轉而通知其他的learner有個value被接受了。這種方法需要額外的一個round來讓所有的learner發現被選中的value。同時這樣也是非常不可靠的,因為這個distinguished learner可能故障。但它只需要acceptor和leaner數目的和的回復數。
更一般地,acceptor可以將它們的接受情況發送給一個distinguished learner的集合,而它們中的任意一個都能在value被選中的時候通知所有的learner。通過提供更大集合的distinguished learner在增加通信復雜度的同時提供了更高的可靠性。
因為message的丟失,可能沒有learner知道已經有value被選中了。learner可以直接問acceptor它們接受了什么proposal,但是acceptor的故障可能讓我們無法知道是否有majority接受了一個特定的proposal。這種情況下,learner只能在有新的proposal被接受的時候才能確定被選中的value是什么。如果一個learner想要知道一個value是否被選中,它可以讓一個proposer發送一個proposal,使用上文描述的算法。
2.4 Progress
我們很容易構建這樣一個場景,兩個proposer持續發送比對方的number高的proposal,並且最終它們兩者沒有一個被選中。Proposer p通過proposal number n1完成了phase 1。另一個proposer q接着通過了proposal number n2 > n1完成了phase 1。proposer p在phase 2的以n1標記的accept request會被所有acceptor拒絕,因為它們已經承諾不接受任何number小於n2的proposal。因此proposer p開始用新的proposal number n3 > n2來開始並完成phase 1,而這又導致了proposer q在phase 2的accept被忽略。如此反復進行。
為了保證流程的執行,我們必須選出一個distinguished proposer,作為唯一的proposal發送者。如果distinguished proposer能和majority進行通信,並且使用了一個比所有已經使用的proposer number都大的number,那么它就能成功發送一個已經被接受的proposal。通過拒絕已有的proposal並且通過更高的proposal number重試,distinguished proposer最終會選擇到一個足夠大的proposer number。
如果系統足夠多的部分都工作正常(proposer, acceptors以及交互網絡),那么通過選出一個單一的distinguished proposer就能保持系統的活力。由Fischer, Lynch, and Patterson著名的結論可得,選舉一個proposer的可靠算法必須要么使用randomness,要么使用real time——比如,使用超時。但是,無論選舉成功還是失敗,安全性總是可以保證的。
2.5 The Implementation
Paxos算法假設了一個進程網絡。在這個共識算法中,每個進程扮演着proposer, acceptor和learner的角色。該算法需要選擇一個leader,來扮演distinguished proposer和distinguished learner的角色。Paxos共識算法正如上文所描述的那樣,請求和回復都以ordinary message的形式發送。(Response message會用相應的proposal number標記為了防止混淆)我們需要使用stable storage(會在故障時候保存)來維護那些acceptor必須保存的信息。acceptor會在真正發送response之前將它記錄下來。
接下來所有的內容都將用於描述如何保證兩個proposal不會有相同的number。不同的proposer會從不相交的數據集中選擇number,所以不同的proposer不會發送具有相同number的proposal。每個proposer都會用stable storage記住它嘗試發送的最高number的proposal並用一個比所有已經使用過的number都高的number開始phase 1。
3 Implementing a State Machine
實現一個分布式系統最簡單的方式就是一個client的集合向一個central server發送命令。central server可以被看做是一個以一定順序執行client命令的deterministic state machine;它通過將輸入作為命令,並產生輸出和一個新的狀態。比如,分布式銀行系統的client可以看做是teller,而所有用戶的account balancer可以看做state-machine的狀態。一個撤銷操作可以通過執行當且僅當balance大於amount withdrawn的時候減小account's balance這一state machine command完成。
使用單一的central server的實現,如果central server發生故障,整個系統就會發生故障。因此,我們使用了一個server的集合,它們各自獨立地實現一個state machine。因為state machine是確定性的,所以在執行完相同序列的命令之后,所有的server都會產生相同的狀態序列和輸出。client可以使用任意server的輸出。
為了保證所有的server執行相同序列的state machine commands,我們實現了一個序列的Paxos共識算法的單獨實例,第i個實例選擇的value作為序列中第i個state machine command。每一個server都扮演了該算法中的所有角色(proposer,acceptor和learner)。現在,我假設server集合是固定的,因此該共識算法的所有實例都使用相同的agent的集合。
在通常的操作中,只有一個server能夠被選為leader並作為distinguished proposer(唯一的proposer發送者)在共識算法的所有實例中。client將命令發送給leader,leader決定每個命令應該放在序列的哪個地方。如果leader決定一個特定的client命令應該作為第135個命令,那么它就會將該命令作為共識算法第135個實例。這通常都會成功。但也有可能因為發生故障或者有另一個server認為自己是leader並且對第135條命令是什么有它自己的想法。但是共識算法確保了第135條命令最多只有一個。
Paxos共識算法效率的關鍵在於直到phase 2之前都不對提出的value進行選擇。回憶一下,是在完成了phase 1之后才知道要發送的value要么已經被決定了,要么proposer可以被任意選擇。
我現在要討論的是在正常執行時,Paxos state machine是怎么工作的。之后,還會描述什么情況下會出錯。我考慮的是當前一個leader剛剛發送故障但是新的leader還沒有選舉出來的情況(系統剛剛啟動時是一個特殊的情況,那時候還沒有任何命令被提交)。
新的leader,也是共識算法所有實例的leader,應該了解已經選擇的大多數命令。假設它知道命令1-134,138和139——即實例1-134,138和139選擇的值(接下來我們會知道命令序列中的gap是怎么產生的)。之后,它將執行實例135-137的phase 1以及所有大於139的實例(下面我將描述這是如何完成的)。假設這些操作的執行結果確定了實例135和140的value,但是其他實例的value還是未確定的。之后,leader將會執行實例135和140的phase 2,從而選擇了命令135和140。
leader以及那些獲取了leader已知的所有command的server現在可以執行命令1-135。然而,它仍然不能執行命令138-140,即使它已經知道它的內容了,因為命令136和137並沒有被選擇。leader可以將接下來client請求的兩個命令作為命令136和137。然而,我們通過發送特殊的讓狀態不發生改變的"noop"命令來馬上填充gap(通過執行共識算法的實例136和137的phase 2來實現)。一旦這些no-op命令被選中,命令138-140就可以執行了。
命令1-140已經選擇完畢。leader也完成了共識算法所有大於140的實例的phase 1。它將client發送的下一個請求賦值為141,並且將它作為共識算法實例141的value。之后再將用戶的下一個請求作為命令142,如此往復。
leader可以在它知道已經發送的命令141被選擇之前就發送命令142。可能發送命令141的所有數據都會丟失,命令142也可能在所有server都不知道leader發送的命令141的任何內容之前被選擇。當leader沒有收到它希望得到的關於實例141的phase 2信息的回復時,它會對這些信息進行重發。如果所有運行正常的話,發送的命令將會被選中。然而,在一開始可能會發生故障,從而在已選中的命令序列中留下一個gap。一般來說,假設一個leader可以提前獲取α個命令——這意味着在命令1到i被選中的前提下,它可以發送命令i + 1到i + α之間的命令。因此,一個至多為α−1條命令大的gap可能會出現。
一個新的被選中的leader可以執行無數多個共識算法實例的phase 1——在上面的場景中,即為實例135-137以及所有大於139的實例。通過對所有實例使用同一個proposal number,它可以用給其他server發送一個single reasonably short message來實現。在phase 1,如果一個acceptor已經從一些proposer收到phase 2信息的時候,它就會不僅僅回復一個簡單的Ok。(在例子中,就是對於實例135和140)因此,server(作為acceptor)可以用一個single reasonably short message來回復所有的instance。在無數個實例的phase 1這樣執行不會產生任何問題。
因為leader的故障和新的leader的選舉都是小概率事件,因此執行state machine command的花費——即實現command/value的共識——主要是共識算法phase 2的執行。可以看出Paxos共識算法的phase 2在所有會出現故障的情況能達到共識的所有算法里有着最小的可能花費。因此,Paxos算法基本上是最優的。
關於系統執行的正常操作假設除了當前leader發生故障,新的leader還未選出的短暫時間外,總是存在一個單一的leader。在一些意外的情況下,leader的選舉可能發送故障。如果沒有server作為leader執行,那么就不能有新的命令被發送。如果有多個server認為它們是leader,那么它們可以對共識算法的同一個實例發送value,而這會防止任何value被選擇。然而安全性總是被保留的——兩個不同的server用於不會不同意已經被選為第i個state machine command的value。單一leader的選舉僅僅只是為了保證流程的執行。
如果server的集合是可以改變的,我們必須要有辦法確定哪些server實現了共識算法的哪些實例。實現這個最簡單的方法就是通過state machine它自己。當前的server的集合可以作為狀態的一部分並且可以隨着ordinary state-machine command而改變。在執行完第i個state machine command之后,我們可以讓leader提前獲取α個命令,通過讓server的集合執行共識算法的第i + α個實例。
References
[1] Michael J. Fischer, Nancy Lynch, and Michael S. Paterson. Impossibility of distributed consensus with one faulty process. Journal of the ACM, 32(2):374–382, April 1985.
[2] Idit Keidar and Sergio Rajsbaum. On the cost of fault-tolerant consensus when there are no faults—a tutorial. TechnicalReport MIT-LCS-TR-821, Laboratory for Computer Science, Massachusetts Institute Technology, Cambridge, MA, 02139, May 2001. also published in SIGACT News 32(2) (June 2001).
[3] Leslie Lamport. The implementation of reliable distributed multiprocess systems. Computer Networks, 2:95–114, 1978.
[4] Leslie Lamport. Time, clocks, and the ordering of events in a distributed system. Communications of the ACM, 21(7):558–565, July 1978.
[5] Leslie Lamport. The part-time parliament. ACM Transactions on Computer Systems, 16(2):133–169, May 1998.