1. 程式人生 > >區塊鏈共識機制:分散式系統的Paxos協議

區塊鏈共識機制:分散式系統的Paxos協議

前言:第一次接觸paxos可能很多人不理解這玩意兒有啥用,近幾天一直在研究paxos,不敢說理解的多到位,但是把自己理解的記錄下來,供大家參考。文章主要參考知行學社的《分散式系統與Paxos演算法視訊課程》和知乎話題https://zhuanlan.zhihu.com/p/29706905,希望能對大家有幫助。


一、什麼是Paxos,解決什麼問題?

(一)Paxos是分散式系統中,在非同步通訊環境下,可以容忍在只有多數派機器存活(容許半數一下節點宕機)的情況下,仍然能完成一個一致性寫入的協議。

非同步通訊環境並非只有paxos能解決一致性問題,經典的兩階段和三階段提交(參考:https://blog.yangx.site/2018/08/05/paxos/

也能達到同樣的效果。但是paxos和它們這些妖豔水貨不一樣,因為在分散式環境裡面,除了訊息網路傳輸的惡劣環境,還有另外一個讓人痛心疾首的,就是機器的當機,甚至永久失聯。在這種情況下,兩階段提交將無法完成一個一致性的寫入,而paxos,只要多數派機器存活(一半以上)就能完成寫入,並保證一致性。

(二)解決什麼問題

paxos用來確定分散式系統中確定一個不可變變數的取值。(不理解沒關係,往下看)

(1)取值可以是任意二進位制資料。

(2)一旦確定將不再更改,並且可以被獲取到(不可變性,可讀取性

一般的分散式儲存系統中

(1)資料本身可變,採用多副本進行儲存。

(2)多個副本的更新操作序列【Op1,Op2,……,Opn】是相同的、不變的。

(3)用Paxos依次來確定不可變數Opi的取值,即第i個操作是什麼?(重點)

(4)沒確定完Opi之後,讓各個資料副本執行Opi,依次類推,從而實現分散式系統的一致性。

應用案例:google的chubby、Megastore和Spanner都採用Paxos來對資料副本的更新序列達成一致。


二、paxos的實現過程和實現原理是怎樣的?分為哪些步驟?

為方便理解,以下通過案例來循序漸進的講。

(一)案例設計:(分散式系統的理解可以參考資料劉傑的《分散式系統原理介紹》

1、設計一個分散式系統,用來儲存名稱為var的變數。系統描述如下:

(1)系統內部有多個Acceptor組成,負責儲存和管理var變數。

(2)外部有多個Proposer機器任意併發呼叫API,向系統提交不同的var取值。

(3)var的取值可以是任意二進位制資料。

(4)系統對外的API庫介面為:propse(var,V) => <ok,f> or <error>,如果var的值已經被系統確定了,則返回V,如果沒被系統確定則返回error;如果系統中的某一Proposer將var的設定為V,則f就代表V,否則f為其他Proposer設定的值。

2、系統需要保證var的取值滿足一致性

(1)如果var的取值沒有確定,則var的取值為null;

(2)一旦var的取值被確定,則不可被更改。並且可以一直獲取到這個值。

3、系統需要滿足容錯特性

(1)可以容忍任意的Proposer機器出現故障。

(2)可以容忍半數一下的Acceptor出現故障。

4、注意,為了容易理解,暫不考慮

(1)網路故障。

(2)Acceptor故障丟失var的資訊。

5、設計這一系統的難點是什麼?

(1)管理多個Proposer的併發執行

(2)保證var變數的不可變性

(3)容忍任意Proposer機器故障

(4)容忍半數一下Acceptor機器故障

(二)方案一:基於互斥訪問權的實現:

1、基於互斥訪問權的acceptor的實現:

(1)Acceptor儲存變數var和一個互斥鎖lock

(2)介面Acceptor::prepare():

加互斥鎖,給予var的互斥訪問權,並返回var當前的取值f。

介面Acceptor::release():

解互斥鎖,收回var的互斥訪問權

介面Acceptor::accept(var,V):

如果已經加鎖,並且var沒有取值,則設定var為V。並且釋放鎖。

2、propose(var,V)的兩階段實現:

第一階段,通過Acceptor::prepare獲取互斥訪問權和當前var的取值,如果不能獲取到,說明鎖被他人所佔,返回<error>;如果可以獲取互斥訪問權,則進入第二階段,否則,結束。

第二階段,根據當前var的取值f,選擇執行。

如果f為null,則通過Acceptor::accept(var,V)提交資料V。

如果f不為空,則通過Acceptor::release()釋放訪問權,返回<ok,f>。

3、補充概念:互斥鎖(英語:英語:Mutual exclusion,縮寫 Mutex)是一種用於多執行緒程式設計中,防止兩條執行緒同時對同一公共資源(比如全域性變數)進行讀寫的機制。需要此機制的資源的例子有:旗標、佇列、計數器、中斷處理程式等用於在多條並行執行的程式碼間傳遞資料、同步狀態等的資源。

總結:

解決了多個Proposer的併發執行問題並保證var變數的不可變性,但是不能容忍任意Proposer機器故障,若Proposer在釋放互斥訪問權之前發生故障,會導致系統陷入死鎖。

 

方案二:引入搶佔式訪問權

1、特點:

(1)Acceptor可以讓某個Proposer獲取到的訪問權失效,不再接受它的訪問。之後,可以將訪問權發放給其他的Proposer,讓其他Proposer訪問acceptor。

(2)Proposor向Acceptor申請訪問權時指定標號epoch(越大的epoch越新),獲取到訪問權之後,才能向acceptor提交取值。

(3)Acceptor接受到更大的新epoch的申請,馬上讓舊的epoch的訪問權失效,不再接受他們提交的取值,然後給新epoch發放訪問權,只接受新epoch提交的取值。

2、為何能解決proposer機器故障

新的epoch可以搶佔舊epoch,讓舊epoch的訪問權失效,舊epoch的proposer將無法執行,新epoch的proposer將開始執行。

為了保證一致性,不同epoch的proposer之間採用“後者認同前者”的原則:

  1. 在肯定舊epoch無法生成確定性取值時,新的epoch會提交自己的value,不會衝突。
  2. 一旦舊的epoch形成確定性取值,新的epoch肯定可以獲取到此取值,並且會認同此取值,不會破壞。

3、具體實現:

基於搶佔式訪問權的Acceptor實現:

(1)Acceptor儲存的狀態

當前var的取值<accepted_epoch, accepted_value>

最新發放訪問權的epoch(latest_prepared_epoch)

(2)Acceptor::prepare(epoch):

只接受比latest_prepared_epoch更大的epoch,並給予訪問權,

記錄latest_prepared_epoch = epoch,返回當前var的取值。

(3)Acceptor::accept(var,prepared_epoch,V):

驗證latest_prepared_epoch == prepared_epoch,

設定var的取值<accepted_epoch,accepted_value> = <prepared_epoch, v>

4、Propose(var,V)的兩階段實現:

第一階段,獲取epoch輪次的訪問權和當前var 的取值

簡單獲取當前時間戳為epoch,通過Acceptor::prepare(epoch),獲取eooch輪次的訪問權和當前var的取值,如果不能獲取,返回<error>。

第二階段,採用“後者確認前者”的原則執行。

(1)在肯定舊epoch無法生成確定性取值時,新的epoch會提交自己的value,不會衝突。

(2)一旦舊epoch形成確定性取值,新的epoch肯定可以獲取到此取值,並且會認同此取值,不會破壞。

  1. 如果var的取值為空,則肯定舊epoch無法生成確定性取值,則通過Acceptor::accept(var,epoch,V)提交資料V,成功後返回<ok,V>;如果accept失敗,返回<error>,此時Acceptor被新epoch搶佔或者acceptor出現故障。
  2. 如果var取值存在,則此值肯定是確定性取值,此時認同它不再更改,直接返回<ok,accepted_value>。

 

核心思想:讓Proposer將按照epoch遞增的順序搶佔式的一次執行,後者會認同前者。

解決問題:可以避免Proposer機器故障帶來的死鎖問題,並且仍可以保證var取值的一致性。

存在問題:仍需要引入多Acceptor。單機模組Acceptor可以使故障導致整個系統宕機,無法提供服務。

 

方案三、都讓開,Paxos要閃亮登場,亮瞎咱們的狗眼了,Paxos是在方案2的基礎上引入多Acceptor。

1、特點

(1)Acceptor的實現保持不變。仍採用“喜新厭舊”的原則執行。

(2)Paxos採用“少數服從多數”的思路,一旦某epoch的取值f被半數以上的Acceptor接受,則認為此var取值被確定為f,不再更改。

2、Paxos的組成元素:

(1)三個參與角色:

  1. Proposer:提議發起者。Proposer 可以有多個,Proposer 提出議案(value)。所謂 value,可以是任何操作,比如“設定某個變數的值為value”。不同的 Proposer 可以提出不同的 value,例如某個Proposer 提議“將變數 X 設定為 1”,另一個 Proposer 提議“將變數 X 設定為 2”,但對同一輪 Paxos過程,最多隻有一個 value 被批准。
  2. Acceptor:提議接受者;Acceptor 有 N 個,Proposer 提出的 value 必須獲得超過半數(N/2+1)的 Acceptor批准後才能通過。Acceptor 之間完全對等獨立。
  3. Learner:提議學習者。上面提到只要超過半數accpetor通過即可獲得通過,那麼learner角色的目的就是把通過的確定性取值同步給其他未確定的Acceptor。

2、Proposer的兩階段執行:(為方便理解,先不引入learner)

Proposer(var,V)第一階段:選定epoch,獲取epoch訪問權和對於的var取值,需要獲取半數以上的acceptor的訪問權和對於的一組var取值。

第二階段,採用“後者認同前者”的原則執行。

(1)在肯定舊epoch無法生成確定性取值時,新的 會提交自己的取值,不會衝突。

(2)一旦舊epoch形成確定性取值,新的epoch肯定可以獲取到此取值,並且會認同此取值,不會破壞。

3、多Acceptor實現:

(1)如果獲取的var取值都為空,則舊epoch無法形成確定性取值。此時努力使新的<epoch,V>成為確定性取值。向epoch對應的所有acceptor提交取值<epoch,V>;

如果收到半數以上成功,則返回<ok,V>;

否則,返回<error>,此時被新epoch搶佔或者acceptor傳送故障。

(2)如果var的取值存在(需同時詢問至少半數以上Acceptor),認同最大accepted_epoch對應的取值f,幫助取值f提交確認(參考下方原理圖),努力使<epoch,f>成為確定性取值。

如果f出現半數以上,則說明f已經被確定性取值,直接返回<ok,f>

否則,向epoch對應的所有acceptor提交取值<epoch,f>,按所能就接受到的最新的序號取值提交,比如接收到<#1,2>,<#2,3>和<#3,4>,則對所有的acceptor提交Accept(#3,4),最終達成一致性。

 

4、特殊情況彙總

(1)第一種情況:單proposer,三個acceptor,Proposer提議正常,未超過accpetor失敗情況

問題:如果第二階段,只有2個accpetor響應接收提議成功,另外1個沒有響應怎麼處理呢?

處理:proposer發現只有2個成功,已經超過半數,那麼還是認為提議成功,並把訊息傳遞給learner,由learner角色將確定的提議通知給所有accpetor,最終使最後未響應的accpetor也同步更新,通過learner角色使所有Acceptor達到最終一致性。

(2)第二種情況:單proposer,三個acceptor,Proposer提議正常,但超過accpetor失敗情況

問題:假設有2個accpetor失敗,又該如何處理呢?

處理:由於未達到超過半數同意條件,proposer要麼直接提示失敗,要麼遞增版本號重新發起提議,如果重新發起提議對於第一次寫入成功的accpetor不會修改,另外兩個accpetor會重新接受提議,達到最終成功。

(3)情況再複雜一點:還是一樣有3個accpetor,但有兩個proposer。

情況一:proposer1和proposer2序列執行

proposer1和最開始情況一樣,把value設定為v1,並接受提議。

proposer1提議結束後,proposer2發起提議流程:

第一階段A:proposer1發起prepare(epoch,v2)

第一階段B:Acceptor收到proposer的訊息,發現內部value已經寫入確定了,返回(#1,v1)

第二階段A:proposer收到3個Acceptor的響應,發現超過半數都是v1,說明name已經確定為v1,接受這個值,不在發起提議操作。

情況二:proposer1和proposer2交錯執行

proposer1提議accpetor1成功,但寫入accpetor2和accpetor3時,發現版本號已經小於accpetor內部記錄的版本號(儲存了proposer2的版本號),直接返回失敗。

proposer2寫入accpetor2和accpetor3成功,寫入accpetor1失敗,但最終還是超過半數寫入v2成功,value變數最終確定為v2;

proposer1遞增版本號再重試發現超過半數為v2,接受name變數為v2,也不再寫入v1。name最終確定還是為v2

情況三:proposer1和proposer2第一次都只寫成功1個Acceptor怎麼辦

都只寫成功一個,未超過半數,那麼Proposer會遞增版本號重新發起提議,這裡需要分多種情況:

  1. 3個Acceptor都響應提議,發現Acceptor1{#1,v1} ,Acceptor2{#2,v2},Acceptor{null,null},Processor選擇最大的{v2,n2}發起第二階段,成功後name值為v2;
  2. 2個Acceptor都響應提議,
    1. 如果是Acceptor1{#1,v1} ,Acceptor2{#2,v2},那麼選擇最大的{v2,n2}發起第二階段,成功後name值為v2;
    2. 如果是Acceptor1{#1,v1} ,Acceptor3{null,null},那麼選擇最大的{v1,n1}發起第二階段,成功後name值為v1;
    3. Acceptor2{#2,v2} ,Acceptor3{null,null},那麼選擇最大的{v2,n2}發起第二階段,成功後name值為v2;
  3. 只有1個Acceptor響應提議,未達到半數,放棄或者遞增版本號重新發起提議

可以看到,都未達到半數時,最終值是不確定的!

5、總結:

Paxos演算法的核心思想:

整個paxos協議過程看似複雜難懂,但只要把握和理解這兩點就基本理解了paxos的精髓

第一階段accpetor的處理流程:如果本地已經寫入了,不再接受和同意後面的所有請求,並返回本地寫入的值;如果本地未寫入,則本地記錄該請求的版本號,並不再接受其他版本號的請求,簡單來說只信任最後一次提交的版本號的請求,使其他版本號寫入失效;

第二階段proposer的處理流程:未超過半數accpetor響應,提議失敗;超過半數的accpetor值都為空才提交自身要寫入的值,否則選擇非空值裡版本號最大的值提交,最大的區別在於是提交的值是自身的還是使用以前提交的。

特點

(1)在搶佔式訪問權的基礎上引入多acceptor

(2)保證一個epoch,只有一個proposer執行,proposer按照epoch遞增的順序依次執行。

(3)新epoch的proposer採用“後者認同前者”的思路進行。

  1. 在肯定舊epoch無法生成確定性取值時,新的epoch會提交自己的取值,不會衝突。
  2. 一旦舊epoch形成確定性取值,新的epoch肯定可以獲取到此取值,並且會認同此取值,不會破壞。

Paxos演算法可以滿足容錯要求

  1. 半數半數的acceptor出現故障時,存活的acceptor仍然可以生產var的確定性取值。
  2. 一旦var取值被確定,即使出現半數以下acceptor故障,此取值可以被獲取,並且將不再被更改。

Paxos演算法的Liveness問題

新輪次的搶佔會讓舊輪次停止執行,如果每一輪次在第二階段執行成功之前都被新一輪搶佔,則導致活鎖,如何解決??

答:

引入leader,也就是multi paxos的情形。先選舉leader,leader任期內不需要執行prepare,直接accept即可,在同一個leader任期內只有leader發起proposal,《Multi-Paxos: An Implementation and Evaluation》,不再展開。

思考題:

  1. 什麼情況下可以認為var的取值被確定,不再更改?
  2. Paxos兩階段分別在做什麼?
  3. 一個epoch是夠會有多個proposer進入第二階段執行?
  4. 什麼情況下,proposer可以將var的取值確定為自己提交的取值?
  5. 在第二階段,如果獲取的var的取值都為空,為什麼可以保證舊epoch無法過程確定性取值?
  6. 新epoch搶佔成功之後,舊epoch的proposer將如何執行?
  7. 如何保證新epoch不會破壞已經達成的確定性取值?
  8. 為什麼在第二階段存在var取值時,只需要考慮accepted_epoch最大的取值f?
  9. 在形成確定性取值之後出現任意半數一下的acceptor故障,為何確定性取值不會被更改?
  10. 如果proposer在執行過程中,任意半數以下的acceptor出現故障,此時將如何執行?
  11. 正在執行的proposer和任意半數以下acceptor都出現故障時,var的取值可能是什麼情況?為何之後新的proposer可以形成確定性取值?

 

參考答案:

  1. 半數以上Acceptor寫入var值就被確認,不再更改。
  2. 第一階段請求訪問權和儲存的值,第二階段空時提交;
  3. 不會,一個epoch只有一個proposer執行,其他的proposer遞增epoch順序。
  4. 自己提交時無確定的值,且自己的提交的值被確認的acceptor數量達到一半以上。
  5. Acceptor採用喜新厭舊的做法,而且現在至少有一半的acceptor接受提交新epoch,所以有舊epoch提交時會自動拒絕,這樣至少一半以上的epoch會拒絕舊epoch,舊epoch永遠無法確認。
  6. 一旦被新epoch搶佔成功,舊epoch的訪問權失效,舊epoch的proposer將無法執行。
  7. 已經達成確認的取值,節點數大於1/2,新epoch請求時,發現已確認了,半數以上節點都是這個值,不再提議,弱發現確認的節點數不到1/2,只要不全是空就會遞增版本號同樣提交取到的這個值,不會賦予新值(後者確認前者原則)。
  8. 喜新厭舊,存在新epoch的var值時,舊epoch會被acceptor自動拒絕。
  9. 確定性取值需要半數以上確認。
  10. 正常執行,容錯率達到了半數。
  11. 因為新的proposer仍然可以達到一半以上的acceptor確認。

推薦參考資料:

(下載地址:https://pan.baidu.com/s/1AwkZ8OHnEn_9I8GzBnj97g,包含以下1、4、5、8、9

參考資料:

1、知行學社的《分散式系統與Paxos演算法視訊課程》

2、分散式系統一致性及Paxos詳解https://blog.yangx.site/2018/08/05/paxos/

3、知乎話題https://zhuanlan.zhihu.com/p/29706905

4、劉傑的《分散式系統原理介紹》 ,裡面有關於paxos的詳細介紹,例子非常多,也有包括paxos協議的證明過程,大而全,質量相當高的一份學習資料!

5、ppt《可靠分散式系統基礎 Paxos 的直觀解釋》,雖然是隻是一份ppt沒有講解視訊,但看ppt也能理解整個的paxos介紹和推導過程,寫的很具體,配圖很清晰明瞭;

6、微信的幾篇公眾號文章:《微信PaxosStore:深入淺出Paxos演算法協議》https://mp.weixin.qq.com/s/aJoXSQo9-zmukN2RsiZ3_g(微信PaxosStore:深入淺出Paxos演算法協議 )、《微信開源:生產級paxos類庫PhxPaxos實現原理介紹》https://mp.weixin.qq.com/s/6VWUA5EDV2UIq4NqmQYWUA(微信自研生產級paxos類庫PhxPaxos實現原理介紹 ),文章寫的都挺好,不適合入門,需要有一定基礎才好理解;

7、原始碼:微信開源的phxpaxos:https://github.com/tencent-wechat/phxpaxos,結合程式碼對協議理解更深。

8、paxos作者Lamport《paxos made simple》的論文。

9、《Multi-Paxos: An Implementation and Evaluation》論文。


以上均為個人結合參考資料理解,如有錯誤,懇請大家批評指正!