1. 程式人生 > >分散式事務設計-兩段式提交

分散式事務設計-兩段式提交

http://yale.iteye.com/blog/1397917

事務是一個很重要的概念,它必須滿足ACID特性,在單機的資料庫中,這很容易實現。但在分散式資料庫中,各個表分散在各臺不同的機器上,如何對這些表實施分散式的事務處理就成為一個比較困難的問題,其中兩段式提交就是解決分散式事務的一種方式。

兩段式提交設計本身的思路非常的容易理解,步驟如下:

1. 協調員伺服器(協調員)傳送一條投票請求訊息給所有參與這次事務的伺服器(參與者)。

2. 當一個參與者收到一條投票請求,它會向協調員傳送一條響應請求訊息,該響應訊息包含了參與者的投票:YES 或者NO。如果參與者的訊息的投票是NO,那就意味著由於某些原因,參與者不能參與這次事務,等價於收到了ABORT決定,本次事務的工作到此為止。

3. 協調員收集所有參與者的響應投票,如果所有的響應投票都是YES,那麼協調員就會做出決定:COMMIT,並且會把COMMIT訊息傳送給所有參與者。否則,協調員則會做出決定:ABORT,此時協調員會把ABORT訊息發給那些投票為YES的那些參與者(投票為NO的參與者已經單方面ABORT了這次事務,協調員不必再發送訊息給這些參與者)。傳送完決定後,協調員對於本次事務的工作就此停止了。

4. 投了YES票的參與者等待著來自協調員的決定(COMMIT或者ABORT),然後根據決定做完相應的操作,然後本次事務的工作也就此為止。

步驟1,2屬於兩段式提交的階段1,步驟3,4屬於兩段式提交的階段2。

在整個過程中,參與者會存在一段不確定時間段(從它傳送YES的票開始,到它收到COMMIT/ABORT的決定結束),在此時間段內,參與者的程序會被block住,它需要等待接下來的決定。而協調員則不存在任何不確定時間段,它可以繼續處理其它的事務請求,傳送其它事務的投票請求,在做完COMMIT/ABORT決定之後,它可以馬上去幹別的事情,無需任何等待。因為協調員的工作不具有原子性,它可以交叉得做任何事。而參與者完成的是事務,具有原子性,它做出承諾後,他必須保持好事務的現場,避免別的事務的交叉感染,從而違反了ACID中的Isolated。

從描述來看非常簡單,很容易理解,但是請注意,在整個過程中的任何時間點,都有可能發生的各種各樣的故障,有的是鏈路故障,有的是伺服器故障。如果詳細考慮這些情況,實現就不是這麼簡單了。

考慮第一個問題,在整個執行的過程中,無論是參與者的程序,還是協調者的程序,他們在做下一步的處理前都必須等待訊息。但是,訊息可能會失敗,並不總是能夠到達。為了避免無休止的等待訊息,因此需要加入Timeout 。當訊息超過一定的時間還沒到來的時候,我們必須做出處理,這些處理我們稱之為Timeout-Action。當伺服器或者伺服器的程序(無論是協調員還是參與者)從一次失敗中恢復過來的時候,我們希望伺服器的程序能夠嘗試著獲得一個和其他程序一致的決定。這很好理解,COMMIT/ABORT的決定已經由協調員發出了,那麼恢復的參與者程序也希望能夠得到這個決定從而參與完成該事務。當然,在參與者從失敗中恢復過來的時候,由於其它的一些可能的失敗,可能COMMIT/ABORT的決定還未能做出,此時該參與者也需要做出相應的正確處理。因此,伺服器的程序必須儲存一些資訊,比如是一些Log。有了這些Log,才能使得從失敗中恢復的程序能夠正確恢復事務處理。

Timeout-Action

程序需要在3個地方等待訊息:在(2),(3),(4)步開始的地方:

在(2)步驟中,參與者程序需要等來來自協調員程序的投票請求。此時如果在等待投票請求時發生了timeout,參與者伺服器就可以簡單得停止該事務的工作就可以了。

在(3)步驟中,協調員需要等待接受所有參與者迴應的YES或NO的投票,在此時,協調員還未達成任何決定,參與者也沒有提交任何資料,因此協調員在Timeout發生後,只需要傳送ABORT決定給所有的參與者就可以了。

