天天看點

Zookeeper筆記二-各種一緻性協定解釋

我們知道Zookeeper的一緻性是解決分布式事務的。

那麼分布式事務代表的是強一緻性。

強一緻性解決的代表有以下協定(注意這幾個協定跟zookeeper是沒任何關系的,這是分布式的理論基礎):

1.  2PC(二階送出),顧名思義它分成兩個階段,先由一方進行提議(propose)并收集其他節點的回報(vote),再根據回報決定送出(commit)或中止(abort)事務。我們将提議的節點稱為協調者(coordinator),其他參與決議節點稱為參與者(participants, 或cohorts)。

在階段1中,協調者發起一個提議,分别問詢各參與者是否接受。

在階段2中,協調者根據參與者的回報,送出或中止事務,如果參與者全部同意則送出,隻要有一個參與者不同意就中止。

如下圖:

Zookeeper筆記二-各種一緻性協定解釋

在異步環境并且沒有節點當機的模型下,2PC可以滿足全認同、值合法、可結束,是解決一緻性問題的一種協定。但如果再加上節點當機的考慮,2PC是否還能解決一緻性問題呢? 

答案是可以的。

因為協調者如果在發起提議後當機,那麼參與者将進入阻塞狀态、一直等待協調者回應以完成該次決議。這時需要另一角色把系統從不可結束的狀态中帶出來,我們把新增的這一角色叫協調者備份。協調者當機一定時間後,協調者備份接替原協調者工作,通過問詢各參與者的狀态,決定階段2是送出還是中止。這也要求 協調者/參與者 記錄曆史狀态,以備協調者當機後備份對參與者查詢、協調者當機恢複後重新找回狀态。

從協調者接收到一次事務請求、發起提議到事務完成,經過2PC協定後增加了2次RTT(propose+commit),帶來的時延增加相對較少。

這種方式有一定的缺點,就是增加了備份參與者,節點的溝通就越頻繁,出現網絡問題的機率就越大。

是以二階段送出的總結是:2PC可以在異步網絡+節點當機恢複的模型下實作一緻性

2.  3PC(三階段送出):其實就是對二階段送出進行改進了。

在二階段送出中中一個參與者的狀态隻有它自己和協調者知曉,假如協調者提議後出現自身當機,在備用啟用之前,一個參與者又當機了,其他參與者就會進入既不能復原、又不能強制commit的阻塞狀态,直到參與者當機恢複。

那麼會有這樣的疑問:

1.可不可以去掉阻塞,使系統可以在commit/abort前復原到決議發起前的初始狀态呢?

2.當次決議中,參與者間可不可以知道對方的狀态,又或者參與者間根本不依賴對方的狀态

三階段送出增加了一個準備送出階段來解決以上問題。如下圖:

Zookeeper筆記二-各種一緻性協定解釋

參閱這如果在不同階段當機,3階段送出的處理方式:

第一階段:協調者或備份未收到當機參與者的回報,直接中止事務;當機的參與者恢複後,讀取記錄發現未發出贊成回報,則自行中止該次事務。

第二階段:協調者未收到當機參與者的precommit ACK,之前已經收到了當機參與者的贊成回報,因為隻有之前已經收到了當機參與者的贊成回報才進入了階段二,協調者進行事務送出;協調者備份可以通過問詢其他參與者獲得這些資訊;當機的參與者恢複後發現收到precommit或已經發出贊成回報,則自行commit這次事務。

第三階段:即使協調者或協調者備份未收到當機參與者的事務送出的 ACK,也要結束本次事務;當機的參與者恢複後發現收到事務送出或者送出預授權,也将自行commit本次事務

3PC總結:三階段送出有了準備送出(prepare to commit)階段,3PC的事務處理延時也增加了1個RTT,變為3個RTT(propose+precommit+commit),但是它防止參與者當機後整個系統進入阻塞态,增強了系統的高可用性,對一些特殊的業務場景是非常有用的。

這些2PC,3PC展現的層面,在JAVAEE中展現出來的是隻要是JTA(Java Transaction API)。

JTA:提供了跨資料庫連接配接(或其他JTA資源)的事務管理能力。JTA事務管理則由JTA容器實作,J2ee架構中事務管理器與應用程式,資料總管,以及應用伺服器之間的事務通訊。

JTA提供的支援有Jboss,Jboss類似與Tomcat的一個容器。Tomcat是天生不支援JTA的,要引入JOTM(Java Open Transaction Manager)可以解決。

3.  NWR。N:代表資料分了多少片,即多少個節點。W:每次寫入多少個節點算成功。R:每次讀取多少個節點算成功。如下圖:

Zookeeper筆記二-各種一緻性協定解釋

 這樣NWR也最終可以達到強一緻性。

 Zookeeper的核心算法——Paxos算法(Master選舉算法)。

Paxos協定分為兩個階段。

這裡我們首先要明确什麼是提案:提案:[編号,值]即[key,value]。當Master當機後,從節點向Acceptor提出提案讓我這個從節點做Master,這些體驗要給Acceptor決策。

整個過程分為兩個階段:

  • phase1(準備階段)
    • Proposer向超過半數(n/2+1)Acceptor發起prepare消息(發送編号)
    • 如果prepare符合協定規則Acceptor回複promise消息,否則拒絕
  • phase2(決議階段或投票階段)
    • 如果超過半數Acceptor回複promise,Proposer向Acceptor發送accept消息(此時包含真實的值)
    • Acceptor檢查accept消息是否符合規則,消息符合則準許accept請求

