微服務架構中的數據一致性

   在微服務中,一個邏輯原子操作通常可以跨多個微服務。即使是單一系統也可能使用多個數據庫或消息傳遞解決方案。有了幾個獨立的數據存儲解決方案,如果某個分佈式流程參與者失敗,我們就會冒數據不一致的風險——例如沒有下訂單就向客戶收費,或者沒有通知客戶訂單成功。

 

    本文我將分享一些我學到的使微服務之間的數據最終保持一致的技術。爲什麼實現這個目標如此具有挑戰性?一旦我們有多個存儲數據的地方(不是在一個數據庫中),一致性就不會自動解決,程序猿在設計系統時需要注意一致性。就目前而言,在我看來,業界還沒有一個廣爲人知的解決方案來自動更新多個不同數據源中的數據——業界短期內應該不會這種問題的解決方案出來。

 

    兩階段提交(2PC)模式的XA協議是一種解決此問題的一種嘗試之一。但是在現代的大規模應用程序中(特別是在雲環境中),2PC的性能似乎不太好。爲了彌補2PC的不足,根據不同的業務場景我們需要基於ACID爲基礎在業務層面做一些一致性問題的處理。

    

Saga模式

在微服務中處理一致性問題的最著名的方法是Saga模式。可以將Sagas視爲多個事務的應用程序級分佈式協調。根據用例和需求,可以優化自己基於Saga模式的實現。相反,XA協議試圖覆蓋所有場景。Saga模式在微服務之前就在ESB和SOA體系結構中被使用。現在,它已經成功地過渡到微服務。跨越多個服務的每個原子業務操作可能由技術級別上的多個事務組成。Saga模式的關鍵思想是回滾處理。對於已經提交的事物是不能做回滾操作的,這就需要通過調用補償機制來實現——通過引入“取消”操作。

 

補償操作

除了取消之外,還應該考慮使服務具有冪等性,以便在出現故障時重試或重新啓動某些操作。故障應該被監控,對故障的反應應該是積極的。

 

一致性比較

如果在處理過程中,負責調用補償操作的系統崩潰或重新啓動,該怎麼辦?在這種情況下,用戶可能會收到一條錯誤消息,補償邏輯應該被觸發,或者——當處理異步用戶請求時,執行邏輯應該被恢復。要查找崩潰的事務並恢復操作或應用補償,我們需要協調來自多個服務的數據。對賬是在金融領域工作過的工程師所熟悉的一種技術。你有沒有想過銀行是如何確保你的轉賬沒有丟失,或者在兩家不同的銀行之間是如何轉賬的?快速的答案是對賬。

 

在會計上,覈對是確保兩套記錄(通常是兩個賬戶的餘額)一致的過程。對賬是用來確保離開賬戶的錢與實際花費的錢相匹配。這是通過確保在一個特定的會計期末餘額匹配來實現的。- Jean Scheid,“理解資產負債表賬戶對賬”,Bright Hub, 2011年4月8日

 

回到微服務,使用相同的原則,我們可以在某個操作觸發器上協調來自多個服務的數據。當檢測到故障時,可以按預定的基礎或由監視系統觸發操作。最簡單的方法是逐記錄比較。這個過程可以通過比較聚合值來優化。在這種情況下,其中一個系統將是每個記錄的真實來源。

 

事件日誌

多階段事物。如何確定在協調過程中哪些事務可能已經失敗,哪些步驟已經失敗?一種解決方案是檢查每個事務的狀態。在某些情況下,此功能不可用(想象一下發送電子郵件或生成其他類型消息的無狀態郵件服務)。在其他一些情況下,可能希望實時看到事務狀態,特別是在具有多步驟的複雜場景中。例如,預訂機票、酒店和轉機的多步驟訂單。

 

複雜的分佈式過程

在這些情況下,事件日誌可以提供幫助。日誌記錄是一種簡單但功能強大的技術。許多分佈式系統依賴於日誌。“寫前日誌記錄”是數據庫如何實現事務行爲或在內部維護副本之間的一致性。同樣的技術也可以應用於微服務設計。在進行實際的數據更改之前,服務會寫一個日誌條目,說明其進行更改的意圖。實際上,事件日誌可以是協調服務擁有的數據庫中的表或集合。

 

示例事件日誌

事件日誌不僅可以用於恢復事務處理,還可以用於向系統用戶、客戶或支持團隊提供可見性。但是,在簡單的場景中,服務日誌可能是冗餘的,狀態端點或狀態字段就足夠了。

 

編制與編排

至此,你可能認爲sagas只是編制場景的一部分。但是sagas也可以用於編排,其中每個微服務只知道流程的一部分。Sagas包括處理分佈式事務的正流和負流的知識。在編排中,每個分佈式事務參與者都有這種知識。

 

Single-Write與事件

到目前爲止描述的一致性解決方案並不容易。它們確實很複雜。但是有一種更簡單的方法:每次修改一個數據源。我們可以將這兩個步驟分開,而不是更改服務的狀態並在一個進程中發出事件。

 

最先改變

在主業務操作中,我們修改自己的服務狀態,而獨立的流程可靠地捕獲更改並生成事件。這種技術稱爲更改數據捕獲(Change Data Capture, CDC)。實現這種方法的一些技術是Kafka Connect或Debezium。

 

