P2P系統中一個最重要也是最複雜的問題就是共識,也就是對於分散在各處的網絡節點,如何讓他們對某件事情達成一致。因爲網絡的複雜性(網絡傳輸有延遲、數據無序到達、節點可能宕機不響應,惡意節點僞造數據等等),節點之間達成共識非常複雜。比特幣區塊鏈作爲一個分佈式的賬本,它的節點間如何形成共識,可以說是比特幣以及任何區塊鏈都需要解決的核心問題,理解共識算法也是區塊鏈學習的重中之重。
分佈式系統的共識算法主要分爲兩類:CFT算法(Crash falut)和BFT(Byzantine fault)算法。CFT算法主要解決網絡中節點可能出錯(比如宕機),但是節點不會作惡(比如僞造數據)的情況下節點之間如何達成共識,而BFT算法則針對網絡中可能存在節點作惡的情況下節點間達成共識的方法。
從本文開始,將分多篇文章來了解常用的共識算法,包括CFT類算法的經典算法paxos,BFT類算法的PBFT算法,比特幣中使用的PoW工作量證明算法,權益證明算法PoS等。本文先從paxos算法開始。
1 分佈式系統共識問題的基本原理
在瞭解分佈式系統的共識算法前,先來熟悉幾個和分佈式系統共識有關的基本原理。1.1 FLP不可能原理
任何問題一般都存在一個理論上的下限(最壞的情況),那麼對於分佈式系統的共識問題,其理論上的解是什麼呢?經過科學家的證明,異步分佈式系統的共識問題的通用解決方法是:無解,也就是說即便是在網絡通信可靠的情況下,可擴展的分佈式系統的共識問題是無解的。這個定理由Fischer,Lynch和Patterson三位科學家於1985年發表的論文中給出了證明,也叫做FLP不可能原理。這個定理也告訴人們不要試圖去設計一套在任意場景下都適用的共識算法,否則等同於浪費時間。
關於FLP定理的證明,可以參考下面這篇文章:
定理的證明用到了圖論的知識,對於像筆者這樣數學已經忘的差不多的同學,可以先記住這個無解的定理。
1.2 CAP原理
先說說CAP原理中的C/A/P是指什麼:
Consistency:一致性,是指分佈式系統中的所有節點的數據備份,在同一時刻都具有同樣的值(強一致性),換言之,所有用戶任一時刻訪問系統,都能得到相同的結果(不能是用戶A得到1,用戶B得到0);
Availability:可用性,是指分佈式系統中的一部分節點出現故障以後,系統依然能夠響應用戶的請求,換句話說就是用戶在任何時候都能向系統發出請求並得到響應(不能出現不響應的情況);
Partition:分區容忍性,如果分佈式系統不能再一定的時限內達成數據的一致性,意味着系統產生了分區,一旦產生了分區,就需要在C和A之間做出選擇:要麼犧牲可用性,不響應用戶;要麼犧牲一致性,給用戶響應不一致的結果;
CAP原理是指分佈式系統不可能同時滿足一致性、可用性和分區容忍性,實際實現時只能在三者之間做出權衡。用一句古話說就是魚與熊掌不可兼得。
此定理也是由FLP定理證明的作者之一的Lynch給出證明。
1.3 BASE原理
BASE是對CAP原理中一致性和可用性權衡以後的結果:
Base Available:基本可用性,是指在系統的部分節點出現故障以後,允許損失一部分可用性;
Soft state:軟狀態,允許系統中的數據存在中間狀態,允許不同節點之間的數據副本的同步過程存在延遲;
Eventually consistent:最終一致性,是指系統中的數據,在經過一定的時間以後會最終達到一致狀態,也就是降低一致性要求,不要求實時的強一致性。比特幣區塊鏈就是一個很好的例子,在區塊鏈延長的過程中,有可能會出現分叉,不同的節點上區塊不一樣(不一致),但是經過一定時間以後,隨着所有節點都切換到最長的主鏈,分叉的情況就會消失(最終一致)。
2 Paxos共識算法
2.1 paxos算法介紹
paxos算法是由Leslie Lamport於1990年提出的一種非拜占庭模型的分佈式系統共識算法。爲了描述算法,Lamport虛擬了一個希臘城邦paxos(這也是算法名字的由來),這個島按照議會民主制的政治模式制訂法律,但是沒有人願意將自己的全部時間和精力放在這種事情上。所以無論是議員,議長或者傳遞紙條的服務員都不能承諾別人需要時一定會出現,也無法承諾批准決議或者傳遞消息的時間。但是這裏假設沒有拜占庭將軍問題(Byzantine failure,即雖然有可能一個消息被傳遞了兩次,但是絕對不會出現錯誤的消息);只要等待足夠的時間,消息就會被傳到。另外,Paxos 島上的議員是不會反對其他議員提出的決議。
paxos算法的推導過程理解起來有一定困難,但是從操作層面講,paxos解決共識的思路其實並不難。
首先算法將網絡中的節點劃分爲3種角色:提案者,接受者和學習者。
提案者:負責提出提案,這裏提案可以是任何可以達成共識的東西,比如某個要寫入DB的值,一條日誌等等;
接受者:接收提案,確定接受還是拒絕提案;
學習者:獲取並同步最終選擇的提案;
提案是一個由提案編號和提案值組成的pair,即[proposalNo, proposalValue],一個提案被半數以上的接受者接受即認爲達成共識。提案編號是最終能達成共識的關鍵,每個接受者都會記錄已響應過的提案的最大編號,並且承諾不會接受比此提案號小的提案。
具體操作時,paxos可以分爲兩個階段:
第一階段:準備階段,提案者選擇一個提案號,發送提案給接受者,試探能否得到半數以上接受者的響應;
第二階段:如果第一階段收到超過半數的接受者的響應,則提交提案,如果能夠得到半數以上接受者的響應,則共識完成;
2.2 paxos算法推導過程
paxos算法最初的論文被公認爲是晦澀難懂,後來作者Lamport又發佈了《paxos made simple》,用更加容易理解的方式對paxos做了闡述。可以從下面的鏈接下載原論文:
論文中通過從簡單到複雜逐步添加約束條件的方式來論證其共識算法。雖然作者已經做了簡化,但畢竟還是比較學術化,理解論文中提出的幾個約束條件還是有一定的困難,詳細的過程讀者可以自行閱讀論文。
paxos算法的論證過程雖然比較難理解,但是實際的操作過程卻比較簡單,網上有人用一個形象的例子來說明paxos達成共識的過程:
假設有2個商人P1和P2想從政府買塊地,商人想要最終拿下這塊地,需要經過3個政府官員A1,A2和A3的審批,只有經過2個以上的官員同意,才能最終拿下地皮,現在的目標是:最終只能有一個商人拿到地。另外假設商人要想通過官員的審批,必須給一定數量的賄賂費。
我們看看這樣一個場景下,如何達成共識(選定一個商人把地批給他)。
拿地是一件爭分奪秒的事情,於是商人P1和P2開始準備了:
(1) 假設P1的行動快一些,他很快找到了官員A1和A2,告訴兩人只要批了地就給100W的感謝費,兩個官員很高興,告訴P1回去拿錢吧;
注:這一步實際上是P1在進行paxos算法中的準備階段;
(2) 商人P2在P1之前先找了官員A3,告訴他只要批了地就給他200W,A3愉快的接受了;
(3) P2又跑到官員A1和A2那,承諾只要批地,就給200W,因爲這份費用比此前P1承諾的高,於是貪財的官員A1和A2變卦了;
注:以上兩步是P2在進行paxos的準備階段;
(4) 商人P1此前已經初步賄賂A1和A2成功,於是滿心歡喜的拿着準備好的錢找到了A1和A2,結果卻是A1和A2都告訴他:對不起,Mr XX,就在剛剛有人承諾了200W,你是個聰明人,你懂得該怎麼做的。商人P1很是鬱悶,告訴A1和A2:容我想想再給答覆;
注:以上P1拿錢給A1和A2,對應與paxos算法的提交階段,因爲此前P1已經得到了3位官員中2位的同意;
(5) 就在P1還在猶豫要不要提高賄賂費的時候,商人P2按之前承諾的向A1和A2的賬戶分別打入200W,於是A1,A2拿錢辦事,批准通過,因爲超過半數的官員審批通過,於是在政府網站上向大衆公佈P2最終拿地成功,所有人都知道了這個結果。
注意上面的過程中的一個問題:假設上面第(4)步中P1被拒絕以後,立刻向官員承諾一個更高的費用,那麼當商人P2拿着錢到A1和A2時,同樣也會被拒絕,於是P2又可能會擡價,這樣交替下去就可能造成死循環,這就是paxos算法中的活鎖問題。
2.3 paxos算法的實現
libpaxos是一個開源的paxos算法的實現,我們可以結合這個開源庫的代碼學習paxos算法。這裏只挑重點來分析,對算法有興趣的同學可以自行蕩一份代碼下來學習之。
前面提到paxos算法是將節點的角色分爲了提案者:proposer,接受者acceptor和leaner,重點是proposer和acceptor,在libpaxos中這兩個角色的實現分別位於evproposer.c和evacceptor.c文件中。
以下是提案者的初始化:
struct evproposer*
evproposer_init_internal(int id, struct evpaxos_config* c, struct peers* peers)
{
struct evproposer* p;
int acceptor_count = evpaxos_acceptor_count(c);
p = malloc(sizeof(struct evproposer));
p->id = id;
p->preexec_window = paxos_config.proposer_preexec_window;
peers_subscribe(peers, PAXOS_PROMISE, evproposer_handle_promise, p);
peers_subscribe(peers, PAXOS_ACCEPTED, evproposer_handle_accepted, p);
peers_subscribe(peers, PAXOS_PREEMPTED, evproposer_handle_preempted, p);
peers_subscribe(peers, PAXOS_CLIENT_VALUE, evproposer_handle_client_value, p);
peers_subscribe(peers, PAXOS_ACCEPTOR_STATE,
evproposer_handle_acceptor_state, p);
// Setup timeout
struct event_base* base = peers_get_event_base(peers);
p->tv.tv_sec = paxos_config.proposer_timeout;
p->tv.tv_usec = 0;
p->timeout_ev = evtimer_new(base, evproposer_check_timeouts, p);
event_add(p->timeout_ev, &p->tv);
p->state = proposer_new(p->id, acceptor_count);
p->peers = peers;
event_base_once(base, 0, EV_TIMEOUT, evproposer_preexec_once, p, NULL);
return p;
}
首先提案者註冊了處理接受者響應消息的回調:
evproposer_handle_promise處理PAXOS_PROMISE消息,這個對應paxos的準備階段:提案者提出一個提案,然後接受者初步接受了此提案,並承諾不會接受比提案者提案編號更小的提案;
evproposer_handle_accepted處理PAXOS_ACCEPTED消息,這個對應paxos的提交階段:提案者提交提案,接受者接受了提案;
evproposer_handle_preemted處理PAXOS_PREEMTED消息:當接受者響應一個比提案者的提案更大的編號時,提案者知道有更新的提案了,此時提案者重新選擇一個提案編號再次進行paxos的準備階段。
2.3.1 準備階段
以下的代碼關鍵地方都做了註釋,讀者看註釋應該就能明白。準備階段:向acceptor發送提案,如果超過半數的acceptor接受此提案,則進入提交階段。
(1) proposer發送提案:
static void
proposer_preexecute(struct evproposer* p)
{
int i;
paxos_prepare pr;
int count = p->preexec_window - proposer_prepared_count(p->state);
if (count <= 0) return;
for (i = 0; i < count; i++) {
//生成提案
proposer_prepare(p->state, &pr);
//向連接的每個接受者發送提案
peers_foreach_acceptor(p->peers, peer_send_prepare, &pr);
}
paxos_log_debug("Opened %d new instances", count);
}
生成的提案信息將會被保存到一個instance實例中:
void
proposer_prepare(struct proposer* p, paxos_prepare* out)
{
int rv;
iid_t iid = ++(p->next_prepare_iid);
ballot_t bal = proposer_next_ballot(p, 0);
struct instance* inst = instance_new(iid, bal, p->acceptors);
khiter_t k = kh_put_instance(p->prepare_instances, iid, &rv);
assert(rv > 0);
kh_value(p->prepare_instances, k) = inst;
*out = (paxos_prepare) {inst->iid, inst->ballot};
}
(2) acceptor接受第一階段提案
下面的代碼片段展示了acceptor接收到proposer發送的準備階段的提案以後的處理:
static void
evacceptor_handle_prepare(struct peer* p, paxos_message* msg, void* arg)
{
paxos_message out;
paxos_prepare* prepare = &msg->u.prepare;
struct evacceptor* a = (struct evacceptor*)arg;
paxos_log_debug("Handle prepare for iid %d ballot %d",
prepare->iid, prepare->ballot);
if (acceptor_receive_prepare(a->state, prepare, &out) != 0) {
send_paxos_message(peer_get_buffer(p), &out);
paxos_message_destroy(&out);
}
}
acceptor_receive_prepare的代碼:
int
acceptor_receive_prepare(struct acceptor* a, paxos_prepare* req, paxos_message* out)
{
paxos_accepted acc;
if (req->iid <= a->trim_iid)
return 0;
memset(&acc, 0, sizeof(paxos_accepted));
if (storage_tx_begin(&a->store) != 0)
return 0;
//檢索本地已接受的提案
int found = storage_get_record(&a->store, req->iid, &acc);
//沒有此提案或者提案者有編號更大的提案,更新本地的提案信息
if (!found || acc.ballot <= req->ballot) {
paxos_log_debug("Preparing iid: %u, ballot: %u", req->iid, req->ballot);
acc.aid = a->id;
acc.iid = req->iid;
//提案號更新爲收到的更大的提案的編號
acc.ballot = req->ballot;
if (storage_put_record(&a->store, &acc) != 0) {
storage_tx_abort(&a->store);
return 0;
}
}
if (storage_tx_commit(&a->store) != 0)
return 0;
paxos_accepted_to_promise(&acc, out); //響應提案者
return 1;
}
(3) proposer處理第一階段提案的響應
static void
evproposer_handle_promise(struct peer* p, paxos_message* msg, void* arg)
{
struct evproposer* proposer = arg;
paxos_prepare prepare;
paxos_promise* pro = &msg->u.promise;
int preempted = proposer_receive_promise(proposer->state, pro, &prepare);
//如果接受者響應了更高的提案編號,則重新選擇提案號發送提案
if (preempted)
peers_foreach_acceptor(proposer->peers, peer_send_prepare, &prepare);
try_accept(proposer);
}
proposer_receive_promise的實現如下:
int
proposer_receive_promise(struct proposer* p, paxos_promise* ack,
paxos_prepare* out)
{
khiter_t k = kh_get_instance(p->prepare_instances, ack->iid);
if (k == kh_end(p->prepare_instances)) {
paxos_log_debug("Promise dropped, instance %u not pending", ack->iid);
return 0;
}
struct instance* inst = kh_value(p->prepare_instances, k);
//忽略編號更小的提案
if (ack->ballot < inst->ballot) {
paxos_log_debug("Promise dropped, too old");
return 0;
}
//接收端的提案編號比發送的提案編號更大,重新選擇一個提案號生成提案
if (ack->ballot > inst->ballot) {
paxos_log_debug("Instance %u preempted: ballot %d ack ballot %d",
inst->iid, inst->ballot, ack->ballot);
proposer_preempt(p, inst, out); //重新生成一個提案號
return 1;
}
//如果此前指定的接受者已經接受了此提案,直接返回,否則提案被接受的數量加1
if (quorum_add(&inst->quorum, ack->aid) == 0) {
paxos_log_debug("Duplicate promise dropped from: %d, iid: %u",
ack->aid, inst->iid);
return 0;
}
paxos_log_debug("Received valid promise from: %d, iid: %u",
ack->aid, inst->iid);
if (ack->value.paxos_value_len > 0) {
paxos_log_debug("Promise has value");
//使用接受者返回的提案值更新提案值
if (ack->value_ballot > inst->value_ballot) {
if (instance_has_promised_value(inst))
paxos_value_free(inst->promised_value);
inst->value_ballot = ack->value_ballot;
inst->promised_value = paxos_value_new(ack->value.paxos_value_val,
ack->value.paxos_value_len);
paxos_log_debug("Value in promise saved, removed older value");
} else
paxos_log_debug("Value in promise ignored");
}
return 0;
}
接下來提案者嘗試第二階段的請求。
2.3.2 提交階段
提案者將檢查是否有經過半數以上acceptor接受的第一階段的提案,如果有就開始提交階段。
static void
try_accept(struct evproposer* p)
{
paxos_accept accept;
//檢查是否有被半數以上的acceptor接受的第一階段的提案,有就發起提交階段請求
while (proposer_accept(p->state, &accept))
peers_foreach_acceptor(p->peers, peer_send_accept, &accept);
//檢查是否還有需要發起準備階段的提案,有則發起準備階段請求
proposer_preexecute(p);
}
proposer_accept的實現請看下面的代碼片段:
int
proposer_accept(struct proposer* p, paxos_accept* out)
{
khiter_t k;
struct instance* inst = NULL;
khash_t(instance)* h = p->prepare_instances;
// Find smallest inst->iid
for (k = kh_begin(h); k != kh_end(h); ++k) {
if (!kh_exist(h, k))
continue;
else if (inst == NULL || inst->iid > kh_value(h, k)->iid)
inst = kh_value(h, k);
}
//沒有提案信息或者第一階段的提案沒有被半數以上acceptor同意,直接返回
if (inst == NULL || !quorum_reached(&inst->quorum))
return 0;
paxos_log_debug("Trying to accept iid %u", inst->iid);
// Is there a value to accept?
//如果沒有提案值,提案者自己準備一個提案值
if (!instance_has_value(inst))
inst->value = carray_pop_front(p->values);
//還是沒有提案值,直接取消
if (!instance_has_value(inst) && !instance_has_promised_value(inst)) {
paxos_log_debug("Proposer: No value to accept");
return 0;
}
// We have both a prepared instance and a value
//提案已經被半數acceptor接受,進入第二階段,將第一階段的提案信息移動到第二階段提案信息中,計數清0
proposer_move_instance(p->prepare_instances, p->accept_instances, inst);
//提案者生成提交階段的請求
instance_to_accept(inst, out);
return 1;
}
acceptor收到提案者的提交請求後的處理參考下面代碼片段:
static void
evacceptor_handle_accept(struct peer* p, paxos_message* msg, void* arg)
{
paxos_message out;
paxos_accept* accept = &msg->u.accept;
struct evacceptor* a = (struct evacceptor*)arg;
paxos_log_debug("Handle accept for iid %d bal %d",
accept->iid, accept->ballot);
if (acceptor_receive_accept(a->state, accept, &out) != 0) {
if (out.type == PAXOS_ACCEPTED) { //提案被接受者選定了
peers_foreach_client(a->peers, peer_send_paxos_message, &out);
} else if (out.type == PAXOS_PREEMPTED) {
//接受者接受了更大編號的提案,告訴提案者此提案已經被取代了,提案者需重新發起第一階段
send_paxos_message(peer_get_buffer(p), &out);
}
paxos_message_destroy(&out);
}
}
acceptor_receive_accept處理收到的請求,可能有兩種情況:要麼提案被acceptor選定,要麼此前acceptor又收到了編號更高的提案,駁回,讓提案者重新從第一階段開始:
int
acceptor_receive_accept(struct acceptor* a,
paxos_accept* req, paxos_message* out)
{
paxos_accepted acc;
if (req->iid <= a->trim_iid)
return 0;
memset(&acc, 0, sizeof(paxos_accepted));
if (storage_tx_begin(&a->store) != 0)
return 0;
int found = storage_get_record(&a->store, req->iid, &acc);
//檢索本地提案,如果提案者提交的提案編號比本地的大,則選定
if (!found || acc.ballot <= req->ballot) {
paxos_log_debug("Accepting iid: %u, ballot: %u", req->iid, req->ballot);
paxos_accept_to_accepted(a->id, req, out);
if (storage_put_record(&a->store, &(out->u.accepted)) != 0) {
storage_tx_abort(&a->store);
return 0;
}
} else {
//acceptor有比提案者提交的提案更大的提案號,通知提案者重新開始第一階段
paxos_accepted_to_preempted(a->id, &acc, out);
}
if (storage_tx_commit(&a->store) != 0)
return 0;
paxos_accepted_destroy(&acc);
return 1;
}
我們繼續跟進代碼看看提案者收到響應後的處理:
第一種情況:提案者第二階段提交的提案被接受者接受,此時該提案將是最終被選定的提案,處理邏輯參考下面代碼段:
static void
evproposer_handle_accepted(struct peer* p, paxos_message* msg, void* arg)
{
struct evproposer* proposer = arg;
paxos_accepted* acc = &msg->u.accepted;
//檢查提案是否被半數以上的接受者接受
if (proposer_receive_accepted(proposer->state, acc))
//如果還有已經被半數以上接受者接受的第一階段提案,發起第二階段請求
try_accept(proposer);
}
proposer_receive_accepted的代碼如下:
int
proposer_receive_accepted(struct proposer* p, paxos_accepted* ack)
{
khiter_t k = kh_get_instance(p->accept_instances, ack->iid);
if (k == kh_end(p->accept_instances)) {
paxos_log_debug("Accept ack dropped, iid: %u not pending", ack->iid);
return 0;
}
//找到已完成第一階段的提案信息
struct instance* inst = kh_value(p->accept_instances, k);
if (ack->ballot == inst->ballot) {
//提案被接受者接受的數量加1
if (!quorum_add(&inst->quorum, ack->aid)) {
paxos_log_debug("Duplicate accept dropped from: %d, iid: %u",
ack->aid, inst->iid);
return 0;
}
//如果提案被半數的接受者接受,則此次共識完成,此提案的值爲最終選定的值
if (quorum_reached(&inst->quorum)) {
paxos_log_debug("Proposer: Quorum reached for instance %u", inst->iid);
if (instance_has_promised_value(inst)) {
if (inst->value != NULL && paxos_value_cmp(inst->value, inst->promised_value) != 0) {
carray_push_back(p->values, inst->value);
inst->value = NULL;
}
}
kh_del_instance(p->accept_instances, k);
instance_free(inst);
}
return 1;
} else {
return 0;
}
}
第二種情況:接受者告訴提案者有編號更高的提案,此時提案者需要生成新的提案號,重新進入第一階段:
int
proposer_receive_preempted(struct proposer* p, paxos_preempted* ack,
paxos_prepare* out)
{
khiter_t k = kh_get_instance(p->accept_instances, ack->iid);
if (k == kh_end(p->accept_instances)) {
paxos_log_debug("Preempted dropped, iid: %u not pending", ack->iid);
return 0;
}
//找到已完成第一階段的提案信息
struct instance* inst = kh_value(p->accept_instances, k);
//接受者有更大的提案號
if (ack->ballot > inst->ballot) {
paxos_log_debug("Instance %u preempted: ballot %d ack ballot %d",
inst->iid, inst->ballot, ack->ballot);
//將之前接受者承諾的提案值清掉(因爲此時提案已經被取代了)
if (instance_has_promised_value(inst))
paxos_value_free(inst->promised_value);
//將提案信息重新移動到第一階段的提案集合中
proposer_move_instance(p->accept_instances, p->prepare_instances, inst);
//重新生成一個提案號,發起第一階段請求
proposer_preempt(p, inst, out);
return 1;
} else {
return 0;
}
}
以上截取了libpaxos的關鍵代碼,感覺libpaxos更大的作用應該是用來讓開發人員瞭解paxos算法,很難直接拿來做產品級應用,另外這個庫也沒有實現leader角色來解決paxos算法的活鎖問題。
2.3.3 paxos算法的交互圖
前面兩小節截取了libpaxos的代碼展示了paxos算法的具體實現,代碼看起來還是略微有點繁瑣,這一節畫個圖在來總結一下paxos算法的流程:
2.3.4 paxos算法的優化
paxos算法有很多變種和優化算法,這裏只說一下multi paxos算法。
之前提到paxos算法存在活鎖的問題:一個提案者提案被拒以後用更高的提案,另一個提案者發現被提案被拒以後也增加提案編號,從而形成死循環,造成活鎖。
multi paxos算法對paxos進行了優化,引入leader這個角色以避免活鎖問題。首先選舉出一個節點作爲leader,之後所有的提案先提交給leder節點,因爲通過leader可以控制提案提交進度,從而避免活鎖發生。
考慮前面商人買地的那個例子:此時官員們不直接和商人碰面,而是由官員指定一個總代理,啥事情都先跟代理說,再由代理統一彙報。於是P1跑到代理那承諾說:只要能批,咱就給領導100W酬勞,但是代理可以選擇不立刻就去把這事給官員彙報,他可以等一等,結果不久後P2來承諾說事成之後200W,代理就可以選擇報價高的拿給官員審批。
可以參考paxos和multi paxos算法的流程圖仔細體會一下:
paxos算法:
multi paxos算法:
從圖中可以看到最大的區別在於,multi paxos算法沒有了第一階段(prepare階段),而是直接由leader發送提案(直接進行第二階段),如果收到半數以上的acceptor的應答就達成共識。
引入leader節點雖然可以解決活鎖問題,但是又引出其他一些問題:比如leader應該如何選舉產生等等。
2.3.5 paxos算法在區塊鏈中的適用場景
paxos主要用於解決非拜占庭模型的共識問題,一般適用於私鏈,因爲私鏈僅在組織內部使用,因此可以不考慮集羣中存在拜占庭節點問題,而公鏈或者聯盟鏈則必須要考慮存在惡意節點的情況,因此不適合用paxos或者raft這類非拜占庭模型的共識算法。
3 小結
本文學習了分佈式系統經典的paxos共識算法。
paxos是CFT類共識算法,不考慮拜占庭錯誤即節點可能作惡的情況;
paxos算法將節點分成三個角色:提案者(proposer),接受者(acceptor)和學習者(learner)
paxos算法分成兩個階段來完成共識:
(1) 準備階段:提案者發出提案,試探是否能得到半數以上acceptor的同意;
(2) 提交階段:如果提案在準備階段得到半數以上的支持,則嘗試提交此提案,如果響應的acceptor超過半數以上,則此提案被選定,完成共識;否則提案者需要新選定一個提案編號重新進入準備階段;