在(4)步驟中,參與者p已經投了YES票,正在等待來自協調員的COMMIT或ABORT命令。在這個時間節點上,p處在不確定時間段。因此此時,p不能在timeout的時候簡單得單方面作出決定,他需要向其他伺服器做諮詢才能知道該如何處理。最簡單的終止設計可以是這樣的:p依然被block住,一直詢問等待協調員,直到p重新建立起和協調員之間的聯絡。接著,協調員就會告訴p已經作出的決定(協調員沒有不確定時間期),然後p就可以接著處理決定。

簡單終止協議的缺點是參與者p會被不必要得block住一段時間。比如,假如有2個參與者p和q,協調員把COMMIT/ABORT決定成功傳送給q了,但是在它給p傳送的決定失敗了。的確,p這時是處在不確定時期,但是q已經不在不確定期了,如果p能夠和q通訊的話,p可以從q那裡得到協調員發出的決定,不必一直block等到協調員恢復。

這需要參與者能夠互相知道對方,參與者之間可以直接交換資訊,不必總是通過協調員的中介。要實現這種自由的資訊交換也並不是十分困難,協調員在傳送投票請求的時候可以把所有參與者的ID列表附在投票請求訊息後面傳送給所有的參與者,這樣參與者p在收到投票請求後就可以直接和其他所有的參與者進行交流了。這麼做也不會帶來什麼副作用,在收到投票請求之前,參與者之間還是互相不認識,因此在此之前(2),(3)發生的timeout還是可以單方面得中止任務或者停止事務。這個思路就出現另外的一個設計-協同終止設計,設計如下:

當一個參與者p在其不確定時間段內發生了timeout,他會依次向所有其他的程序傳送一個詢問請求訊息,詢問做出的決定是什麼或者是否能單方面得做出一個決定(因為如果有一個被詢問的參與者已經向協調員回覆了一個NO的投票,那麼詢問者自然就可以單方面得做出決定ABORT這次事務,因為只要有一個參與者回覆了NO,那麼協調員做出的決定肯定是ABORT,無需再向協調員確認了)。在這種場景下,參與者p就被稱之為發起人,作出詢問回答的伺服器程序 q就可以稱之為迴應人。那麼迴應人q可能有3種情況:

1. q已經收到了COMMIT/ABORT決定:q只需要把該決定迴應給p,然後p就可以自行處理了。

2. q還沒進行投票:q此時可以單方面做出決定,因為此時協調員已經發生故障,此時q可以迴應ABORT給p,p就可以自己做出處理。

3. q已經回覆YES投票給協調員,處在不確定期內,也沒有收到來自協調員的決定。此時q也無法給p任何幫助。

根據這個設計,如果p傳送詢問請求給q,碰巧q處在情況(1)或者(2)時,p馬上就可以達成(也就是獲得)一個決定而無需任何block。如果p能通訊的其他所有的程序都處在情況(3),那麼p也會被block住,直到足夠的故障被修復使得p至少能夠和一個處在情況(1)或(2)的參與者程序q通訊。需要注意的是詢問請求可以發給所有的其他伺服器程序,包括協調員程序,這樣至少可以確認協調員在沒有故障的狀態下可以回覆投票請求,避免了碰巧所有其他的參與者程序都在不確定期而無法提供幫助迴應這樣的窘境。

總之,協同終止設計可以降低block的概率,但不能完全排除它。

恢復

一個伺服器程序p剛剛從一次故障中恢復,我們希望p能夠獲得一個和其它程序們已經達成的決定一致的決定,如果不能馬上恢復這個決定,那麼至少在其它的故障被修復後能夠恢復這個決定。

當一個伺服器程序p把系統恢復到了故障發生時現場儲存的狀態,我們來進一步考慮一下。如果p是在它傳送YES投票到協調員之前就發生故障了,那麼該程序就可以單方面的決定取消這次事務,傳送NO投票給協調員,不做任何處理。同樣,如果p是在已經收到COMMIT/ABORT決定之後或者自己已經作出ABORT的決定之後發生故障了,那麼此時p由於已經做出了決定,p就可以作出相應的處理,比如說取消事務操作,或者繼續把COMMIT決定的操作執行完畢。在這些情況下,p都能夠獨立得進行故障恢復。