限制條件:

   1.Acceptor必須接受他接收到的第一個提案。注意:這個是不完備的。如果恰好一半Acceptor接受的提案具有value A,另一半接受的提案具有value B,那麼就無法形成多數派,無法準許任何一個value。

  2.當且僅當Acceptor沒有回應過編号大于n的prepare請求時,Acceptor接受(accept)編号為n的提案。

  3.隻有Acceptor沒有接受過提案Proposer才能采用自己的Value,否者Proposer的Value提案為Acceptor中編号最大的Proposer Value;

  4.一個提案被選中需要過半數的Acceptor接受。

根據上述過程當一個proposer發現存在編号更大的提案時将終止提案。這意味着提出一個編号更大的提案會終止之前的提案過程。如果兩個proposer在這種情況下都轉而提出一個編号更大的提案,就可能陷入活鎖,違背了Progress的要求。這種情況下的解決方案是選舉出一個leader,僅允許leader提出提案。但是由于消息傳遞的不确定性,可能有多個proposer自認為自己已經成為leader。Lamport在The Part-Time Parliament一文中描述并解決了這個問題。

step1:,設定時鐘:proposer令localClock=globalClock.incrementAndGet()。為了讓這套系統能正确運作,我們需要一個精确的時鐘。由于作業系統的實體時鐘經常是有偏差的,是以我們決定采用一個邏輯時鐘。時鐘的目的是給系統中 發生的每一個事件編排一個序号。假設我們有一台單獨的機器提供了一個全局的計數器服務。它隻支援一個方法:incrementAndGet()。這個方法 的作用是将計數器的值加一,并且傳回增加後的值。我們将這個計數器稱為globalClock。globalClock的初始值為0。然後,系統中的每個其它機器,都有一個自己的localClock,它的初始值來自globalClock。

step2:prepare:proposer向所有Acceptor發送一個prepare消息。接收方應傳回它最近一次accept的value,以及 accept的時間,若在它還沒有accept過value,那麼就傳回空。proposer隻有在收到過半數的response之後,才可進入下一個階段。一旦收到reject消息,那麼就重頭來。

step3:構造Proposal:proposer從prepare階段收到的所有values中選取時間戳最新的一個。如果沒有,那麼它自己提議一個value。

step4:發送Proposal:proposer把value發送給其它所有機器,消息的時間戳取自localClock。接收方隻要檢查消息時間戳合法,那麼就接受此value,把這個value和時間戳寫入到硬碟上,然後答複OK,否則拒絕接受。proposer若收到任何的reject答複,則回到 step1。否則,在收到過半數的OK後,此Proposal被通過。

算法圖解如下:

Zookeeper筆記二-各種一緻性協定解釋

Phase1(準備階段)

每個Server都向Proposer發消息稱自己要成為leader,Server1往Proposer1發、Server2往Proposer2發、Server3往Proposer3發;

現在每個Proposer都接收到了Server1發來的消息但時間不一樣,Proposer2先接收到了,然後是Proposer1,接着才是Proposer3;

Proposer2首先接收到消息是以他從系統中取得一個編号1,Proposer2向Acceptor2和Acceptor3發送一條,編号為1的消

息;接着Proposer1也接收到了Server1發來的消息,取得一個編号2,Proposer1向Acceptor1和Acceptor2發送一條,編号為2的消息; 最後Proposer3也接收到了Server3發來的消息,取得一個編号3,Proposer3向Acceptor2和Acceptor3發送一條,編号為3的消息;

這時Proposer1發送的消息先到達Acceptor1和Acceptor2,這兩個都沒有接收過請求是以接受了請求傳回[2,null]給Proposer1,并承諾不接受編号小于2的請求;

此時Proposer2發送的消息到達Acceptor2和Acceptor3,Acceprot3沒有接收過請求傳回[1,null]給Proposer2,并承諾不接受編号小于1的請求,但這時Acceptor2已經接受過Proposer1的請求并承諾不接受編号小于的2的請求了,是以Acceptor2拒絕Proposer2的請求;

最後Proposer3發送的消息到達Acceptor2和Acceptor3,Acceptor2接受過提議,但此時編号為3大于Acceptor2的承諾2與Accetpor3的承諾1,是以接受提議傳回[3,null];

Proposer2沒收到過半的回複是以重新取得編号4,并發送給Acceptor2和Acceptor3,然後Acceptor2和Acceptor3通過

Phase2(決議階段)

Proposer3收到過半(三個Server中兩個)的傳回,并且傳回的Value為null,是以Proposer3送出了[3,server3]的議案;

Proposer1收到過半傳回,傳回的Value為null,是以Proposer1送出了[2,server1]的議案;

Proposer2收到過半傳回,傳回的Value為null,是以Proposer2送出了[4,server2]的議案;

Acceptor1、Acceptor2接收到Proposer1的提案[2,server1]請求,Acceptor2承諾編号大于4是以拒絕了通過,Acceptor1通過了請求;

Proposer2的提案[4,server2]發送到了Acceptor2、Acceptor3,提案編号為4是以Acceptor2、Acceptor3都通過了提案請求;

Acceptor2、Acceptor3接收到Proposer3的提案[3,server3]請求,Acceptor2、Acceptor3承諾編号大于4是以拒絕了提案;

此時過半的Acceptor都接受了Proposer2的提案[4,server2],Larner感覺到了提案的通過,Larner學習提案,server2成為Leader;