改變數據捕捉與Debezium和Kafka連接

然而,有時並不需要特定的框架。有些數據庫提供了跟蹤操作日誌的友好方式,例如MongoDB Oplog。如果數據庫中沒有此類功能,則可以通過時間戳輪詢更改,或者使用不可變記錄的最後處理ID查詢更改。避免不一致的關鍵是使數據更改通知成爲一個單獨的進程。在本例中,數據庫記錄是真實數據的唯一來源。只有首先發生變化時,纔會捕捉到變化。

 

改變數據捕獲沒有特定的工具

更改數據捕獲的最大缺點是業務邏輯的分離。更改捕獲過程很可能存在於代碼庫中,與更改邏輯本身分離——這很不方便。更改數據捕獲最著名的應用是與域無關的更改複製,例如與數據倉庫共享數據。對於域事件,最好採用不同的機制,例如顯式發送事件。

 

Event-First

如果我們不首先寫入數據庫,而是觸發一個事件,並與我們自己和其他服務共享該事件,會發生什麼情況呢?在這種情況下,事件成爲唯一的來源。這將是事件源的一種形式,其中我們自己的服務的狀態有效地成爲一個讀模型,而每個事件都是一個寫模型。

 

其次一方面,它是一個命令查詢責任隔離(CQRS)模式,在這種模式中我們分離了讀和寫模型,但是CQRS本身並不關注解決方案中最重要的部分——使用多個服務來處理事件。

 

相反,事件驅動的體系結構關注多個系統使用的事件,但不強調事件是數據更新的唯一原子部分這一事實。因此,我想將“event-first”作爲這種方法的名稱:通過向我們自己的服務和任何其他感興趣的微服務發出單個事件來更新微服務的內部狀態。

 

“事件優先”方法的挑戰也是CQRS本身的挑戰。想象一下,在下訂單之前,我們想要檢查商品的可用性。如果兩個實例同時收到相同項目的訂單,該怎麼辦?兩者都將併發地檢查讀模型中的庫存併發出訂單事件。如果沒有某種掩護方案,我們可能會遇到麻煩。

 

處理這些情況的通常方法是樂觀併發性:將一個讀模型版本放入事件中,如果消費者端上的讀模型已經更新,則在消費者端忽略它。另一種解決方案是使用悲觀併發控制,例如在檢查項的可用性時爲項創建一個鎖。

 

“事件優先”方法的另一個挑戰是任何事件驅動的體系結構的挑戰——事件的順序。由多個併發使用者以錯誤的順序處理事件可能會給我們帶來另一種一致性問題,例如處理尚未創建的客戶的訂單。

 

在實踐中,“事件優先”方法很難在需要線性化的場景或具有許多數據約束(如唯一性檢查)的場景中實現。但它在其他情況下確實很出色。然而,由於其異步性,併發性和競態條件的挑戰仍然需要解決。

 

設計的一致性

將系統拆分爲多個微服務的方式有很多。我們努力將不同的微服務與不同的領域相匹配。但是領域的邊界在哪裏呢?很難區分界定。沒有簡單的規則來定義微服務的劃分。

 

與其只關注領域驅動的設計,我建議更務實地考慮設計選項的所有含義。其中一個影響是微服務隔離與事務邊界的一致性。事務只駐留在微服務中的系統不需要上述任何解決方案。在設計系統時,我們一定要考慮事務邊界。在實踐中,以這種方式設計整個系統可能很困難,但是我認爲我們應該以最小化數據一致性挑戰爲目標。

 

接受不一致

雖然匹配帳戶餘額非常重要,但在許多用例中,一致性的重要性要低得多。想象一下爲分析或統計目的收集數據。即使我們從系統中隨機丟失10%的數據,分析的業務價值也很可能不會受到影響。

 

與事件共享數據

選擇哪種解決方案

數據的原子更新需要在兩個不同的系統之間達成一致,如果一個值是0或1,則需要達成一致。當談到微服務時,它歸結爲兩個參與者之間的一致性問題,所有實用的解決方案都遵循一個簡單的規則:在給定的時刻,對於每個數據記錄,需要找到系統信任的數據源。

 

真實的業務來源可能是事件、數據庫或某個服務。在分佈式微服務系統中實現數據的一致性是開發人員的職責所在。下面是我的一些個人總結:

 

  • 嘗試設計一個不需要分佈式一致性的系統。不幸的是,這對於複雜的系統來說幾乎是不可能的。

 

  • 嘗試通過每次修改一個數據源來減少不一致的數量。

 

  • 考慮事件驅動架構。除了松耦合之外,事件驅動體系結構的強大功能是實現數據一致性的一種自然方式,方法是將事件作爲事實的單一來源,或者將事件作爲變更數據捕獲的結果生成事件。

 

  • 有一些複雜的業務場景仍然需要服務、故障處理和補償之間的同步調用。有時候可能需要做對賬操作。

 

  • 將服務功能設計爲可逆的,決定如何處理故障場景,並在設計階段的早期實現一致性。

歡迎關注本人公衆號交流學習

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