數(shù)據(jù)一致性是構(gòu)建業(yè)務(wù)系統(tǒng)需要考慮的重要問題 嘱朽, 以往我們是依靠數(shù)據(jù)庫來保證數(shù)據(jù)的一致性。但是在微服務(wù)架構(gòu)以及分布式環(huán)境下實現(xiàn)數(shù)據(jù)一致性是一個很有挑戰(zhàn)的的問題读处。最近在研究分布式事物,分布式的解決方案有很多解決方案,也讓我在研究的同時也引發(fā)了很多思考猎唁。今天我想講的是分布式事物解決方案是和saga有關(guān)。
原文地址:微服務(wù)場景下的數(shù)據(jù)一致性解決方案
incubator-servicecomb-saga地址:incubator-servicecomb-saga
servicecomb-saga-csharp(servicecomb-saga netcore sdk)地址:servicecomb-saga-csharp
根據(jù)原文做一些解釋性的地方 方便更加理解
單體應(yīng)用的數(shù)據(jù)一致性
我就給大家講一個國外經(jīng)常用到的例子吧顷蟆,就是假如有一家大型的企業(yè)诫隅,下屬有航空公司、租車公司帐偎、和連鎖酒店逐纬。這個大公司為客戶提供一站式的旅游行程規(guī)劃服務(wù),這樣客戶只需要提供出行目的地削樊, 這個大公司能幫助客戶預(yù)訂機票豁生、租車、以及預(yù)訂酒店漫贞。從業(yè)務(wù)的角度甸箱,我們必須保證上述三個服務(wù)的預(yù)訂都完成才能滿足一個成功的旅游行程,否則不能成行迅脐。
我們的單體應(yīng)用要滿足這個需求非常簡單芍殖,只需將這個三個服務(wù)請求放到同一個數(shù)據(jù)庫事務(wù)中,數(shù)據(jù)庫會幫我們保證全部成功或者全部回滾谴蔑。
這三個服務(wù)上線公司滿意豌骏,客戶也很滿意
微服務(wù)場景下的數(shù)據(jù)一致性
隨之時間的推移,這個大企業(yè)的行程規(guī)劃服務(wù)非常成功树碱,用戶量劇增上百倍肯适。企業(yè)的下屬航空公司、租車公司成榜、和連鎖酒店也相繼推出了更多服務(wù)以滿足客戶需求框舔, 我們的應(yīng)用和開發(fā)團隊也因此日漸龐大。如今我們的單體應(yīng)用已變得如此復(fù)雜赎婚,以至于沒人了解整個應(yīng)用是怎么運作的刘绣。更糟的是新功能的上線現(xiàn)在需要所有研發(fā)團隊合作, 日夜奮戰(zhàn)數(shù)周才能完成挣输∥撤铮看著市場占有率每況愈下,公司高層對研發(fā)部門越來越不滿意撩嚼。
經(jīng)過數(shù)輪討論停士,領(lǐng)導(dǎo)最終決定將龐大的單體應(yīng)用一分為四:機票預(yù)訂服務(wù)挖帘、租車服務(wù)、酒店預(yù)訂服務(wù)恋技、和支付服務(wù)拇舀。服務(wù)各自使用自己的數(shù)據(jù)庫,并通過HTTP協(xié)議通信蜻底。 負責(zé)各服務(wù)的團隊根據(jù)市場需求按照自己的開發(fā)節(jié)奏發(fā)版上線骄崩。如今我們面臨新的挑戰(zhàn):如何保證最初三個服務(wù)的預(yù)訂都完成才能滿足一個成功的旅游行程, 否則不能成行的業(yè)務(wù)規(guī)則薄辅?現(xiàn)在服務(wù)有各自的邊界要拂,而且數(shù)據(jù)庫選型也不盡相同,通過數(shù)據(jù)庫保證數(shù)據(jù)一致性的方案已不可行站楚。
Sagas
經(jīng)過一段時間的查找脱惰,我發(fā)現(xiàn)了一篇論文,1987年Hector & Kenneth 發(fā)表論文 Sagas論文地址
Saga是一個長活事務(wù)(Long Live Transaction (LLT))源请,可被分解成可以交錯運行的子事務(wù)集合枪芒。其中每個子事務(wù)都是一個保持數(shù)據(jù)庫一致性的真實事務(wù)(LLT = T1 + T2 + T3 + ... + Tn)。每個本地事務(wù)Tx 有對應(yīng)的補償 Cx谁尸。
在大企業(yè)的業(yè)務(wù)場景下舅踪,一個行程規(guī)劃的事務(wù)就是一個Saga,其中包含四個子事務(wù):機票預(yù)訂良蛮、租車抽碌、酒店預(yù)訂、和支付决瞳。
根據(jù)上面提到的公式
當每個saga子事務(wù) T1, T2, …, Tn 都有對應(yīng)的補償定義 C1, C2, …, Cn-1, 那么saga系統(tǒng)可以保證 [1]子事務(wù)序列 T1, T2, …, Tn得以完成 (最佳情況)或者序列
T1, T2, …, Tj, Cj, …,
C2, C1, 0 < j < n,
得以完成
換句話說货徙,通過上述定義的事務(wù)/補償,saga保證滿足以下業(yè)務(wù)規(guī)則:
所有的預(yù)訂都被執(zhí)行成功皮胡,如果任何一個失敗痴颊,都會被取消
如果最后一步付款失敗,所有預(yù)訂也將被取消屡贺,這些取消就是所謂的補償蠢棱。
Saga的恢復(fù)方式
原論文中描述了兩種類型的Saga恢復(fù)方式:
向后恢復(fù) 補償所有已完成的事務(wù),如果任一子事務(wù)失敗甩栈。向前恢復(fù) 重試失敗的事務(wù)泻仙,假設(shè)每個子事務(wù)最終都會成功
顯然,向前恢復(fù)沒有必要提供補償事務(wù)量没,如果你的業(yè)務(wù)中玉转,子事務(wù)(最終)總會成功,或補償事務(wù)難以定義或不可能殴蹄,向前恢復(fù)更符合你的需求究抓。
理論上補償事務(wù)永不失敗猾担,然而,在分布式世界中漩蟆,我們來想想極端的情況垒探,無非就是往三種可能去考慮,成功怠李,失敗,超時(有可能成功蛤克,也有可能失敗)捺癞。那么服務(wù)器可能會宕機,網(wǎng)絡(luò)可能會失敗构挤,甚至數(shù)據(jù)中心也可能會停電髓介。在這種情況下我們能做些什么? 最后的手段是提供回退措施筋现,比如人工干預(yù)唐础。
補充說明:ACID與SAGA
- 原子性(Atomicity):Saga只提供ACD保證,原子性(通過Saga協(xié)調(diào)器實現(xiàn))
- 一致性(Consistency):本地事務(wù) + Saga log
- 隔離性(Isolation):Saga不保證
- 持久性(Durability):Saga log 提供
有很多朋友會說怎么不提供隔離性啊矾飞?
例子地址:地址
- 兩個Saga事務(wù)同時操作一個資源會出現(xiàn)數(shù)據(jù)語義不一致的的情況
- 兩個Saga事務(wù)同時操作一個訂單 一膨,彼此操作會覆蓋對方(更新丟失)
- 兩個Saga事務(wù)同時訪問扣款賬號槐沼,無法看到退款 (臟讀取問題)
- 在一個Saga事務(wù)內(nèi)优构,數(shù)據(jù)被其他事務(wù)修改前后的讀取值不一致(模糊讀取問題)
面對以上問題我們應(yīng)該如何應(yīng)對隔離性問題呢?
下面給出對應(yīng)的解決方案
- 隔離的本質(zhì)是控制并發(fā)镇防,防止并發(fā)事務(wù)操作相同資源而引起結(jié)果錯亂
- 在應(yīng)用層面加入邏輯鎖的邏輯申眼。
- 業(yè)務(wù)層面采用預(yù)先凍結(jié)資金的方式隔離此部分資金瞒津。
- 業(yè)務(wù)操作過程中通過及時讀取當前狀態(tài)的方式獲取更新。
使用Saga的條件
Saga看起來很有希望滿足我們的需求括尸。所有長活事務(wù)都可以這樣做嗎巷蚪?這里有一些限制:
Saga只允許兩個層次的嵌套,頂級的Saga和簡單子事務(wù) [1]
在外層濒翻,全原子性不能得到滿足屁柏。也就是說,sagas可能會看到其他sagas的部分結(jié)果 [1]
每個子事務(wù)應(yīng)該是獨立的原子行為 [2]
在我們的業(yè)務(wù)場景下肴焊,航班預(yù)訂前联、租車、酒店預(yù)訂和付款是自然獨立的行為娶眷,而且每個事務(wù)都可以用對應(yīng)服務(wù)的數(shù)據(jù)庫保證原子操作似嗤。
我們在行程規(guī)劃事務(wù)層面也不需要原子性。一個用戶可以預(yù)訂最后一張機票届宠,而后由于信用卡余額不足而被取消烁落。同時另一個用戶可能開始會看到已無余票乘粒, 接著由于前者預(yù)訂被取消,最后一張機票被釋放伤塌,而搶到最后一個座位并完成行程規(guī)劃灯萍。
補償也有需考慮的事項:
補償事務(wù)從語義角度撤消了事務(wù)Ti的行為,但未必能將數(shù)據(jù)庫返回到執(zhí)行Ti時的狀態(tài)每聪。(例如旦棉,如果事務(wù)觸發(fā)導(dǎo)彈發(fā)射, 則可能無法撤消此操作)
但這對我們的業(yè)務(wù)來說不是問題药薯。其實難以撤消的行為也有可能被補償绑洛。例如,發(fā)送電郵的事務(wù)可以通過發(fā)送解釋問題的另一封電郵來補償童本。
現(xiàn)在我們有了通過Saga來解決數(shù)據(jù)一致性問題的方案真屯。它允許我們成功地執(zhí)行所有事務(wù),或在任何事務(wù)失敗的情況下穷娱,補償已成功的事務(wù)绑蔫。 雖然Saga不提供ACID保證,但仍適用于許多數(shù)據(jù)最終一致性的場景泵额。那我們?nèi)绾卧O(shè)計一個Saga系統(tǒng)配深?
Saga Log
Saga保證所有的子事務(wù)都得以完成或補償,但Saga系統(tǒng)本身也可能會崩潰梯刚。Saga崩潰時可能處于以下幾個狀態(tài):
- Saga收到事務(wù)請求凉馆,但尚未開始。因子事務(wù)對應(yīng)的微服務(wù)狀態(tài)未被Saga修改亡资,我們什么也不需要做澜共。
- 一些子事務(wù)已經(jīng)完成。重啟后锥腻,Saga必須接著上次完成的事務(wù)恢復(fù)嗦董。
- 子事務(wù)已開始,但尚未完成瘦黑。由于遠程服務(wù)可能已完成事務(wù)京革,也可能事務(wù)失敗,甚至服務(wù)請求超時幸斥,saga只能重新發(fā)起之前未確認完成的子事務(wù)匹摇。這意味著子事務(wù)必須冪等。
- 子事務(wù)失敗甲葬,其補償事務(wù)尚未開始廊勃。Saga必須在重啟后執(zhí)行對應(yīng)補償事務(wù)。
補償事務(wù)已開始但尚未完成经窖。解決方案與上一個相同坡垫。這意味著補償事務(wù)也必須是冪等的梭灿。
所有子事務(wù)或補償事務(wù)均已完成,與第一種情況相同冰悠。
為了恢復(fù)到上述狀態(tài)堡妒,我們必須追蹤子事務(wù)及補償事務(wù)的每一步。我們決定通過事件的方式達到以上要求溉卓,并將以下事件保存在名為saga log的持久存儲中:
- Saga started event 保存整個saga請求皮迟,其中包括多個事務(wù)/補償請求
- Transaction started event 保存對應(yīng)事務(wù)請求
- Transaction ended event 保存對應(yīng)事務(wù)請求及其回復(fù)
- Transaction aborted event 保存對應(yīng)事務(wù)請求和失敗的原因
- Transaction compensated event 保存對應(yīng)補償請求及其回復(fù)
- Saga ended event 標志著saga事務(wù)請求的結(jié)束,不需要保存任何內(nèi)容
通過將這些事件持久化在saga log中的诵,我們可以將saga恢復(fù)到上述任何狀態(tài)万栅。
由于Saga只需要做事件的持久化,而事件內(nèi)容以JSON的形式存儲西疤,Saga log的實現(xiàn)非常靈活,數(shù)據(jù)庫(SQL或NoSQL)休溶,持久消息隊列代赁,甚至普通文件可以用作事件存儲, 當然有些能更快得幫saga恢復(fù)狀態(tài)兽掰。
Saga請求的數(shù)據(jù)結(jié)構(gòu)
在我們的業(yè)務(wù)場景下芭碍,航班預(yù)訂、租車孽尽、和酒店預(yù)訂沒有依賴關(guān)系窖壕,可以并行處理,但對于我們的客戶來說杉女,只在所有預(yù)訂成功后一次付費更加友好瞻讽。 那么這四個服務(wù)的事務(wù)關(guān)系可以用下圖表示:
將行程規(guī)劃請求的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)為有向非循環(huán)圖恰好合適。 圖的根是saga啟動任務(wù)熏挎,葉是saga結(jié)束任務(wù)速勇。
Parallel Saga
如上所述,航班預(yù)訂坎拐,租車和酒店預(yù)訂可以并行處理烦磁。但是這樣做會造成另一個問題:如果航班預(yù)訂失敗,而租車正在處理怎么辦哼勇?我們不能一直等待租車服務(wù)回應(yīng)都伪, 因為不知道需要等多久。
最好的辦法是再次發(fā)送租車請求积担,獲得回應(yīng)陨晶,以便我們能夠繼續(xù)補償操作。但如果租車服務(wù)永不回應(yīng)磅轻,我們可能需要采取回退措施珍逸,比如手動干預(yù)逐虚。
超時的預(yù)訂請求可能最后仍被租車服務(wù)收到,這時服務(wù)已經(jīng)處理了相同的預(yù)訂和取消請求谆膳。
因此叭爱,服務(wù)的實現(xiàn)必須保證補償請求執(zhí)行以后,再次收到的對應(yīng)事務(wù)請求無效漱病。 Caitie McCaffrey在她的演講Distributed Sagas: A Protocol for Coordinating Microservices中把這個稱為可交換的補償請求 (commutative compensating request)买雾。
分布式saga架構(gòu)
分布式的Saga借鑒了zipkin的思想,Omega就是類似探針的形式杨帽,上報saga事件漓穿,然后Alpha是屬于Saga的ProcessManager.也就是協(xié)調(diào)器的東西。
- alpha充當協(xié)調(diào)者的角色注盈,主要負責(zé)對事務(wù)的事件進行持久化存儲以及協(xié)調(diào)子事務(wù)的狀態(tài)晃危,使其得以最終與全局事務(wù)的狀態(tài)保持一致。
- omega是微服務(wù)中內(nèi)嵌的一個agent老客,負責(zé)對網(wǎng)絡(luò)請求進行攔截并向alpha上報事務(wù)事件僚饭,并在異常情況下根據(jù)alpha下發(fā)的指令執(zhí)行相應(yīng)的補償操作。
接下來我們看下Omega的內(nèi)部實現(xiàn)
omega是微服務(wù)中內(nèi)嵌的一個agent胧砰。當服務(wù)收到請求時鳍鸵,omega會將其攔截并從中提取請求信息中的全局事務(wù)id作為其自身的全局事務(wù)id(即Saga事件id),并提取本地事務(wù)id作為其父事務(wù)id尉间。在預(yù)處理階段偿乖,alpha會記錄事務(wù)開始的事件;在后處理階段哲嘲,alpha會記錄事務(wù)結(jié)束的事件贪薪。因此,每個成功的子事務(wù)都有一一對應(yīng)的開始及結(jié)束事件撤蚊。
我們再看下 他們是如何通信的
服務(wù)間通信的流程與Zipkin的類似古掏。在服務(wù)生產(chǎn)方,omega會攔截請求中事務(wù)相關(guān)的id來提取事務(wù)的上下文侦啸。在服務(wù)消費方槽唾,omega會在請求中注入事務(wù)相關(guān)的id來傳遞事務(wù)的上下文。通過服務(wù)提供方和服務(wù)消費方的這種協(xié)作處理光涂,子事務(wù)能連接起來形成一個完整的全局事務(wù)庞萍。
借助zipkin的思想就可以讓整一個事務(wù)組形成一個鏈式結(jié)構(gòu)。
Saga 具體處理流程
Saga處理場景是要求相關(guān)的子事務(wù)提供事務(wù)處理函數(shù)同時也提供補償函數(shù)忘闻。Saga協(xié)調(diào)器alpha會根據(jù)事務(wù)的執(zhí)行情況向omega發(fā)送相關(guān)的指令钝计,確定是否向前重試或者向后恢復(fù)。
成功場景
成功場景下,每個事務(wù)都會有開始和有對應(yīng)的結(jié)束事件私恬。
異常場景
異常場景下债沮,omega會向alpha上報中斷事件,然后alpha會向該全局事務(wù)的其它已完成的子事務(wù)發(fā)送補償指令本鸣,確保最終所有的子事務(wù)要么都成功疫衩,要么都回滾。
超時場景 (需要調(diào)整)
超時場景下荣德,已超時的事件會被alpha的定期掃描器檢測出來闷煤,與此同時,該超時事務(wù)對應(yīng)的全局事務(wù)也會被中斷涮瞻。
以上都是介紹完incubator-servicecomb-saga 總體架構(gòu)鲤拿。我覺得它的idea很nice,所以我和水哥,還有老杜做了一個很有趣的事情署咽。什么事情呢近顷?就是實現(xiàn)了Omega這個客戶端,github地址在這里:servicecomb-saga-csharp,目前實現(xiàn)上面的三種場景宁否。
下篇結(jié)合實際的sample和大家講解下netcore下的實現(xiàn)幕庐,這篇文章讓大家整體的了解什么是saga。