Paxos在分佈式環境下應用非常廣泛,是一致性算法裏面優越的代表。Google的粗粒度鎖服務Chubby的設計開發者Burrows曾經說過:所有一致性協議本質上要麼是Paxos要麼是其變體。
一、Paxos概念
Paxos算法是基於消息傳遞且具有高度容錯特性的一致性算法,是目前公認的解決分佈式一致性問題最有效的算法之一。
Paxos算法是Lamport宗師提出的一種基於消息傳遞的分佈式一致性算法,使其獲得2013年圖靈獎。
自Paxos問世以來就持續壟斷了分佈式一致性算法,Paxos這個名詞幾乎等同於分佈式一致性。
Google的很多大型分佈式系統都採用了Paxos算法來解決分佈式一致性問題,如Chubby、Megastore以及Spanner等。開源的ZooKeeper,以及MySQL 5.7推出的用來取代傳統的主從複製的MySQL Group Replication等紛紛採用Paxos算法解決分佈式一致性問題。
但是它也有兩個明顯的缺點:
- 難以理解
- 在工程是實現上比較複雜。
二、問題產生的背景
1、系統異常情況
在常見的分佈式系統中,總會發生諸如機器宕機或網絡異常(包括消息的延遲、丟失、重複、亂序,還有網絡分區)等情況。
Paxos算法需要解決的問題就是如何在一個可能發生上述異常的分佈式系統中,快速且正確地在集羣內部對某個數據的值達成一致,並且保證不論發生以上任何異常,都不會破壞整個系統的一致性。
這裏某個數據的值並不只是狹義上的某個數,它可以是一條日誌,也可以是一條命令(command)。根據應用場景不同,某個數據的值有不同的含義。
2、相關概念
在Paxos算法中,有三種角色:
- Proposer (提案者)
- Acceptor (人大代表)
- Learners (廣大羣衆)
需要注意的是,在具體的算法實現過程中,並不是一個進程只能擔任其中一種角色,它有可能會同時充當多個。比如一個進程既是Proposer又是Acceptor還是Learner。
還有一個很重要的概念叫提案(Proposal)。最終要達成一致的value就在提案裏。
這個提案包括什麼呢?是僅僅包括一個信息數值嗎? 目前咱們先認爲僅僅是一個普普通通的value。
Paxos算法過程和我國的立法過程是極其相似的(法律案的提出、法律案的審議、法律案的表決、法律的公佈四個階段),所謂的提案就是新頒佈法律。
Proposer (提案者)可以提出(propose)提案;Accoptor可以接受(accept)提案;如果某個提案被選定(chosen),那麼該提案裏的value就被選定了。
回到剛剛說的『對某個數據的值達成一致』,指的是Proposer、Acceptor、Learner都認爲同一個value被選定(chosen)。那麼,Proposer、Acceptor、Learner分別在什麼情況下才能認爲某個value被選定呢?
- Proposer:只要Proposer發的提案被Acceptor接受(剛開始先認爲只需要一個Acceptor接受即可,在推導過程中會發現需要半數以上的Acceptor同意纔行),Proposer就認爲該提案裏的value被選定了。
- Acceptor:只要Acceptor接受了某個提案,Acceptor就認爲該提案裏的value被選定了。
- Learner:作爲一個學習者,Acceptor告訴Learner哪個value被選定,Learner就認爲那個value被選定。
3、問題產生
假設有一組可以提出(propose)value的進程集合(提案者團隊),一個一致性算法需要保證提出的這麼多value中,僅僅只有一個相同的value被選定(chosen)。也就是說要麼沒有value被提出,只要提出了value並且被選定,那麼大家最終學習到的value必須是一致的。對於一致性算法,安全性(safaty)要求如下:
- 只有被提出的value才能被選定。
- 只有一個value被選定。
- 如果某個進程認爲某個value被選定了,那麼這個value必須是真的被選定的那個。
“Paxos的目標:保證最終有一個value會被選定,當value被選定後,進程最終也能獲取到被選定的value。”
如果假設不同角色之間可以通過發送消息來進行通信,那麼:
- 每個角色以各自任意的速度進行通信執行,在這個過程中可能會因爲各種原因出錯而導致執行停止或重啓。當一個value被選定之後,因爲故障原因才恢復正常的角色因爲失去了某些重要的信息,導致它們無法確定被選定的值。
- 消息在傳遞過程中可能出現任意時長的延遲,可能會重複,也可能丟失。但是消息不會被損壞,即消息內容不會被篡改(拜占庭將軍問題)。
以上都是可能會遇到的問題,要怎麼解決???
二、推導過程
1、最簡單的方案——只有一個Acceptor
假設只有一個Acceptor(可以有多個Proposer),只要Acceptor接受它收到的第一個提案,則該提案被選定,該提案裏的value就是被選定的value。這樣就保證只有一個value會被選定。
但是,如果這個唯一的Acceptor宕機了,那麼整個系統就無法工作了!
因此,一個Acceptor是不可行的,必須要有多個Acceptor!
如下圖 只有一全Acceptor
2、多個Acceptor
當有多個Acceptor的時候,如何保證在多個Proposer和多個Acceptor的情況下選定一個value呢?
大家可以自己先進行思考。
首先,我們的最終目標是無論有多少Proposer提出提案,有且僅有一個value被選定。
那麼,我們可以先定義一個約束:
“P1:一個Acceptor必須接受它收到的第一個提案。”
但是,這樣又會出現其它的問題:如果每個Proposer所提出的提案value是不同的,並且將提案發送給不同的Acceptor。根據P1約束,每個Acceptor都接受它收到的第一個提案,就會出現不同value被選定的情況,出現了不一致。
剛剛是因爲『一個提案只要被一個Acceptor接受,則該提案的value就被選定了』才導致了出現上面不一致的問題。因此,我們需要加一個規定:
“規定:一個提案被選定需要被半數以上的Acceptor接受”
一個提案被半數以上接受,說明『一個Acceptor必須能夠接受不止一個提案!』,不然可能導致最終沒有value被選定。比如上圖的情況。v1、v2、v3都沒有被選定,因爲它們都只被一個Acceptor的接受,並沒有被超過半數以上的Acceptor接受。
最開始將【提案 = value】已經無法滿足現在的需求,因爲當一個Proposer發送多個提案到一個Acceptor的時候,需要使用一個編號來區分被提出的順序。現在【提案=提案編號+value】。
雖然允許多個提案被選定,但必須保證所有被選定的提案都具有相同的value值。否則又會出現不一致。
“P2:如果某個value爲v的提案被選定了,那麼每個編號更高的被選定提案的value必須也是v。”
一個提案只有被Acceptor接受纔可能被選定,因此我們可以把P2約束改寫成對Acceptor接受的提案的約束P2a。
“P2a:如果某個value爲v的提案被選定了,那麼每個編號更高的被Acceptor接受的提案的value必須也是v。”
只要滿足了P2a,就能滿足P2。
但是,考慮如下的情況:以立法過程爲背景,假設總的有5個人大代表(Acceptor)。
人民法院(Proposer2)提出[M1,V1]的提案,人大代表2-5號(半數以上)均接受了該提案,於是對於人大代表2-5號和人民法院來講,它們都認爲V1提案是被選定的。此時,人大代表1在辦完其它事務之後也參與到其中(之前人大代表1沒有收到過任何提案),此時最高人民檢察院(另一個提案者Proposer1)向人大代表1發送了[M2,V2]的提案(V2≠V1且M2>M1),對於人大代表1來講,這是它收到的第一個提案。根據P1(一個Acceptor必須接受它收到的第一個提案。),人大代表1必須接受該提案!同時人大代表1認爲V2被選定。這就出現了兩個問題:
- 人大代表1認爲V2被選定,人大代表2-5和人民法院認爲V1被選定。出現了不一致。
- V1被選定了,但是編號更高的被人大代表1接受的提案[M2,V2]的value爲V2,且V2≠V1。這就跟P2a(如果某個value爲v的提案被選定了,那麼每個編號更高的被Acceptor接受的提案的value必須也是v)矛盾了。
所以,我們要對P2a約束進行加強!
P2a是對Acceptor接受的提案約束,但其實提案是Proposer提出來的,所有我們可以對Proposer提出的提案進行約束。得到P2b:
“P2b:如果某個value爲v的提案被選定了,那麼之後任何Proposer提出的編號更高的提案的value必須也是v。”
那麼,如何確保在某個value爲v的提案被選定後,Proposer提出的編號更高的提案的value都是v呢?
只要滿足P2c即可:
“P2c:對於任意的N和V,如果提案[N, V]被提出,那麼存在一個半數以上的Acceptor組成的集合S,滿足以下兩個條件中的任意一個:
- S中每個Acceptor都沒有接受過編號小於N的提案。
- S中Acceptor接受過的最大編號的提案的value爲V。”
Proposer生成提案
爲了滿足P2b,這裏有個比較重要的思想:Proposer生成提案之前,應該先去『學習』已經被選定或者可能被選定的value,然後以該value作爲自己提出的提案的value。如果沒有value被選定,Proposer纔可以自己決定value的值。這樣才能達成一致。這個學習的階段是通過一個『Prepare請求』實現的。
於是我們得到了如下的提案生成算法:
- Proposer選擇一個新的提案編號N,然後向某個Acceptor集合(半數以上)發送請求,要求該集合中的每個Acceptor做出如下響應(response)。
(a) 向Proposer承諾保證不再接受任何編號小於N的提案。
(b) 如果Acceptor已經接受過提案,那麼就向Proposer響應已經接受過的編號小於N的最大編號的提案。
我們將該請求稱爲編號爲N的Prepare請求。
- 如果Proposer收到了半數以上的Acceptor的響應,那麼它就可以生成編號爲N,Value爲V的提案[N,V]。
- 這裏的V是所有的響應中編號最大的提案的Value。
- 如果所有的響應中都沒有提案,那 麼此時V就可以由Proposer自己選擇(一般爲當前提案)。
- 生成提案後,Proposer將該提案發送給半數以上的Acceptor集合,並期望這些Acceptor能接受該提案。我們稱該請求爲Accept請求。(注意:此時接受Accept請求的Acceptor集合不一定是之前響應Prepare請求的Acceptor集合)
Acceptor接受提案
Acceptor可以忽略任何請求(包括Prepare請求和Accept請求)而不用擔心破壞算法的安全性。因此,我們這裏要討論的是什麼時候Acceptor可以響應一個請求。
我們對Acceptor接受提案給出如下約束:
“P1a:一個Acceptor只要尚未響應過任何編號大於N的Prepare請求,那麼他就可以接受這個編號爲N的提案。”
如果Acceptor收到一個編號爲N的Prepare請求,在此之前它已經響應過編號大於N的Prepare請求。根據P1a,該Acceptor不可能接受編號爲N的提案。因此,該Acceptor可以忽略編號爲N的Prepare請求。當然,也可以回覆一個error,讓Proposer儘早知道自己的提案不會被接受。
因此,一個Acceptor只需記住:
1. 已接受的編號最大的提案
2. 已響應的請求的最大編號。
三、Paxos算法描述
經過上面的推導,我們總結下Paxos算法的流程。
Paxos算法分爲兩個階段。具體如下:
1.階段一:
- Proposer選擇一個提案編號N,然後向半數以上的Acceptor發送編號爲N的Prepare請求。
- 如果一個Acceptor收到一個編號爲N的Prepare請求,且N大於該Acceptor已經響應過的所有Prepare請求的編號,那麼它就會將它已經接受過的編號最大的提案(如果有的話) 作爲響應反饋給Proposer,同時該Acceptor承諾不再接受任何編號小於N的提案。
2.階段二:
- 如果Proposer收到半數以上Acceptor對其發出的編號爲N的Prepare請求的響應,那麼它就會發送一個針對[N,V]提案的Accept請求給半數以上的Acceptor(和之前的Acceptor不一定相同)。注意:V就是收到的響應中編號最大的提案的value,如果響應中不包含任何提案,那麼V就由Proposer自己決定。
- 如果Acceptor收到一個針對編號爲N的提案的Accept請求,只要該Acceptor沒有對編號大於N的Prepare請求做出過響應,它就接受該提案。
3.算法實現流程
(1)提議者發出提案,發起一次投票,發現者接收到投票請求,將提案轉發給參與者;
(2)參與者接收到投票請求後,會對提案進行投票,投票正確時發送投票確認消息;
(3)發起者收到參與者發回的投票確認消息,如果收到的投票確認消息超過半數,則發起者發出accept消息,將提案接受;
(4)參與者接收到accept消息後,如果投票正確,則發送accept確認消息;
(5)發起者收到accept確認消息後,如果收到的accept確認消息超過半數,則發起者發出commit消息,將提案接受,並執行操作;
(6)參與者接收到commit消息後,如果投票正確,則發送commit確認消息;
(7)發起者收到commit確認消息後,如果收到的commit確認消息超過半數,則發起者發出ack消息,將提案接受,並將操作結果返回給發起者;
(8)參與者接收到ack消息後,如果投票正確,則發送ack確認消息;
(9)發起者收到ack確認消息後,如果收到的ack確認消息超過半數,則發起者發出完成消息,將提案接受,並將操作結果返回給發起者,完成Paxos算法。
算法實現流程畫圖補充。。。。
四、java代碼實現
注:代碼不完整,不可用於實際項目,僅是對算法梳理流程,以便有於大家易於理解算法思想,後續抽空寫一個完全的代碼項目工程。
查看代碼
import java.util.*;
public class Paxos {
// proposer
public static class Proposer {
private int id;
private int value;
private int maxValue;
private int vNumber;
private int aNumber;
public Proposer(int id, int value, int maxValue) {
this.id = id;
this.value = value;
this.maxValue = maxValue;
this.vNumber = 0;
this.aNumber = 0;
}
// 第一階段:建議階段
public void prepare() {
System.out.println("Proposer " + id + ": Suggesting...");
vNumber = maxValue + 1;
System.out.println("Proposer " + id + ": Suggested VNumber: " + vNumber);
}
// 第二階段:接受階段
public void accept() {
System.out.println("Proposer " + id + ": Accepting...");
aNumber = vNumber;
System.out.println("Proposer " + id + ": Accepted ANumber: " + aNumber);
}
// 第三階段:承諾階段
public void commit() {
System.out.println("Proposer " + id + ": Committing...");
System.out.println("Proposer " + id + ": Committed Value: " + value);
}
public int getId() {
return id;
}
public int getValue() {
return value;
}
public int getVNumber() {
return vNumber;
}
public int getANumber() {
return aNumber;
}
}
// acceptor
public static class Acceptor {
private int id;
private int vNumber;
private int aNumber;
private int value;
public Acceptor(int id) {
this.id = id;
this.vNumber = 0;
this.aNumber = 0;
this.value = 0;
}
// 接受來自proposer的prepare消息
public void receivePrepare(int vNumber) {
System.out.println("Acceptor " + id + ": Receiving prepare message...");
if (vNumber > this.vNumber) {
this.vNumber = vNumber;
System.out.println("Acceptor " + id + ": Accepted VNumber: " + vNumber);
} else {
System.out.println("Acceptor " + id + ": Rejected VNumber: " + vNumber);
}
}
// 接受來自proposer的accept消息
public void receiveAccept(int aNumber, int value) {
System.out.println("Acceptor " + id + ": Receiving accept message...");
if (aNumber >= this.vNumber) {
this.aNumber = aNumber;
this.value = value;
System.out.println("Acceptor " + id + ": Accepted ANumber: " + aNumber);
System.out.println("Acceptor " + id + ": Accepted Value: " + value);
} else {
System.out.println("Acceptor " + id + ": Rejected ANumber: " + aNumber);
}
}
public int getId() {
return id;
}
public int getVNumber() {
return vNumber;
}
public int getANumber() {
return aNumber;
}
public int getValue() {
return value;
}
}
// learner
public static class Learner {
private int id;
private int value;
public Learner(int id) {
this.id = id;
this.value = 0;
}
public void learn(int value) {
this.value = value;
System.out.println("Learner " + id + ": Learnt Value: " + value);
}
public int getId() {
return id;
}
public int getValue() {
return value;
}
}
// 測試用例
public static void testPaxos() {
// 創建proposer
Proposer proposer = new Proposer(1, 100, 200);
// 創建acceptor
List<Acceptor> acceptors = new ArrayList<>();
acceptors.add(new Acceptor(1));
acceptors.add(new Acceptor(2));
acceptors.add(new Acceptor(3));
// 創建learner
Learner learner = new Learner(1);
// 開始協商
proposer.prepare();
for (Acceptor acceptor : acceptors) {
acceptor.receivePrepare(proposer.getVNumber());
}
proposer.accept();
for (Acceptor acceptor : acceptors) {
acceptor.receiveAccept(proposer.getANumber(), proposer.getValue());
}
proposer.commit();
learner.learn(proposer.getValue());
// 輸出結果
System.out.println("Learner 1: Final Value: " + learner.getValue());
}
public static void main(String[] args) {
testPaxos();
}
}
五、存在問題討論
1、Learner學習者是如何被選定的value?
Learner學習(獲取)被選定的value有如下三種方案:
方案一
Acceptor接受到一個提案,就將該提案發送給所有Learners.
- 優點:Learner能夠快速獲取被選定的value
- 缺點:通信次數爲M*N(M爲提案數,N爲Learner數)
方案二
Acceptor接受一個提案,就將提案發送給主Learner,主Learner再通知其它Learner
- 優點:通信次數減少(M+N-1)(M爲提案數,N爲Learner數,M個提案發送給主Learner,然後主Learner通知N-1個Learner)
- 缺點:單點故障問題(主Learner可能出現故障)
方案三
Acceptor接受一個提案,就將提案發送給Learner團,Learner團再通知其它Learner
- 優點:解決了方案二單點故障問題,可靠性好
- 缺點:實現複雜,網絡通信複雜度高
2、如何保證Paxos算法的活性?
通過選取主Proposer,就可以保證Paxos算法的活性。通過選取主Proposer,並規定只有主Proposer才能提出議案。這樣一來只要主Proposer和過半的Acceptor能夠正常進行網絡通信,那麼但凡主Proposer提出一個編號更高的提案,該提案終將會被批准,這樣通過選擇一個主Proposer,整套Paxos算法就能夠保持活性。至此,我們得到一個既能保證安全性,又能保證活性的分佈式一致性算法——Paxos算法。
插圖說明。。。
六、總結
通過對Paxos算法解讀分析、它的特性以及算法的具體推導過程做了詳細的闡述。Paxos算法是現在很多一致性算法的變體,在互聯網時代分佈式環境應用非常廣泛,非常值得我們學習。