但是,如果p發生故障時是處在它的不確定期時,那麼它就無法在恢復時獨立得做決定了,這就是問題的複雜之處。因為它投了YES,在p故障時,可能其他的參與者全部投了YES並且協調者做出了COMMIT的決定。又或者p發生故障時,其他參與者並未全部投票YES,因此協調者作出的是ABORT的決定。此時p無法根據本地資訊就能獨立得進行恢復,他需要和其他程序進行交流。在這種情況下,p所面臨的情況是和time-action的情況(3)是一樣的。(設想一下,p設定了一個非常長的timeout 時間,整個故障期間都沒有超過timeout的期限)。因此此時p也採用前面提到的終止設計來解決問題。

為了儲存故障發生時的狀態,每個程序都必須維護一個DT Log(Database Transaction Log)。每個程序只能訪問他自己伺服器上的DT Log。假設我們採用的是協同終止設計,我們來看看如果管理這些DT log.

1. 當協調員傳送投票請求之前或之後,它寫了一條開始兩階段記錄在DT log中。該記錄大概類似這樣:

Xml程式碼  收藏程式碼
  1. {  
  2. Type: start-2PC,  
  3. time: 2011-10-30 19:20:20,  
  4. Participants:  
  5. [  
  6.     {  
  7.         Hostname:participant-1,  
  8.         Ip:192.168.0.3  
  9.           },  
  10.           {  
  11.               Hostname:participant-2,  
  12.               Ip:192.168.0.4  
  13.           },  
  14.           {  
  15.               Hostname:participant-3,  
  16.               Ip:192.168.0.5  
  17.           }  
  18.        ]  
  19. }  

2. 如果參與者執行緒傳送了YES投票,那麼他必須在傳送投票之前寫這麼YES 投票記錄在DT Log中,大概類似這樣:

Xml程式碼  收藏程式碼
  1. {  
  2.      Type: VOTE,  
  3.      Value:YES,  
  4.      time: 2011-10-30 19:20:20,  
  5.      Coordinator: 192.168.0.2  
  6.      OtherParticipants:  
  7.      [  
  8.         {  
  9.            Hostname:participant-2,  
  10.            Ip:192.168.0.4  
  11.          },  
  12.          {  
  13.              Hostname:participant-3,  
  14.              Ip:192.168.0.5  
  15.           }  
  16.       ]  
  17. }  

  如果參與者傳送了NO投票,那麼它可以在傳送投票之前或之後寫一條ABORT ACCEPT記錄在DT log中。

3. 在協調員傳送COMMIT決定給所有參與者程序之前,他寫入一條COMMIT DECISION記錄。

4. 當協調員傳送ABORT決定給所有參與者程序之前或之後,它寫入一條ABORT DECISION記錄

5. 參與者伺服器程序在收到COMMIT/ABORT決定之後,參與者程序寫入一條COMMIT ACCEPT/ABORT ACCPET記錄。

對上述Log做一些說明,一旦參與者伺服器程序在DT日誌中寫入COMMIT ACCEPT或者ABORT ACCEPT記錄後,DM(database manager)就可以執行commit或者abort資料庫操作。具體來講還有很多細節,比如系統中的DT Log可能是DM Log中的一部分,因此DT Log中的COMMIT ACCEPT/ABORT ACCEPT記錄是通過本地DM的Commit/Abort子程式來實現的,在子程式中進行具體的操作之前,DM會寫入COMMIT ACCEPT/ABORT ACCEPT記錄到日誌中去。

有了這個日誌系統,當伺服器S就可以按照下面的方式進行恢復:

