詳解微服務(wù)架構(gòu)中的數(shù)據(jù)一致性
在微服務(wù)中,一個(gè)邏輯上原子操作可以經(jīng)常跨越多個(gè)微服務(wù)。即使是單片系統(tǒng)也可能使用多個(gè)數(shù)據(jù)庫(kù)或消息傳遞解決方案。使用多個(gè)獨(dú)立的數(shù)據(jù)存儲(chǔ)解決方案,如果其中一個(gè)分布式流程參與者出現(xiàn)故障,我們就會(huì)面臨數(shù)據(jù)不一致的風(fēng)險(xiǎn) - 例如在未下訂單的情況下向客戶收費(fèi)或未通知客戶訂單成功。在本文中,我想分享一些我為使微服務(wù)之間的數(shù)據(jù)最終保持一致而學(xué)到的技術(shù)。
為什么實(shí)現(xiàn)這一目標(biāo)如此具有挑戰(zhàn)性?只要我們有多個(gè)存儲(chǔ)數(shù)據(jù)的地方(不在單個(gè)數(shù)據(jù)庫(kù)中),就不能自動(dòng)解決一致性問(wèn)題,工程師在設(shè)計(jì)系統(tǒng)時(shí)需要注意一致性。目前,在我看來(lái),業(yè)界還沒(méi)有一個(gè)廣為人知的解決方案,可以在多個(gè)不同的數(shù)據(jù)源中自動(dòng)更新數(shù)據(jù) - 我們可能不應(yīng)該等待很快就能獲得一個(gè)。
以自動(dòng)且無(wú)障礙的方式解決該問(wèn)題的一種嘗試是實(shí)現(xiàn)兩階段提交(2PC)模式的XA協(xié)議。但在現(xiàn)代高規(guī)模應(yīng)用中(特別是在云環(huán)境中),2PC似乎表現(xiàn)不佳。為了消除2PC的缺點(diǎn),我們必須交易ACID for BASE并根據(jù)要求以不同方式覆蓋一致性問(wèn)題。
Saga模式
在多個(gè)微服務(wù)中處理一致性問(wèn)題的最著名的方法是Saga模式。 您可以將Sagas視為多個(gè)事務(wù)的應(yīng)用程序級(jí)分布式協(xié)調(diào)。 根據(jù)用例和要求,您可以優(yōu)化自己的Saga實(shí)施。 相反,XA協(xié)議試圖涵蓋所有場(chǎng)景。 Saga模式也不是新的。 它在過(guò)去已知并用于ESB和SOA體系結(jié)構(gòu)中。 ***,它成功地轉(zhuǎn)變?yōu)槲⒎?wù)世界。 跨越多個(gè)服務(wù)的每個(gè)原子業(yè)務(wù)操作可能包含技術(shù)級(jí)別的多個(gè)事務(wù)。 Saga Pattern的關(guān)鍵思想是能夠回滾其中一個(gè)單獨(dú)的交易。 眾所周知,開(kāi)箱即用的已經(jīng)提交的單個(gè)事務(wù)無(wú)法進(jìn)行回滾。 但這是通過(guò)引入補(bǔ)償操作來(lái)實(shí)現(xiàn)的 - 通過(guò)引入“取消”操作。

除了取消之外,您還應(yīng)該考慮使您的服務(wù)具有冪等性,以便在出現(xiàn)故障時(shí)重試或重新啟動(dòng)某些操作。 應(yīng)監(jiān)控故障,并應(yīng)積極主動(dòng)地應(yīng)對(duì)故障。
對(duì)賬
如果在進(jìn)程的中間負(fù)責(zé)調(diào)用補(bǔ)償操作的系統(tǒng)崩潰或重新啟動(dòng),該怎么辦? 在這種情況下,用戶可能會(huì)收到錯(cuò)誤消息,并且應(yīng)該觸發(fā)補(bǔ)償邏輯,或者 - 當(dāng)處理異步用戶請(qǐng)求時(shí),應(yīng)該恢復(fù)執(zhí)行邏輯。

要查找崩潰的事務(wù)并恢復(fù)操作或應(yīng)用補(bǔ)償,我們需要協(xié)調(diào)來(lái)自多個(gè)服務(wù)的數(shù)據(jù)。對(duì)賬
是在金融領(lǐng)域工作的工程師所熟悉的技術(shù)。你有沒(méi)有想過(guò)銀行如何確保你的資金轉(zhuǎn)移不會(huì)丟失,或者兩個(gè)不同的銀行之間如何匯款?快速回答是對(duì)賬。

