https://lamport.azurewebsites.net/pubs/paxos-simple.pdf
第一章 Paxos算法背景
Paxos算法是Lamport宗師提出的一種基於消息傳遞的分布式一致性算法,使其獲得2013年圖靈獎。
Paxos由Lamport於1998年在《The Part-Time Parliament》論文中首次公開,最初的描述使用希臘的一個小島Paxos作為比喻,描述了Paxos小島中通過決議的流程,並以此命名這個算法,但是這個描述理解起來比較有挑戰性。后來在2001年,Lamport覺得同行不能理解他的幽默感,於是重新發表了朴實的算法描述版本《Paxos Made Simple》。
自Paxos問世以來就持續壟斷了分布式一致性算法,Paxos這個名詞幾乎等同於分布式一致性。Google的很多大型分布式系統都采用了Paxos算法來解決分布式一致性問題,如Chubby、Megastore以及Spanner等。開源的ZooKeeper,以及MySQL 5.7推出的用來取代傳統的主從復制的MySQL Group Replication等紛紛采用Paxos算法解決分布式一致性問題。
1.1 拜占庭將軍問題
拜占庭帝國有一支龐大的軍隊,這只軍隊有多個小分隊(A-B-C-D-E),分別位於帝國的各個角落。
一天,E分隊的將軍打算對某個倒霉國家發動進攻,但是必須要得到所有小分隊的同意才能執行。無奈這座帝國太大,各個小分隊之間的信息不得不依靠信使來傳遞。於是,E分隊的將軍分別派出a、b、c、d四個信使傳遞進攻任務的信息給其他各個分隊。此時問題來了,沒法保證信息准確可靠的傳遞給他們!假如d信使本就是個叛徒,亦或是a信使在路上被人劫持,再或是b信使走到一半無路可走,等等情況。這個其實非常類似於分布式系統中各節點間通信,尤其是網絡問題,很容易導致通信在某兩個節點間中斷。該問題有沒有解決辦法呢?有!但不是今天的重點。那么,Paxos算法跟拜占庭將軍問題之間是什么關系呢?答案就是:Paxos算法的前提,不存在拜占庭將軍問題(即通信是保證可靠的不會被篡改,但可以存在丟失延遲等問題)。
現實中是否存在某個環境,不存在拜占庭將軍問題呢?當然有,否則Paxos算法就沒有用武之地了。我們知道zookeeper解決一致性問題,就是對Paxos算法進行了實現,而類似於zookeeper這類分布式系統,本身就被部署在了一個安全的局域網環境中,尤其是生產環境,出現該問題的概率非常小,可以簡單的認為不存在就行了。
第二章 Paxos算法流程
Paxos算法解決的問題正是分布式一致性問題,即一個分布式系統中的各個進程如何就某個值(決議)達成一致。
Paxos算法運行在允許宕機故障的異步系統中,不要求可靠的消息傳遞,可容忍消息丟失、延遲、亂序以及重復。它利用大多數 (Majority) 機制保證了2F+1的容錯能力,即2F+1個節點的系統最多允許F個節點同時出現故障。
一個或多個提議進程 (Proposer) 可以發起提案 (Proposal),Paxos算法使所有提案中的某一個提案,在所有進程中達成一致。系統中的多數派同時認可該提案,即達成了一致。最多只針對一個確定的提案達成一致。
2.1 系統角色
Paxos將系統中的角色分為提議者 (Proposer),決策者 (Acceptor),和最終決策學習者 (Learner):
- Proposer: 提出提案 (Proposal)。Proposal信息包括提案編號 (Proposal ID) 和提議的值 (Value)。
- Acceptor:參與決策,回應Proposers的提案。收到Proposal后可以接受提案,若Proposal獲得多數Acceptors的接受,則稱該Proposal被批准。
- Learner:不參與決策,從Proposers/Acceptors學習最新達成一致的提案(Value)。
在多副本狀態機中,每個副本同時具有Proposer、Acceptor、Learner三種角色。
算法最終目標:每個Proposer、Acceptor和Learner都認為同一個Proposal中的value被選中。
2.2 提案編號
編號 (Proposal ID)由兩部分組成。高位是整個提案過程中的輪數(Round),低位是我們剛才的服務器編號。每個服務器呢,都會記錄到自己至今為止看到過,或者用到過的最大的輪數。
那么,當某一台服務器,想要發起一個新提案的時候,就要用它拿到的最大輪數加上 1,作為新提案的輪數,並且把自己的服務器編號拼接上去,作為提案號發放出去。並且這個提案號必須要存儲在磁盤上,避免節點在掛掉之后,不知道最新的提案號是多少。
通過這個方式,我們就讓這個提案號做到了兩點:首先是不會有重復的提案號,不會存在兩個服務器發出相同提案號的情況;其次是提案號能夠按照數值大小,區分出先后和大小。即使是同一狀態下不同服務器發出的提案,也能比較大小。
2.3 Prepare 階段
Prepare 階段那么,當提案者Proposer收到一條來自客戶端的請求之后,它就會以提案者的身份發起提案。提案包括了前面的提案號,我們把這個提案號就叫做 M。這個提案會廣播給所有的Acceptor接受者,這個廣播請求被稱為 Prepare 請求。(注意這里只發送編號沒有內容)
而所有的 Acceptor 在收到提案的時候,會返回一個響應給提案者。這個響應包含的信息是這樣的:
- 首先,所有的Acceptor接受者一旦收到前面的 Prepare 請求之后,都會promise承諾它接下來,永遠不會接受提案號比當前提案號(Proposal ID) M 小的請求;
- 其次,如果Acceptor接受者之前已經接受過其他提案的內容(假設是 X)了,那么它要存儲下已經接受過的內容和對應的提案號。並且在此之后,把這個提案號和已經接受過的內容 X,一起返回給Proposer提案者。而如果沒有接受過,就把內容填為 NULL。
這樣一個來回,就稱之為 Paxos 算法里的 Prepare 階段。要注意,這里的Acceptor接受者只是返回告知Proposer提案者信息,它還沒有真正接受請求。這個過程,本質上是提案者去查詢所有的Acceptor接受者,是否已經接受了別的提案。
2.4 Accept 階段
當Proposer提案者收到超過半數的響應之后呢,整個提案就進入第二個階段,也稱之為 Accept 階段。Proposer提案者會再次發起一個廣播請求,里面包含這樣的信息:
- 首先仍然是一個提案號,這個提案號就是剛才的 Prepare 請求里的提案號 M;
- 其次,是提案號里面的內容,一般我們也稱之為提案的值。不過這個值,就有兩種情況了。
第一種情況,是之前Acceptor接受者已經接受過值了。那么這里的值,是所有Acceptor接受者返回過來,接受的值當中,提案號最大的那個提案的值。也就是說,提案者說,既然之前已經做出決策了,那么我們就遵循剛才的決策就好了。
而第二種情況,如果所有的Proposer提案者返回的都是 NULL,那么這個請求里,Proposer提案者就放上自己的值,然后告訴大家,請大家接受我這個值。
那么接受到這個 Accept 請求的接受者,在此時就可以選擇接受還是拒絕這個提案的值。通常來說:
- 如果Acceptor接受者沒有遇到其他並發的提案,自然會接受這個值。一旦提案者收到超過半數的接受者“接受”的請求。那么它就會確定,自己提交的值被選定了。
- 但也有可能,接受者剛才已經答應了某個新的提案者說,不能接受一個比提案號 N 早的請求。而 N>M,所以這個時候Acceptor接受者會拒絕 M。
- 不管是接受還是拒絕,這個時候Acceptor接受者都會把最新的提案編號 N,返回給Proposer提案者。
- 還是要注意,這個時候接受者接受了請求,並不代表這個請求在整個系統中被“選擇”了。
提案者還是會等待至少一半的接受者返回的響應。如果其中有人拒絕,那么提案者就需要放棄這一輪的提案,重新再來:生成新的提案號、發起 Prepare 請求、發起 Accept 請求。而當超過一半人表示接受請求的時候,提案者就認為提案通過了。當然,這個時候我們的提案雖然沒有變,但是提案號已經變了。而當沒有人拒絕,並且超過一半人表示接受請求的時候,提案者就認為提案通過了。
Paxos算法偽代碼描述如下:
第三章 案例
這里還是以拜占庭將軍問題為例,這里兩個參謀作為Proposer,三個將軍作為Acceptor
案例1
參謀1提出一個Prepare請求,三位將軍收到提案后,進行了響應,因為之前沒有接受過其他的提案,三位將軍返回null,OK即可。
參謀1收到超過半數響應后,進入第二階段,發送accept請求(包含提案編號、提案值-進攻時間),三位將軍之前沒有遇到其他提案,會接受這個值Accepted,提案達成了。
參謀2再提出一個Prepare請求,編號2,但三位將軍已經接受過之前編號1的提案了,會將提案號和已經接受過的內容 返回給參謀2
這里參謀2收到多數響應,還會發送accept請求(編號2)。之前接受者已經接受過值了。那么這里的值,是所有接受者返回過來(進攻時間1)
案例2
來一個復制的並發例子
參謀1提出一個Prepare請求(編號1),將軍1和將軍2收到提案后,進行了響應,但到將軍3的通訊中斷了(通訊兵被俘虜),但參謀1收到超過半數響應后,進入Accept階段。
這時參謀2也提出一個Prepare請求(編號2),將軍2和將軍3收到提案后,但到將軍1的通訊中斷了(通訊兵被俘虜)沒有收到,將軍2會承諾不再接受比編號2小的提案了,注意將軍2這時沒有接受提案內容。將軍2和將軍3也構成了相應的多數派,參謀2進入Accept階段。
參謀1發送accept請求,將軍1沒有什么問題,接受了提案的值,但將軍2剛才已經接受了編號2的提案,不能再接受比2小的編號1提案,給拒絕了。有人拒絕,那么提案者就需要放棄這一輪的提案,重新再來。
參謀2也發送accept請求,指定編號2,進攻時間2,將軍2和將軍3之前都沒有接受過值,便接受了提案的進攻時間2,滿足了多數派,達成了一致
參謀1之前傳達失敗,重新提出Prepare請求(編號3)。將軍1已經接受過編號1的提案了,返回編號1進攻時間1;將軍2已經接受過編號2的提案了,返回編號2進攻時間2,進入accept階段。
將軍1和將軍2已經接受過值了,參謀1選取編號大的提案的值(既然之前已經做出決策了,那么我們就遵循剛才的決策就好了),發送accept請求(編號3,進攻時間2),將軍1和將軍2之前沒有接受過比這個議案編號更大的議案了,所以選擇接受,返回成功,整個系統達成了共識。
第四章 總結
在 Paxos 算法這個過程中,其實一直在確保一件事情,就是所有節點,需要對當前接受了哪一個提案達成多數共識。
但Paxos算法的開銷太大了。無論是否系統里面出現並發的情況,任何一個共識的達成,都需要兩輪 RPC 調用。而且,所有的數據寫入,都需要在所有的接受者節點上都寫入一遍。
所以,雖然 Paxos 算法幫助我們解決了單點故障,並且在沒有單點的情況下,實現了共識算法,確保所有節點的日志順序是相同的。但是,原始的 Paxos 算法的性能並不好。只是簡單地寫入一條日志,我們就可能要解決多個 Proposer 之間的競爭問題,有可能需要有好幾輪的網絡上的 RPC 調用。
當然,我們可以用各種手段在共識算法層面進行優化,比如一次性提交一組日志,而不是一條日志。這也是后續 Multi-Paxos 這些算法想到的解決方案。但是,如果我們往一個數據庫同步寫入日志都要通過 Paxos 算法,那么無論我們怎么優化,性能都是跟不上的。根本原因在於,在 Paxos 算法里,一個節點就需要承接所有的數據請求。雖然在可用性上,我們沒有單點的瓶頸了,但是在性能上,我們的瓶頸仍然是單個節點。