天天看點

分布式初探——分布式事務與兩階段送出協定

本文始發于個人公衆号:TechFlow

今天的文章咱們聊的是分布式原理當中的原子性,也稱為分布式事務。不知道會不會有人覺得奇怪,分布式系統CAP原則當中并沒有原子性,這個原子性是從哪裡冒出來的?

其實并不奇怪,之前我們在介紹各種一緻性原則的時候,雖然沒有明确提出來,但是原子性的相關内容已經隐藏在其中了。讓我們回顧一下,分布式系統當中的一緻性簡單可以分為強一緻性和弱一緻性。強一緻性很好了解,簡單可以了解成主節點每次更新都通過同步的方式,同步更新所有副本。而弱一緻性則是統稱所有不滿足強一緻性的模型,可以簡單了解成通過異步更新的方式實作的一緻性模型。

想象一下更新的時候,有節點出錯的情況。如果是強一緻性,很好辦,因為我們采用同步更新,是以更新失敗的話,主節點立刻就能感覺。要麼重試這次的更新,要麼復原放棄,或者是判斷該從庫是否已經當機,将它移除資源池。

如果是異步的更新機制就麻煩了,因為沒有一個統籌大局的主庫了。沒有節點知道其他節點更新成功了沒有,如果部分成功了,部分失敗了,那麼資料的一緻性就完全沒辦法保障了,髒資料到處都是,這個系統也就沒法用了。是以必須要保證即使是異步更新,也要做到原子性,要麼所有節點一起更新成功,要麼一起失敗復原,不允許出現一部分成功了,另一部分沒有的情況。

那麼怎麼解決這個問題呢?這就需要用到兩階段送出協定了。

  兩階段送出  

兩階段送出協定的算法思路其實不難,非常直覺,很好了解。前文當中說了,之是以會出現部分節點更新成功,而部分節點更新失敗的情況,主要原因就是因為沒有一個節點統籌全局,是以沒辦法做出整體決策導緻的。既然如此,那麼解決的政策也很簡單,我們加上一個整體協調的節點即可。整個協調節點會首先分發任務到各個節點,各個節點收到任務成功之後,由協調節點統一指揮,決定執行任務還是復原。這個思路和戰争非常相似,一個将軍坐鎮中央,各個小分隊散落四方。将軍統一指揮,各分隊進退有素。

我們整理一下整個過程,可以将它分成兩個階段,分别是表決階段和送出階段。

分布式初探——分布式事務與兩階段送出協定

我們來看上面這張圖,首先1号節點是協調節點,可以了解成将軍節點,其他節點都是小兵節點。首先表決階段也可以了解成任務分發階段,由将軍節點将本次更新的内容分發給各個小兵節點。小兵節點收到任務之後,如果目前情況允許執行該任務,則回複可以執行,否則回複無法執行。

第二個階段是送出階段,也就是執行階段。由将軍節點搜集各個小兵節點的回複消息,隻要有一個小兵節點回複無法執行,那麼說明這次的任務無法執行,是以将軍節點會再次發送消息給各個小兵節點,告知任務取消,此時各個小兵節點删除此次任務。否則,将軍節點會發送執行任務的指令,各個小兵節點各自執行任務。

這個就是整個兩階段送出協定的内容,是不是非常直覺,非常好了解?

我們接着深入其中的細節,試着畫出将軍節點和小兵節點狀态的狀态機。

分布式初探——分布式事務與兩階段送出協定

這是将軍節點的狀态機,一共隻有四種狀态。一開始是init狀态,表示初始化,也就是分發任務之前的狀态。當它給各個小兵節點分發任務之後,轉變到等待狀态,線程挂起等待,等待各個小兵節點的回複。等所有回複到齊了之後,決定是否執行任務,如果執行則轉移到執行狀态,否則轉移到取消狀态。

分布式初探——分布式事務與兩階段送出協定

小兵節點的狀态機也類似,小兵節點初始化之後等待将軍發放任務的消息。如果小兵判斷目前任務無法執行,那麼會直接報告将軍節點并跳轉到取消狀态。否則會進入就緒狀态,表示目前節點可以執行任務,但是要等将軍的指揮。接着,根據将軍的指令決定執行任務或者是取消復原。

上面這兩個狀态機應該也很好了解,但是這當中有一個小問題。我們觀察一下将軍節點和小兵節點的狀态機,會發現兩者當中都存在挂起等待的狀态。将軍節點做決策之前需要等待所有小兵的回複,而小兵節點執行之前也同樣需要等待将軍的指令。我們不禁想問一個問題,如果消息傳遞的過程當中存在丢包應該怎麼辦?如果更嚴重一些,某個小兵節點或者是将軍節點當機了呢,應該怎麼辦?

以現在的模式顯然是無法解決的,我們需要加入優化。

