在微服務中,一個邏輯上原子操作可以經常跨越多個微服務。即使是單片系統也可能使用多個數據庫或消息傳遞解決方案。使用多個獨立的數據存儲解決方案,如果其中一個分布式流程參與者出現故障,我們就會面臨數據不一致的風險 - 例如在未下訂單的情況下向客戶收費或未通知客戶訂單成功。在本文中,我想分享一些我為使微服務之間的數據最終保持一致而學到的技術。
為什么實現這一目標如此具有挑戰性?只要我們有多個存儲數據的地方(不在單個數據庫中),就不能自動解決一致性問題,工程師在設計系統時需要注意一致性。目前,在我看來,業界還沒有一個廣為人知的解決方案,可以在多個不同的數據源中自動更新數據 - 我們可能不應該等待很快就能獲得一個。
以自動且無障礙的方式解決該問題的一種嘗試是實現兩階段提交(2PC)模式的XA協議。但在現代高規模應用中(特別是在云環境中),2PC似乎表現不佳。為了消除2PC的缺點,我們必須交易ACID for BASE并根據要求以不同方式覆蓋一致性問題。
Saga模式
在多個微服務中處理一致性問題的最著名的方法是Saga模式。 您可以將Sagas視為多個事務的應用程序級分布式協調。根據用例和要求,您可以優化自己的Saga實施。相反,XA協議試圖涵蓋所有場景。Saga模式也不是新的。它在過去已知并用于ESB和SOA體系結構中。最后,它成功地轉變為微服務世界。跨越多個服務的每個原子業務操作可能包含技術級別的多個事務。Saga Pattern的關鍵思想是能夠回滾其中一個單獨的交易。眾所周知,開箱即用的已經提交的單個事務無法進行回滾。但這是通過引入補償操作來實現的 - 通過引入“取消”操作。
圖片
除了取消之外,您還應該考慮使您的服務具有冪等性,以便在出現故障時重試或重新啟動某些操作。應監控故障,并應積極主動地應對故障。
對賬
如果在進程的中間負責調用補償操作的系統崩潰或重新啟動,該怎么辦?在這種情況下,用戶可能會收到錯誤消息,并且應該觸發補償邏輯,或者 - 當處理異步用戶請求時,應該恢復執行邏輯。
圖片
要查找崩潰的事務并恢復操作或應用補償,我們需要協調來自多個服務的數據。對賬
是在金融領域工作的工程師所熟悉的技術。你有沒有想過銀行如何確保你的資金轉移不會丟失,或者兩個不同的銀行之間如何匯款?快速回答是對賬。
圖片
在會計中,對賬是確保兩組記錄(通常是兩個賬戶的余額)達成一致的過程。對帳用于確保離開帳戶的資金與實際支出的資金相匹配。這是通過確保在特定會計期間結束時余額匹配來完成的。- Jean Scheid,“了解資產負債表賬戶調節”,Bright Hub,2011年4月8日
回到微服務,使用相同的原則,我們可以在一些動作觸發器上協調來自多個服務的數據。當檢測到故障時,可以按計劃或由監控系統觸發操作。最簡單的方法是運行逐記錄比較。可以通過比較聚合值來優化該過程。在這種情況下,其中一個系統將成為每條記錄的真實來源。
事件簿
想象一下多步驟交易。如何在對帳期間確定哪些事務可能已失敗以及哪些步驟失敗?一種解決方案是檢查每個事務的狀態。在某些情況下,此功能不可用(想象一下發送電子郵件或生成其他類型消息的無狀態郵件服務)。在其他一些情況下,您可能希望立即了解事務狀態,尤其是在具有許多步驟的復雜方案中。例如,預訂航班,酒店和轉機的多步訂單。
圖片
復雜的分布式流程
在這些情況下,事件日志可以提供幫助。記錄是一種簡單但功能強大的技術。許多分布式系統依賴于日志。“預寫日志記錄”是數據庫在內部實現事務行為或維護副本之間一致性的方式。相同的技術可以應用于微服務設計。在進行實際數據更改之前,服務會寫入有關其進行更改的意圖的日志條目。實際上,事件日志可以是協調服務所擁有的數據庫中的表或集合。
圖片
事件日志不僅可用于恢復事務處理,還可用于為系統用戶,客戶或支持團隊提供可見性。但是,在簡單方案中,服務日志可能是冗余的,狀態端點或狀態字段就足夠了。
編配(Orchestration)與編排(choreography)
到目前為止,您可能認為sagas只是編配(orchestration )方案的一部分。但是sagas也可以用于編排(choreography ),每個微服務只知道過程的一部分。Sagas包括處理分布式事務的正流和負流的知識。在編排(choreography )中,每個分布式事務參與者都具有這種知識。
單次寫入事件
到目前為止描述的一致性解決方案并不容易。他們確實很復雜。但有一種更簡單的方法:一次修改一個數據源。我們可以將這兩個步驟分開,而不是改變服務的狀態并在一個過程中發出事件。
更改為先
在主要業務操作中,我們修改自己的服務狀態,而單獨的進程可靠地捕獲更改并生成事件。這種技術稱為變更數據捕獲(CDC)。實現此方法的一些技術是Kafka Connect或Debezium。
使用Debezium和Kafka Connect更改數據捕獲
但是,有時候不需要特定的框架。一些數據庫提供了一種友好的方式來拖尾其操作日志,例如MongoDB Oplog。如果數據庫中沒有此類功能,則可以通過時間戳輪詢更改,或使用上次處理的不可變記錄ID查詢更改。避免不一致的關鍵是使數據更改通知成為一個單獨的過程。在這種情況下,數據庫記錄是單一的事實來源。只有在首先發生變化時才會捕獲更改。
無需特定工具即可更改數據捕獲
更改數據捕獲的最大缺點是業務邏輯的分離。更改捕獲過程很可能與更改邏輯本身分開存在于您的代碼庫中 - 這很不方便。最知名的變更數據捕獲應用程序是與域無關的變更復制,例如與數據倉庫共享數據。對于域事件,最好采用不同的機制,例如明確發送事件。
事件第一
讓我們來看看顛倒的單一事實來源。如果不是先寫入數據庫,而是先觸發一個事件,然后與自己和其他服務共享。在這種情況下,事件成為事實的唯一來源。這將是一種事件源的形式,其中我們自己的服務狀態有效地成為讀取模型,并且每個事件都是寫入模型。
事件優先方法
一方面,它是一個命令查詢責任隔離(CQRS)模式,我們將讀取和寫入模型分開,但CQRS本身并不關注解決方案中最重要的部分 - 使用多個服務來消耗事件。
相比之下,事件驅動的體系結構關注于多個系統所消耗的事件,但并未強調事件是數據更新的唯一原子部分。所以我想引入“事件優先”作為這種方法的名稱:通過發出單個事件來更新微服務的內部狀態 - 包括我們自己的服務和任何其他感興趣的微服務。
“事件優先”方法面臨的挑戰也是CQRS本身的挑戰。想象一下,在下訂單之前,我們想要檢查商品的可用性。如果兩個實例同時收到同一項目的訂單怎么辦?兩者都將同時檢查讀取模型中的庫存并發出訂單事件。如果沒有某種覆蓋方案,我們可能會遇到麻煩。
處理這些情況的常用方法是樂觀并發:將讀取模型版本放入事件中,如果讀取模型已在消費者端更新,則在消費者端忽略它。另一種解決方案是使用悲觀并發控制,例如在檢查項目可用性時為項目創建鎖定。
“事件優先”方法的另一個挑戰是任何事件驅動架構的挑戰 - 事件的順序。多個并發消費者以錯誤的順序處理事件可能會給我們帶來另一種一致性問題,例如處理尚未創建的客戶的訂單。
諸如Kafka或AWS Kinesis之類的數據流解決方案可以保證將按順序處理與單個實體相關的事件(例如,僅在創建用戶之后為客戶創建訂單)。例如,在Kafka中,您可以按用戶ID對主題進行分區,以便與單個用戶相關的所有事件將由分配給該分區的單個使用者處理,從而允許按順序處理它們。相反,在Message Brokers中,消息隊列具有一個訂單,但是多個并發消費者在給定順序中進行消息處理(如果不是不可能的話)。在這種情況下,您可能會遇到并發問題。
實際上,在需要線性化的情況下或在具有許多數據約束的情況(例如唯一性檢查)中,難以實現“事件優先”方法。但它在其他情況下確實很有用。但是,由于其異步性質,仍然需要解決并發和競爭條件的挑戰。
設計一致性
有許多方法可以將系統拆分為多個服務。我們努力將單獨的微服務與單獨的域匹配。但域名有多細化?有時很難將域與子域或聚合根區分開來。沒有簡單的規則來定義您的微服務拆分。
我建議務實并考慮設計方案的所有含義,而不是只關注領域驅動的設計。其中一個影響是微服務隔離與事務邊界的對齊情況。事務僅駐留在微服務中的系統不需要上述任何解決方案。在設計系統時我們一定要考慮事務邊界。在實踐中,可能很難以這種方式設計整個系統,但我認為我們應該致力于最大限度地減少數據一致性挑戰。
接受不一致
雖然匹配帳戶余額至關重要,但有許多用例,其中一致性不那么重要。想象一下,為分析或統計目的收集數據。即使我們從系統中隨機丟失了10%的數據,也很可能不會影響分析的業務價值。
與事件共享數據
選擇哪種解決方案
數據的原子更新需要兩個不同系統之間達成共識,如果單個值為0或1則達成協議。當涉及到微服務時,它歸結為兩個參與者之間的一致性問題,并且所有實際解決方案都遵循一條經驗法則:
在給定時刻,對于每個數據記錄,您需要找到系統信任的數據源
事實的來源可能是事件,數據庫或其中一項服務。實現微服務系統的一致性是開發人員的責任。我的方法如下:
- 嘗試設計一個不需要分布式一致性的系統。不幸的是,對于復雜的系統來說,這幾乎是不可能的。
- 嘗試通過一次修改一個數據源來減少不一致的數量。
- 考慮事件驅動的架構。除了松散耦合之外,事件驅動架構的強大優勢是通過將事件作為單一事實來源或由于更改數據捕獲而產生事件來實現數據一致性的自然方式。
- 更復雜的場景可能仍然需要服務,故障處理和補償之間的同步調用。知道有時候你可能需要在之后進行調和。
- 設計您的服務功能是可逆的,決定如何處理故障情況并在設計階段早期實現一致性。