微服務場景下的數據一致性解決方案 - saga

數據一致性是構建業務系統需要考慮的重要問題 , 以往我們是依靠數據庫來保證數據的一致性。但是在微服務架構以及分佈式環境下實現數據一致性是一個很有挑戰的的問題。ServiceComb作爲開源的微服務框架致力解決微服務開發過程中的問題。我們最近發起的ServiceComb-Saga項目來解決分佈式環境下的數據最終一致性問題。本文將向大家介紹爲什麼數據一致性如此重要?Saga又是什麼?

單體應用的數據一致性

想象一下如果我們經營着一家大型企業,下屬有航空公司、租車公司、和連鎖酒店。我們爲客戶提供一站式的旅遊行程規劃服務,這樣客戶只需要提供出行目的地, 我們幫助客戶預訂機票、租車、以及預訂酒店。從業務的角度,我們必須保證上述三個服務的預訂都完成才能滿足一個成功的旅遊行程,否則不能成行。

我們的單體應用要滿足這個需求非常簡單,只需將這個三個服務請求放到同一個數據庫事務中,數據庫會幫我們保證全部成功或者全部回滾。

x

當這個功能上線以後,公司非常滿意,客戶也非常高興。

微服務場景下的數據一致性

這幾年中,我們的行程規劃服務非常成功,企業蒸蒸日上,用戶量也翻了數十倍。企業的下屬航空公司、租車公司、和連鎖酒店也相繼推出了更多服務以滿足客戶需求, 我們的應用和開發團隊也因此日漸龐大。如今我們的單體應用已變得如此複雜,以至於沒人瞭解整個應用是怎麼運作的。更糟的是新功能的上線現在需要所有研發團隊合作, 日夜奮戰數週才能完成。看着市場佔有率每況愈下,公司高層對研發部門越來越不滿意。

經過數輪討論,我們最終決定將龐大的單體應用一分爲四:機票預訂服務、租車服務、酒店預訂服務、和支付服務。服務各自使用自己的數據庫,並通過HTTP協議通信。 負責各服務的團隊根據市場需求按照自己的開發節奏發版上線。如今我們面臨新的挑戰:如何保證最初三個服務的預訂都完成才能滿足一個成功的旅遊行程, 否則不能成行的業務規則?現在服務有各自的邊界,而且數據庫選型也不盡相同,通過數據庫保證數據一致性的方案已不可行。

x

Sagas

幸運的是我們在互聯網找到一篇精彩的論文,文中提出的數據一致性解決方案Saga恰好滿足我們的業務要求。

Saga是一個長活事務,可被分解成可以交錯運行的子事務集合。其中每個子事務都是一個保持數據庫一致性的真實事務。

在我們的業務場景下,一個行程規劃的事務就是一個Saga,其中包含四個子事務:機票預訂、租車、酒店預訂、和支付。

x

Chris Richardson在他的文章Pattern: Saga中對Saga有所描述。 Caitie McCaffrey也在她的演講中提到如何在微軟的光暈 4遊戲中如何應用saga解決數據一致性問題。

Saga的運行原理

Saga中的事務相互關聯,應作爲(非原子)單位執行。任何未完全執行的Saga是不滿足要求的,如果發生,必須得到補償。要修正未完全執行的部分, 每個saga子交易T1應提供對應補償事務C1

我們根據上述規則定義以下事務及其相應的事務補償:

x

當每個saga子事務 T1, T2, …, Tn 都有對應的補償定義 C1, C2, …, Cn-1, 那麼saga系統可以保證 [1]

子事務序列 T1, T2, …, Tn得以完成 (最佳情況)
或者序列 T1, T2, …, Tj, Cj, …, C2, C1, 0 < j < n, 得以完成
換句話說,通過上述定義的事務/補償,saga保證滿足以下業務規則:

所有的預訂都被執行成功,如果任何一個失敗,都會被取消
如果最後一步付款失敗,所有預訂也將被取消

Saga的恢復方式

