天天看點

為什麼說傳統分布式事務不再适用于微服務架構

傳統應用使用本地事務和分布式事務保證資料一緻性,但是在微服務架構中資料都是服務私有的,需要通過服務提供的api來通路,是以分布式事務不再适用微服務架構。那麼微服務架構又該如何保證資料一緻性呢?本文就來談談這個話題。

傳統分布式事務不是微服務中資料一緻性的最佳選擇

微服務架構中應滿足資料最終一緻性原則

微服務架構實作最終一緻性的三種模式

對賬是最後的終極防線

  傳統分布式事務

  我們先來看下第一部分,傳統使用本地事務和分布式事務保證一緻性。

為什麼說傳統分布式事務不再适用于微服務架構

  傳統單機應用一般都會使用一個關系型資料庫,好處是應用可以使用acid。為保證一緻性我們隻需要:開始一個事務,改變(插入,删除,更新)很多行,然後送出事務(如果有異常時復原事務)。更進一步,借助開發平台中的資料通路技術和架構(如

spring),我們需要做的事情更少,隻需要關注資料本身的改變。

  随着組織規模不斷擴大,業務量不斷增長,單機應用和資料庫已經不足以支援龐大的業務量和資料量,這個時候需要對應用和資料庫進行拆分,這就出現了一個應用需要同時通路兩個或兩個以上的資料庫情況。開始我們用分布式事務來保證一緻性,也就是我們常說的兩階段送出協定(2pc)。

為什麼說傳統分布式事務不再适用于微服務架構

  本地事務和分布式事務現在已經非常成熟,相關介紹很豐富,此處不再讨論。我們下面來談談為什麼分布式事務不适用于微服務架構。

為什麼說傳統分布式事務不再适用于微服務架構

  首先,對于微服務架構來說,資料通路變得更加複雜,這是因為資料都是微服務私有的,唯一可通路的方式就是通過 api。這種打包資料通路方式使得微服務之間松耦合,并且彼此之間獨立,更容易進行性能擴充。

  其次,不同的微服務經常使用不同的資料庫。應用會産生各種不同類型的資料,關系型資料庫并不一定是最佳選擇。

  例如,某個産生和查詢字元串的應用采用 elasticsearch 的字元搜尋引擎;某個産生社交圖檔資料的應用可以采用圖資料庫,例如neo4j。

  基于微服務的應用一般都使用 sql 和 nosql 結合的模式。但是這些非關系型資料大多數并不支援 2pc。

  可見在微服務架構中已經不能選擇分布式事務了。

  最終一緻性原則

  依據

cap

理論,必須在可用性(availability)和一緻性(consistency)之間做出選擇。如果選擇提供一緻性需要付出在滿足一緻性之前阻塞其他并發通路的代價。這可能持續一個不确定的時間,尤其是在系統已經表現出高延遲時或者網絡故障導緻失去連接配接時。

  依據目前的成功經驗,可用性一般是更好的選擇,但是在服務和資料庫之間維護資料一緻性是非常根本的需求,微服務架構中應選擇滿足最終一緻性。

  最終一緻性是指系統中的所有資料副本經過一定時間後,最終能夠達到一緻的狀态。

  當然選擇了最終一緻性,就要保證到最終的這段時間要在使用者可接受的範圍之内。那麼我們怎麼實作最終一緻性呢?

  從一緻性的本質來看,是要保證在一個業務邏輯中包含的服務要麼都成功,要麼都失敗。那我們怎麼選擇方向呢?保證成功還是保證失敗呢?

為什麼說傳統分布式事務不再适用于微服務架構

  我們說業務模式決定了我們的選擇。實作最終一緻性有三種模式:可靠事件模式、業務補償模式、tcc 模式。

  可靠事件模式

  可靠事件模式屬于事件驅動架構,當某件重要事情發生時,例如更新一個業務實體,微服務會向消息代理釋出一個事件。消息代理會向訂閱事件的微服務推送事件,當訂閱這些事件的微服務接收此事件時,就可以完成自己的業務,也可能會引發更多的事件釋出。

  1. 如訂單服務建立一個待支付的訂單,釋出一個“建立訂單”的事件。

