1. 程式人生 > 實用技巧 >2PC/3PC、paxos與ZAB協議

2PC/3PC、paxos與ZAB協議

>>> hot3.png

2PC

即Two-phase Commit,參與者將操作成敗通知協調者,再由協調者根據所有參與者的反饋情報決定各參與者是否要提交操作還是中止操作。

第一階段(提交請求階段)

  1. 協調者節點向所有參與者節點詢問是否可以執行提交操作,並開始等待各參與者節點的響應。
  2. 參與者將鎖定資源,節點執行詢問發起為止的所有事務操作,並將Undo資訊Redo資訊寫入日誌。
  3. 各參與者節點響應協調者節點發起的詢問。如果參與者節點的事務操作實際執行成功,則它返回一個"同意"訊息;如果參與者節點的事務操作實際執行失敗,則它返回一個"中止"訊息。

第二階段(提交執行階段)

成功

當協調者節點從所有參與者節點獲得的相應訊息都為"同意"時:

  1. 協調者節點向所有參與者節點發出"正式提交"的請求。
  2. 參與者節點正式完成操作,並釋放在整個事務期間內佔用的資源。
  3. 參與者節點向協調者節點發送"完成"訊息。
  4. 協調者節點收到所有參與者節點反饋的"完成"訊息後,完成事務。

失敗

如果任一參與者節點在第一階段返回的響應訊息為"終止",或者 協調者節點在第一階段的詢問超時之前無法獲取所有參與者節點的響應訊息時:

  1. 協調者節點向所有參與者節點發出"回滾操作"的請求。
  2. 參與者節點利用之前寫入的Undo資訊執行回滾,並釋放在整個事務期間內佔用的資源。
  3. 參與者節點向協調者節點發送"回滾完成"訊息。
  4. 協調者節點收到所有參與者節點反饋的"回滾完成"訊息後,取消事務。

存在的問題

  1. 阻塞問題。當有部分參與者宕機或者與協調者網路通訊故障,那麼正常回應的參與者將進入阻塞狀態,無法對鎖定的資源進行任何操作,只有等待超時中斷事務,極大的限制了系統的效能。
  2. 單點問題。2PC完全依賴協調者的正常工作,一旦協調者出現問題,那麼整個協議將癱瘓,如果協調者在階段二中出現問題的話,那麼其他參與者將會一直處於鎖定事務資源的狀態中。
  3. 參與者直到接受到doCommit請求才知道協調者決定。假設在階段2協調者與第一個參與者A在通訊後雙雙宕機,那麼剩下的機器即使重新選擇協調者後,新的協調者無論是允許還是拒絕執行,那麼都有可能與A機器的資料不一致。

3PC

3PC是2PC的改進版本,將2PC的第一階段:提交事務階段一分為二,形成CanCommit、PreCommit和doCommit三個階段組成的事務處理協議。

3PC為了解決問題3參與者直到接受到doCommit請求才知道協調者決定而加入了canCommit階段。這樣即使出現了3問題,剩下的參與者也知道此次事務要提交,因為經過前兩階段的詢問,無論是參與者還是協調者都已知道此次事物是要提交的。

為了解決單點問題和阻塞問題,3PC在參與者這邊也引入了超時機制。在階段一和階段二無論協調者還是參與者故障,那麼在等待超時後,將拒絕此次事務,故障機器在恢復後,也會拒絕此次事務。在階段三一旦參與者無法及時收到來自協調者的資訊之後,他會預設執行commit,而不會一直持有事務資源並處於阻塞狀態,而等到故障機器恢復,也會對事務進行提交從而達到資料的一致性。

很多部落格說3PC為了解決阻塞問題加入了canCommit階段,說canCommit階段沒有鎖定資源,降低了持有資源的時間。我認為canCommit階段還是持有了資源的,因為階段二隻是告知各參與者協調者所做的決定,而且階段三的超時自動提交依賴的是一階段參與者對事務的保證。

存在的問題

  1. 雖然3PC解決了大多數場景的一致性問題,但是少數場景還是會出現資料不一致的情況。比如在傳送preCommit中協調者故障,那麼收到參與者在收到請求後將進入階段三,而剩下的參與者還在階段二,根據超時機制將產生資料的不一致。
  2. 3PC在要進行3輪通訊,可想而知一輪事務的決策將是漫長的,對系統的負載也要求極高。

paxos

瞭解了2PC和3PC之後,我們可以發現,無論是二階段提交還是三階段提交都無法徹底解決分散式的一致性問題。Google Chubby的作者Mike Burrows說過, there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos.意即世上只有一種一致性演算法,那就是Paxos。

首先將一次決議角色分為proposers,acceptors,和learners(允許身兼數職)。proposers提出提案,提案資訊包括提案編號和提議的value;acceptor收到提案後可以接受(accept)提案,若提案獲得多數acceptors的接受,則稱該提案被批准(chosen);learners只能“學習”被批准的提案。