原論文中描述了兩種類型的Saga恢復方式:

向後恢復 補償所有已完成的事務,如果任一子事務失敗

向前恢復 重試失敗的事務,假設每個子事務最終都會成功

顯然,向前恢復沒有必要提供補償事務,如果你的業務中,子事務(最終)總會成功,或補償事務難以定義或不可能,向前恢復更符合你的需求。

理論上補償事務永不失敗,然而,在分佈式世界中,服務器可能會宕機,網絡可能會失敗,甚至數據中心也可能會停電。在這種情況下我們能做些什麼? 最後的手段是提供回退措施,比如人工干預。

使用Saga的條件

Saga看起來很有希望滿足我們的需求。所有長活事務都可以這樣做嗎?這裏有一些限制:

Saga只允許兩個層次的嵌套,頂級的Saga和簡單子事務 [1]
在外層,全原子性不能得到滿足。也就是說,sagas可能會看到其他sagas的部分結果 [1]
每個子事務應該是獨立的原子行爲 [2]
在我們的業務場景下,航班預訂、租車、酒店預訂和付款是自然獨立的行爲,而且每個事務都可以用對應服務的數據庫保證原子操作。
我們在行程規劃事務層面也不需要原子性。一個用戶可以預訂最後一張機票,而後由於信用卡餘額不足而被取消。同時另一個用戶可能開始會看到已無餘票, 接着由於前者預訂被取消,最後一張機票被釋放,而搶到最後一個座位並完成行程規劃。

補償也有需考慮的事項:

補償事務從語義角度撤消了事務Ti的行爲,但未必能將數據庫返回到執行Ti時的狀態。(例如,如果事務觸發導彈發射, 則可能無法撤消此操作)
但這對我們的業務來說不是問題。其實難以撤消的行爲也有可能被補償。例如,發送電郵的事務可以通過發送解釋問題的另一封電郵來補償。

現在我們有了通過Saga來解決數據一致性問題的方案。它允許我們成功地執行所有事務,或在任何事務失敗的情況下,補償已成功的事務。 雖然Saga不提供ACID保證,但仍適用於許多數據最終一致性的場景。那我們如何設計一個Saga系統?

Saga Log

Saga保證所有的子事務都得以完成或補償,但Saga系統本身也可能會崩潰。Saga崩潰時可能處於以下幾個狀態:

Saga收到事務請求,但尚未開始。因子事務對應的微服務狀態未被Saga修改,我們什麼也不需要做。
一些子事務已經完成。重啓後,Saga必須接着上次完成的事務恢復。
子事務已開始,但尚未完成。由於遠程服務可能已完成事務,也可能事務失敗,甚至服務請求超時,saga只能重新發起之前未確認完成的子事務。這意味着子事務必須冪等。
子事務失敗,其補償事務尚未開始。Saga必須在重啓後執行對應補償事務。
補償事務已開始但尚未完成。解決方案與上一個相同。這意味着補償事務也必須是冪等的。
所有子事務或補償事務均已完成,與第一種情況相同。
爲了恢復到上述狀態,我們必須追蹤子事務及補償事務的每一步。我們決定通過事件的方式達到以上要求,並將以下事件保存在名爲saga log的持久存儲中:

Saga started event 保存整個saga請求,其中包括多個事務/補償請求
Transaction started event 保存對應事務請求
Transaction ended event 保存對應事務請求及其回覆
Transaction aborted event 保存對應事務請求和失敗的原因
Transaction compensated event 保存對應補償請求及其回覆
Saga ended event 標誌着saga事務請求的結束,不需要保存任何內容
x

通過將這些事件持久化在saga log中,我們可以將saga恢復到上述任何狀態。

由於Saga只需要做事件的持久化,而事件內容以JSON的形式存儲,Saga log的實現非常靈活,數據庫(SQL或NoSQL),持久消息隊列,甚至普通文件可以用作事件存儲, 當然有些能更快得幫saga恢復狀態。

