我們知道Zookeeper的一緻性是解決分布式事務的。
那麼分布式事務代表的是強一緻性。
強一緻性解決的代表有以下協定(注意這幾個協定跟zookeeper是沒任何關系的,這是分布式的理論基礎):
1. 2PC(二階送出),顧名思義它分成兩個階段,先由一方進行提議(propose)并收集其他節點的回報(vote),再根據回報決定送出(commit)或中止(abort)事務。我們将提議的節點稱為協調者(coordinator),其他參與決議節點稱為參與者(participants, 或cohorts)。
在階段1中,協調者發起一個提議,分别問詢各參與者是否接受。
在階段2中,協調者根據參與者的回報,送出或中止事務,如果參與者全部同意則送出,隻要有一個參與者不同意就中止。
如下圖:
在異步環境并且沒有節點當機的模型下,2PC可以滿足全認同、值合法、可結束,是解決一緻性問題的一種協定。但如果再加上節點當機的考慮,2PC是否還能解決一緻性問題呢?
答案是可以的。
因為協調者如果在發起提議後當機,那麼參與者将進入阻塞狀态、一直等待協調者回應以完成該次決議。這時需要另一角色把系統從不可結束的狀态中帶出來,我們把新增的這一角色叫協調者備份。協調者當機一定時間後,協調者備份接替原協調者工作,通過問詢各參與者的狀态,決定階段2是送出還是中止。這也要求 協調者/參與者 記錄曆史狀态,以備協調者當機後備份對參與者查詢、協調者當機恢複後重新找回狀态。
從協調者接收到一次事務請求、發起提議到事務完成,經過2PC協定後增加了2次RTT(propose+commit),帶來的時延增加相對較少。
這種方式有一定的缺點,就是增加了備份參與者,節點的溝通就越頻繁,出現網絡問題的機率就越大。
是以二階段送出的總結是:2PC可以在異步網絡+節點當機恢複的模型下實作一緻性
2. 3PC(三階段送出):其實就是對二階段送出進行改進了。
在二階段送出中中一個參與者的狀态隻有它自己和協調者知曉,假如協調者提議後出現自身當機,在備用啟用之前,一個參與者又當機了,其他參與者就會進入既不能復原、又不能強制commit的阻塞狀态,直到參與者當機恢複。
那麼會有這樣的疑問:
1.可不可以去掉阻塞,使系統可以在commit/abort前復原到決議發起前的初始狀态呢?
2.當次決議中,參與者間可不可以知道對方的狀态,又或者參與者間根本不依賴對方的狀态
三階段送出增加了一個準備送出階段來解決以上問題。如下圖:
參閱這如果在不同階段當機,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:每次讀取多少個節點算成功。如下圖:
這樣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被通過。
算法圖解如下:
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;