1> 如果S檢查DT Log發現了記錄,那麼S就知道自己是一臺協調員。如果發現日誌還包含了COMMIT DECISION或者ABORT DECISION日誌,那就證明在故障發生之前已經產生了決定,他可以選擇重新發送這些決定。如果沒有發現這兩條記錄中的任何一條,那麼S就可以單方面得決定Abort,同時向日志中寫入ABORT DECISION記錄,並重發決定。需要注意的是,要先插入COMMIT DECISION日誌,再發送COMMIT決定給各個參與者程序,這很關鍵。為什麼順序這麼關鍵呢?試想一下,如果傳送決定訊息在前,插入日誌在後,那麼就會有一種可能,訊息COMMIT DECISION傳送完了但日誌還沒來得及寫入的時候伺服器發生故障了,當伺服器恢復之後,按照前面的邏輯,它會認為還未做出任何決定,於是又單方面的決定ABORT DECISION,這下就和實際情況衝突了,參與者就會受到兩條完全衝突的決定:ABORT DECISION和COMMIT DECISION,系統會無法處理。如果寫日誌在前,傳送訊息在後,系統也有可能在兩個時間點之間發生故障,協調員恢復時會看見日誌,因此不會做任何事或者把決定重新發送一遍,因為決定事先已經達成,即使有可能訊息還沒有傳送,但至少不會做出自相矛盾的決定令參與者無法是從。

2> 如果S沒有發現任何記錄,S就會認為自己是一臺參與者。那麼就會有三種情況:

1. DT log中包含了COMMIT ACCEPT或者ABORT ACCEPT記錄,那參與者已經獲得了決定,那麼參與者可以自己來決定,可以根據記錄來檢視相應的操作是否完成,如果還未完成可以繼續從而完成相應操作。

2. 如果日誌中沒有包含VOTE YES記錄以及任何COMMIT ACCEPT或者ABORT ACCEPT記錄,我們無法得到它當時是選擇YES還是NO。我們寫VOTE YES記錄的時間也要比傳送實際訊息早,儘可能早得儲存決定。此時S可以單方面得決定ABORT ACCEPT。

3. 如果日誌中包含VOTE YES記錄但沒有任何COMMIT ACCEPT或者ABORT ACCEPT記錄。那麼參與者是在不確定期發生故障的,因此它採用終止協議來獲得決定。

對於一個實際的系統而言,系統需要處理的是很多的事務,因此不同事務的日誌是交錯得存放在DT Log裡。因此每條日誌記錄需要包含事務的名字。而且隨著時間的積累,事務越來越多,日誌的體積也會越來越龐大。因此需要定期對日誌進行垃圾回收。日誌垃圾回收有2個準則:

GC1:一臺伺服器不能刪除事務T的日誌,直到它的RM(Recovery Manager)已經處理完了RM-Commit(T)或者RM-Abort(T)

GC2:一臺伺服器不能刪除事務T的日誌,直到該伺服器收到訊息,所有其他伺服器的RM-Commit(T)或者Rm-Abort(T)已經處理完畢。

對於GC1,通過本地的資訊很容易得到。對於GC2,則需要伺服器之間能夠相互通訊,你可以讓協調員來執行GC2,或者完全分散式得由各個伺服器通過相互交流完成GC2.

由於實際系統同時併發得處理很多事務,因此在某臺伺服器恢復的時候,我們還需要考慮一些細節問題。當伺服器恢復時,它需要把繼續完成那些還未COMMIT或ABORT的事務,這些事務在完全恢復之前都會被block住從而無法訪問資料庫這部分資源,這會造成浪費。因此解決的方法是不是在整個恢復階段一直hold住這些待恢復並且在故障之前處於不確定期被block住得事務的所有的讀寫鎖,而是把這些鎖暫時全部釋放,然後再通過重新爭取鎖的方式來和新到的事務來競爭鎖,這樣避免了在整個恢復階段所有的block資源都無法訪問。具體的流程是這樣的,伺服器恢復後,先處理那些沒有被block住的事務,為這些事務做出決定。然後再處那些故障前被block的事務,這時候恢復程式先釋放這些事務的所有讀寫鎖,然後再與故障之後新的事務一起競爭重新請求這些讀寫鎖。一旦恢復程式先釋放了待恢復的block事務的讀寫鎖,那麼這些事務所持有的資料庫資源就可以被訪問了。當然由於有競爭,原來本來可以COMMIT的事務可能由於資源競爭被ABORT掉了,但帶來的好處是吞吐量大大提高。在原來的方案中,事務的鎖可以儲存在DT Log裡,在競爭的方案中,鎖可以不必儲存,因為伺服器程序可以根據Log自行決定。