為什麼說傳統分布式事務不再适用于微服務架構

  2. 支付服務消費“建立訂單”事件,支付完成後釋出一個“支付完成”事件。

為什麼說傳統分布式事務不再适用于微服務架構

  3. 訂單服務消費“支付完成”事件,訂單狀态更新為待出庫。

為什麼說傳統分布式事務不再适用于微服務架構

  進而就實作了完成的業務流程。但是這并不是一個完美的流程。

為什麼說傳統分布式事務不再适用于微服務架構

  這個過程可能導緻出現不一緻的地方在于:某個微服務在更新了業務實體後釋出事件卻失敗;雖然微服務釋出事件成功,但是消息代理未能正确推送事件到訂閱的微服務;接受事件的微服務重複消費了事件。

  可靠事件模式在于保證可靠事件投遞和避免重複消費,可靠事件投遞定義為:

每個服務原子性的業務操作和釋出事件。

消息代理確定事件傳遞至少一次。

  避免重複消費要求服務實作幂等性,如支付服務不能因為重複收到事件而多次支付。

  因為現在流行的消息隊列都實作了事件的持久化和 at least once 的投遞模式,『消息代理確定事件投遞至少一次』已經滿足,今天不做展開。

  下面分享的内容主要從可靠事件投遞和實作幂等性兩方面來讨論,我們先來看可靠事件投遞。

  首先我們來看一個實作的代碼片段,這是從某生産系統上截取下來的。

為什麼說傳統分布式事務不再适用于微服務架構

  根據上述代碼及注釋,初看可能出現 3 種情況:

操作資料庫成功,向消息代理投遞事件也成功。

操作資料庫失敗,不會向消息代理中投遞事件了。

操作資料庫成功,但是向消息代理中投遞事件時失敗,向外抛出了異常,剛剛執行的更新資料庫的操作将被復原。

  從上面分析的幾種情況來看,貌似沒有問題。但是仔細分析不難發現缺陷所在,在上面的處理過程中存在一段隐患時間視窗。

為什麼說傳統分布式事務不再适用于微服務架構

微服務 a 投遞事件的時候可能消息代理已經處理成功,但是傳回響應的時候網絡異常,導緻 append 操作抛出異常。最終結果是事件被投遞,資料庫卻被復原。

為什麼說傳統分布式事務不再适用于微服務架構

在投遞完成後到資料庫 commit 操作之間如果微服務 a 當機也将造成資料庫操作因為連接配接異常關閉而被復原。最終結果還是事件被投遞,資料庫卻被復原。這個實作往往運作很長時間都沒有出過問題,但是一旦出現了将會讓人感覺莫名,很難發現問題所在。

  下面給出兩種可靠事件投遞的實作方式。

  1. 本地事件表

  本地事件表方法将事件和業務資料儲存在同一個資料庫中,使用一個額外的“事件恢複”服務來恢複事件,由本地事務保證更新業務和釋出事件的原子性。考慮到事件恢複可能會有一定的延時,服務在完成本地事務後可立即向消息代理釋出一個事件。

為什麼說傳統分布式事務不再适用于微服務架構

微服務在同一個本地事務中記錄業務資料和事件。

微服務實時釋出一個事件立即通知關聯的業務服務,如果事件釋出成功立即删除記錄的事件。

事件恢複服務定時從事件表中恢複未釋出成功的事件,重新釋出,重新釋出成功才删除記錄的事件。

  其中第Ⅱ條的操作主要是為了增加釋出事件的實時性,由第三條保證事件一定被釋出。

  本地事件表方式業務系統和事件系統耦合比較緊密,額外的事件資料庫操作也會給資料庫帶來額外的壓力,可能成為瓶頸。

  2. 外部事件表

  外部事件表方法将事件持久化到外部的事件系統,事件系統需提供實時事件服務以接受微服務釋出事件,同時事件系統還需要提供事件恢複服務來确認和恢複事件。

為什麼說傳統分布式事務不再适用于微服務架構

