編者的話|本文來自 Nginx 官方博客,是「Chris Richardson 微服務(wù)」系列的第五篇文章。第一篇文章介紹了微服務(wù)架構(gòu)模式,并且討論了使用微服務(wù)的優(yōu)缺點;第二和第三篇描述了微服務(wù)架構(gòu)模塊間通訊的不同方面;第四篇研究了服務(wù)發(fā)現(xiàn)中的問題。本篇研究微服務(wù)架構(gòu)帶來的分布式數(shù)據(jù)管理問題。
作者介紹:Chris Richardson,是世界著名的軟件大師,經(jīng)典技術(shù)著作《POJOS IN ACTION》一書的作者,也是 cloudfoundry.com 最初的創(chuàng)始人,Chris Richardson 與 Martin Fowler、Sam Newman、Adrian Cockcroft 等并稱為世界十大軟件架構(gòu)師。

Chris Richardson 所著所有文章已獨家授權(quán) DaoCloud 翻譯并刊載。
本系列包含 7 篇文章,介紹了微服務(wù)的設(shè)計、構(gòu)建和部署,并與傳統(tǒng)的單體架構(gòu)進(jìn)行了比較。本系列將分析微服務(wù)架構(gòu)的各種因素,你也將了解微服務(wù)架構(gòu)模型的優(yōu)劣、是否適合你的項目,以及如何應(yīng)用。
Chris Richardson 微服務(wù)系列全 7 篇:
本期內(nèi)容:
一、微服務(wù)以及分布式數(shù)據(jù)管理中存在的問題
單體應(yīng)用通常使用單個關(guān)系型數(shù)據(jù)庫,由此帶來的好處在于應(yīng)用能夠使用 ACID 事務(wù),后者提供了重要的操作特性:
- 原子化:原子粒度的更改
- 一致性:數(shù)據(jù)庫的狀態(tài)始終保持一致
- 隔離:并發(fā)執(zhí)行的事務(wù)顯示為串行執(zhí)行
- 持久:事務(wù)一旦提交就不會被撤銷
如此,應(yīng)用能夠簡單地開始事務(wù)、更改(插入、更新和刪除)多行、以及提交事務(wù)。
使用關(guān)系型數(shù)據(jù)庫的另一大好處是它支持 SQL。SQL 是一門豐富、可聲明的和標(biāo)準(zhǔn)化的查詢預(yù)約。用戶能夠輕松通過查詢將多個表中的數(shù)據(jù)組合起來,然后 RDBMS 查詢調(diào)度器決定執(zhí)行查詢的最優(yōu)方法。用戶不必關(guān)心底層細(xì)節(jié),比如如何訪問數(shù)據(jù)庫。此外,由于所有的應(yīng)用數(shù)據(jù)在一個數(shù)據(jù)庫中,很容易查詢。
然而,微服務(wù)架構(gòu)中的數(shù)據(jù)訪問變得復(fù)雜許多。每個微服務(wù)擁有的數(shù)據(jù)專門用于該微服務(wù),僅通過其 API 訪問。這種數(shù)據(jù)封裝保證了微服務(wù)松散耦合,并且可以獨立更新。但如果多個服務(wù)訪問相同數(shù)據(jù),架構(gòu)更新會耗費時間、也需要所有服務(wù)的協(xié)調(diào)更新。
更糟糕的是,不同的微服務(wù)通常使用不同類型的數(shù)據(jù)庫?,F(xiàn)代應(yīng)用存儲和處理各種類型的數(shù)據(jù),而關(guān)系型數(shù)據(jù)庫并非總是好選擇。對于一些使用場景,特定的 NoSQL 數(shù)據(jù)庫能提供更方便的數(shù)據(jù)模型、更好的性能和可擴展性。譬如,服務(wù)使用 Elasticsearch 這樣的文本搜索引擎來存儲和查詢文本;同樣地,存儲社交圖譜數(shù)據(jù)的服務(wù)可能需要使用 Neo4j 這樣的圖譜數(shù)據(jù)庫。因此,基于微服務(wù)的應(yīng)用通常會混合使用 SQL 和 NoSQL 數(shù)據(jù)庫,即多語言留存(polyglot persistence approach)。
分區(qū)的、多語言留存的架構(gòu)對于數(shù)據(jù)存儲有很多好處,包括服務(wù)的松耦合、更好的性能和可擴展性。然而,它也確實給分布式數(shù)據(jù)管理帶來了挑戰(zhàn)。
第一個挑戰(zhàn)就是如何實現(xiàn)業(yè)務(wù)邏輯,保持多種服務(wù)的一致性。為了說明為何這是一個問題,我們以在線 B2B 商店為例。Customer Service(下文使用客戶服務(wù))維護與用戶有關(guān)的信息,包括信用信息。Order Service(下文使用訂單服務(wù))管理訂單,驗證新訂單沒有超出用戶的信用額度。在單體應(yīng)用里,訂單服務(wù)可以簡單地使用 ACID 事務(wù)來核對提供的信用信息和創(chuàng)建訂單。
相反,在微服務(wù)架構(gòu)中,如下圖所示,訂單表和客戶表為各自對應(yīng)的服務(wù)私有。
訂單服務(wù)無法直接訪問客戶表,只能通過客戶服務(wù)提供的 API。訂購服務(wù)可能使用分布式事務(wù),也被稱為兩步提交(2PC)。然而,2PC 通常不是現(xiàn)代應(yīng)用的可行選項。CAP 定理需要用戶在可用性和 ACID 風(fēng)格的一致性中二選一,通??捎眯允歉玫倪x擇。此外,許多現(xiàn)代技術(shù),譬如大多數(shù) NoSQL 數(shù)據(jù)庫并不支持 2PC。維護整個服務(wù)和數(shù)據(jù)庫中的數(shù)據(jù)一致性是至關(guān)重要的,因此我們需要另一種解決方案。
第二個挑戰(zhàn)就是如何實現(xiàn)檢索多個服務(wù)數(shù)據(jù)的查詢。假設(shè)應(yīng)用需要顯示一位客戶和他的最近的訂單。如果訂單服務(wù)為檢索客戶訂單提供了 API,那么可以使用應(yīng)用端獲取該數(shù)據(jù)。應(yīng)用通過客戶服務(wù)檢索該客戶,通過訂單服務(wù)檢索該顧客的訂單。但是假如訂單服務(wù)只支持通過訂單主鍵查詢訂單(可能使用僅支持鍵值檢索的 NoSQL 數(shù)據(jù)庫),這種情況下,就沒有合適的方法來檢索所需數(shù)據(jù)。
二、事件驅(qū)動的架構(gòu)
對于許多應(yīng)用,解決方案就是事件驅(qū)動的架構(gòu)。在這一架構(gòu)里,當(dāng)有顯著事件發(fā)生時,譬如更新業(yè)務(wù)實體,某個微服務(wù)會發(fā)布事件,其它微服務(wù)則訂閱這些事件。當(dāng)某一微服務(wù)接收到事件就可以更新自己的業(yè)務(wù)實體,實現(xiàn)更多事件被發(fā)布。
用戶能夠使用事件來實現(xiàn)跨多個服務(wù)的業(yè)務(wù)邏輯。事務(wù)由一系列步驟組成,每一步都有一個微服務(wù)更新業(yè)務(wù)實體,然后發(fā)布觸發(fā)下一步的事件。下面的系列圖展示了如何使用事件驅(qū)動的方法在創(chuàng)建訂單時檢查可用信用。微服務(wù)通過消息代理來交換事件。
1. 訂單服務(wù)創(chuàng)建狀態(tài)為 NEW 的訂單,并發(fā)布“訂單已創(chuàng)建”事件。
2. 客戶服務(wù)獲取“訂單已創(chuàng)建”事件,為此訂單保留信用,發(fā)布“信用保留”事件。
3. 訂單服務(wù)獲取“信用保留”事件,把訂單狀態(tài)修改為 OPEN。
更為復(fù)雜的場景可能涉及更多的步驟,比如在核對客戶信用的同時預(yù)留庫存。
基于(a)每個服務(wù)自動更新數(shù)據(jù)庫和發(fā)布事件,以及(b)消息代理確保事件傳遞至少一次,用戶能夠跨多個服務(wù)完成業(yè)務(wù)邏輯。注意它們并非 ACID 業(yè)務(wù)。這種模式提供弱確定性,比如最終一致性。這種事務(wù)模型也被稱作 BASE 模型。
用戶也可以使用事件來維護不同微服務(wù)擁有的預(yù)連接數(shù)據(jù)的物化視圖。維護此視圖的服務(wù)訂閱相關(guān)事件,并更新視圖。例如,維護客戶訂單視圖的客戶訂單視圖更新服務(wù)會訂閱由客戶服務(wù)和訂單服務(wù)發(fā)布的事件。
當(dāng)客戶訂單查看更新服務(wù)收到客戶或者訂單事件,就會更新客戶訂單查看的數(shù)據(jù)存儲。用戶能夠使用類似 MongoDB 的文檔數(shù)據(jù)庫查看用戶訂單,并為每位客戶存儲一個文檔。用戶訂單預(yù)覽查詢服務(wù)通過客戶訂單預(yù)覽數(shù)據(jù)存儲,處理來自客戶和最近訂單的請求。
事件驅(qū)動的架構(gòu)有優(yōu)點也有缺點。它使得事務(wù)跨多個服務(wù)并提供最終一致性,也可以讓應(yīng)用維護物化視圖。缺點之一在于,它的編程模型要比使用 ACID 事務(wù)的更加復(fù)雜。為了從應(yīng)用級別的失效中恢復(fù),還需要完成補償性事務(wù),例如,如果信用檢查不成功則必須取消訂單。此外,由于臨時事務(wù)造成的改變顯而易見,因而應(yīng)用必須處理不一致的數(shù)據(jù)。此外,如果應(yīng)用從物化視圖中讀取的數(shù)據(jù)沒有更新時,也會遇到不一致的問題。此架構(gòu)的另一缺點就是用戶必須檢測并忽略重復(fù)事件。
三、實現(xiàn)原子化
事件驅(qū)動的架構(gòu)還存在以原子粒度更新數(shù)據(jù)庫并發(fā)布事件的問題。例如,訂單服務(wù)必須在訂單表中插入一行,然后發(fā)布“訂單已創(chuàng)建”事件。這兩個操作需要原子化實現(xiàn)。如果服務(wù)在更新數(shù)據(jù)庫之后、發(fā)布事件之前崩潰,系統(tǒng)變得不一致。確保原子化的標(biāo)準(zhǔn)做法是使用包含數(shù)據(jù)庫和消息代理的分布式事務(wù)。然而,基于以上描述的 CAP 理論,這并非我們所想。
使用本地事務(wù)發(fā)布事件
實現(xiàn)原子化的方法是使用多步驟進(jìn)程來發(fā)布事件,該進(jìn)程只包含本地事務(wù)。訣竅就是在存儲業(yè)務(wù)實體狀態(tài)的數(shù)據(jù)庫中,有一個事件表來充當(dāng)消息隊列。應(yīng)用啟動一個(本地)數(shù)據(jù)庫事務(wù),更新業(yè)務(wù)實體的狀態(tài),在事件表中插入一個事件,并提交該事務(wù)。獨立的應(yīng)用線程或進(jìn)程查詢事件表,將事件發(fā)不到消息代理,然后使用本地事務(wù)標(biāo)注事件并發(fā)布。下圖展示了這一設(shè)計。
訂單服務(wù)在訂單表中插入一行,然后在事件表中插入“訂單已創(chuàng)建”的事件。時間發(fā)布線程或進(jìn)程在事件表中查詢未發(fā)布的事件并發(fā)布,然后更新事件表,將該事件標(biāo)記為已發(fā)布。
這種方法優(yōu)缺點兼具。優(yōu)點之一是保證每個更新都有對應(yīng)的事件發(fā)布,并且無需依賴 2PC。此外,應(yīng)用發(fā)布業(yè)務(wù)級別的事件,消除了推斷事件的需要。這種方法也有缺點。由于開發(fā)者必須牢記發(fā)布事件,因此有很大可能出錯。此外這一方法對于某些使用 NoSQL 數(shù)據(jù)庫的應(yīng)用是個挑戰(zhàn),因為 NoSQL 本身交易和查詢能力有限。
通過此方法,應(yīng)用使用本地事務(wù)來更新狀態(tài)和發(fā)布事件,排除了對 2PC 的需要。接下來,我們了解使用應(yīng)用更新狀態(tài)實現(xiàn)原子化的方法。
挖掘數(shù)據(jù)庫事務(wù)日志
無需 2PC 實現(xiàn)原子化的另一種方式是由線程或者進(jìn)程通過挖掘數(shù)據(jù)庫事務(wù)或提交日志來發(fā)布事件。應(yīng)用更新數(shù)據(jù)庫,數(shù)據(jù)庫的事務(wù)日志記錄這些變更。事務(wù)日志挖掘線程或進(jìn)程讀取這些日志,并把事件發(fā)布到消息代理。如下圖所示:
這一方法的范例是開源的 LinkedIn Databus 項目。Databus 挖掘 Oracle 事務(wù)日志并發(fā)布與之對應(yīng)的事件。LinkedIn 使用 Databus 維持各種來源的數(shù)據(jù)存儲與記錄系統(tǒng)一致。
另一個范例則是 AWS DynamoDB 采用的流機制,AWS DynamoDB 是一個可管理的 NoSQL 數(shù)據(jù)庫。每個 DynamoDB 流包括 DynamoDB 表在過去 24 小時之內(nèi)的時序變化,包括創(chuàng)建、更新和刪除操作。應(yīng)用能夠讀取這些變更,將其作為事件發(fā)布。
事務(wù)日志挖掘具有多個優(yōu)點。首先,它能保證無需使用 2PC 就能針對每個更新發(fā)布事件。其次,通過將日志發(fā)布于應(yīng)用的業(yè)務(wù)邏輯分離,事務(wù)日志挖掘能夠簡化應(yīng)用。事務(wù)日志挖掘也有缺點,主要缺點就是事務(wù)日志的格式與每個數(shù)據(jù)庫對應(yīng),甚至隨著數(shù)據(jù)庫版本而變化。此外,很難從底層事務(wù)日志更新記錄中逆向工程這些業(yè)務(wù)事件。
通過讓應(yīng)用更新數(shù)據(jù)庫,事務(wù)日志挖掘消除了對 2PC 的需要。接下來我們會討論另一種方法——消除更新,只依賴事件。
使用事件源
通過采用一種截然不同的、以事件為中心的方法來留存業(yè)務(wù)實體,事件源無需 2PC 實現(xiàn)了原子化。不同于存儲實體的當(dāng)前狀態(tài),應(yīng)用存儲狀態(tài)改變的事件序列。應(yīng)用通過重播事件來重構(gòu)實體的當(dāng)前狀態(tài)。每當(dāng)業(yè)務(wù)實體的狀態(tài)改變,新事件就被附加到事件列表。鑒于保存事件是一個單一的操作,本質(zhì)上也是原子化的。
要了解事件源如何運行,可以以訂單實體為例。在傳統(tǒng)的方法中,每個訂單映射為訂單表的一行,例如一個 ORDERLINEITEM 表。使用事件源的時候,訂單服務(wù)以狀態(tài)更改事件的方式存儲訂單,包括已創(chuàng)建、已批準(zhǔn)、已發(fā)貨、已取消等。每個事件都包含足夠的數(shù)據(jù)去重建訂單狀態(tài)。
事件長期保存在事件數(shù)據(jù)庫,使用 API 添加和檢索實體的事件。事件存儲類似上文提及的消息代理,通過 API 讓服務(wù)訂閱事件,將所有事件傳達(dá)到所有感興趣的訂閱者。事件存儲是事件驅(qū)動的微服務(wù)架構(gòu)的支柱。
事件源有不少優(yōu)點。它解決了實施事件驅(qū)動的微服務(wù)架構(gòu)時的一個關(guān)鍵問題,能夠只要狀態(tài)改變就可靠地發(fā)布事件。另外,它也解決了微服務(wù)架構(gòu)中的數(shù)據(jù)一致性問題。由于儲存事件而不是域?qū)ο?,它也避免了對象關(guān)系抗阻不匹配的問題(object?relational impedance mismatch problem)。事件源提供了 100% 可靠的業(yè)務(wù)實體變化的審計日志,使得獲取任何時間點的實體狀態(tài)成為可能。事件源的另一大優(yōu)勢在于業(yè)務(wù)邏輯由松耦合的、事件交換的業(yè)務(wù)實體構(gòu)成,便于從單體應(yīng)用向微服務(wù)架構(gòu)遷移。
事件源也有缺點。由于采用了不同或不熟悉的編程風(fēng)格,會有學(xué)習(xí)曲線。事件存儲只直接支持通過主鍵查詢業(yè)務(wù)實體,用戶還需要使用 Command Query Responsibility Segregation (CQRS) 來完成查詢。因此,應(yīng)用必須處理最終一致的數(shù)據(jù)。
四、總結(jié)
在微服務(wù)架構(gòu)中,每個微服務(wù)都有其私有數(shù)據(jù)存儲,不同的微服務(wù)可能使用不同的 SQL 和 NoSQL 數(shù)據(jù)庫。這些數(shù)據(jù)庫架構(gòu)帶來便利的同時,也給分布式數(shù)據(jù)管理帶來挑戰(zhàn)。第一個挑戰(zhàn)就是如何實現(xiàn)業(yè)務(wù)事務(wù),保持多個服務(wù)的一致性。第二個挑戰(zhàn)就是如何從多個服務(wù)中檢索數(shù)據(jù),實現(xiàn)查詢。
對于許多應(yīng)用,解決方案就是使用事件驅(qū)動的架構(gòu)。事件驅(qū)動的架構(gòu)帶來的挑戰(zhàn)是如何原子化地更新狀態(tài)和發(fā)布事件。有幾個方法可以做到這一點,包括把數(shù)據(jù)庫用作消息隊列、事務(wù)日志挖掘和事件源。







