我們知道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;