業務服務在事務送出前,通過實時事件服務向事件系統請求發送事件,事件系統隻記錄事件并不真正發送。

業務服務在送出後,通過實時事件服務向事件系統确認發送,事件得到确認後事件系統才真正釋出事件到消息代理。

業務服務在業務復原時,通過實時事件向事件系統取消事件。

如果業務服務在發送确認或取消之前停止服務了怎麼辦呢?事件系統的事件恢複服務會定期找到未确認發送的事件向業務服務查詢狀态,根據業務服務傳回的狀态決定事件是要釋出還是取消。

  該方式将業務系統和事件系統獨立解耦,都可以獨立伸縮。但是這種方式需要一次額外的發送操作,并且需要釋出者提供額外的查詢接口。

  介紹完了可靠事件投遞再來說一說幂等性的實作,有些事件本身是幂等的,有些事件卻不是。

  本身具有幂等性的事件需要考慮執行順序

  如果事件本身描述的是某個時間點的固定值(如賬戶餘額為 100),而不是描述一條轉換指令(如餘額增加 10),那麼這個事件是幂等的。

  我們要意識到事件可能出現的次數和順序是不可預測的,需要保證幂等事件的順序執行,否則結果往往不是我們想要的。

  如果我們先後收到兩條事件,(1)賬戶餘額更新為100,(2)賬戶餘額更新為120。

  1. 微服務收到事件(1)

為什麼說傳統分布式事務不再适用于微服務架構

  2. 微服務收到事件(2)

  

為什麼說傳統分布式事務不再适用于微服務架構

  3. 微服務再次收到事件(1)

為什麼說傳統分布式事務不再适用于微服務架構

  顯然結果是錯誤的,是以我們需要保證事件(2)一旦執行事件(1)就不能再處理,否則賬戶餘額仍不是我們想要的結果。

  為保證事件的順序一個簡單的做法是在事件中添加時間戳,微服務記錄每類型的事件最後處理的時間戳,如果收到的事件的時間戳早于我們記錄的,丢棄該事件。如果事件不是在同一個伺服器上發出的,那麼伺服器之間的時間同步是個難題,更穩妥的做法是使用一個全局遞增序列号替換時間戳。

  對于本身不具有幂等性的操作,主要思想是為每條事件存儲執行結果,當收到一條事件時我們需要根據事件的 id 查詢該事件是否已經執行過,如果執行過直接傳回上一次的執行結果,否則排程執行事件。

為什麼說傳統分布式事務不再适用于微服務架構
為什麼說傳統分布式事務不再适用于微服務架構

  在這個思想下我們需要考慮重複執行一條事件和查詢存儲結果的開銷。

  重複處理開銷小的事件

  如果重複處理一條事件開銷很小,或者可預見隻有非常少的事件會被重複接收,可以選擇重複處理一次事件,在将事件資料持久化時由資料庫抛出唯一性限制異常。

  重複處理開銷大事件使用事件存儲過濾重複事件

  如果重複處理一條事件的開銷相比額外一次查詢的開銷要高很多,使用一個過濾服務來過濾重複的事件,過濾服務使用事件存儲存儲已經處理過的事件和結果。

  當收到一條事件時,過濾服務首先查詢事件存儲,确定該條事件是否已經被處理過,如果事件已經被處理過,直接傳回存儲的結果;否則排程業務服務執行處理,并将處理完的結果存儲到事件存儲中。

  一般情況下上面的方法能夠運作得很好,如果我們的微服務是

rpc

類的服務我們需要更加小心,可能出現的問題在于,(1)過濾服務在業務處理完成後才将事件結果存儲到事件存儲中,但是在業務處理完成前有可能就已經收到重複事件,由于是

