微服務數據一致性的演進:SAGA、CQRS、Event Sourcing由來和局限

微服務數據一致性的演進:SAGA、CQRS、Event Sourcing由來和侷限

作者:Grygoriy Gonchar

原題:Data consistency in microservices architecture

原文:http://t.cn/EZVDA6h

全文7188字,閱讀約需要15分鐘

白小白:

講微服務數據一致性的文章,網上比較多。此前EAWorld與發過幾篇,包括《微服務架構下的數據一致性保證(一)》、《微服務架構下的數據一致性保證(二)》、《微服務架構下的數據一致性保證(三):補償模式》,以及《使用消息系統進行微服務間通訊時,如何保證數據一致性》。本篇文章在我看來,是從一個縱向的維度把相關的一致性概念的演進過程,講的比較清晰,簡單的邏輯是這樣的:

1、分佈式帶來一致性挑戰;

2、2PC效率太低,選擇SAGA保證最終一致;

3、SAGA的補償環節可能失敗,需要進行對賬;

4、對賬需要日誌,需要記錄日誌;

5、用更改優先的方式記日誌,更適合跨域操作,域內推薦事件優先;

6、用事件優先的方式記日誌,即屬於CQRS的一種模式;

7、CQRS或者事件優先,也有併發和亂序的挑戰,很難實現;

8、退而求其次,可以採用“設計一致性”;

9、實在無法一致,那就接受不一致。

微服務數據一致性的演進:SAGA、CQRS、Event Sourcing由來和侷限

(圖示)分佈式進程故障

