,有趣實用的分布式架構頻道。
本文根據 SOFAChannel#10 直播分享整理,主題:分布式事務 Seata 長事務解決方案 Saga 模式詳解。
回顧視訊以及 PPT 檢視位址見文末。歡迎加入直播互動釘釘群:23372465,不錯過每場直播。
大家好,我是陳龍,花名: 屹遠(_long187@github_),是螞蟻金服分布式事務核心研發,也是 Seata Committer。今天分享的主題是《分布式事務 Seata 長事務解決方案 Saga 模式詳解》,将從金融分布式應用開發的痛點出發,結合 Saga 分布式事務的理論和使用場景,講解如何使用 Seata Saga 狀态機來進行服務編排和分布式事務處理,建構更有彈性的金融應用,同時也會從架構、原理、設計、高可用、最佳實踐等方面剖析 Saga 狀态機的實作。
Seata:
https://github.com/seata/seata金融分布式應用開發的痛點
分布式系統有一個比較明顯的問題就是,一個業務流程需要組合一組服務。這樣的事情在微服務下就更為明顯了,因為這需要業務上的一緻性的保證。也就是說,如果一個步驟失敗了,那麼要麼復原到以前的服務調用,要麼不斷重試保證所有的步驟都成功。---《左耳聽風-彈力設計之“補償事務”》
而在金融領域微服務架構下的業務流程往往會更複雜,流程很長,比如一個網際網路微貸業務流程調十幾個服務很正常,再加上異常處理的流程那就更複雜了,做過金融業務開發的同學會很有體感。
是以在金融分布式應用開發過程中我們面臨一些痛點:
- 業務一緻性難以保障
我們接觸到的大多數業務(比如在管道層、産品層、內建層的系統),為了保障業務最終一緻性,往往會采用“補償”的方式來做,如果沒有一個協調器來支援,開發難度是比較大的,每一步都要在 catch 裡去處理前面所有的“復原”操作,這将會形成“箭頭形”的代碼,可讀性及維護性差。或者重試異常的操作,如果重試不成功可能要轉異步重試,甚至最後轉人工處理。這些都給開發人員帶來極大的負擔,開發效率低,且容易出錯。
- 業務狀态難以管理
業務實體很多、實體的狀态也很多,往往做完一個業務活動後就将實體的狀态更新到了資料庫裡,沒有一個狀态機來管理整個狀态的變遷過程,不直覺,容易出錯,造成業務進入一個不正确的狀态。
- 業務監控運維難
業務的執行情況監控一般通過列印日志,再基于日志監控平台檢視,大多數情況是沒有問題的,但是如果業務出錯,這些監控缺乏當時的業務上下文,對排查問題不友好,往往需要再去資料庫裡查。同時日志的列印也依賴于開發,容易遺漏。
- 缺乏統一的差錯守護能力
對于補償事務往往需要有“差錯守護觸發補償”、“人工觸發補償”操作,沒有統一的差錯守護和處理規範,這些都要開發者逐個開發,負擔沉重。
理論基礎
對于事務我們都知道 ACID,也很熟悉 CAP 理論最多隻能滿足其中兩個,是以,為了提高性能,出現了 ACID 的一個變種 BASE。ACID 強調的是一緻性(CAP 中的 C),而 BASE 強調的是可用性(CAP 中的 A)。在很多情況下,我們是無法做到強一緻性的 ACID 的。特别是我們需要跨多個系統的時候,而且這些系統還不是由一個公司所提供的。BASE 的系統傾向于設計出更加有彈力的系統,在短時間内,就算是有資料不同步的風險,我們也應該允許新的交易可以發生,而後面我們在業務上将可能出現問題的事務通過補償的方式處理掉,以保證最終的一緻性。
是以我們在實際開發中會進行取舍,對于更多的金融核心以上的業務系統可以采用補償事務,補償事務處理方面在30多年前就提出了 Saga 理論,随着微服務的發展,近些年才逐漸受到大家的關注。目前業界比較也公認 Saga 是作為長事務的解決方案。
https://github.com/aphyr/dist-sagas/blob/master/sagas.pdf http://microservices.io/patterns/data/saga.html
Saga 模式用一種非常純樸的方式來處理一緻性:補償。上圖左側是正常的事務流程,當執行到 T3 時發生了錯誤,則開始執行右邊的事務補償流程,返向執行T3、T2、T1 的補償服務,其中 C3 是 T3 的補償服務、C2 是 T2 的補償服務、C1 是 T1 的補償服務,将T3、T2、T1 已經修改的資料補償掉。
使用場景
一些場景下,我們對資料有強一緻性的需求時,會采用在業務層上需要使用“兩階段送出”這樣的分布式事務方案。而在另外一些場景下,我們并不需要這麼強的一緻性,那就隻需要保證最終一緻性就可以了。
例如螞蟻金服目前在金融核心系統使用的就是 TCC 模式,金融核心系統的特點是一緻性要求高(業務上的隔離性)、短流程、并發高。
而在很多金融核心以上的業務(比如在管道層、産品層、內建層的系統),這些系統的特點是最終一緻即可、流程多、流程長、還可能要調用其它公司的服務(如金融網絡)。這是如果每個服務都開發 Try、Confirm、Cancel 三個方法成本高。如果事務中有其它公司的服務,也無法要求其它公司的服務也遵循 TCC 這種開發模式。同時流程長,事務邊界太長,加鎖時間長,會影響并發性能。
是以 Saga 模式的适用場景是:
- 業務流程長、業務流程多;
- 參與者包含其它公司或遺留系統服務,無法提供 TCC 模式要求的三個接口;
- 典型業務系統:如金融網路(與外部機構對接)、網際網路微貸、管道整合、分布式架構下服務內建等業務系統;
- 銀行業金融機構使用廣泛;
其優勢:
- 一階段送出本地事務,無鎖,高性能;
- 參與者可異步執行,高吞吐;
- 補償服務易于實作,因為一個更新操作的反向操作是比較容易了解的;
其缺點:
- 不保證隔離性,後面我們會講到如何應對隔離性的缺失。
基于狀态機引擎的 Saga 實作
基于狀态機引擎的 Saga 實作的基本原理:
- 通過狀态圖來定義服務調用的流程并生成 json 定義檔案;
- 狀态圖中一個節點可以是調用一個服務,節點可以配置它的補償節點(虛線關聯的節點);
- 狀态圖 json 由狀态機引擎驅動執行,當出現異常時狀态引擎反向執行已成功節點對應的補償節點将事務復原;
- 異常發生時是否進行補償也可由使用者自定義決定;
- 可以實作服務編排需求,路由、異步、重試、參數轉換、參數映射、服務執行狀态判斷、異常捕獲等功能 ;
Seata 目前的 Saga 模式采用了狀态機+DSL 方案來實作,原因有以下幾個:
- 狀态機+DSL 方案在實際生産中應用更廣泛;
- 可以使用 Actor 模型或 SEDA 架構等異步處理引擎來執行,提高整體吞吐量;
- 通常在核心系統以上層的業務系統會伴随有“服務編排”的需求,而服務編排又有事務最終一緻性要求,兩者很難分割開,狀态機+DSL 方案可以同時滿足這兩個需求;
- 由于 Saga 模式在理論上是不保證隔離性的,在極端情況下可能由于髒寫無法完成復原操作,比如舉一個極端的例子,分布式事務内先給使用者 A 充值,然後給使用者 B 扣減餘額,如果在給A使用者充值成功,在事務送出以前,A 使用者把線消費掉了,如果事務發生復原,這時則沒有辦法進行補償了,有些業務場景可以允許讓業務最終成功,在復原不了的情況下可以繼續重試完成後面的流程,狀态機+DSL的方案可以實作“向前”恢複上下文繼續執行的能力, 讓業務最終執行成功,達到最終一緻性的目的。
狀态定義語言(Seata State Language)
- 通過狀态圖來定義服務調用的流程并生成 JSON 狀态語言定義檔案;
- 狀态圖中一個節點可以是調用一個服務,節點可以配置它的補償節點;
- 狀态類型支援單項選擇、并發、異步、子狀态機、參數轉換、參數映射、服務執行狀态判斷、異常捕獲等;
- JSON 定義相對于 XML(如 BPMN、BPEL 等)更加簡潔易讀、學習成本低;
狀态機 JSON 示例:
"狀态機" 屬性說明:
- Name:表示狀态機的名稱,必須唯一;
- Comment:狀态機的描述;
- Version:狀态機定義版本;
- StartState:啟動時運作的第一個"狀态";
- States:狀态清單,是一個 map 結構,key 是"狀态"的名稱,在狀态機内必須唯一;
"狀态" 屬性說明:
- Type:"狀态" 的類型,比如有:
- ServiceTask:執行調用服務任務;
- Choice:單條件選擇路由;
- CompensationTrigger:觸發補償流程;
- Succeed:狀态機正常結束;
- Fail:狀态機異常結束;
- SubStateMachine:調用子狀态機;
- ServiceName:服務名稱,通常是服務的beanId;
- ServiceMethod:服務方法名稱;
- CompensateState:該"狀态"的補償"狀态";
- Input:調用服務的輸入參數清單;
- Output:将服務傳回的參數指派到狀态機上下文中;
- Status:服務執行狀态映射,架構定義了三個狀态,SU 成功、FA 失敗、UN 未知,我們需要把服務執行的狀态映射成這三個狀态,幫助架構判斷整個事務的一緻性;
- Catch:捕獲到異常後的路由;
- Retry:服務調用重試政策;
- Nex:服務執行完成後下一個執行的"狀态"。
更多詳細的狀态語言解釋請看
《Seata Saga 官網文檔》。
狀态機設計器
Seata Saga 提供了一個可視化的狀态機設計器友善使用者使用,代碼和運作指南請參考:
https://github.com/seata/seata/tree/develop/saga/seata-saga-statemachine-designer狀态機設計器截圖:
狀态機設計器示範位址:
http://seata.io/saga_designer/index.html狀态機引擎原理
- 圖中的狀态圖是先執行 stateA,再執行 stataB,然後執行 stateC;
- "狀态"的執行是基于事件驅動的模型,stataA 執行完成後,會産生路由消息放入 EventQueue,事件消費端從 EventQueue 取出消息,執行 stateB;
- 在整個狀态機啟動時會調用 Seata Server 開啟分布式事務,并生産 xid, 然後記錄"狀态機執行個體"啟動事件到本地資料庫;
- 當執行到一個"狀态"時會調用 Seata Server 注冊分支事務,并生産 branchId, 然後記錄"狀态執行個體"開始執行事件到本地資料庫;
- 當一個"狀态"執行完成後會記錄"狀态執行個體"執行結束事件到本地資料庫, 然後調用 Seata Server 上報分支事務的狀态;
- 當整個狀态機執行完成,會記錄"狀态機執行個體"執行完成事件到本地資料庫, 然後調用 Seata Server 送出或復原分布式事務。
狀态機引擎設計
狀态機引擎的設計主要分成三層, 上層依賴下層,從下往上分别是:
- Eventing 層:
- 實作事件驅動架構, 可以壓入事件, 并由消費端消費事件, 本層不關心事件是什麼消費端執行什麼,由上層實作;
- ProcessController 層:
- 由于上層的 Eventing 驅動一個“空”流程執行的執行,"state"的行為和路由都未實作,由上層實作;
基于以上兩層理論上可以自定義擴充任何"流程"引擎。這兩層的設計是參考了内部金融網絡平台的設計。
- StateMachineEngine 層:
- 實作狀态機引擎每種 state 的行為和路由邏輯;
- 提供 API、狀态機語言倉庫;
狀态機引擎高可用
狀态機引擎是無狀态的,它是内嵌在應用中。
當應用正常運作時:
- 狀态機引擎會上報狀态到Seata Server;
- 狀态機執行日志存儲在業務的資料庫中;
當一台應用執行個體當機時:
- Seata Server 會感覺到,并發送事務恢複請求到還存活的應用執行個體;
- 狀态機引擎收到事務恢複請求後,從資料庫裡裝載日志,并恢複狀态機上下文繼續執行;
Saga 模式下服務設計的實踐經驗
下面是實踐中總結的在 Saga 模式下微服務設計的一些經驗。
Seata Saga 模式對微服務的接口參數沒有任何要求,這使得 Saga 模式可用于內建遺留系統或外部機構的服務。
允許空補償
- 空補償:原服務未執行,補償服務執行了;
- 出現原因:
- 原服務逾時(丢包);
- Saga 事務觸發復原;
- 未收到原服務請求,先收到補償請求;
是以服務設計時需要允許空補償,即沒有找到要補償的業務主鍵時傳回補償成功并将原業務主鍵記錄下來。
防懸挂控制
- 懸挂:補償服務比原服務先執行;
-
- 原服務逾時(擁堵);
- Saga 事務復原,觸發復原;
- 擁堵的原服務到達;
是以要檢查目前業務主鍵是否已經在空補償記錄下來的業務主鍵中存在,如果存在則要拒絕服務的執行。
幂等控制
- 原服務與補償服務都需要保證幂等性, 由于網絡可能逾時,可以設定重試政策,重試發生時要通過幂等控制避免業務資料重複更新。
對于一些老系統的服務可能沒有實作幂等,也有“繞過”方案:可以不設定重試政策,讓狀态機不要重試服務調用,然後通過“反查”或者“人工訂正”服務的執行狀态,然後再恢複狀态機繼續執行。
缺乏隔離性的應對
由于 Saga 事務不保證隔離性,在極端情況下可能由于髒寫無法完成復原操作,比如舉一個極端的例子,分布式事務内先給使用者A充值,然後給使用者B扣減餘額,如果在給A使用者充值成功,在事務送出以前,A使用者把餘額消費掉了,如果事務發生復原,這時則沒有辦法進行補償了。
這就是缺乏隔離性造成的典型的問題,實踐中一般的應對方法是:
- 業務流程設計時遵循“甯可長款,不可短款”的原則,長款意思是客戶少了錢機構多了錢,以機構信譽可以給客戶退款,反之則是短款,少的錢可能追不回來了,是以在業務流程設計上一定是先扣款;
- 有些業務場景可以允許讓業務最終成功,在復原不了的情況下可以繼續重試完成後面的流程,是以狀态機引擎除了提供“復原”能力還需要提供“向前”恢複上下文繼續執行的能力,讓業務最終執行成功,達到最終一緻性的目的;
Seata Saga 優勢
我們在實踐中發現,長流程的業務場景,往往有服務編排的需求,同時又要保證服務之間的資料一緻性。
目前開源社群也有一些 Saga 事務架構,如:Apache Camel Saga、Eventuate Tram Saga、Apache ServiceComb Saga 等等。也有一些服務編排的架構,如 uber cadence、netflix conductor、zeebe-io zeebe、ing-bank Baker、AWS Step Functions 等等。
但是它們要麼隻有 Saga 事務處理能力、要麼隻有服務編排能力,Seata Saga 是将這兩者能力非常優雅的結合在一起,為使用者提供一個簡化研發、降低異常處理難度、高性能事件驅動的産品。
基于注解攔截器的 Saga 實作(規劃中)
還有一種 Saga 的實作是基于注解+攔截器的實作,Seata 目前沒有實作,可以看上面的僞代碼來了解一下,one 方法上定義了 @SagaCompensable 的注解,用于定義 one 方法的補償方法是 compensateOne 方法。然後在業務流程代碼 processA 方法上定義 @SagaTransactional 注解,啟動 Saga 分布式事務,通過攔截器攔截每個正向方法當出現異常的時候觸發復原操作,調用正向方法的補償方法。
兩種 Saga 實作優劣對比
狀态機引擎的最大優勢是可以通過事件驅動的方法異步執行提高系統吞吐,可以實作服務編排需求,在 Saga 模式缺乏隔離性的情況下,可以多一種“向前重試”的事情恢複政策,進而提高系統容錯能力,缺點是業務入侵高。
注解加攔截器的的最大優勢是,開發簡單、學習成本低,缺點是無法做到“事後向前重試”,因為無法恢複線程上下文(事後是指:當出現異常後重試多次沒有成功,然後由守護任務,如 Seata Server 繼續發起重試),在缺乏隔離性的情況下,缺少一種事務處理手段,會增加一定的運維成本。
總結
很多時候我們不需要強調強一性,我們基于 BASE 和 Saga 理論去設計更有彈性的系統,在分布式架構下獲得更好的性能和容錯能力。分布式架構沒有銀彈,隻有适合特定場景的方案,事實上 Seata Saga 是一個具備“服務編排”和“Saga 分布式事務”能力的産品,總結下來它的适用場景是:
- 适用于微服務架構下的“長事務”處理;
- 适用于微服務架構下的“服務編排”需求;
- 适用于金融核心系統以上的有大量組合服務的業務系統(比如在管道層、産品層、內建層的系統);
- 适用于業務流程中需要內建遺留系統或外部機構提供的服務的場景(這些服務不可變不能對其提出改造要求);
以上就是本次分享的全部内容,如果大家對于 Seata 還想要有更多的了解,歡迎在
官網浏覽相關文章 ,或者在項目中檢視
具體代碼