rpc 服務也不能依賴資料庫的唯一性限制;(2)業務服務的處理結果可能出現位置狀态,一般出現在正常送出請求但是沒有收到響應的時候。

  對于問題(1)可以按步驟記錄事件處理過程,比如事件的記錄事件的處理過程為“接收”、“發送請求”、“收到應答”、“處理完成”。好處是過濾服務能及時的發現重複事件,進一步還能根據事件狀态作不同的處理。

  對于問題(2)可以通過一次額外的查詢請求來确定事件的實際處理狀态,要注意額外的查詢會帶來更長時間的延時,更進一步可能某些 rpc 服務根本不提供查詢接口。此時隻能選擇接收暫時的不一緻,時候采用對賬和人工接入的方式來保證一緻性。

  補償模式

  為了描述友善,這裡先定義兩個概念:

業務異常:業務邏輯産生錯誤的情況,比如賬戶餘額不足、商品庫存不足等。

技術異常:非業務邏輯産生的異常,如網絡連接配接異常、網絡逾時等。

  補償模式使用一個額外的協調服務來協調各個需要保證一緻性的微服務,協調服務按順序調用各個微服務,如果某個微服務調用異常(包括業務異常和技術異常)就取消之前所有已經調用成功的微服務。

  補償模式建議僅用于不能避免出現業務異常的情況,如果有可能應該優化業務模式,以避免要求補償事務。如賬戶餘額不足的業務異常可通過預先當機金額的方式避免,商品庫存不足可要求商家準備額外的庫存等。

  我們通過一個執行個體來說明補償模式,一家旅行公司提供預訂行程的業務,可以通過公司的網站提前預訂飛機票、火車票、酒店等。

  假設一位客戶規劃的行程是:

上海-北京6月19日9點的某某航班。

某某酒店住宿3晚。

北京-上海6月22日17點火車。

  在客戶送出行程後,旅行公司的預訂行程業務按順序串行的調用航班預訂服務、酒店預訂服務、火車預訂服務。最後的火車預訂服務成功後整個預訂業務才算完成。

為什麼說傳統分布式事務不再适用于微服務架構

  如果火車票預訂服務沒有調用成功,那麼之前預訂的航班、酒店都得取消。取消之前預訂的酒店、航班即為補償過程。

為什麼說傳統分布式事務不再适用于微服務架構

  為了降低開發的複雜性和提高效率,協調服務實作為一個通用的補償架構。補償架構提供服務編排和自動完成補償的能力。

  要實作補償過程,我們需要做到兩點:

  首先要确定失敗的步驟和狀态,進而确定需要補償的範圍。

為什麼說傳統分布式事務不再适用于微服務架構

  在上面的例子中我們不僅要知道第 3 個步驟(預訂火車)失敗,還要知道失敗的原因。如果是因為預訂火車服務傳回無票,那麼補償過程隻需要取消前兩個步驟就可以了;但是如果失敗的原因是因為網絡逾時,那麼補償過程除前兩個步驟之外還需要包括第 3 個步驟。

  其次要能提供補償操作使用到的業務資料。

  比如一個支付微服務的補償操作要求參數包括支付時的業務流水

id、賬号和金額。理論上說實際完成補償操作可以根據唯一的業務流水 id

就可以,但是提供更多的要素有益于微服務的健壯性,微服務在收到補償操作的時候可以做業務的檢查,比如檢查賬戶是否相等,金額是否一緻等等。

為什麼說傳統分布式事務不再适用于微服務架構

  做到上面兩點的辦法是記錄完整的業務流水,可以通過業務流水的狀态來确定需要補償的步驟,同時業務流水為補償操作提供需要的業務資料。

為什麼說傳統分布式事務不再适用于微服務架構

  當客戶的一個預訂請求達到時,協調服務(補償架構)為請求生成一個全局唯一的業務流水号,并在調用各個工作服務的同時記錄完整的狀态。

記錄調用 bookflight 的業務流水,調用 bookflight 服務,更新業務流水狀态。

記錄調用 bookhotel 的業務流水,調用 bookhotel 服務,更新業務流水狀态。

記錄調用 booktrain 的業務流水,調用 booktrain 服務,更新業務流水狀态。

  當調用某個服務出現異常時,比如第 3 步驟(預訂火車)異常。