然後一致性要滿足安全性和活性:

  • 安全性:只有被提出的提案才能被選定;只能有一個值被選定;如果某個proposer認為提案被選定,那麼這個提案必須是真的被選定
  • 活性:一次決議總有提案被選定

那現在我們有P1,P2,P3三個程序,要通過一個關於V的決議,顯而易見我們得到一個約束:

P1 acceptor必須接受它第一個收到的提案

那麼我們就有這樣一個場景,P1決定令V=1,P2決定令V=2,P3決定令V=3,這種場景下acceptors無法形成多數派,所以我們需要新的約束。因為程序都是平等的,所以不能對程序進行約束,比如說拒絕某個程序的提案,不讓某個程序提出提案,所以我們轉而提案進行增強。考慮維護一個全域性的遞增序列對提案進行編號(P_ID),這樣P1,P2,P3就有足夠的憑證進行裁決,不妨假設接受大的P_ID。假設P3的P_ID最大,那麼一定時間後,三個程序中V的值將等於3,但是在這一定時間中的某段時間,V的值將被決定為2(P2的提案提前於P3的提案到達P1),這違反了一致性的安全性。那麼怎麼讓P1拒絕P2或者P3的提案呢,P1在接受到P2的提案之前並沒有接受到任何訊息,P2的提案到達時也只知道自己接受了P2的提案。對acceptor已經沒有更強的約束了,所以我們轉而對proposer進行約束。既然只能選定一個值,那麼我們要求proposer提出提案選值的時候必須是存活acceptors中的批准的P_ID最大的提案值。

P2 proposer提出提案選值的時候必須是回覆acceptors中的接受的P_ID最大的提案值

當然我們來看看完全體的演算法是怎麼描述的

階段一

  1. Proposer選擇一個提案編號M,然後向超過半數的Acceptor傳送編號為M的prepare請求。
  2. 如果一個Acceptor收到一個編號為M的prepare請求,且編號M大於該Acceptor已經響應過的所有prepare請求的編號,那麼他會將他已經批准過的最大編號的提案作為響應反饋給Proposer,同時承諾不會再批准任何編號小於M的提案。

階段二

  1. 如果Proposer收到半數以上的Acceptor對於其發出的編號為M的prepare請求的響應,那麼他會發送一個編號為M值為V的Accept請求給Acceptor。V的值就是收到的響應中最大的提案的值,如果響應不包含任何提案,那麼V可以是任意值。
  2. 如果Acceptor收到編號為M的提案的Accept請求,只要改Acceptor尚未對編號大於M的prepare請求做出響應,他就可以通過這個提案。

那麼如同2PC/3PC中的協調者和部分參與者doCommit而導致的資料一致性問題解決了嗎?

我們假設有五個程序ABCDE,某時刻A提出提案P1令V=1,並對BC傳送了prepare請求,BC收到prepare請求回覆給了A,A對B傳送的V=1的accept請求後終止,B在接受到Accept請求後也終止了。此時E深感自己責任重大,提出V=2的提案P2併發送prepare請求給CD,當C在接受到P2的preapre後,將回復P1{V=1}給E,E在收到C的響應後根據協議只能含淚把值改為1在傳送Accept請求。

這樣看來好像是隻要先有提案提出,那麼以後提案的值都是第一次提案的值,這樣好像也滿足了安全性和活性。但是根據Paxos演算法,也沒有明確要求提案的值不能變。那麼被決定之前提案的值能不能變呢?有這樣一個場景,還是有五個程序ABCDE,某時刻A提出提案P1令V=1,並對BC傳送了prepare請求,但是可憐的C在還未收到prepare請求時掛掉了,當然A也沒收到多數派的響應,自然不會發送accept請求。而後C又成功上線,E命運使然的提出V=2的提案P2併發送prepare請求給CD,然後得到了CD的接受形成了自己的多數派使得P2被批准,但是如果E傳送prepare請求給含有A或B的多數派那結果將完全不同。而無論哪種情況度沒有違反活性和安全性的要求,paxos的作用也僅僅是確定一個值且在分散式系統保持一致性,至於你想說的是我的值可以進行多次改寫啊,那就要將paxos和狀態機聯絡起來。

ZAB

paxos通過多數派和限制提案的值完美的解決了我們的問題,但是理論到實際是個艱難的過程。比如怎樣在分散式環境下維持一個全域性唯一遞增的序列,如果是靠資料庫的自增主鍵,那麼整個系統的穩定和效能的瓶頸全都集中於這個單點。paxos演算法也沒有限制Proposer的個數,Proposer個數越多,那麼達成一致所造成的碰撞將越多,甚至產生活鎖,如果限制Proposer的個數為一個,那麼就要考慮唯一的Proposer崩潰要怎麼處理。paxos只是確定了一個值,離我們實際運用想要多次讀寫還有距離,上文也說過想要多次讀寫,要將paxos和狀態機聯絡起來。