在會(huì)計(jì)中,對(duì)賬是確保兩組記錄(通常是兩個(gè)賬戶的余額)達(dá)成一致的過(guò)程。對(duì)帳用于確保離開(kāi)帳戶的資金與實(shí)際支出的資金相匹配。這是通過(guò)確保在特定會(huì)計(jì)期間結(jié)束時(shí)余額匹配來(lái)完成的。 - Jean Scheid,“了解資產(chǎn)負(fù)債表賬戶調(diào)節(jié)”,Bright Hub,2011年4月8日
回到微服務(wù),使用相同的原則,我們可以在一些動(dòng)作觸發(fā)器上協(xié)調(diào)來(lái)自多個(gè)服務(wù)的數(shù)據(jù)。當(dāng)檢測(cè)到故障時(shí),可以按計(jì)劃或由監(jiān)控系統(tǒng)觸發(fā)操作。最簡(jiǎn)單的方法是運(yùn)行逐記錄比較??梢酝ㄟ^(guò)比較聚合值來(lái)優(yōu)化該過(guò)程。在這種情況下,其中一個(gè)系統(tǒng)將成為每條記錄的真實(shí)來(lái)源。
事件簿
想象一下多步驟交易。如何在對(duì)帳期間確定哪些事務(wù)可能已失敗以及哪些步驟失???一種解決方案是檢查每個(gè)事務(wù)的狀態(tài)。在某些情況下,此功能不可用(想象一下發(fā)送電子郵件或生成其他類型消息的無(wú)狀態(tài)郵件服務(wù))。在其他一些情況下,您可能希望立即了解事務(wù)狀態(tài),尤其是在具有許多步驟的復(fù)雜方案中。例如,預(yù)訂航班,酒店和轉(zhuǎn)機(jī)的多步訂單。

復(fù)雜的分布式流程
在這些情況下,事件日志可以提供幫助。記錄是一種簡(jiǎn)單但功能強(qiáng)大的技術(shù)。許多分布式系統(tǒng)依賴于日志。 “預(yù)寫(xiě)日志記錄”是數(shù)據(jù)庫(kù)在內(nèi)部實(shí)現(xiàn)事務(wù)行為或維護(hù)副本之間一致性的方式。相同的技術(shù)可以應(yīng)用于微服務(wù)設(shè)計(jì)。在進(jìn)行實(shí)際數(shù)據(jù)更改之前,服務(wù)會(huì)寫(xiě)入有關(guān)其進(jìn)行更改的意圖的日志條目。實(shí)際上,事件日志可以是協(xié)調(diào)服務(wù)所擁有的數(shù)據(jù)庫(kù)中的表或集合。

