原文鏈接:Event-Driven Data Management for Microservices
- 微服務介紹
- 構建微服務之使用API網關
- 構建微服務之:微服務架構中的進程間通信
- 微服務中的服務發現
- 微服務之事件驅動的數據管理(本文)
- 選擇一種微服務部署策略
- 重構單體應用到微服務
這是使用微服務架構構建應用系列的第五篇文章.第一篇文章介紹了微服務架構模式并討論了使用微服務的優勢和劣勢 ;第二篇和第三篇文章討論了微服務架構不同層面的通信問題;第四篇文章密切探討了服務發現的相關問題;本文章呢,我們換個口味,看看微服務架構模式中的分布式的數據管理問題。
微服務與分布式數據管理問題
一個單體應用一般只有一個關系型數據庫,使用一個關系型數據的優勢是應用可以實現ACID,為業務操作進行保證:
- Atomicity – 操作是原子性的
- Consistency – 數據庫中的狀態永遠是一致的
- Isolation – 盡管事務是并發的執行,但他們表現的像順序執行,事務之間不會彼此影響
- Durable – 一旦事務提交就不能再撤銷
因此,應用可以很簡單的開始一個事務,操作(增刪改)多條數據,然后提交事務。
使用關系型數據庫的另一個巨大優勢是可以使用SQL,一種豐富的聲明式的標準查詢語言。 你可以很簡單的寫一條關聯多個數據表的查詢語句,關系型數據庫管理系統會解析SQL來決定最優執行方案執行該查詢。你不必關心諸如數據庫如何訪問這種低層次的細節問題,另外,你所有的應用數據在一個數據庫中,查詢起來很容易。
很不幸的是,當轉換到微服務架構的時候,數據訪問變的越來越復雜。這是因為每個服務擁有的數據是該服務所私有的,要想訪問該服務的數據只能通過API。對數據進行封裝可以確保微服務之間是松散耦合的,各個服務可以獨立于其他服務進行演進。如果多個服務訪問同樣的數據,那么Schema的更新是很耗時而且需要協調所有服務進行更新。
不同微服務可能使用不同的數據庫會讓事情變得更糟。現代應用程序存儲和處理各種各樣的數據,關系數據庫并不總是最好的選擇。在一些用例下,一種特殊的NoSQL數據庫可能有更加方便的數據模型并且提供更好的性能與擴展性 ,比如,對一個存儲和查詢文檔的服務來講,使用Elasticsearch這樣的文檔查詢引擎可能更加合適;同樣的,存儲社交圖譜的服務更合適使用Neo4J這樣的圖數據庫 。總之,微服務架構的應用經常使用SQL和NoSQL的混合存儲,通常叫做多態型數據持久性方案。
一個分區的多態型的數據持久性方案有諸多好處:松耦合、更高的性能、擴展性等,然而這也引入了分布式數據管理的挑戰!
第一個挑戰就是如何跨多個服務實現業務事務、維護數據的一致性。為什么這是一個問題?讓我們看一個在線B2B商店的例子:用戶服務維護諸如用戶信用額度這樣的信息,訂單服務管理訂單并且確保新訂單沒有超過用戶的信用額度限制。在傳統單體應用中,訂單服務可以在創建新訂單時簡單的使用ACID事務去查看用戶可用的信用額。
然而在微服務架構中,ORDER 和CUSTOMER表是各個服務所私有的,如下圖所示:
訂單服務不能直接去訪問用戶表而只能使用用戶服務提供的API。訂單服務可能潛在的使用分布式事務,也就是兩段提交,然而現在的應用中兩段提交通常不是一個可行的選擇。CAP理論要求你在可用性和ACID風格的一致性之間進行選擇 ,而且。許多流行的技術,比如NoSQL并不支持兩段提交,這就尷尬了!維護跨服務和跨數據庫的數據一致性是很有必要的,所以我們需要另一種解決方案。
另一項挑戰就是如何在多服務中獲取數據。比如。假設我們應用需要展示用戶信息和他最近的訂單,如果訂單服務提供API查詢用戶訂單,那么你可以使用 應用程序連接查詢數據 :應用從用戶服務查詢用戶信息,從訂單服務查詢訂單。 假設,訂單服務僅支持主鍵查詢訂單(也許使用了僅支持主鍵查詢的NoSQL),這種情況下,沒有好的方法來檢索數據。
事件驅動架構
對很多應用來講,方案就是使用事件驅動架構。在該架構中,一個微服務在一些業務發生時發布一個事件,比如更新一條業務記錄,其他微服務會訂閱該事件,當微服務收到事件后會更新自己的業務記錄,導致其他的事件被發布。
你可以使用事件實現跨服務的業務事務。一個事務包含一系列步驟,每個步驟由某微服務更新業務記錄并發布事件觸發下一步驟組成。下面的圖顯示如何使用事件驅動方式在創建訂單時檢查可用信用額度 ,微服務間通過Message Broker交換事件。
- 訂單服務創建狀態為NEW的訂單并發布Order Created事件
Paste_Image.png - 客戶服務消費Order Created事件,為該訂單預訂信用并發布Credit Reserved事件
Paste_Image.png - 訂單服務消費Credit Reserved事件并更改訂單狀態為OPEN
Paste_Image.png
更復雜的場景可能調用多個步驟,比如保留訂單同時檢查用戶信用。
假設(a)每個服務原子性的更新數據庫并發布事件– 并且– (b) Message Broker保證事件至少被交付一次,那么你就可以實現跨服務的業務事務了。我們必須注意到一點是,這并不是ACID事務,他們提供更弱一點的最終一致性,這種事務模型可以參考 BASE model.
你也可以使用事件來維護提前關聯了多個微服務的物化視圖。該服務通過訂閱相關事件并更新視圖,比如,用戶訂單視圖通過訂閱訂單事件和用戶事件來更新用戶訂單視圖。
當客戶訂單視圖更新服務接收到來自客戶或者訂單的事件時,它將更新客戶訂單視圖。你可以使用Mongodb這樣的文檔數據庫為每個用戶存儲一份文檔來實現客戶訂單更新視圖,客戶訂單視圖查詢服務可以查詢客戶訂單視圖數據庫提供客戶信息與最近客戶訂單數據。
事件驅動架構有很多優勢也有一些劣勢。它使得跨服務事務成為可能,并提供了最終一致性,另一個優勢就是它使得應用可以維護物化視圖;但是劣勢之一是編程模型比ACID事務模型更為復雜 ,通常我們要實現補償事務來恢復應用級別的錯誤。比如,一旦信用額度確認失敗,你必須要取消訂單,并且應用要處理不一致的數據, 這是因為隨意的事務是可見的,應用如果讀取未更新的物化視圖,將會獲取到不一致的數據,另外一個劣勢是,訂閱必須可以檢測并忽略重復的事件。
實現原子性
在事件驅動的架構中,還有一個更新數據與發布事件的原子性問題。比如,訂單服務需要插入一條數據到 ORDER表并且發布Order Created事件,這兩個操作原子性完成是很有必要的:假如在數據庫更新完成后,事件發布之前服務掛掉了,系統將會存在不一致。標準確保原子性的方式是使用分布式事務調用數據庫系統和Message Broker。然而,根據前面我們描述的理由,比如CAP理論,我們是想避免這么干的。
使用本地事務發布事件
應用發布事件并保證原子性的方法之一是采用多步驟本地事務方法。技巧是有一張EVENT表,表其實就是模擬一個message queue。當然數據庫中還存著業務數據的狀態,應用開始一個本地數據庫事務,更新業務數據記錄并往EVENT表中插入一條數據,最后提交事務。一個單獨的應用線程或進程輪詢EVENT表,并根據查詢結果往Message Broker推送事件消息,然后使用本地事務標記事件被發布。下圖描述了該設計:
訂單服務插入數據到ORDER表并且插入Order Created event 到EVENT表。數據發布者線程或進程輪詢EVENT表中未發布的事件,發布事件,然后更新EVENT表標記事件已被發布。
這種方案有優勢也有劣勢。優勢之一是保證了在不使用兩段提交前提下事件在每次更新后一定被發布 ,當然,應用發布了業務級別的事件,減少了再去推斷事件類型的麻煩。劣勢之一是這種方案很容易出錯,因為要求程序員必須記得更新后去發布事件,該方案另一個局限是使用NoSQL時,由于NoSQL的事務和查詢能力局限,實現起來困難。
這種方案使用本地事務來更新數據和發布事件減少了兩段提交的使用,現在讓我們來看只需更新狀態就達到原子性的方法。
挖掘數據庫事務日志
另一種不需要兩段提交就實現原子性的方法是挖掘數據庫事務日志或提交日志實現事件發布,這就要求變化操作要被記錄在數據庫事務日志當中,事務日志挖據線程或進程讀取事務日志并發布事件到 Message Broker。下圖展示了這種方案:
例子之一就是開源的LinkedIn Databus 項目。 Databus 挖掘 Oracle事務日志并發布相應的事件, LinkedIn使用 Databus 來保持各種派生數據存儲與記錄系統的一致。
另一個例子就是NoSQL數據庫streams mechanism in AWS DynamoDB, DynamoDB 流包含在過去 24 小時向 DynamoDB 表中的記錄所做的時序性變化 (創建、 更新和刪除操作)。應用程序可以從流中讀取這些更改并將它們作為事件發布。
事務日志挖掘有優勢也有劣勢。優勢之一是在不使用兩段提交的前提下保證了事件一定被發布,事務日志挖據也可以通過拆分應用業務邏輯事件的發布簡化整個應用。該方案一個巨大的劣勢是:事務日志每個數據庫都不同,甚至相同數據庫不同版本也會不同(攤手),而且我們很難從低級別事務日志的更新記錄中反推高級別的業務事件。
事務日志挖掘通過更新數據庫來避免使用兩段提交。現在讓我們看看一種消除了更新,并完全依靠的事件的方案:
使用事件源
事件源通過使用截然不同,以事件為中心的方案持久化業務實體,達到了不使用兩段提交前提下 的原子性。這種方案存儲一系列狀態變化的事件而不是存儲當前實體的狀態。應用可以通過重放事件來重新構建實體的當前狀態。一旦業務實體發生變化,一個新的事件就會被添加到事件列表中,由于保存事件是單一操作,本身就是原子性的。
為查看事件源如何工作,我們以訂單實體為例子。傳統方案下,每個訂單會映射到ORDER表并記錄數據到ORDER_LINE_ITEM。但當使用事件源編程時,訂單服務以存儲訂單狀態變化事件來存儲訂單:訂單創建、批準、運輸、取消。每個事件有充足的信息來重新構建訂單。
事件存儲到專門存儲事件的數據庫中,數據庫提供添加和查詢實體事件的API。這個事件數據庫也會像我們上面描述的Message Broker一樣提供讓其他服務訂閱事件的API,事件數據庫向對其感興趣的訂閱者發布事件,這個事件數據庫是事件驅動型微服務的骨架。
事件源的優勢:它解決了一個實現事件源架構的關鍵問題,使得事件發生時可靠的發布事件成為可能,因此,它解決了微服務架構中數據一致性的問題。當然,因為他持久化事件而非領域對象,也避免了面向對象到關系型數據庫中的阻抗不匹配問題。事件源也為實體變動提供了100%可靠的審計日志,并使其能夠執行可以確定在任何時間點實體狀態的時態查詢。事件源的另一巨大優勢是,業務邏輯與交換事件的實體是松耦合的,這使得遷移一個單體應用到微服務架構更加簡單。
事件源也有一些劣勢:對程序員來講這是完全不同的,非常陌生的編程風格,學習曲線陡峭。事件數據庫僅僅直接支持主鍵方式查詢業務實體,必須使用 Command Query Responsibility Segregation (CQRS) 來實現查詢。因此,應用必須來處理最終一致性。
總結
在微服務架構中,每個服務擁有自己的數據存儲。不同服務可能是一不同的SQL或者NoSQL數據庫。這樣的數據庫架構有很多優勢,當然也帶來了分布式數據管理的挑戰。挑戰之一就是如何實現跨多服務的業務邏輯事務來維持一致性,挑戰之二就是如何從多服務查詢數據。
對很多應用來講,使用事件驅動架構來應對這些挑戰。實現事件驅動架構的挑戰之一是如何保證更新數據狀態和發布事件的原子性 ,有幾種方案來實現,包括以數據庫為message queue,事務日志挖掘,事件源。
剩余的文章將討論其他方面的問題。