由Seata看分佈式事務取捨

微服務興起這幾年涌現出不少分佈式事務框架,比如ByteTCC、TCC-transaction、EasyTransaction以及最近很火爆的Seata。最近剛看了Seata的源碼(v0.5.2),藉機記錄一下自己對分佈式事務的一些理解。(3年前這類框架還沒成熟,項目需要自己也寫過一個柔性事務框架

本文分五部分,首先明確分佈式事務概念的演變,然後簡單說下爲什麼大家不用XA,第三部分闡述兩階段提交的提升,第四部分介紹Seata的架構的亮點與問題,第五部分談下分佈式事務的取捨。 限於篇幅一些網上可搜索的細節本文不展開闡述。(例如XA、Saga、TCC、Seata等原理的的詳細介紹)

1.分佈式事務的泛化

提起分佈式事務,最早指的涉及多個資源的數據庫事務問題。

wiki對分佈式事務的定義:A distributed transaction is a database transaction in which two or more network hosts are involved.

不過事務一詞含義隨着SOA架構逐漸擴大,根據上下文不同,可分爲兩類:

  1. System transaction
  2. Business transaction

前者多指數據庫事務,後者則多對應一個業務交易。與此同時,分佈式事務的含義也在泛化,尤其SOA、微服務概念流行起來後,多指的是一個業務場景,需要編排很多獨立部署的服務時,如何保證交易整體的原子性與一致性問題,這類分佈式事務也稱作長事務(long-lived transaction),例如一個定行程的交易,它由購買航班、租車以及預訂酒店構成,而航班預訂可能需要一兩天才能確認。爲了統一對概念的理解,本文默認指的都是這類長事務。

分佈式事務概念泛化的同時,也帶來了一個技術問題,微服務下這類分佈式事務的ACID該如何保證?是否仍然可以用傳統兩階段提交/XA去解決?很可惜,基於數據庫的XA有點像扶不起的阿斗,中看不中用。

2.爲什麼XA大家都不用?

其實也並非不用,例如在IBM大型機上基於CICS很多跨資源是基於XA協議實現的分佈式事務,XA也事實上算分佈式事務處理的規範了,但在爲什麼互聯網中很少使用,究其原因我覺得有幾個:

  • 性能(阻塞性協議,增加響應時間、鎖時間、死鎖)
  • 數據庫支持完善度(MySQL 5.7之前都有缺陷)
  • 協調者依賴獨立的J2EE中間件(早期重量級Weblogic、Jboss,後期輕量級Atomikos、Narayana和Bitronix)
  • 運維複雜,DBA缺少這方面經驗
  • 並不是所有資源都支持XA協議。
  • 大廠懂所以不使用,小公司不懂所以不敢用

準確講XA是一個規範、協議,它只是定義了一系列的接口,只是目前大多數實現XA的都是數據庫或者MQ,所以提起XA往往多指基於資源層的底層分佈式事務解決方案。其實現在也有些數據分片框架或者中間件也支持XA協議,畢竟它的兼容性、普遍性更好。

3.兩階段提交的“提升”

基於數據庫的XA協議本質上就是兩階段提交,但由於性能原因在互聯網高併發場景下並不適用。如果數據庫只能保證本地ACID時,那麼出現其中交易異常後,如何實現整個交易原子性A,從而保證一致性C呢?另外在處理過程中如何保證隔離性呢?
最直接就是按照邏輯依次調用服務,當出現異常怎麼辦?那就對那些已經成功的進行補償,補償成功就一致了,這種樸素的模型就是Saga。但Saga這種方式並不能保證隔離性,於是出現了TCC,在實際交易邏輯前先做業務檢查、對涉及到的業務資源進行“預留”,或者說是一種“中間狀態”,如果都預留成功則完成這些預留資源的真正業務處理,典型的如票務座位等場景。當然還有像Ebay提出的基於消息表,即可靠消息最終一致模型,但本質上這也屬於Saga模式的一種特定實現,它的關鍵點有兩個:1.基於應用共享事務記錄執行軌跡,2.然後通過異步重試確保交易最終一致(這也使得這種方式不適用那些業務上允許補償回滾的場景)。

這類分佈式事務場景並不是微服務纔出現的,在SOA時代其實就有了,常見的Saga、TCC、可靠消息最終一致等模型也都是很多年前就有了,只是最近幾年隨着微服務興起,這些方案又重新被人關注了起來。

仔細對比這些方案與XA,會發現這些方案本質上都是將兩階段提交從資源層提升到了應用層。
• Saga的核心就是補償,一階段就是服務的正常順序調用(數據庫事務正常提交),如果都執行成功,則第二階段則什麼都不做;但如果其中有執行發生異常,則依次調用其補償服務(一般多逆序調用未已執行服務的反交易)來保證整個交易的一致性。應用實施成本一般
• TCC的特點在於業務資源檢查與加鎖,一階段進行校驗,資源鎖定,如果第一階段都成功,二階段對鎖定資源進行交易邏輯,否則,對鎖定資源進行釋放。應用實施成本較高
• 基於可靠消息最終一致,一階段服務正常調用,同時同事務記錄消息表,二階段則進行消息的投遞,消費。應用實施成本較低

具體到基於這些模型實現的分佈式事務框架,也多借鑑了DTP(Distributed Transaction Processing)模型。

DTP模型

  • RM負責本地事務的提交,同時完成分支事務的註冊、鎖的判定,扮演事務參與者角色。
  • TM負責整體事務的提交與回滾的指令的觸發,扮演事務的總體協調者角色。
    不同框架在實現時,各組件角色的功能、部署形態會根據需求進行調整,例如TM有的是以jar包形式與應用部署在一起,有的則剝離出來需要單獨部署(例如Seata中將TM的主要功能放到一個邏輯上集中的Server上,叫做TC( Transaction Coordinator ))

4. Seata架構得與失

今年初,阿里發佈了開源分佈式事務框架Fescar,後來跟螞蟻TCC方案整合後改名爲Seata,目前版本雖然只到0.6,但GitHub star已經過9k,一方面可見阿里在圈內推廣能力,另外一方面也說明大家對阿里分佈式事務框架的期待。Seata的使用方式以及原理在其github wiki上已經闡述的很清晰(https://github.com/seata/seata/wiki),網上也已有很多源代碼剖析的文章。接下來我們通過分析Seata AT模式原理,來看看它的亮點與問題。

Seata對MT以及TCC的支持亮點有限,這兩種模式更多是爲了兼容已有應用生態。

Seata團隊畫了一個的詳細調用流程圖,對照此圖閱讀其源碼會輕鬆很多。


Seata執行流程圖
4.1 亮點

相比與其它分佈式事務框架,Seata架構的亮點主要有幾個:

  1. 應用層基於SQL解析實現了自動補償,從而最大程度的降低業務侵入性;
  2. 將分佈式事務中TC(事務協調者)獨立部署,負責事務的註冊、回滾;
  3. 通過全局鎖實現了寫隔離與讀隔離。

這些特性的具體實現機制其官網以及github上都有詳細介紹,這裏不展開介紹。

4.2 性能損耗

我們看看Seata增加了哪些開銷(純內存運算類的忽略不計):
一條Update的SQL,則需要全局事務xid獲取(與TC通訊)、before image(解析SQL,查詢一次數據庫)、after image(查詢一次數據庫)、insert undo log(寫一次數據庫)、before commit(與TC通訊,判斷鎖衝突),這些操作都需要一次遠程通訊RPC,而且是同步的。另外undo log寫入時blob字段的插入性能也是不高的。每條寫SQL都會增加這麼多開銷,粗略估計會增加5倍響應時間(二階段雖然是異步的,但其實也會佔用系統資源,網絡、線程、數據庫)。

前後鏡像如何生成?
通過druid解析SQL,然後複用業務SQL中的where條件,然後生成Select SQL執行。

4.3 性價比

爲了進行自動補償,需要對所有交易生成前後鏡像並持久化,可是在實際業務場景下,這個是成功率有多高,或者說分佈式事務失敗需要回滾的有多少比率?這個比例在不同場景下是不一樣的,考慮到執行事務編排前,很多都會校驗業務的正確性,所以發生回滾的概率其實相對較低。按照二八原則預估,即爲了20%的交易回滾,需要將80%的成功交易的響應時間增加5倍,這樣的代價相比於讓應用開發一個補償交易是否是值得?值得我們深思。

業界還有種思路,通過數據庫binlog恢復SQL執行前後鏡像,這樣省去了同步undo log生成記錄,減少了性能損耗,同時對業務零侵入,個人感覺是一種更好的方式。

4.4 全局鎖
4.4.1 熱點數據

Seata在每個分支事務中會攜帶對應的鎖信息,在before commit階段會依次獲取鎖(因爲需要將所有SQL執行完才能拿到所有鎖信息,所以放在commit前判斷)。相比XA,Seata 雖然在一階段成功後會釋放數據庫鎖,但一階段在commit前全局鎖的判定也拉長了對數據鎖的佔有時間,這個開銷比XA的prepare低多少需要根據實際業務場景進行測試。全局鎖的引入實現了隔離性,但帶來的問題就是阻塞,降低併發性,尤其是熱點數據,這個問題會更加嚴重。

4.4.2 回滾鎖釋放時間

Seata在回滾時,需要先刪除各節點的undo log,然後才能釋放TC內存中的鎖,所以如果第二階段是回滾,釋放鎖的時間會更長。

4.4.3 死鎖問題

Seata的引入全局鎖會額外增加死鎖的風險,具體可見https://github.com/seata/awesome-seata/blob/master/wiki/en-us/Fescar-AT.md,但如果出現死鎖,會不斷進行重試,最後靠等待全局鎖超時,這種方式並不優雅,也延長了對數據庫鎖的佔有時間。

4.5 其它問題
  • 對於部分採用Seata的應用,如何保證數據不髒讀、幻讀?

Seata提供了一個@GlobalLock的註解,可以提供輕量級全局鎖判定的功能(不生成undo log),但還是需要集成使用Seata。

  • TC在邏輯上是單點,如何做到高可用、高性能還是需要後續版本不斷優化。
  • 單機多數據源跨服務目前不支持。

5. 分佈式事務的取捨

嚴格的ACID事務對隔離性的要求很高,在事務執行中必須將所有的資源鎖定, 對於長事務來說,整個事務期間對數據的獨佔,將嚴重影響系統併發性能。 因此,在高併發場景中,對ACID的部分特性進行放鬆從而提高性能,這便產生了BASE柔性事務。柔性事務的理念則是通過業務邏輯將互斥鎖操作從資源層面上移至業務層面。通過放寬對強一致性要求,來換取系統吞吐量的提升。另外提供自動的異常恢復機制,可以在發生異常後也能確保事務的最終一致。

基於XA的分佈式事務如果要嚴格保證ACID,實際需要事務隔離級別爲SERLALIZABLE。

由上可見柔性事務需要應用層進行參與,因此這類分佈式事務框架一個首要的功能就是怎麼最大程度降低業務改造成本,然後就是儘可能提高性能(響應時間、吞吐),最好是保證隔離性。
一個好的分佈式事務框架應用儘可能滿足以下特性:
1. 業務改造成本低
2. 性能損耗低
3. 隔離性保證完整
但如同CAP,這三個特性是相互制衡的,往往只能滿足其中兩個,我們可以畫一個三角約束:


基於業務補償的Saga滿足1.2;TCC滿足2.3;Seata滿足1.3。

當然如果我們要自己設計一個分佈式事務框架,還需要考慮很多其它特性,在明確目標場景偏好後進行權衡取捨,這些特性包括但不限於以下:

  • 業務侵入性(基於註解、XML,補償邏輯)
  • 隔離性(寫隔離/讀隔離/讀未提交,業務隔離/技術隔離)
  • TM/TC部署形態(單獨部署、與應用部署一起)
  • 錯誤恢復(自動恢復、手動恢復)
  • 性能(回滾的概率、付出的代價,響應時間、吞吐)
  • 高可用(註冊中心、數據庫)
  • 持久化(數據庫、文件、多副本一致算法)
  • 同步/異步(2PC執行方式)
  • 日誌清理(自動、手動)
    ...

結語

分佈式事務一直是業界難題,難在於CAP定理,在於分佈式系統8大錯誤假設,在於FLP不可能原理,在於我們習慣於單機事務ACID做對比。無論是數據庫領域XA、Google percolator或Calvin模型,還是微服務下Saga、TCC、可靠消息等方案,都沒有完美解決分佈式事務問題,它們不過是各自在性能、一致性、可用性等方面做取捨,尋求某些場景偏好下的權衡。

其實由於網絡的不確定性,分佈式下很多問題都是難題,最好的方案是避免分佈式事務:)

最後回到主題,Seata解決了分佈式事務難題了嗎?看你最在意哪方面了?如果你希望業務儘量少感知,DB操作簡單,那它會給你帶來驚喜;但如果你更看重響應時間,DB寫操作較多,調用鏈條較長,那它可能會讓失望。最後希望Seata開源項目越做越好!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章