事件日志不僅可用于恢復(fù)事務(wù)處理,還可用于為系統(tǒng)用戶,客戶或支持團(tuán)隊(duì)提供可見(jiàn)性。但是,在簡(jiǎn)單方案中,服務(wù)日志可能是冗余的,狀態(tài)端點(diǎn)或狀態(tài)字段就足夠了。
編配(Orchestration)與編排(choreography)
到目前為止,您可能認(rèn)為sagas只是編配(orchestration )方案的一部分。但是sagas也可以用于編排(choreography ),每個(gè)微服務(wù)只知道過(guò)程的一部分。 Sagas包括處理分布式事務(wù)的正流和負(fù)流的知識(shí)。在編排(choreography )中,每個(gè)分布式事務(wù)參與者都具有這種知識(shí)。
單次寫(xiě)入事件
到目前為止描述的一致性解決方案并不容易。他們確實(shí)很復(fù)雜。但有一種更簡(jiǎn)單的方法:一次修改一個(gè)數(shù)據(jù)源。我們可以將這兩個(gè)步驟分開(kāi),而不是改變服務(wù)的狀態(tài)并在一個(gè)過(guò)程中發(fā)出事件。
更改為先
在主要業(yè)務(wù)操作中,我們修改自己的服務(wù)狀態(tài),而單獨(dú)的進(jìn)程可靠地捕獲更改并生成事件。這種技術(shù)稱為變更數(shù)據(jù)捕獲(CDC)。實(shí)現(xiàn)此方法的一些技術(shù)是Kafka Connect或Debezium。
使用Debezium和Kafka Connect更改數(shù)據(jù)捕獲
但是,有時(shí)候不需要特定的框架。一些數(shù)據(jù)庫(kù)提供了一種友好的方式來(lái)拖尾其操作日志,例如MongoDB Oplog。如果數(shù)據(jù)庫(kù)中沒(méi)有此類功能,則可以通過(guò)時(shí)間戳輪詢更改,或使用上次處理的不可變記錄ID查詢更改。避免不一致的關(guān)鍵是使數(shù)據(jù)更改通知成為一個(gè)單獨(dú)的過(guò)程。在這種情況下,數(shù)據(jù)庫(kù)記錄是單一的事實(shí)來(lái)源。只有在首先發(fā)生變化時(shí)才會(huì)捕獲更改。
無(wú)需特定工具即可更改數(shù)據(jù)捕獲
更改數(shù)據(jù)捕獲的***缺點(diǎn)是業(yè)務(wù)邏輯的分離。更改捕獲過(guò)程很可能與更改邏輯本身分開(kāi)存在于您的代碼庫(kù)中 - 這很不方便。最知名的變更數(shù)據(jù)捕獲應(yīng)用程序是與域無(wú)關(guān)的變更復(fù)制,例如與數(shù)據(jù)倉(cāng)庫(kù)共享數(shù)據(jù)。對(duì)于域事件,***采用不同的機(jī)制,例如明確發(fā)送事件。
事件***
讓我們來(lái)看看顛倒的單一事實(shí)來(lái)源。如果不是先寫(xiě)入數(shù)據(jù)庫(kù),而是先觸發(fā)一個(gè)事件,然后與自己和其他服務(wù)共享。在這種情況下,事件成為事實(shí)的唯一來(lái)源。這將是一種事件源的形式,其中我們自己的服務(wù)狀態(tài)有效地成為讀取模型,并且每個(gè)事件都是寫(xiě)入模型。
事件優(yōu)先方法
一方面,它是一個(gè)命令查詢責(zé)任隔離(CQRS)模式,我們將讀取和寫(xiě)入模型分開(kāi),但CQRS本身并不關(guān)注解決方案中最重要的部分 - 使用多個(gè)服務(wù)來(lái)消耗事件。
相比之下,事件驅(qū)動(dòng)的體系結(jié)構(gòu)關(guān)注于多個(gè)系統(tǒng)所消耗的事件,但并未強(qiáng)調(diào)事件是數(shù)據(jù)更新的唯一原子部分。所以我想引入“事件優(yōu)先”作為這種方法的名稱:通過(guò)發(fā)出單個(gè)事件來(lái)更新微服務(wù)的內(nèi)部狀態(tài) - 包括我們自己的服務(wù)和任何其他感興趣的微服務(wù)。
“事件優(yōu)先”方法面臨的挑戰(zhàn)也是CQRS本身的挑戰(zhàn)。想象一下,在下訂單之前,我們想要檢查商品的可用性。如果兩個(gè)實(shí)例同時(shí)收到同一項(xiàng)目的訂單怎么辦??jī)烧叨紝⑼瑫r(shí)檢查讀取模型中的庫(kù)存并發(fā)出訂單事件。如果沒(méi)有某種覆蓋方案,我們可能會(huì)遇到麻煩。
處理這些情況的常用方法是樂(lè)觀并發(fā):將讀取模型版本放入事件中,如果讀取模型已在消費(fèi)者端更新,則在消費(fèi)者端忽略它。另一種解決方案是使用悲觀并發(fā)控制,例如在檢查項(xiàng)目可用性時(shí)為項(xiàng)目創(chuàng)建鎖定。
“事件優(yōu)先”方法的另一個(gè)挑戰(zhàn)是任何事件驅(qū)動(dòng)架構(gòu)的挑戰(zhàn) - 事件的順序。多個(gè)并發(fā)消費(fèi)者以錯(cuò)誤的順序處理事件可能會(huì)給我們帶來(lái)另一種一致性問(wèn)題,例如處理尚未創(chuàng)建的客戶的訂單。
諸如Kafka或AWS Kinesis之類的數(shù)據(jù)流解決方案可以保證將按順序處理與單個(gè)實(shí)體相關(guān)的事件(例如,僅在創(chuàng)建用戶之后為客戶創(chuàng)建訂單)。例如,在Kafka中,您可以按用戶ID對(duì)主題進(jìn)行分區(qū),以便與單個(gè)用戶相關(guān)的所有事件將由分配給該分區(qū)的單個(gè)使用者處理,從而允許按順序處理它們。相反,在Message Brokers中,消息隊(duì)列具有一個(gè)訂單,但是多個(gè)并發(fā)消費(fèi)者在給定順序中進(jìn)行消息處理(如果不是不可能的話)。在這種情況下,您可能會(huì)遇到并發(fā)問(wèn)題。
實(shí)際上,在需要線性化的情況下或在具有許多數(shù)據(jù)約束的情況(例如唯一性檢查)中,難以實(shí)現(xiàn)“事件優(yōu)先”方法。但它在其他情況下確實(shí)很有用。但是,由于其異步性質(zhì),仍然需要解決并發(fā)和競(jìng)爭(zhēng)條件的挑戰(zhàn)。
設(shè)計(jì)一致性
有許多方法可以將系統(tǒng)拆分為多個(gè)服務(wù)。我們努力將單獨(dú)的微服務(wù)與單獨(dú)的域匹配。但域名有多細(xì)化?有時(shí)很難將域與子域或聚合根區(qū)分開(kāi)來(lái)。沒(méi)有簡(jiǎn)單的規(guī)則來(lái)定義您的微服務(wù)拆分。
我建議務(wù)實(shí)并考慮設(shè)計(jì)方案的所有含義,而不是只關(guān)注領(lǐng)域驅(qū)動(dòng)的設(shè)計(jì)。其中一個(gè)影響是微服務(wù)隔離與事務(wù)邊界的對(duì)齊情況。事務(wù)僅駐留在微服務(wù)中的系統(tǒng)不需要上述任何解決方案。在設(shè)計(jì)系統(tǒng)時(shí)我們一定要考慮事務(wù)邊界。在實(shí)踐中,可能很難以這種方式設(shè)計(jì)整個(gè)系統(tǒng),但我認(rèn)為我們應(yīng)該致力于***限度地減少數(shù)據(jù)一致性挑戰(zhàn)。
接受不一致
雖然匹配帳戶余額至關(guān)重要,但有許多用例,其中一致性不那么重要。想象一下,為分析或統(tǒng)計(jì)目的收集數(shù)據(jù)。即使我們從系統(tǒng)中隨機(jī)丟失了10%的數(shù)據(jù),也很可能不會(huì)影響分析的業(yè)務(wù)價(jià)值。
與事件共享數(shù)據(jù)
選擇哪種解決方案
數(shù)據(jù)的原子更新需要兩個(gè)不同系統(tǒng)之間達(dá)成共識(shí),如果單個(gè)值為0或1則達(dá)成協(xié)議。當(dāng)涉及到微服務(wù)時(shí),它歸結(jié)為兩個(gè)參與者之間的一致性問(wèn)題,并且所有實(shí)際解決方案都遵循一條經(jīng)驗(yàn)法則:
在給定時(shí)刻,對(duì)于每個(gè)數(shù)據(jù)記錄,您需要找到系統(tǒng)信任的數(shù)據(jù)源
事實(shí)的來(lái)源可能是事件,數(shù)據(jù)庫(kù)或其中一項(xiàng)服務(wù)。實(shí)現(xiàn)微服務(wù)系統(tǒng)的一致性是開(kāi)發(fā)人員的責(zé)任。我的方法如下:
- 嘗試設(shè)計(jì)一個(gè)不需要分布式一致性的系統(tǒng)。不幸的是,對(duì)于復(fù)雜的系統(tǒng)來(lái)說(shuō),這幾乎是不可能的。
- 嘗試通過(guò)一次修改一個(gè)數(shù)據(jù)源來(lái)減少不一致的數(shù)量。
- 考慮事件驅(qū)動(dòng)的架構(gòu)。除了松散耦合之外,事件驅(qū)動(dòng)架構(gòu)的強(qiáng)大優(yōu)勢(shì)是通過(guò)將事件作為單一事實(shí)來(lái)源或由于更改數(shù)據(jù)捕獲而產(chǎn)生事件來(lái)實(shí)現(xiàn)數(shù)據(jù)一致性的自然方式。
- 更復(fù)雜的場(chǎng)景可能仍然需要服務(wù),故障處理和補(bǔ)償之間的同步調(diào)用。知道有時(shí)候你可能需要在之后進(jìn)行調(diào)和。
- 設(shè)計(jì)您的服務(wù)功能是可逆的,決定如何處理故障情況并在設(shè)計(jì)階段早期實(shí)現(xiàn)一致性。