Saga請求的數據結構

在我們的業務場景下,航班預訂、租車、和酒店預訂沒有依賴關係,可以並行處理,但對於我們的客戶來說,只在所有預訂成功後一次付費更加友好。 那麼這四個服務的事務關係可以用下圖表示:

x

將行程規劃請求的數據結構實現爲有向非循環圖恰好合適。 圖的根是saga啓動任務,葉是saga結束任務。

x

Parallel Saga
如上所述,航班預訂,租車和酒店預訂可以並行處理。但是這樣做會造成另一個問題:如果航班預訂失敗,而租車正在處理怎麼辦?我們不能一直等待租車服務迴應, 因爲不知道需要等多久。

最好的辦法是再次發送租車請求,獲得迴應,以便我們能夠繼續補償操作。但如果租車服務永不迴應,我們可能需要採取回退措施,比如手動干預。

超時的預訂請求可能最後仍被租車服務收到,這時服務已經處理了相同的預訂和取消請求。

x

因此,服務的實現必須保證補償請求執行以後,再次收到的對應事務請求無效。 Caitie McCaffrey在她的演講Distributed Sagas: A Protocol for Coordinating Microservices中把這個稱爲可交換的補償請求 (commutative compensating request)。

ACID and Saga
ACID是具有以下屬性的一致性模型:

原子性(Atomicity)
一致性(Consistency)
隔離性(Isolation)
持久性(Durability)
Saga不提供ACID保證,因爲原子性和隔離性不能得到滿足。原論文描述如下:

full atomicity is not provided. That is, sagas may view the partial results of other sagas [1]

通過saga log,saga可以保證一致性和持久性。

Saga 架構

最後,我們的Saga架構如下:

x

Saga Execution Component解析請求JSON並構建請求圖
TaskRunner 用任務隊列確保請求的執行順序
TaskConsumer 處理Saga任務,將事件寫入saga log,並將請求發送到遠程服務
在上文中,我談到了ServiceComb下的Saga是怎麼設計的。 然而,業界還有其他數據一致性解決方案,如兩階段提交(2PC)和Try-Confirm / Cancel(TCC)。那saga相比之下有什麼特別?

兩階段提交 Two-Phase Commit (2PC)

兩階段提交協議是一種分佈式算法,用於協調參與分佈式原子事務的所有進程,以保證他們均完成提交或中止(回滾)事務。

2PC包含兩個階段:

投票階段 協調器向所有服務發起投票請求,服務回答yes或no。如果有任何服務回覆no以拒絕或超時,協調器則在下一階段發送中止消息。
x

決定階段 如果所有服務都回復yes,協調器則向服務發送commit消息,接着服務告知事務完成或失敗。如果任何服務提交失敗, 協調器將啓動額外的步驟以中止該事務。
x

在投票階段結束之後與決策階段結束之前,服務處於不確定狀態,因爲他們不確定交易是否繼續進行。當服務處於不確定狀態並與協調器失去連接時, 它只能選擇等待協調器的恢復,或者諮詢其他在確定狀態下的服務來得知協調器的決定。在最壞的情況下, n個處於不確定狀態的服務向其他n-1個服務諮詢將產生O(n2)個消息。

另外,2PC是一個阻塞協議。服務在投票後需要等待協調器的決定,此時服務會阻塞並鎖定資源。由於其阻塞機制和最差時間複雜度高, 2PC不能適應隨着事務涉及的服務數量增加而擴展的需要。

有關2PC實現的更多細節可參考2和3。

Try-Confirm/Cancel (TCC)

TCC也是補償型事務模式,支持兩階段的商業模型。

嘗試階段 將服務置於待處理狀態。例如,收到嘗試請求時,航班預訂服務將爲客戶預留一個座位,並在數據庫插入客戶預訂記錄,將記錄設爲預留狀態。 如果任何服務失敗或超時,協調器將在下一階段發送取消請求。
x

