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

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

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

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

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

事件日志不僅可用于恢復(fù)事務(wù)處理,還可用于為系統(tǒng)用戶,客戶或支持團隊提供可見性。但是,在簡單方案中,服務(wù)日志可能是冗余的,狀態(tài)端點或狀態(tài)字段就足夠了。
編配(Orchestration)與編排(choreography)
到目前為止,您可能認為Sagas只是編配(orchestration )方案的一部分。但是Sagas也可以用于編排(choreography ),每個微服務(wù)只知道過程的一部分。Sagas包括處理分布式事務(wù)的正流和負流的知識。在編排(choreography )中,每個分布式事務(wù)參與者都具有這種知識。
單次寫入事件
到目前為止描述的一致性解決方案并不容易。他們確實很復(fù)雜。但有一種更簡單的方法:一次修改一個數(shù)據(jù)源。我們可以將這兩個步驟分開,而不是改變服務(wù)的狀態(tài)并在一個過程中發(fā)出事件。
更改為先
在主要業(yè)務(wù)操作中,我們修改自己的服務(wù)狀態(tài),而單獨的進程可靠地捕獲更改并生成事件。這種技術(shù)稱為變更數(shù)據(jù)捕獲(CDC)。實現(xiàn)此方法的一些技術(shù)是Kafka Connect或Debezium。

使用Debezium和Kafka Connect更改數(shù)據(jù)捕獲
但是,有時候不需要特定的框架。一些數(shù)據(jù)庫提供了一種友好的方式來拖尾其操作日志,例如MongoDB Oplog。如果數(shù)據(jù)庫中沒有此類功能,則可以通過時間戳輪詢更改,或使用上次處理的不可變記錄ID查詢更改。避免不一致的關(guān)鍵是使數(shù)據(jù)更改通知成為一個單獨的過程。在這種情況下,數(shù)據(jù)庫記錄是單一的事實來源。只有在首先發(fā)生變化時才會捕獲更改。

無需特定工具即可更改數(shù)據(jù)捕獲
更改數(shù)據(jù)捕獲的最大缺點是業(yè)務(wù)邏輯的分離。更改捕獲過程很可能與更改邏輯本身分開存在于您的代碼庫中 - 這很不方便。最知名的變更數(shù)據(jù)捕獲應(yīng)用程序是與域無關(guān)的變更復(fù)制,例如與數(shù)據(jù)倉庫共享數(shù)據(jù)。對于域事件,最好采用不同的機制,例如明確發(fā)送事件。
事件第一
讓我們來看看顛倒的單一事實來源。如果不是先寫入數(shù)據(jù)庫,而是先觸發(fā)一個事件,然后與自己和其他服務(wù)共享。在這種情況下,事件成為事實的唯一來源。這將是一種事件源的形式,其中我們自己的服務(wù)狀態(tài)有效地成為讀取模型,并且每個事件都是寫入模型。