為什麼說傳統分布式事務不再适用于微服務架構

  協調服務(補償架構)同樣會記錄第 3 步的狀态,同時會另外記錄一條事件,說明業務出現了異常。然後就是執行補償過程了,可以從業務流水的狀态中知道補償的範圍,補償過程中需要的業務資料從記錄的業務流水中擷取。

  對于一個通用的補償架構來說,預先知道微服務需要記錄的業務要素是不可能的。那麼就需要一種方法來保證業務流水的可擴充性,這裡介紹兩種方法:大表和關聯表。

為什麼說傳統分布式事務不再适用于微服務架構

  大表顧明思議就是設計時除必須的字段外,還需要預留大量的備用字段,架構可以提供輔助工具來幫助将業務資料映射到備用字段中。

  關聯表,分為架構表和業務表,技術表中儲存為實作補償操作所需要的技術資料,業務表儲存業務資料,通過在技術表中增加業務表名和業務表主鍵來建立和業務資料的關聯。

  大表對于架構層實作起來簡單,但是也有一些難點,比如預留多少字段合适,每個字段又需要預留多少長度。另外一個難點是如果向從資料層面來查詢資料,很難看出備用字段的業務含義,維護過程不友好。

  關聯表在業務要素上更靈活,能支援不同的業務類型記錄不同的業務要素;但是對于架構實作上難度更高,另外每次查詢都需要複雜的關關聯作,性能方面會受影響。

  有了上面的完整的流水記錄,協調服務就可以根據工作服務的狀态在異常時完成補償過程。但是補償由于網絡等原因,補償操作并不一定能保證 100%成功,這時候我們還要做更多一點。

為什麼說傳統分布式事務不再适用于微服務架構

  補償過程作為一個服務調用過程同樣存在調用不成功的情況,這個時候需要通過重試的機制來保證補償的成功率。當然這也就要求補償操作本身具備幂等性。

  關于幂等性的實作在前面做過讨論。

  如果隻是一味的失敗就立即重試會給工作服務造成不必要的壓力,我們要根據服務執行失敗的原因來選擇不同的重試政策。

為什麼說傳統分布式事務不再适用于微服務架構

  1) 如果失敗的原因不是暫時性的,由于業務因素導緻(如業務要素檢查失敗)的業務錯誤,這類錯誤是不會重發就能自動恢複的,那麼應該立即終止重試。

  2) 如果錯誤的原因是一些罕見的異常,比如因為網絡傳輸過程出現資料丢失或者錯誤,應該立即再次重試,因為類似的錯誤一般很少會再次發生。

  3) 如果錯誤的原因是系統繁忙(比如 http 協定傳回的 500 或者另外約定的傳回碼)或者逾時,這個時候需要等待一些時間再重試。

  重試操作一般會指定重試次數上線,如果重試次數達到了上限就不再進行重試了。這個時候應該通過一種手段通知相關人員進行處理。

  對于等待重試的政策如果重試時仍然錯誤,可逐漸增加等待的時間,直到達到一個上限後,以上限作為等待時間。

  如果某個時刻聚集了大量需要重試的操作,補償架構需要控制請求的流量,以防止對工作服務造成過大的壓力。

  另外關于補償模式還有幾點補充說明。

微服務實作補償操作不是簡單的回退到業務發生時的狀态,因為可能還有其他的并發的請求同時更改了狀态。一般都使用逆操作的方式完成補償。

補償過程不需要嚴格按照與業務發生的相反順序執行,可以依據工作服務的重用程度優先執行,甚至是可以并發的執行。

有些服務的補償過程是有依賴關系的,被依賴服務的補償操作沒有成功就要及時終止補償過程。

如果在一個業務中包含的工作服務不是都提供了補償操作,那我們編排服務時應該把提供補償操作的服務放在前面,這樣當後面的工作服務錯誤時還有機會補償。

設計工作服務的補償接口時應該以協調服務請求的業務要素作為條件,不要以工作服務的應答要素作為條件。因為還存在逾時需要補償的情況,這時補償架構就沒法提供補償需要的業務要素。

  tcc模式(try-confirm-cancel)

  一個完整的 tcc 業務由一個主業務服務和若幹個從業務服務組成,主業務服務發起并完成整個業務活動,tcc 模式要求從服務提供三個接口:try、confirm、cancel。