確認階段 將服務設爲確認狀態。確認請求將確認客戶預訂的座位,這時服務已可向客戶收取機票費用。數據庫中的客戶預訂記錄也會被更新爲確認狀態。 如果任何服務無法確認或超時,協調器將重試確認請求直到成功,或在重試了一定次數後採取回退措施,比如人工干預。
x

與saga相比,TCC的優勢在於,嘗試階段將服務轉爲待處理狀態而不是最終狀態,這使得設計相應的取消操作輕而易舉。

例如,電郵服務的嘗試請求可將郵件標記爲準備發送,並且僅在確認後發送郵件,其相應的取消請求只需將郵件標記爲已廢棄。但如果使用saga, 事務將發送電子郵件,及其相應的補償事務可能需要發送另一封電子郵件作出解釋。

TCC的缺點是其兩階段協議需要設計額外的服務待處理狀態,以及額外的接口來處理嘗試請求。另外,TCC處理事務請求所花費的時間可能是saga的兩倍, 因爲TCC需要與每個服務進行兩次通信,並且其確認階段只能在收到所有服務對嘗試請求的響應後開始。

有關TCC的更多細節可參考Transactions for the REST of Us.

事件驅動的架構

和TCC一樣,在事件驅動的架構中,長活事務涉及的每個服務都需要支持額外的待處理狀態。接收到事務請求的服務會在其數據庫中插入一條新的記錄, 將該記錄狀態設爲待處理併發送一個新的事件給事務序列中的下一個服務。

因爲在插入記錄後服務可能崩潰,我們無法確定是否新事件已發送,所以每個服務還需要額外的事件表來跟蹤當前長活事務處於哪一步。

x

一旦長活事務中的最後一個服務完成其子事務,它將通知它在事務中的前一個服務。接收到完成事件的服務將其在數據庫中的記錄狀態設爲完成。

x

如果仔細比較,事件驅動的架構就像非集中式的基於事件的TCC實現。如果去掉待處理狀態而直接把服務記錄設爲最終狀態,這個架構就像非集中式的基於事件的saga實現。 去中心化能達到服務自治,但也造成了服務之間更緊密的的耦合。假設新的業務需求在服務B和C之間的增加了新的流程D。在事件驅動架構下,服務B和C必須改動代碼以適應新的流程D。

x

Saga則正好相反,所有這些耦合都在saga系統中,當在長活事務中添加新流程時,現有服務不需要任何改動。

更多細節可參考Event-Driven Data Management for Microservices.

集中式與非集中式實現

這個Saga系列的文章討論的都是集中式的saga設計。但saga也可用非集中式的方案來實現。那麼非集中式的版本有什麼不同?

非集中式saga沒有專職的協調器。啓動下一個服務調用的服務就是當前的協調器。例如,

服務A收到要求服務A,B和C之間的數據一致性的事務請求。
A完成其子事務,並將請求傳遞給事務中的下一個服務,服務B.
B完成其子事務,並將請求傳遞給C,依此類推。
如果C處理請求失敗,B有責任啓動補償事務,並要求A回滾。
x

與集中式相比,非集中式的實現具有服務自治的優勢。但每個服務都需要包含數據一致性協議,並提供其所需的額外持久化設施。

我們更傾向於自治的業務服務,但服務還關聯很多應用的複雜性,如數據一致性,服務監控和消息傳遞, 將這些棘手問題集中處理,能將業務服務從應用的複雜性中釋放,專注於處理複雜的業務,因此我們採用了集中式的saga設計。

另外,隨着長活事務中涉及的服務數量增長,服務之間的關係變得越來越難理解,很快便會呈現下圖的死星形狀。

Summary

本文將saga與其他數據一致性解決方案進行了比較。Saga比兩階段提交更易擴展。在事務可補償的情況下, 相比TCC,saga對業務邏輯幾乎沒有改動的需要,而且性能更高。集中式的saga設計解耦了服務與數據一致性邏輯及其持久化設施, 並使排查事務中的問題更容易。

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