事件優(yōu)先方法
一方面,它是一個命令查詢責(zé)任隔離(CQRS)模式,我們將讀取和寫入模型分開,但CQRS本身并不關(guān)注解決方案中最重要的部分 - 使用多個服務(wù)來消耗事件。
相比之下,事件驅(qū)動的體系結(jié)構(gòu)關(guān)注于多個系統(tǒng)所消耗的事件,但并未強調(diào)事件是數(shù)據(jù)更新的唯一原子部分。所以我想引入“事件優(yōu)先”作為這種方法的名稱:通過發(fā)出單個事件來更新微服務(wù)的內(nèi)部狀態(tài) - 包括我們自己的服務(wù)和任何其他感興趣的微服務(wù)。
“事件優(yōu)先”方法面臨的挑戰(zhàn)也是CQRS本身的挑戰(zhàn)。想象一下,在下訂單之前,我們想要檢查商品的可用性。如果兩個實例同時收到同一項目的訂單怎么辦?兩者都將同時檢查讀取模型中的庫存并發(fā)出訂單事件。如果沒有某種覆蓋方案,我們可能會遇到麻煩。
處理這些情況的常用方法是樂觀并發(fā):將讀取模型版本放入事件中,如果讀取模型已在消費者端更新,則在消費者端忽略它。另一種解決方案是使用悲觀并發(fā)控制,例如在檢查項目可用性時為項目創(chuàng)建鎖定。
“事件優(yōu)先”方法的另一個挑戰(zhàn)是任何事件驅(qū)動架構(gòu)的挑戰(zhàn) - 事件的順序。多個并發(fā)消費者以錯誤的順序處理事件可能會給我們帶來另一種一致性問題,例如處理尚未創(chuàng)建的客戶的訂單。
諸如Kafka或AWS Kinesis之類的數(shù)據(jù)流解決方案可以保證將按順序處理與單個實體相關(guān)的事件(例如,僅在創(chuàng)建用戶之后為客戶創(chuàng)建訂單)。例如,在Kafka中,您可以按用戶ID對主題進行分區(qū),以便與單個用戶相關(guān)的所有事件將由分配給該分區(qū)的單個使用者處理,從而允許按順序處理它們。相反,在Message Brokers中,消息隊列具有一個訂單,但是多個并發(fā)消費者在給定順序中進行消息處理(如果不是不可能的話)。在這種情況下,您可能會遇到并發(fā)問題。
實際上,在需要線性化的情況下或在具有許多數(shù)據(jù)約束的情況(例如唯一性檢查)中,難以實現(xiàn)“事件優(yōu)先”方法。但它在其他情況下確實很有用。但是,由于其異步性質(zhì),仍然需要解決并發(fā)和競爭條件的挑戰(zhàn)。
設(shè)計一致性
有許多方法可以將系統(tǒng)拆分為多個服務(wù)。我們努力將單獨的微服務(wù)與單獨的域匹配。但域名有多細化?有時很難將域與子域或聚合根區(qū)分開來。沒有簡單的規(guī)則來定義您的微服務(wù)拆分。
我建議務(wù)實并考慮設(shè)計方案的所有含義,而不是只關(guān)注領(lǐng)域驅(qū)動的設(shè)計。其中一個影響是微服務(wù)隔離與事務(wù)邊界的對齊情況。事務(wù)僅駐留在微服務(wù)中的系統(tǒng)不需要上述任何解決方案。在設(shè)計系統(tǒng)時我們一定要考慮事務(wù)邊界。在實踐中,可能很難以這種方式設(shè)計整個系統(tǒng),但我認為我們應(yīng)該致力于最大限度地減少數(shù)據(jù)一致性挑戰(zhàn)。
接受不一致
雖然匹配帳戶余額至關(guān)重要,但有許多用例,其中一致性不那么重要。想象一下,為分析或統(tǒng)計目的收集數(shù)據(jù)。即使我們從系統(tǒng)中隨機丟失了10%的數(shù)據(jù),也很可能不會影響分析的業(yè)務(wù)價值。

與事件共享數(shù)據(jù)
選擇哪種解決方案
數(shù)據(jù)的原子更新需要兩個不同系統(tǒng)之間達成共識,如果單個值為0或1則達成協(xié)議。當(dāng)涉及到微服務(wù)時,它歸結(jié)為兩個參與者之間的一致性問題,并且所有實際解決方案都遵循一條經(jīng)驗法則:
在給定時刻,對于每個數(shù)據(jù)記錄,您需要找到系統(tǒng)信任的數(shù)據(jù)源
事實的來源可能是事件,數(shù)據(jù)庫或其中一項服務(wù)。實現(xiàn)微服務(wù)系統(tǒng)的一致性是開發(fā)人員的責(zé)任。我的方法如下:
- 嘗試設(shè)計一個不需要分布式一致性的系統(tǒng)。不幸的是,對于復(fù)雜的系統(tǒng)來說,這幾乎是不可能的。
- 嘗試通過一次修改一個數(shù)據(jù)源來減少不一致的數(shù)量。
- 考慮事件驅(qū)動的架構(gòu)。除了松散耦合之外,事件驅(qū)動架構(gòu)的強大優(yōu)勢是通過將事件作為單一事實來源或由于更改數(shù)據(jù)捕獲而產(chǎn)生事件來實現(xiàn)數(shù)據(jù)一致性的自然方式。
- 更復(fù)雜的場景可能仍然需要服務(wù),故障處理和補償之間的同步調(diào)用。知道有時候你可能需要在之后進行調(diào)和。
- 設(shè)計您的服務(wù)功能是可逆的,決定如何處理故障情況并在設(shè)計階段早期實現(xiàn)一致性。