在微服務中,邏輯上的原子操作經常跨越多個微服務。即使是單體應用架構下,也可能使用多個數據庫或消息隊列解決方案。使用幾種獨立的數據存儲解決方案,如果分佈式流程參與者之一失敗,我們將面臨數據不一致的風險,例如,向客戶收費而不下訂單,或者不通知客戶訂單成功。在本文中,我想分享一些在微服務體系架構下確保數據最終一致(http://t.cn/EzBWZSN)的技術。

為什麼要做到這一點如此具有挑戰性?只要我們有多個存儲數據的地方(而不是在一個數據庫中),一致性就不會自動解決,工程師在設計系統時就需要考慮一致性。就目前而言,在我看來,業界還沒有一個廣為人知的解決方案來原子化地更新多個不同數據源中的數據,而且在可預見的將來,也不會有一個很快就可以使用的解決方案。

白小白:

數據一致性的挑戰首先就源於CAP理論以及妥協方案BASE理論 (擴展閱讀http://t.cn/Re3i4xt)的限定,所謂原子化地更新多個不同數據源,可以用訂機票的例子來說明:

1、原子操作:你付了錢,航空公司也出了票,這兩件事必須同時完成,這就是一個原子操作。

2、不一致:付了錢但沒出票,或者沒花錢卻出了票,就是數據不一致。

3、最終一致:付了錢,票沒出,但兩天後錢退到了你的賬戶,這就是最終一致。退錢的過程,就叫做業務補償。

簡單理解分佈式環境的CAP,即一致性(C)和可用性(A)之間的權衡,給定的時間窗口和客戶體驗的前提下,想要原子化操作的各個數據源都一致(C)是不可能的,總會有某個時刻,表示錢的數據和表示票的數據,可能對不上,如果強調強一致性(C),就要在用戶點擊付款後等待很長的時間,來驗證賬戶情況和票務情況,這就影響了可用性(A),而在分佈式環境,重要的是用戶體驗,即使中間存在數據不一致,也可以通過補償等手段來達成最終一致。這了形成了BASE理論的基本狀況,即BA,基本可用(允許適當的響應時間或者功能體驗來儘量保證一致性),S,軟狀態(允許付了錢沒出票的中間狀態),E,最終一致。符合BASE的例子,有點像是我們在微信裡買火車票或者機票時,會有個搶票過程,這實際上就是響應時間和功能上的妥協,畢竟沒有在點擊按鈕後很快的反饋出票的結果,而先付款,後搶票,實際上就是軟狀態,即允許數據不一致的中間狀態。

以自動化和省力的方式解決這個問題的一個嘗試是以XA協議(http://t.cn/EzBmvFS)實現兩階段提交(2PC)模式(http://t.cn/RckficO)但是在現代大規模應用程序中(特別是在雲環境中),2PC的性能似乎不太好。為了消除2PC的缺點,我們必須犧牲ACID來遵循BASE原則,並根據需要採用不同的方式來滿足數據一致性的要求。

白小白:

這裡的ACID是指數據庫事務正確執行的四個基本要素,結合上下文可以簡化理解為CAP,以避免引入更多的概念。XA協議就是實現兩階段提交的一種規範。而所謂兩階段提交,就是把分佈式數據源的操作分為兩個步驟,即準備和提交兩個階段來確保數據一致性。準備階段,詢問所有參與方,是否可以進行操作。如果所有參與方都回復OK,就進行操作,否則有某一方回覆不OK,就進行回滾。這有點像是我們我們玩的殺人遊戲:

準備階段:“天黑請閉眼,殺手請睜眼,殺手請殺人”。

提交階段:“天亮請睜眼,孫悟空死了,沒有遺言”

1、在準備階段,法官將向所有殺手詢問狀態,即殺手得睜眼,有一個沒睜眼的就沒法玩。所以我們經常會經歷法官說“殺手請睜眼,殺手請睜眼”的循環。

2、在準備階段,但也有可能三個殺手兩個睜了眼,然後睜眼的兩個殺了一個人。有一個殺手死活不睜眼,這時候法官只能宣佈遊戲重來。這就是參與方有不OK的過程,重來就是回滾,即使殺了人也不算。

3、在準備階段,只有三個人都睜眼了,並且成功的指出了殺誰,法官才會最終宣佈死亡者,這就是提交。

可以看出,在準備階段,參與者的協調會耗費大量的時間,這也是作者說,2PC的性能似乎不太好的原因。關於2階段、3階段及SAGA以外的數據一致性方案,感興趣的,可以參考這篇文章(http://t.cn/EzBus4y)。

一、SAGA模式

在多個微服務中處理一致性問題的最著名方法是SAGA模式(http://t.cn/EzB3uQA)可以將SAGA視為多個事務的應用程序級分佈式協調機制。根據用例和需求,可以優化自己的SAGA實現,XA協議正相反,試圖以通用方案涵蓋所有的場景。

SAGA模式也並非什麼新概念。它過去在ESB和SOA體系結構中就得到認知和使用,並最終成功地向微服務世界過渡。跨多個服務的每個原子業務操作可能由一個技術級別上的多個事務組成。Saga模式的關鍵思想是能夠回滾單個事務。正如我們所知道的,對於已經提交的單個事務來說,回滾是不可能的。但通過調用補償行動,即通過引入“Cancel”操作可以變相的實現這一點。

微服務數據一致性的演進:SAGA、CQRS、Event Sourcing由來和侷限

(圖)補償操作

除了取消操作之外,還需要考慮使服務的冪等性,以便在發生故障時可以重新嘗試或重新啟動某些操作。應該對失敗進行監測,對失敗的反應應該積極主動。

白小白:

SAGA對分佈式事務的實現,依賴於應用程序為失敗的情況,寫好修正的邏輯代碼,以便回滾時調用。這也就是“應用程序級”的含義。但在SAGA模式中,一個複雜的事務被拆分成若干個事務,並通過流程管理器串接,從而降低了整體事務管理的複雜性。

對賬

如果在進程中間,負責調用補償操作的系統崩潰或重新啟動怎麼辦?在這種情況下,用戶可能會收到錯誤消息,觸發補償邏輯,在處理異步用戶請求時,重試執行邏輯。

微服務數據一致性的演進:SAGA、CQRS、Event Sourcing由來和侷限

(圖)主要過程失效

要查找崩潰的事務並恢復操作或應用補償,我們需要協調來自多個服務的數據。對從事金融領域工作的工程師來說,對賬是一種熟悉的技術。你有沒有想過,銀行如何確保你的匯款不會丟失,或者在兩家不同的銀行之間是如何發生轉賬的?快速的答案是對賬。

微服務數據一致性的演進:SAGA、CQRS、Event Sourcing由來和侷限

在會計領域,對賬是確保兩套記錄(通常是兩個賬戶的餘額)一致的過程。對賬手段確保離開帳戶的錢與實際花費的錢相符。這是通過確保在特定會計期間結束時的餘額匹配來實現的。(來源,Jean Scheid, “Understanding Balance Sheet Account Reconciliation”, Bright Hub, 8 April 2011)

回到微服務方面,使用相同的理念,我們可以在某些操作觸發器上協調來自多個服務的數據。可以按計劃執行對賬操作,也可以在檢測到出狀況時,由監視系統觸發相關操作。最簡單的方法是按記錄逐條進行比較,當然,也可以通過比較彙總值來優化此過程。在這種情況下,某個系統的數據將成為基準數據來對每條數據進行比對。

白小白:

此處的基準數據,原文是single source of truth,在維基百科中的解釋是,在分佈式系統中,即使數據被分散複製,但應該只被存儲一次,其他副本僅以引用形態來使用該數據。我想,用“基準數據”來翻譯,可能更便於理解。

事件日誌

再來討論多步事務的情況。如何確定在對賬過程中哪些事務在哪些環節上失敗了?一種解決方案是檢查每個事務的狀態。在某些情況下,這個方法並不適用(比如無狀態的郵件服務發送電子郵件或生成其他類型的消息)。在其他一些情況下,我們可能希望獲得事務狀態的即時可見性(也就是實時知曉其當前的狀態),特別是在具有多個步驟的複雜場景中。例如,一個多步驟的訂單,包括預訂航班、酒店和轉乘。

微服務數據一致性的演進:SAGA、CQRS、Event Sourcing由來和侷限

(圖)複雜分佈式過程

在這種情況下,事件日誌可能會有所幫助。日誌記錄是一種簡單但功能強大的技術。許多分佈式系統依賴日誌。“預寫日誌“就是在數據庫內部實現事務行為或保持副本之間的一致性的方法。同樣的技術也可以應用於微服務設計。在進行實際的數據更改之前,服務會編寫一個日誌條目,對即將實施的更改操作進行說明。實現方式上,事件日誌是從屬於協調服務的數據庫中的表或集合。

微服務數據一致性的演進:SAGA、CQRS、Event Sourcing由來和侷限

(圖)事件日誌示例

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

編曲(Orchestration)與編舞(Choreography)

至此,您可能會認為SAGA只適用於編曲場景的一部分。但是SAGA也可以用於編舞場景,每個微服務只知道其中的一部分。SAGA內置了處理分佈式事務的正向流和負向流的相關機制。在編舞場景中,每個分佈式事務參與者都有這樣的知識。

白小白:

此處我沒有翻譯成大家常見的但完全無法理解的編制和編排,而是譯為編曲和編舞,因為我覺得這樣更便於理解這兩種服務的組織方式(受這篇文章的啟發 http://t.cn/EZVefsQ,儘管對於編舞我與作者有不同的理解)。如果舉辦一個軟件開發詞彙翻譯比賽,Orchestration與Choreography,一定是難度榜的前列。Orchestration的字面含義是管絃樂編曲,而Choreography的字面含義是舞蹈編排。深入理解,管絃樂儘管存在曲譜作為表演的規範,但最關鍵的環節在於指揮,讓整個音樂的展現波瀾壯闊;而舞蹈編排,一旦相關的動作和環節編制完成,在臺上的表演完全依賴參與的舞者,並沒有一個指揮能夠在舞蹈表演的過程中起到關鍵作用。這也就是服務Orchestration和服務Choreography的運行模式的區別。

Orchestration模式:使用一箇中心控制機制來管理所有服務的協同和交互,中心瞭解所有參與方的情況和狀態。

Choreography模式:服務根據既定的規則協同工作,每個服務只瞭解與自己有關的內容。

所以,此段最後的“每個分佈式事務參與者都有這樣的知識”,指的是在編舞模式下,每個參與者獨立的按照規則處理自己的事務。但規則是依託一些類似正向流和負向流的知識。此處的正向流,即按照一定的規則重試業務邏輯,執行順序是沿既定的工作流,正向進行。而負向流,即執行補償邏輯,執行的順序是按工作流反向進行。舉例:一個出行計劃,流程是,訂火車->訂機票->訂酒店

正向流:訂火車->訂機票->機票失敗->機票重試…->訂酒店

負向流:訂火車->訂機票->訂酒店->酒店失敗->取消酒店->取消機票->取消火車

二、單一寫入事件

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

“變更優先”原則

在主要業務操作中,我們修改自己的服務狀態,而單獨的流程則可靠地捕獲相關變更並生成事件。這種技術被稱為變更數據捕獲(CDC)。實現此方法的一些技術包括 Kafka Connect(http://t.cn/EzrZOpE)或 Debezium (https://debezium.io/ )。

微服務數據一致性的演進:SAGA、CQRS、Event Sourcing由來和侷限

(圖)用Debezium和Kafka Connect捕獲數據修改狀態

然而,有時不需要特定的框架來進行處理。一些數據庫提供了一種跟蹤操作日誌的友好方法,如MongoDB Oplog(http://t.cn/Ezrw6xj)。如果數據庫中沒有這樣的功能,則可以使用時間戳輪詢更改,或者使用最後處理的ID查詢不可變記錄。避免不一致的關鍵是使數據更改通知成為一個單獨的進程。數據庫記錄在這種情況下為基準數據。一旦發生數據變更,相關數據即被捕獲和記錄。

微服務數據一致性的演進:SAGA、CQRS、Event Sourcing由來和侷限

(圖)在沒有特定工具的情況下更改數據捕獲

變更數據捕獲的最大缺點是業務邏輯的分離。更改捕獲過程很可能存在於您的代碼庫中,與更改邏輯本身分離,這是不方便的。最廣為人知的更改數據捕獲應用場景是領域無關的更改複製,例如通過數據倉庫進行數據共享。對於域內的事件,最好使用不同的機制,比如顯式地發送事件。

白小白:

所謂的不方便,我的理解,不是指更改捕獲過程與業務邏輯的分離,而是指用戶需要為每個業務邏輯單獨的實現更改捕獲邏輯。

由於數據倉庫的數據來自不同的數據源,比如SQL Server或者Oracle或者MySQL,為確保數據的實時更新,需要通過ETL或者CDC的方法來進行數據的加載。其中,在採用CDC方法時,需要在數據變更的源和目標都安裝第三方的CDC應用來進行數據的抽取。CDC捕獲變更的方式是在數據變更發生之後,通過讀取數據庫日誌來進行的,這也是最佳的不影響數據的方式。(《Managing Data in Motion》,April Reeve)。事實上,在中文搜索引擎查找數據變更捕獲時可知,Oracle和SQL Server自身都提供了CDC的工具。而不需要依賴第三方應用。

“事件優先”原則

讓我們對“基準數據”做一個逆向思考。如果我們不是首先寫入數據庫,而是先觸發一個事件並與我們自己和其他服務共享這個事件呢?在這種情況下,事件成為基準數據。這將是一種 event-sourcing的形態,在這種情況下,服務狀態實際上變成了一個讀模型,而每個事件都是一個寫模型。

微服務數據一致性的演進:SAGA、CQRS、Event Sourcing由來和侷限

(圖)事件優先方法

所以,這也是一種命令查詢責任分離(CQRS)模式,將讀寫模型分離開來,但是CQRS本身並沒有關注解決方案中最重要的部分,即如何由多個服務來對事件進行處理。

相反,事件驅動體系結構關注多個系統對事件的處理,但不突出強調事件是數據更新的基準數據。所以我想引入 “事件優先”原則作為此方法的名稱:通過發出單個事件來更新微服務的內部狀態-包括對我們自己的服務和任何其他感興趣的微服務。

白小白:

CQRS,簡單理解就是讀取操作和寫入操作分別處理。

事件溯源,是指將對數據進行增刪改的命令按順序記錄為日誌,這一日誌本身是不可刪改的,只能順序增加。因此,通過對這一系列事件的回放,可以從一個空白的數據庫重構造到最新的狀態。

事件驅動,是一種發佈訂閱模型。某節點發生某個操作後,將產生一個消息,另一節點訂閱這一消息並進行後續操作。節點之間並不知曉對方的存在。

事件驅動本身並不關心數據一致性,而是由MQ來進行保證。這就是文中“不突出強調事件是數據更新的基準數據”這句話的含義。而事件溯源又不關心事件的後續處理,即消息觸發後續操作的過程,也就是文中“如何由多個服務來對事件進行處理”的含義。而“事件優先”原則顯然希望綜合兩個理念的優點,從而也需要承受兩種理念的固有缺陷。

採用“事件優先”方法的挑戰也是CQRS本身的挑戰。想象一下,在下訂單之前,我們要檢查商品的可用性。如果兩個實例同時接收同一項的訂單怎麼辦?兩個實例將以讀取模型同時檢查庫存,並觸發一個訂單事件。如果不解決這個問題,我們可能會遇到麻煩。

處理這些情況的通常方法是樂觀併發:在事件中放置一個讀取模型版本,如果已在使用者端更新讀取模型,則忽略這個讀取操作。另一種解決方案是使用悲觀的併發控制,例如在查詢項目可用性時為其創建鎖。

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

數據流解決方案(如Kafka或AWS Kinesis)可以保證與單個實體相關的事件將按順序處理(例如,只在創建用戶之後才為客戶創建訂單)。例如,在Kafka中,您可以通過用戶ID對主題進行分區,這樣與單個用戶相關的所有事件都將由分配給該分區的單個使用者處理,從而允許按順序處理這些事件。相反,在使用消息代理機制時,消息隊列雖然有其固有的執行順序,但多個併發使用者使得按給定順序進行消息處理非常困難,甚至不可能。這樣就可能會遇到併發問題。

實際上,當線性一致性是必需的,或者在有許多數據約束(如唯一性檢查)的情況下,“事件優先”方法很難實現。但在其他場景中但它確實可以大放異彩。然而,由於它的異步性質,併發和競爭條件的挑戰仍然需要解決。

白小白:

所謂線性一致性(Linearizability)的場景,在分佈式環境下,基本上是不需要考慮的,因為不可實現。(來源,Implementing Linearizability at Large Scale and Low Latency,Stanford University)

三、設計一致性

有許多方法可以將系統分成多個服務。我們努力將不同的微服務與不同的域相匹配。但是這些域有多細呢?有時很難將域與子域或聚合根區分開來。沒有簡單的規則來定義您的微服務拆分。

白小白:

域、子域、聚合,是領域驅動設計的概念,簡單理解就是從業務角度對系統進行的不同顆粒度的劃分,舉例來說,一個電商系統:

域:電商

電商的子域:用戶,訂單,產品等

用戶裡的的聚合概念:地址,銀行賬號等

其中,地址其實是一系列概念的聚合,比如郵編、城市、省份等, “地址”就是聚合根對象,所有對郵編、城市、省份的操作不能直接進行,而要通過“地址”這一聚合根進行轉發。

與其只關注領域驅動的設計,我建議採取務實的態度,並考慮所有設計選項的含義。其中一個含義是微服務隔離與事務邊界的匹配程度。事務只駐留在微服務中的系統不需要上述任何解決方案。在設計系統時,一定要考慮事務邊界。在實踐中,可能很難以這種方式設計整個系統,但我認為我們的目標應該是儘量減少數據一致性的挑戰。

接受不一致

雖然與帳戶餘額匹配是至關重要的,但在許多用例中,一致性的重要性要小得多。比如,為分析或統計目的收集數據。即使我們隨機丟失了10%的系統數據,從分析中獲得的業務價值也很可能不會受到影響

微服務數據一致性的演進:SAGA、CQRS、Event Sourcing由來和侷限

(圖)與事件共享數據

白小白:

SendGrid是一個電子郵件服務平臺,可以幫助市場營銷人員跟蹤他們的電子郵件統計數據。如果需要實時獲取發送郵件的狀態(如:發送成功與否,對方有沒有收到,收到之後的處理-打開,刪除,判定為垃圾郵件等),就需要用到SendGrid的WebHook功能來進行實時的數據通知。

四、選擇哪種解決方案

數據的原子更新需要兩個不同系統之間的協商一致,形成對某值為0或者為1的共識。當涉及到微服務時,它歸結為兩個參與者之間的一致性問題,所有實際的解決方案都遵循一個經驗法則:

在給定的時刻,對於每個數據記錄,需要找到可信的基準數據。

基準數據可以是事件、數據庫或某個服務。在微服務系統中實現一致性是開發人員的責任。我的做法如下:

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

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

3. 考慮一下事件驅動的體系結構。除了鬆散耦合之外,事件驅動體系結構的一大優勢是天然的支持基於事件的數據一致性,可以將事件作為基準數據,也可以由變更數據捕獲(CDC)生成事件。

4. 更復雜的場景可能仍然需要服務、故障處理和補償之間的同步調用。要知道,有時你可能不得不在事後對賬。

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

關於作者:Grygoriy Gonchar,eBay @ebaytechberlin Motors Vertical軟件架構師。前@Kreditech主任架構師。專注於Java,Scala,微服務,安全領域的技術話題。言論僅代表個人觀點。

關於EAWorld:微服務,DevOps,數據治理,移動架構原創技術分享,長按二維碼關注


分享到:


相關文章: