微服務中基于事件驅動的數據管理

博客原文

在nginx官網的blog中,作者Chris Richardson關于微服務的文章有七篇:
1. Introduction to Microservices(微服務介紹)
2. Building Microservices: Using an API Gateway(構建微服務:API網關)
3. Building Microservices: Inter-Process Communication in a Microservices Architecture(構建微服務:微服務架構中的進程間通信)
4. Service Discovery in a Microservices Architecture(微服務架構中的服務發現)
5. Event-Driven Data Management for Microservices(微服務中基于事件驅動的數據管理
6. Choosing a Microservices Deployment Strategy(微服務部署策略)
7. Refactoring a Monolith into Microservices(重構單體應用為微服務)
第5篇文章是我最關注的問題,這里先翻譯它。

碎碎的水滴

01 微服務和分布式數據管理的問題

單體應用一般只會使用一個關系型數據庫,使用一個關系型數據庫的重要優點是,在你的應用中可以使用事務的ACID特性,它能提供一些重要的保證:

  • 原子性 – 改變都是原子級的
  • 一致性 – 數據庫中的狀態總是一致的
  • 隔離性 – 即使事務是并行執行的,也會看起來和串行的一樣
  • 持久性 – 一旦事務被提交,數據的變化是永久性的

這樣的結果就是,在應用中你可以很輕松地開啟一個事務,然后改變(插入、更新和刪除)多行數據,最后一起提交這個事務。

使用一個關系型數據庫的另一個很大的好處是,它提供了豐富的、聲明式的、并且是標準的查詢語句(sql)。你可以很輕松地寫一個查詢語句,從多張表中查詢整合數據。

不幸的是,當我們轉向微服務架構的時候,數據訪問變得復雜了很多。那是因為數據都被每個微服務所私有,其他的微服務只能通過它的API訪問。封裝數據是為了確保微服務之間是松耦合的,能彼此獨立地發展。如果多個服務訪問同一個數據,當數據結構發生變化的時候,需要花費很多時間去協調所有相關的服務更新。

更糟糕的是,不同的微服務經常使用不同的數據庫。現在的應用經常存儲和處理多種多樣的數據,關系型數據庫經常不是一種最好的選擇。在某些情況下,一個特殊的NOSql數據庫會有更多方便使用的數據模型,而且經常會有更高的性能和擴展性。舉個例子,如果是一個存儲和檢索文本的服務,將會使用全文搜索引擎,如Elasticsearch。同樣地,如果是一個社交服務存儲一些圖數據,可能會使用圖數據庫,如Neo4j。因此,基于微服務的應用中經常講SQL和NoSql數據庫混合著使用,這就是所謂的混合持久化(Polyglot Persistence)。

另外,雖然數據存儲的混合持久化架構帶了很多好處,包括:松耦合服務、高性能和易擴展,然而,這也帶來了一些分布式數據管理的挑戰。

第一個挑戰就是,如何實現業務數據的事務,保持跨多個服務的數據一致性。為了證明為什么這是一個問題,讓我們看一個在線B2B商城的例子。客戶服務提供關于客戶的信息,包括他們的信用額度。訂單服務管理訂單,并且驗證新訂單金額不能超過客戶的信用限額。在這個應用的單體架構版本中,訂單服務可以簡單地使用一個ACID的事務來實現校驗信用余額和創建訂單。

相比之下,在微服務架構下訂單表和客戶表是分別屬于各自的服務的。如下圖:

訂單表和客戶表

訂單服務是不能直接訪問客戶表的,它僅能使用客戶服務提供的API接口。訂單服務可以使用分布式事務,例如眾所周知的兩階段提交(2PC)。然而,2PC在現在的應用中通常不是一個可用的選擇。根據CAP原理,需要你在可用性和ACID式的一致性之間做出選擇,通常可用性是較好的選擇。此外,很多現在的技術不支持2PC,例如大部分的NoSql數據庫。維持跨服務的數據一致性,數據庫是必不可少的,因此我們需要另外的解決方案。

第二個挑戰是如何實現從多服務中檢索數據。例如,讓我們想象一下,應用需要展示客戶和他最近的訂單。如果訂單服務提供一個檢索客戶訂單數據的API,你可以檢索到這些數據直接加入到應用中。應用從客戶服務中檢索客戶信息,從訂單服務中檢索客戶訂單信息。然而,假設訂單服務僅支持根據客戶主鍵查詢訂單(也許它使用的是NoSql數據庫,僅支持基于主鍵的檢索)。在這種情況下,顯然是沒有簡單的方法檢索到所需要的數據。

02 基于事件驅動的架構

對于很多應用來說,解決方案是采用基于事件驅動的架構。在這樣的架構中,當有重要的事情發生時微服務發布一個事件,例如當更新一個交易實體時,其它微服務需要訂閱這些事件。當一個微服務接收到一個事件時,它會更新它自己的交易實體,同時這也可能導致更多的事件被發布。

你可以使用事件來實現跨越多個服務的交易的事務性。一個事務包括一系列的步驟,每一步都是由微服務更新一個交易實體和發布一個觸發下一個步驟的事件構成。下面的序列圖顯示了,你如何使用事件驅動的方法在創建訂單的時候檢查可用信用額度。微服務通過Message Broker交換事件。

  1. 訂單創建一個狀態為NEW的訂單,然后發布一個訂單創建的事件。
創建訂單和發布訂單創建事件
  1. 客戶服務訂閱訂單創建事件,為這個訂單預留信用,然后發布一個信用預留的事件。
預留信用和發布信用預留事件
  1. 訂單服務訂閱信用預留事件,然后改變訂單狀態為OPEN。
訂單狀態變更

如果是更復雜的場景可以添加額外的步驟,例如在校驗客戶信用的同事預留庫存。

(1)每個服務要保證更新數據庫和發布事件的原子性,更重要的是后者;(2)Message Broker要確保事件被至少傳遞一次;這樣你就可以實現跨多個服務的交易的事務。需要注意的是,這些不是ACID的事務。它們只是提供了一種相對較弱的擔保,例如最終一致性。這種事務模型已經被稱為基礎模型。

你也可以使用事件來維護一個具體的視圖,將來自多個微服務的數據預加載進來。維護這個視圖的服務需要訂閱相關服務,然后更新這個視圖。例如客戶訂單事務更新服務維護了一個客戶訂單的視圖,它需要訂閱客戶服務和訂單服務的事件。

跨微服務數據聚合

當客戶訂單更新服務接收到客戶或者是訂單事件時,它會更新客戶訂單視圖的數據庫。可以使用文檔數據庫(MongoDB)實現客戶訂單視圖,為每個客戶存儲一個文檔。客戶訂單視圖查詢服務通過查詢客戶訂單視圖數據庫處理查詢一個客戶最近的訂單的請求。

基于事件驅動的架構的優點和缺點:實現了跨多個服務的事務,并提供了最終一致性;另一個優點是可以使應用維護一個實體化的視圖。一個缺點是,編程模型比使用ACID事務時更加復雜了,通常為了從應用級的故障恢復,你必須實現補償事務,例如如果校驗信用余額失敗時你必須取消訂單。還有應用必須能夠處理不一致的數據,這是因為臨時(in-flight)事務的影響是可見。此外,如果實體化的視圖中數據還未更新就被服務讀取到了,這樣你也會看到不一致的數據。另一個缺點是事件的訂閱者必須檢測和忽略重復的事件。

03 實現原子性

在基于事件驅動的架構中還有另外一個問題,就是更新數據庫和發布事件的原子性。例如,訂單服務必須在訂單表中插入一條數據同時發布一個訂單創建的事件,這兩個操作必須原子性地完成,這是基本要求。如果在更新數據庫之后發布事件之前服務崩潰了,系統就變得不一致了。確保原子性的標準方式是,在涉及到的數據庫和Message Broker中使用分布式事務,然而由于上述原因,如CAP原理,這不是我們想要的。

03.1 使用本地事務發布事件

實現原子性的一個方法是,應用使用“僅在本地事務中做多步驟處理”的方法發布事件。關鍵是需要有一個Event表,它的功能是作為一個消息隊列,數據庫中存儲交易實體的狀態。應用可以開啟一個本地數據庫事務,更新交易實體的狀態和插入一條數據到EVENT表,然后提交事務。EVENT表還需要一個獨立的應用線程或者進程來發布EVENT到Message Broker,然后使用本地事務標記EVENT已經被發布了。下圖描述了該設計:

本地事務發布事件

訂單服務插入一條數據到訂單表,同時插入一個訂單創建事件到EVENT表,EVENT表的事件發布線程或進程會將這些未發布的事件發布到Message Broker中,然后更新EVENT表中的事件狀態為已發布。

這個方法的優點和缺點:一個優點是,它能保證每次更新都能將事件發布出去,而又不依賴2PC。應用發布了交易級別的事件,我們不需要推斷具體發生了什么。這個方法的缺點是,易于犯錯,因為程序員必須記得發布事件。這種方法還有一個限制,當我們使用NoSql數據庫時這是一個挑戰,因為NoSql數據庫的事務和查詢的能力有限。

這種方法消除了我們對2PC的需求,應用直接使用本地事務更新數據庫狀態和發布事件。現在讓我們看另外一種實現原子性的方法,應用僅僅需要更新狀態就能實現。

03.2 利用數據庫事務日志

在不使用2PC情況下,保證發布事件的原子性的另一個方法是,創建一個線程或者進程來采集數據庫的事務或者提交日志。應用更新數據庫的時候,數據被改變的結果都被記錄在數據庫的事務日志中。數據庫事務日志的采集者線程或者進程讀取這個事務日志,然后發布事件到Message Broker中。詳細設計如下圖:

數據庫事務日志

開源的項目 LinkedIn Databus 是一個這種方法的例子。Databus采集Oracle數據庫的事務日志,然后根據改變內容發布事件。LinkedIn使用Databus保持各種數據存儲中的系統數據的一致性。

另一個例子是,streams mechanism in AWS DynamoDB,這個是管理NoSql數據庫的工具。DynamoDB流包含過去24小時的實時訂單的改變(創建、更新和刪除操作)數據序列,這些數據項會被存儲在DynamoDB表中。有一個應用可以從這個流中讀取到這些變化,然后將它們發布成事件。

采集事務日志有優點也有缺點。一個優點是,在不使用2PC的情況下,能確保每個更新都被發布成事件;采集事務日志的方法也能簡化應用,它將發布事件從應用的業務邏輯中分離出來。一個主要的缺點是,每個數據庫的事務日志格式都是特有的,甚至是不同的數據庫版本格式也是不同的。同時,在事務日志中從低級別的更新記錄轉變為高級別的業務事件的逆向工程是很困難的。

采集事務日志解除對2PC的需要,應用只需要做一件事:更新數據庫。現在讓我們看另外一種不同的方法,不用更新,只需要事件。

03.3 使用事件源

事件源(Event sourcing)實現原子性,而不使用2PC,這是一個完全不同的方法,就是以事件為中心的方法去持久化業務實體。應用存儲一系列的狀態改變事件,而不是存儲一個實體的當前狀態。應用依賴重放事件來重建實體的當前狀態,無論業務實體的狀態什么時候變化,一個新的事件都會被追加到事件列表中。因為存儲事件是一個單獨的操作,所以它天生具有原子性。

為了展現事件源是如何工作的,舉一個訂單實體的例子,在一個交易方法中,例如每個訂單都被映射成ORDER表中的一行數據和ORDER_LINE_ITEM表中的一行數據。但是當我們使用事件源方法時,訂單服務存儲訂單方式是,存儲訂單的狀態改變事件:創建、通過、發貨、取消。每個事件包含充足的數據去重建訂單狀態。

事件源

事件持久化在一個事件數據庫中,該數據庫有添加和查詢實體事件的API,這個事件數據庫在架構上類似于一個Message Broker,我們可以實現訂閱事件。它提供有訂閱事件的API。該事件數據庫會將所有的事件傳遞到所有感興趣的訂閱者中,事件數據庫是事件驅動的微服務架構的重要保障。

事件源機制有幾個優點,它解決了事件驅動架構中的關鍵問題,無論狀態什么時候變化都能可靠地發布事件。因此,它解決了微服務架構中的數據一致性問題。而且因為它持久化的是事件而不是領域對象,很大程度上還避免了對象和關系型數據庫字段不匹配的問題。事件源提供了100%可靠的業務實體改變的審計日志,而且任何時間點都能查詢到實體的狀態。事件源方法的另外一個好處是,由兩個松耦合的業務實體組成的業務邏輯可以交換事件。這樣就方便了單體應用轉向微服務架構。

事件源機制也有幾個缺點,這是一種不同且陌生的編程模型,因此有一定的學習曲線。事件數據庫僅支持使用主鍵查詢業務實體,你必須使用 Command Query Responsibility Segregation來實現查詢。因此應用程序操作處理最終一致的數據。

04 總結

在微服務架構中,每個微服務都有它自己的數據存儲。不同的微服務可能使用不同的SQL和NoSQL數據庫,雖然這個數據庫架構有很大的好處,但是它創造了分布式數據管理的挑戰。第一個挑戰是,為了維持不同服務之間的數據一致性,如何實現業務邏輯中的事務;第二個挑戰是,如何實現從多個服務中檢索數據。

對于大多數的應用來書,解決方案都是采用事件驅動的架構。實現事件驅動的架構一個挑戰是,實現更新數據狀態和發布事件的原子性。實現這個目的的方法有幾個,包括 使用數據庫作為消息隊列采集數據庫的事務日志事件源機制

在未來的博客中,我們會繼續探討微服務其他方面的話題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容