為什麼說傳統分布式事務不再适用于微服務架構

  1. try

  完成所有業務檢查

  預留必須業務資源

  2. confirm

  真正執行業務

  不作任何業務檢查

  隻使用 try 階段預留的業務資源

  confirm 操作滿足幂等性

  3. cancel:

  釋放 try 階段預留的業務資源

  cancel 操作滿足幂等性

  整個 tcc 業務分成兩個階段完成。

為什麼說傳統分布式事務不再适用于微服務架構

  第一階段:主業務服務分别調用所有從業務的 try 操作,并在活動管理器中登記所有從業務服務。當所有從業務服務的 try 操作都調用成功或者某個從業務服務的 try 操作失敗,進入第二階段。

  第二階段:活動管理器根據第一階段的執行結果來執行 confirm 或 cancel 操作。

  如果第一階段所有 try 操作都成功,則活動管理器調用所有從業務活動的 confirm操作。否則調用所有從業務服務的 cancel 操作。

  需要注意的是第二階段

confirm 或 cancel 操作本身也是滿足最終一緻性的過程,在調用 confirm 或 cancel

的時候也可能因為某種原因(比如網絡)導緻調用失敗,是以需要活動管理支援重試的能力,同時這也就要求 confirm 和 cancel

操作具有幂等性。

  在補償模式中一個比較明顯的缺陷是,沒有隔離性。從第一個工作服務步驟開始一直到所有工作服務完成(或者補償過程完成),不一緻是對其他服務可見的。另外最終一緻性的保證還充分的依賴了協調服務的健壯性,如果協調服務異常,就沒法達到一緻性。

  tcc模式在一定程度上彌補了上述的缺陷,在tcc模式中直到明确的confirm動作,所有的業務操作都是隔離的(由業務層面保證)。另外工作服務可以通過指定 try 操作的逾時時間,主動的 cancel 預留的業務資源,進而實作自治的微服務。

  tcc模式和補償模式一樣需要需要有協調服務和工作服務,協調服務也可以作為通用服務一般實作為架構。與補償模式不同的是 tcc 服務架構不需要記錄詳細的業務流水,完成 confirm 和 cancel 操作的業務要素由業務服務提供。

為什麼說傳統分布式事務不再适用于微服務架構

  在第4步确認預訂之前,訂單隻是pending狀态,隻有等到明确的confirm之後訂單才生效。

為什麼說傳統分布式事務不再适用于微服務架構

  如果3個服務中某個服務try操作失敗,那麼可以向tcc服務架構送出cancel,或者什麼也不做由工作服務自己逾時處理。

為什麼說傳統分布式事務不再适用于微服務架構

  tcc 模式也不能百分百保證一緻性,如果業務服務向 tcc 服務架構送出 confirm後,tcc 服務架構向某個工作服務送出 confirm 失敗(比如網絡故障),那麼就會出現不一緻,一般稱為 heuristic exception。

  需要說明的是為保證業務成功率,業務服務向

tcc 服務架構送出 confirm 以及tcc 服務架構向工作服務送出 confirm/cancel

時都要支援重試,這也就要confirm/cancel 的實作必須具有幂等性。如果業務服務向 tcc 服務架構送出confirm/cancel

失敗,不會導緻不一緻,因為服務最後都會逾時而取消。

  另外 heuristic exception 是不可杜絕的,但是可以通過設定合适的逾時時間,以及重試頻率和監控措施使得出現這個異常的可能性降低到很小。如果出現了heuristic exception 是可以通過人工的手段補救的。

  對賬是最後的終極防線

  如果有些業務由于瞬時的網絡故障或調用逾時等問題,通過上文所講的

3

種模式一般都能得到很好的解決。但是在當今雲計算環境下,很多服務是依賴于外部系統的可用性情況,在一些重要的業務場景下還需要周期性的對賬來保證真實的一緻性。比如支付系統和銀行之間每天日終是都會有對賬過程。

為什麼說傳統分布式事務不再适用于微服務架構

原文連結:[http://wely.iteye.com/blog/2351228]