ZAB協議分為2個部分,讓我們來看看ZAB協議是怎麼解決這些問題的:

(以下內容來源於http://www.jianshu.com/p/fb527a64deee

  • 廣播(boardcast):Zab 協議中,所有的寫請求都由 leader 來處理。正常工作狀態下,leader 接收請求並通過廣播協議來處理。
  • 恢復(recovery):當服務初次啟動,或者 leader 節點掛了,系統就會進入恢復模式,直到選出了有合法數量 follower 的新 leader,然後新 leader 負責將整個系統同步到最新狀態

廣播(boardcast)

廣播的過程實際上是一個簡化的二階段提交過程:

  1. Leader 接收到訊息請求後,將訊息賦予一個全域性唯一的 64 位自增 id,叫做:zxid,通過 zxid 的大小比較即可實現因果有序這一特性。
  2. Leader 通過先進先出佇列(通過 TCP 協議來實現,以此實現了全域性有序這一特性)將帶有 zxid 的訊息作為一個提案(proposal)分發給所有 follower。
  3. 當 follower 接收到 proposal,先將 proposal 寫到硬碟,寫硬碟成功後再向 leader 回一個 ACK。
  4. 當 leader 接收到合法數量的 ACKs 後,leader 就向所有 follower 傳送 COMMIT 命令,同事會在本地執行該訊息。
  5. 當 follower 收到訊息的 COMMIT 命令時,就會執行該訊息

相比於完整的二階段提交,Zab 協議最大的區別就是不能終止事務,follower 要麼回 ACK 給 leader,要麼拋棄 leader,在某一時刻,leader 的狀態與 follower 的狀態很可能不一致,因此它不能處理 leader 掛掉的情況,所以 Zab 協議引入了恢復模式來處理這一問題。從另一角度看,正因為 Zab 的廣播過程不需要終止事務,也就是說不需要所有 follower 都返回 ACK 才能進行 COMMIT,而是隻需要合法數量(2f+1 臺伺服器中的 f+1 臺) 的follower,也提升了整體的效能。

恢復(recovery)

由於之前講的 Zab 協議的廣播部分不能處理 leader 掛掉的情況,Zab 協議引入了恢復模式來處理這一問題。為了使 leader 掛了後系統能正常工作,需要解決以下兩個問題:

  • 已經被處理的訊息不能丟
  • 被丟棄的訊息不能再次出現

已經被處理的訊息不能丟

這一情況會出現在以下場景:當 leader 收到合法數量 follower 的 ACKs 後,就向各個 follower 廣播 COMMIT 命令,同時也會在本地執行 COMMIT 並向連線的客戶端返回「成功」。但是如果在各個 follower 在收到 COMMIT 命令前 leader 就掛了,導致剩下的伺服器並沒有執行都這條訊息。

為了實現已經被處理的訊息不能丟這個目的,Zab 的恢復模式使用了以下的策略:

  1. 選舉擁有 proposal 最大值(即 zxid 最大) 的節點作為新的 leader:由於所有提案被 COMMIT 之前必須有合法數量的 follower ACK,即必須有合法數量的伺服器的事務日誌上有該提案的 proposal,因此,只要有合法數量的節點正常工作,就必然有一個節點儲存了所有被 COMMIT 訊息的 proposal 狀態。
  2. 新的 leader 將自己事務日誌中 proposal 但未 COMMIT 的訊息處理。
  3. 新的 leader 與 follower 建立先進先出的佇列, 先將自身有而 follower 沒有的 proposal 傳送給 follower,再將這些 proposal 的 COMMIT 命令傳送給 follower,以保證所有的 follower 都儲存了所有的 proposal、所有的 follower 都處理了所有的訊息。
    通過以上策略,能保證已經被處理的訊息不會丟

被丟棄的訊息不能再次出現

這一情況會出現在以下場景:當 leader 接收到訊息請求生成 proposal 後就掛了,其他 follower 並沒有收到此 proposal,因此經過恢復模式重新選了 leader 後,這條訊息是被跳過的。 此時,之前掛了的 leader 重新啟動並註冊成了 follower,他保留了被跳過訊息的 proposal 狀態,與整個系統的狀態是不一致的,需要將其刪除。
Zab 通過巧妙的設計 zxid 來實現這一目的。一個 zxid 是64位,高 32 是紀元(epoch)編號,每經過一次 leader 選舉產生一個新的 leader,新 leader 會將 epoch 號 +1。低 32 位是訊息計數器,每接收到一條訊息這個值 +1,新 leader 選舉後這個值重置為 0。這樣設計的好處是舊的 leader 掛了後重啟,它不會被選舉為 leader,因為此時它的 zxid 肯定小於當前的新 leader。當舊的 leader 作為 follower 接入新的 leader 後,新的 leader 會讓它將所有的擁有舊的 epoch 號的未被 COMMIT 的 proposal 清除。

轉載於:https://my.oschina.net/chener/blog/1504093