這裡的優化針對将軍節點和小兵節點需要做區分,兩者的政策是不同的。我們一個一個來看。

首先是将軍節點,如果小兵節點當機或者是發送的消息丢包,那麼将軍節點将會無法決策,或者會長久等待。為了解決這個問題,我們需要加上逾時機制。如果逾時還沒有搜集齊應答,那麼可以判定是有小兵發生故障或者是網絡出現問題,則自動視作任務失敗,廣播任務復原的消息。

其次是小兵節點,如果小兵節點在初始狀态逾時,則自動在本地終止任務,并且發放任務無法執行的消息給将軍節點。如果是在等待将軍決定的時候逾時,這個時候不能簡單的終止任務,因為無法判斷其他節點有沒有執行任務,如果簡單地終止,那麼就會引起資料不一緻。針對這個問題的政策稍稍不同,既不是繼續等待,也不是去詢問将軍節點,而是引入互詢機制,也就是說目前小兵去詢問其他小兵。

因為小兵的狀态隻有那麼幾種,為了友善區分,我們給小兵起個名字。發起詢問的小兵稱為A,被詢問的小兵稱為B。我們列舉一下,總共隻有四種可能。

B的狀态是success,說明B已經收到将軍節點的指令執行了任務。那麼很簡單,不管是什麼原因導緻A沒有收到消息都沒有關系,A直接也執行任務即可。

B的狀态是fail,說明将軍已經取消了任務,但是A沒有收到。那麼也很上面一樣,A直接放棄任務執行即可。

B的狀态是init,說明B一直還在等待任務發放,而A已經領到了任務并且在等待将軍決策了。一般來說這種情況由兩種可能,一種可能是B和将軍的通信出現了問題,是以遲遲收不到将軍的消息,還有一種可能是将軍自己崩潰了。如果将軍崩潰了,那當然沒的說,任務肯定不能繼續了,如果将軍還在,那麼它肯定還在等待B的消息,由于将軍有逾時機制,是以最終仍然會逾時導緻取消任務。是以A把任務置為失敗是正确的。

B的狀态也是等待,說明B這個節點也不清楚狀況,這種情況比較麻煩,隻能再換一個節點詢問。那問題來了,如果所有節點都處于這個狀态呢,應該怎麼辦?顯然在目前的設計裡,無法避免這個問題,會導緻長久的等待。

所有節點在執行任務的時候,需要實時将狀态和消息寫入本地log當中,這樣如果節點當機了,根據log日志也可以恢複之前的狀态。

  三階段送出  

針對上文當中說的二階段送出的那個問題,大資料專家提出了解決方案,就是在執行階段再細分成兩個階段,也就是預執行狀态和執行狀态。因為多了一個階段,是以也稱為三階段送出。

我們來看三階段送出中将軍節點的狀态機:

分布式初探——分布式事務與兩階段送出協定

和之前的相比差别不大,隻不過多了一個預執行的節點。我們再看小兵節點:

分布式初探——分布式事務與兩階段送出協定

小兵節點也一樣,相當于多接受将軍節點一次消息,第一次收到執行消息之後轉到預執行狀态之後,其實節點就會執行任務了,隻是不會送出任務和釋放資源,隻有進入執行結束的狀态之後才會進行這一操作。

用資料庫裡的事務打個比方,在進入預執行狀态之後,節點就會執行事務,但是不會送出事務已經執行完成的狀态,也就是說依然可以復原。是以在預執行之後,如果收到将軍的消息需要取消任務,那麼小兵節點依然會復原,進入狀态失敗。

和兩階段送出相比,在三階段送出當中,如果小兵在等待的狀态逾時,那麼會直接進入任務取消狀态,不會再詢問其他節點。如果小兵執行成功會傳回将軍ACK,如果失敗則不會傳回。将軍如果沒有收到所有小兵的成功消息,則會通知所有小兵復原。如果在預執行狀态等待将軍消息逾時,那麼會直接進入執行成功狀态,隻有收到将軍消息取消任務,才會復原。

顯然三階段送出也不是完美的,雖然降低了阻塞等待的可能性,但是如果小兵節點沒有收到将軍發來的復原消息,那麼也會産生資料不一緻。并且三階段送出的時間開銷要比二階段送出大得多,加上二階段送出出現阻塞的機率非常低,是以絕大多數分布式協定當中還是用的二階段送出。

二階段送出的協定在分布式系統當中廣泛使用,并且它非常直覺,推導的過程也很有意思,狀态機的應用也很巧妙。是以推薦大家都能深入思考,了解一下其中的精髓。

今天的文章就到這裡,掃碼關注我的公衆号,擷取更多文章,你們的支援是我最大的動力。

分布式初探——分布式事務與兩階段送出協定

繼續閱讀