微服務(wù)下分布式事務(wù)模式的詳細(xì)對比
點擊上方“服務(wù)端思維”,選擇“設(shè)為星標(biāo)”
回復(fù)”669“獲取獨家整理的精選資料集
回復(fù)”加群“加入全國服務(wù)端高端社群「后端圈」

作為 Red Hat 咨詢架構(gòu)師,我有幸參與了大量客戶項目。雖然每個客戶都面臨自己特有的挑戰(zhàn),但是我發(fā)現(xiàn)其中有一些共同點。大多數(shù)項目都想知道如何協(xié)調(diào)對多個記錄系統(tǒng)的寫入。要回答這個問題,一般會涉及長篇累牘的解釋,包括雙重寫入(dual write)、分布式事務(wù)、現(xiàn)代化的替代方案以及每種方式可能出現(xiàn)的故障情況和缺點。這樣做通常會讓客戶意識到,將單體應(yīng)用拆分為微服務(wù)架構(gòu)是一個漫長和復(fù)雜的過程,而且通常都需要權(quán)衡。
本文不會深入介紹事務(wù)的細(xì)節(jié),而是總結(jié)了向多個數(shù)據(jù)源協(xié)調(diào)寫入操作的主要方式和模式。我知道,你可能對這些方法有過美好或糟糕的經(jīng)驗。但是實踐中,在正確的環(huán)境和正確的限制條件下,這些方法都能很好地工作。技術(shù)領(lǐng)導(dǎo)者要為自己的環(huán)境選擇最好的方式。
關(guān)于你是否會面臨雙重寫入的問題有一個簡單的指標(biāo),那就是預(yù)期要不要向多個記錄系統(tǒng)進(jìn)行寫入操作。這樣的需求可能并不明顯,在分布式系統(tǒng)設(shè)計的過程中,它可能會以不同的方式進(jìn)行表述。比如說:
你已經(jīng)為每項工作選擇了最佳工具,現(xiàn)在在一個業(yè)務(wù)事務(wù)中,你必須要更新一個 NoSQL 數(shù)據(jù)庫、一個搜索索引和一個緩存。 你所設(shè)計的服務(wù)必須要更新自己的數(shù)據(jù)庫,同時還要把變更相關(guān)的信息以通知的形式發(fā)送給另一個服務(wù)。 你的業(yè)務(wù)事務(wù)跨越了多個服務(wù)的邊界。 你可能需要以冪等的方式實現(xiàn)服務(wù)操作,因為服務(wù)的消費者必須要重試失敗的調(diào)用。
在本文中,我們將會使用一個很簡單的示例場景來評估在分布式事務(wù)中處理雙重寫入的各種方法。我們的場景是一個客戶端應(yīng)用,它會在發(fā)生變更操作的時候,調(diào)用一個微服務(wù)。服務(wù) A 要更新自己的數(shù)據(jù)庫,但是它還要調(diào)用服務(wù) B 進(jìn)行寫入操作,如圖 1 所示。至于數(shù)據(jù)庫的實際類型以及服務(wù)與服務(wù)之間進(jìn)行交互的協(xié)議,這些對于我們的討論都無關(guān)緊要,因為問題都是一樣的。
微服務(wù)中的雙重寫入問題
我們簡要解釋一下為什么這個問題沒有簡單的解決方案。如果服務(wù) A 寫入到了自己的數(shù)據(jù)庫,然后發(fā)送一個通知到隊列中供服務(wù) B 使用(我們將這種方式稱為 local-commit-then-publish),這樣應(yīng)用依然有可能無法可靠地運行。當(dāng)服務(wù) A 寫入到自己的數(shù)據(jù)庫,然后發(fā)送消息到隊列時,依然有很小的概率發(fā)生這樣的事情,即應(yīng)用在提交到數(shù)據(jù)庫后,且在第二個操作之前,發(fā)生了崩潰,這樣的話,就會使系統(tǒng)處于一個不一致的狀態(tài)。如果消息在寫入到數(shù)據(jù)庫之前發(fā)送的話(我們將這種方式稱為 publish-then-local-commit),有可能出現(xiàn)數(shù)據(jù)庫寫入失敗,或者服務(wù) B 接收到事件的時候,服務(wù) A 還沒有提交到數(shù)據(jù)庫,這會出現(xiàn)時效性問題。不管是出現(xiàn)哪種情況,這種場景都會涉及到對數(shù)據(jù)庫和隊列的雙重寫入問題,這就是我們要探討的核心問題。在下面的章節(jié)中,我們將會討論針對這一長期存在的挑戰(zhàn)目前已有的各種解決方案。
將應(yīng)用程序開發(fā)為模塊化單體看起來像一種權(quán)宜之計(hack),或是架構(gòu)演化的一種倒退。但是,我發(fā)現(xiàn)它在實踐中能夠很好地運行。它不是一種微服務(wù)的模式,而是微服務(wù)規(guī)則的一個例外情況,能夠非常嚴(yán)謹(jǐn)?shù)嘏c微服務(wù)相結(jié)合。如果強寫入一致性是驅(qū)動性的需求,甚至要比獨立部署和擴展微服務(wù)的能力更重要時,那么我們就可以采用模塊化單體的架構(gòu)。
采用單體架構(gòu)并不意味著系統(tǒng)設(shè)計得很差或者是件壞事。它并不說明任何質(zhì)量相關(guān)的問題。顧名思義,這是一個按照模塊化方式設(shè)計的系統(tǒng),它只有一個部署單元。需要注意,這是一個精心設(shè)計和實現(xiàn)的模塊化單體,這與隨意創(chuàng)建并隨時間而不斷增長的單體是不同的。在精心設(shè)計的模塊化單體架構(gòu)中,每個模塊都遵循微服務(wù)的原則。每個模塊會封裝對其數(shù)據(jù)的所有訪問,但是操作是以內(nèi)存方法調(diào)用的方式進(jìn)行暴露和消費的。
如果采用這種方式的話,我們必須要將兩個微服務(wù)(服務(wù) A 和服務(wù) B)轉(zhuǎn)換成可以部署到共享運行時的庫模塊(library module)。然后,讓這兩個微服務(wù)共享同一個數(shù)據(jù)庫實例。因為服務(wù)是在一個通用的運行中編寫和部署的,所以它們可以參與相同的事務(wù)。鑒于這些模塊共享同一個數(shù)據(jù)庫實例,所以我們可以使用本地事務(wù)一次性地提交或回滾所有的變更。在部署方法方面也有差異,因為我們希望模塊以庫的方式部署到一個更大的部署單元中,并參與現(xiàn)有的事務(wù)。
即便是在單體架構(gòu)中,也有一些方式來隔離代碼和數(shù)據(jù)。例如,我們可以將模塊隔離成單獨的包、構(gòu)建模塊和源碼倉庫,這些模塊可以由不同的團隊所擁有。通過將表按照命名規(guī)則、模式、數(shù)據(jù)庫實例,甚至數(shù)據(jù)庫服務(wù)器的方式進(jìn)行分組,我們可以實現(xiàn)數(shù)據(jù)的部分隔離。圖 2 的靈感來源于 Axel Fontaine 關(guān)于偉大的模塊化單體的演講,它闡述了應(yīng)用中不同的代碼和數(shù)據(jù)隔離級別。
應(yīng)用程序的代碼和數(shù)據(jù)隔離級別
拼圖的最后一塊是使用一個運行時和一個包裝器服務(wù)(wrapper service),該服務(wù)能夠消費其他的模塊并將其納入到現(xiàn)有事務(wù)的上下文中。所有的這些限制使模塊比典型的微服務(wù)耦合更緊密,但是好處在于包裝器服務(wù)能夠啟動一個事務(wù)、調(diào)用庫模塊來更新它們的數(shù)據(jù)庫,并且以一個操作的形式提交或回滾事務(wù),而不必?fù)?dān)心部分失敗或最終一致性的問題。
在我們的樣例中,如圖 3 所示,我們將服務(wù) A 和服務(wù) B 轉(zhuǎn)換為庫,并將它們部署到一個共享的運行時中,或者也可以將其中的某個服務(wù)作為共享運行時。數(shù)據(jù)庫的表也共享同一個數(shù)據(jù)庫實例,但是它會被拆分為一組由各自的庫服務(wù)管理的表。
具有共享數(shù)據(jù)庫的模塊化單體
在有些行業(yè)中,這種架構(gòu)的收益遠(yuǎn)比其他地方所看重的更快的交付以及更快的變更節(jié)奏重要得多。表 1 總結(jié)了模塊化單體架構(gòu)的優(yōu)點和缺點。
表 1:模塊化單體架構(gòu)的優(yōu)點和缺點
分布式事務(wù)通常是最后的方案,通常會在如下的情況下使用:
當(dāng)對不同資源的寫入操作不允許最終一致性時;
當(dāng)我們必須要寫入到不同種類的數(shù)據(jù)源時;
當(dāng)我們需要確保對消息的處理有且僅有一次,而且無法重構(gòu)系統(tǒng)以實現(xiàn)操作的冪等性時;
當(dāng)與第三方黑盒系統(tǒng)或?qū)崿F(xiàn)了兩階段提交規(guī)范的遺留系統(tǒng)進(jìn)行集成時。
在這些情況下,如果可擴展性不是重要的關(guān)注點的話,我們可以考慮將分布式事務(wù)作為一種可選方案。
兩階段提交技術(shù)要求我們有一個分布式事務(wù)管理器(如 Narayana)和一個可靠的事務(wù)日志存儲層。我們還需要能夠兼容 DTP XA 的數(shù)據(jù)源,以及能夠參與分布式事務(wù)的相關(guān)的 XA 驅(qū)動,比如 RDBMS、消息代理和緩存。如果你足夠幸運有合適的數(shù)據(jù)源,但是運行在一個動態(tài)環(huán)境中,比如 Kubernetes,那么你還需要有一個像 operator 這樣的機制,以確保分布式事務(wù)管理器只有一個實例。事務(wù)管理器必須是高可用的,并且必須能夠訪問事務(wù)日志。
就實現(xiàn)而言,你可以嘗試使用 Snowdrop Recovery Controller,它使用 Kubernetes StatefulSet 模式來實現(xiàn)單例,并使用持久化卷來存儲事務(wù)日志。在這個類別中,我還包含了適用于 SOAP Web 服務(wù)的 Web Services Atomic Transaction(WS-AtomicTransaction)等規(guī)范。所有這些技術(shù)的共同點在于它們實現(xiàn)了 XA 規(guī)范,并且有一個中心化的事務(wù)協(xié)調(diào)器。
在我們的樣例中,如圖 4 所示,服務(wù) A 使用分布式事務(wù)提交所有的變更到自己的數(shù)據(jù)庫中,并且會提交一條消息到隊列中,這個過程中不會出現(xiàn)消息的重復(fù)和丟失。類似的,服務(wù) B 可以使用分布式服務(wù)來消費消息,并在同一個事務(wù)中提交至數(shù)據(jù)庫 B,這個過程中也不會出現(xiàn)任何的重復(fù)數(shù)據(jù)?;蛘?,服務(wù) B 也可以選擇不使用分布式事務(wù),而是使用本地事務(wù)并實現(xiàn)冪等的消費者模式。在本節(jié)中,一個更合適的例子是使用 WS-AtomicTransaction 在一個事務(wù)中協(xié)調(diào)對數(shù)據(jù)庫 A 和數(shù)據(jù)庫 B 的寫入,并完全避免最終一致性。但是,現(xiàn)在這種方式已經(jīng)不太常見了。
跨數(shù)據(jù)庫和消息代理的二階段提交
兩階段提交協(xié)議所提供的保障與模塊化單體中的本地事務(wù)類似,但有些例外情況。因為這里有兩個或更多的獨立數(shù)據(jù)源參與到原子更新之中,所以它們可能會以不同的方式失敗并阻塞整個事務(wù)。但是,由于存在一個中心化的協(xié)調(diào)者,相對于我下面將要討論的其他方式,我們還是能夠很容易地發(fā)現(xiàn)分布式系統(tǒng)的狀態(tài)。
表 2:兩階段提交的優(yōu)點和缺點
對于模塊化單體來講,我們會使用本地事務(wù),這樣我們始終能夠知道系統(tǒng)的狀態(tài)。對基于兩階段提交的分布式事務(wù),我們也能保證狀態(tài)的一致性。唯一的例外情況是事務(wù)協(xié)調(diào)者出現(xiàn)了不可恢復(fù)的故障。但是,如果我們想要減弱一致性的需求,而希望能夠了解整個分布式系統(tǒng)的狀態(tài),并且能從一個地方對其進(jìn)行協(xié)調(diào),那么我們該怎么處理呢?
在這種情況下,我們可以考慮采取一種編排(orchestration)的方式,在這里,某個服務(wù)會擔(dān)任整個分布式狀態(tài)變更的協(xié)調(diào)者和編排者。編排者服務(wù)有責(zé)任調(diào)用其他的服務(wù),直至它們達(dá)到所需的狀態(tài),或者在它們出現(xiàn)故障的時候執(zhí)行糾正措施。編排者使用它的本地數(shù)據(jù)庫來跟蹤狀態(tài)變更,并且要負(fù)責(zé)恢復(fù)與狀態(tài)變更的所有故障。
編排式技術(shù)最流行的實現(xiàn)是 BPMN 規(guī)范的各種具體實現(xiàn),比如 jBPM 和 Camunda。對這種系統(tǒng)的需求并不會因為微服務(wù)或 Serverless 這樣的極度分布式架構(gòu)的出現(xiàn)而消失,相反,這種需求還會增加。為了證明這一點,我們可以看一下較新的有狀態(tài)編排引擎,它們沒有遵循什么規(guī)范,但是卻提供了類似的有狀態(tài)行為,比如 Netflix 的 Conductor、Uber 的 Cadence 和 Apache 的 Airflow。像 Amazon StepFunctions、Azure Durable Functions 和 Azure Logic Apps 這樣的 Serverless 有狀態(tài)函數(shù)也屬于這個類別。還有一些開源庫允許我們實現(xiàn)有狀態(tài)的協(xié)調(diào)和回滾行為,如 Apache Camel 的 Saga 模式實現(xiàn)和 NServiceBus 的 Saga 功能。許多實現(xiàn) Saga 模式的自定義系統(tǒng)也屬于這一類。
編排兩個服務(wù)的分布式事務(wù)
在我們的示例圖中,我們讓服務(wù) A 作為有狀態(tài)的編排者,負(fù)責(zé)調(diào)用服務(wù) B 并在需要的時候通過補償操作從故障中恢復(fù)。這種方式的關(guān)鍵特征是,服務(wù) A 和服務(wù) B 有本地事務(wù)的邊界,但是服務(wù) A 有協(xié)調(diào)整個交互流程的知識和責(zé)任。這也是為什么它的事務(wù)邊界會接觸到服務(wù) B 的端點。在實現(xiàn)方面,我們可以使用同步的交互,就像上圖所示,也可以在服務(wù)之間使用消息隊列(在這種情況下我們也可以使用兩階段提交)。
編排式是一種最終一致的方法,它可能會涉及到重試和回滾才能使分布式系統(tǒng)達(dá)到一致的狀態(tài)。雖然避免了對分布式事務(wù)的需求,但是編排的方式要求參與的服務(wù)提供冪等的操作,以防協(xié)調(diào)者必須進(jìn)行重試操作。參與的服務(wù)還必須要提供恢復(fù)端點,以防協(xié)調(diào)者決定執(zhí)行回滾并修復(fù)全局狀態(tài)。這種方式的最大優(yōu)點是,能夠僅通過本地事務(wù)就能驅(qū)動那些可能不支持分布式事務(wù)的異構(gòu)服務(wù)達(dá)到一致的狀態(tài)。協(xié)調(diào)者和參與的服務(wù)只需要本地事務(wù)即可,而且始終能夠通過協(xié)調(diào)者查詢系統(tǒng)的狀態(tài),即便它目前可能處于部分一致的狀態(tài)。在下面我所描述的其他方式中,是不可能實現(xiàn)這一點的。
表 3:編排式的優(yōu)點和缺點
從迄今為止的討論中,我們可以看到,一個業(yè)務(wù)操作可能會導(dǎo)致服務(wù)間的多次調(diào)用,并且一個業(yè)務(wù)事務(wù)完成端到端的處理所需的時間是不確定的。為了管理這一點,編排式(orchestration)模式會使用一個中心化的控制器服務(wù),它會告訴參與者該做什么。
編排式的一種替代方案就是協(xié)同式(choreography),在這種風(fēng)格的服務(wù)協(xié)調(diào)中,參與者在交換事件時沒有一個中心化的控制點。在這種模式下,每個服務(wù)會執(zhí)行一個本地事務(wù)并發(fā)布事件,從而觸發(fā)其他服務(wù)中的本地事務(wù)。系統(tǒng)中的每個組件都要參與業(yè)務(wù)事務(wù)工作流的決策,而不是依賴一個中心化的控制點。在歷史上,協(xié)同式方式最常見的實現(xiàn)就是使用異步消息層來進(jìn)行服務(wù)的交互。圖 6 說明了協(xié)同式模式的基本架構(gòu)。
通過消息層進(jìn)行服務(wù)協(xié)同化
為了實現(xiàn)基于消息的服務(wù)協(xié)同,我們需要每個參與的服務(wù)執(zhí)行一個本地事務(wù),并通過向消息基礎(chǔ)設(shè)施發(fā)布一個命令或事件,以觸發(fā)下一個服務(wù)。同樣的,其他參與的服務(wù)必須消費一個消息并執(zhí)行本地事務(wù)。從本質(zhì)上來講,這就是在一個較高層級的雙重寫入問題中又出現(xiàn)了另一個雙重寫入的問題。當(dāng)我們開發(fā)一個具有雙重寫入的消息層來實現(xiàn)協(xié)同式模式的時候,我們可以把它設(shè)計成跨本地數(shù)據(jù)庫和消息代理的一個兩階段提交。在前面,我們曾經(jīng)介紹過這種方式。另外,我們也可以采用 publish-then-local-commit 或 local-commit-then-publish 模式:
Publish-then-local-commit:我們可以先嘗試發(fā)布一條消息,然后再提交本地事務(wù)。雖然這種方案聽起來不錯,但是它有一些切實的挑戰(zhàn)。舉例來說,在很多時候,我們需要發(fā)布一個由本地事務(wù)所生成的 ID,而這個 ID 此時還沒有生成,因此無法發(fā)布。另外,本地事務(wù)有可能會失敗,但是我們無法回滾已經(jīng)發(fā)布的消息。這種方式缺乏“讀取自己的寫入”的語義,因此對于大多數(shù)場景來說,這并不是合適的方案。
Local-commit-then-publish:一個稍好一點的辦法是先提交本地事務(wù),然后再發(fā)布消息。在本地事務(wù)提交之后和消息發(fā)布之前這里有很小的概率會出現(xiàn)故障。但即便是出現(xiàn)這樣的情況,你也可以把服務(wù)設(shè)計成冪等的并對操作進(jìn)行重試。這意味著會再次提交本地事務(wù)并發(fā)布消息。如果你能控制下游的消費者并且確保它們是冪等的,那么這種方式就是行之有效的??傮w而言,這是一個很好的實現(xiàn)方案。
實現(xiàn)協(xié)同式架構(gòu)的各種實現(xiàn)方式都限制每個服務(wù)都要通過本地事務(wù)寫入到單一的數(shù)據(jù)源中,而不能寫入到其他的地方中。我們看一下,如何在避免雙重寫入的情況下實現(xiàn)這一點。
假設(shè)服務(wù) A 接收到一個請求并要對數(shù)據(jù)庫 A 進(jìn)行寫入操作,除此之外不再操作其他的數(shù)據(jù)源。服務(wù) B 周期性地輪詢服務(wù) A 并探測新的變更。當(dāng)它讀取到變更時,服務(wù) B 會基于變更更新自己的數(shù)據(jù)庫,并且會更新索引或時間戳來標(biāo)記獲取到了變更。這里的關(guān)鍵在于,這兩個服務(wù)只對自己的數(shù)據(jù)庫進(jìn)行寫入操作,并以本地事務(wù)的形式進(jìn)行提交。如圖 7 所示,這種方式可以描述為服務(wù)協(xié)同(service choreography),或者我們也可以用非常古老的數(shù)據(jù)管道的術(shù)語來對其進(jìn)行描述。至于可供選用的實現(xiàn)方案就更有趣了。
通過輪詢實現(xiàn)的服務(wù)協(xié)同
對于服務(wù) B 來說,最簡單的場景就是連接到服務(wù) A 的數(shù)據(jù)庫并讀取服務(wù) A 的表。但是,業(yè)界會盡量避免共享數(shù)據(jù)表這種級別的耦合,原因在于:服務(wù) A 的實現(xiàn)和數(shù)據(jù)模型的任意變更都可能干擾到服務(wù) B。我們可以對這種場景做一些改進(jìn),例如使用發(fā)件箱(Outbox)模式,為服務(wù) A 提供一個表作為公開接口。這個表可以只包含服務(wù) B 所需的內(nèi)容,它可以設(shè)計得易于查詢和跟蹤變更。如果你覺得這還不夠好的話,進(jìn)一步的改進(jìn)方案是讓服務(wù) B 通過 API 管理層查詢服務(wù) A 的所有變化,而不是直接連接數(shù)據(jù)庫 A。
從根本上來講,所有的這些變種形式都有一個相同的缺點:服務(wù) B 需要不斷地輪詢服務(wù) A。這種方式會給系統(tǒng)帶來不必要的持續(xù)負(fù)載,或者在接收變更時存在不必要的延遲。輪詢微服務(wù)的變更并不是常見的做法,那么我們看一下如何進(jìn)一步改善這個架構(gòu)。
在改進(jìn)協(xié)同式架構(gòu)時,有一種方式很有吸引力,那就是引入像 Debezium 這樣的工具,它使用數(shù)據(jù)庫 A 的事務(wù)日志執(zhí)行變更數(shù)據(jù)捕獲(change data capture,CDC)。這種方式如圖 8 所示。
通過變更數(shù)據(jù)捕獲實現(xiàn)的服務(wù)協(xié)同
Debezium 可以監(jiān)控數(shù)據(jù)庫的事務(wù)日志,執(zhí)行必要的過濾和轉(zhuǎn)換,并將相關(guān)的變更投遞到 Apache Kafka 的主題中。這樣的話,服務(wù) B 就可以監(jiān)聽主題中的通用事件,而不是輪詢服務(wù) A 的數(shù)據(jù)庫或 API。我們通過這種方式,將數(shù)據(jù)庫輪詢轉(zhuǎn)換成了流式變更,并且在服務(wù)間引入了一個隊列,這樣會使得分布式系統(tǒng)更加可靠、可擴展,而且為新的使用場景會引入其他消費者提供了可能性。Debezium 提供了一種優(yōu)雅的方式來實現(xiàn)發(fā)件箱模式,能夠用于基于編排式和協(xié)同式的 Saga 模式實現(xiàn)。
這種方式的一個副作用在于,服務(wù) B 有接收到重復(fù)消息的可能性。這可以通過實現(xiàn)冪等的服務(wù)來解決,可以在業(yè)務(wù)邏輯層面來解決,也可以使用技術(shù)化的去重器(deduplicator,比如 Apache ActiveMQ Artemis 的重復(fù)消息探測或者 Apache Camel 的冪等消費者模式)。
事件溯源(event sourcing)是另外一種服務(wù)協(xié)同的實現(xiàn)模式。在這種模式下,實體的狀態(tài)會被存儲為一系列的狀態(tài)變更事件。當(dāng)有新的更新時,不是更新實體的狀態(tài),而是往事件的列表中追加一個新的事件。往事件存儲中追加新的事件是一個原子性的操作,會在一個本地事務(wù)中完成。如圖 9 所示,這種方式的好處在于,對于消費數(shù)據(jù)更新的其他服務(wù)來講,事件存儲的行為也是一個消息隊列。
通過事件溯源實現(xiàn)的服務(wù)協(xié)同
在我們樣例中,如果要轉(zhuǎn)換成使用事件溯源的話,要把客戶端的請求存儲在一個只能進(jìn)行追加操作的事件存儲中。服務(wù) A 可以通過重放(replay)事件重新構(gòu)建當(dāng)前的狀態(tài)。事件存儲需要讓服務(wù) B 也訂閱相同的更新事件。通過這種機制,服務(wù) A 使用其存儲層作為與其他服務(wù)的通信層。盡管這種機制非常整潔,解決了當(dāng)有狀態(tài)變更時可靠地發(fā)布事件的問題,但是它引入了一種很多開發(fā)人員所不熟悉的編程風(fēng)格,并且圍繞狀態(tài)重建和消息壓縮,會引入額外的復(fù)雜性,這需要專門的存儲。
不管使用哪種方式來檢索數(shù)據(jù)變更,協(xié)同式的模式都解耦了寫入,能夠?qū)崿F(xiàn)獨立的服務(wù)可擴展性,并提升系統(tǒng)整體的彈性。這種方式的缺點在于,決策流是分散的,很難發(fā)現(xiàn)全局的分布式狀態(tài)。要查看一個請求的狀態(tài)需要查詢多個數(shù)據(jù)源,這對于服務(wù)數(shù)量眾多的場景來說是一個挑戰(zhàn)。表 4 總結(jié)了這種方式的優(yōu)點和缺點。
表 4:協(xié)同式的優(yōu)點和缺點
在協(xié)同式模式中,沒有一個中心化的地方可以查詢系統(tǒng)的狀態(tài),但是會有一個服務(wù)的序列,以便于在分布式系統(tǒng)中傳播狀態(tài)。協(xié)同式模式創(chuàng)建了一個處理服務(wù)的序列化管道,所以我們能夠知道當(dāng)一個消息到達(dá)整個過程的特定步驟時,它肯定已經(jīng)通過了前面的所有步驟。如果我們能夠放松這個限制,允許獨立地處理這些步驟的話,情況又會怎樣呢?在這種場景下,服務(wù) B 在處理一個請求的時候,根本不用關(guān)心服務(wù) A 是否已經(jīng)處理過它。
在并行管道的方式中,我們會添加一個路由服務(wù),該服務(wù)接收請求,并在一個本地事務(wù)中通過消息代理將請求轉(zhuǎn)發(fā)至服務(wù) A 和服務(wù) B。如圖 10 所示,從這個步驟開始,兩個服務(wù)可以獨立、并行地處理請求。
通過并行管道進(jìn)行處理
盡管這種模式很容易實現(xiàn),但是它只適用于服務(wù)之間沒有時間約束的場景。例如,服務(wù) B 不管服務(wù) A 是否已經(jīng)處理過該請求,它都能夠?qū)φ埱筮M(jìn)行處理。同時,這種方式需要一個額外的路由服務(wù),或者客戶端知道服務(wù) A 和服務(wù) B,從而能夠給它們發(fā)送消息。
這種方式有一種輕量級的替代方案,被稱為“監(jiān)聽自身(listen to yourself)”模式,在這里,其中有個服務(wù)會同時擔(dān)任路由。在這種替代方式下,當(dāng)服務(wù) A 接收到一個請求時,它不會寫入到自己的數(shù)據(jù)庫中,而是將請求發(fā)送至消息系統(tǒng)中,而消息的目標(biāo)是服務(wù) B 以及服務(wù) A 本身。圖 11 闡述了這種模式。
監(jiān)聽自身模式
在這里,不寫入數(shù)據(jù)庫的原因在于避免雙重寫入。當(dāng)進(jìn)入消息系統(tǒng)之后,消息會在完全獨立的事務(wù)上下文中進(jìn)入服務(wù) B,也會重新返回服務(wù) A。通過這樣一個曲折的處理流程,服務(wù) A 和服務(wù) B 就可以獨立地處理請求,并寫入到各自的數(shù)據(jù)庫中了。
表 5:并行管道的優(yōu)點和缺點
從本文的論述中,你可能已經(jīng)猜到,在微服務(wù)架構(gòu)中,處理分布式事務(wù)并沒有正確或錯誤的模式。每種模式都有其優(yōu)點和缺點。每種模式都能解決一些問題,但是反過來又會產(chǎn)生其他的問題。圖 12 中的圖表簡單總結(jié)了我所闡述的各種雙重寫入模式的主要特征。
雙重寫入模式的特征
不管你采用哪種方式,都要闡述和記錄決策背后的動機,以及該選擇在架構(gòu)上所帶來的長期影響。你還需要得到從長期實現(xiàn)和維護該系統(tǒng)的團隊那里獲取支持。在這里,我根據(jù)數(shù)據(jù)一致性和可擴展性特征來組織和評估本文所描述的各種方法,如圖 13 所示。
各個雙重寫入模式的數(shù)據(jù)一致性和可擴展性特征
我們從可擴展性最強、可用性最高的方法到可擴展性最差、可用性最低的順序來評估各種方法。
如果你的步驟在時間上是解耦的,那么采用并行管道的方法來運行是很合適的。有可能你只能在系統(tǒng)的某些部分使用這種模式,而不是在整個系統(tǒng)中。接下來,假設(shè)步驟間存在時間方面的耦合性,特定的操作和服務(wù)必須要在其他的服務(wù)前執(zhí)行,那么你可以考慮采用協(xié)同式的方式。借助協(xié)同式的服務(wù),我們可以創(chuàng)建一個可擴展的、事件驅(qū)動的架構(gòu),在這里消息會通過一個去中心化的協(xié)同化過程在服務(wù)和服務(wù)之間流動。在這種情況下,使用 Debezium 和 Apache Kafka 的發(fā)件箱模式實現(xiàn)(如 Red Hat OpenShift Streams for Apache Kafka)特別有趣,而且越來越受歡迎
如果協(xié)同式模式不是很合適,你需要一個負(fù)責(zé)協(xié)調(diào)和決策的中心點,那么可以考慮采用編排式模式。這是一個流行的架構(gòu),有基于標(biāo)準(zhǔn)的和自定義的開源實現(xiàn)?;跇?biāo)準(zhǔn)的實現(xiàn)可能會強迫你使用某些事務(wù)語義,而自定義的編排式實現(xiàn)則允許你在所需的數(shù)據(jù)一致性和可擴展性之間進(jìn)行權(quán)衡。
如果你沿著圖示再往左走的話,那么很可能你對數(shù)據(jù)一致性有非常強烈的需求,而且對它所需的重大權(quán)衡有充分的思想準(zhǔn)備。在這種情況下,針對特定數(shù)據(jù)源,通過兩階段提交的分布式事務(wù)是可行的,但是在專門為可擴展性和高度可用性設(shè)計的動態(tài)云環(huán)境中,它很難可靠地實現(xiàn)。如果是這樣的話,那么你可以直接采用比較老式的模塊化單體方式,同時伴以從微服務(wù)運動中學(xué)到的實踐。這種方式可以確保最高的數(shù)據(jù)一致性,但代價是運行時和數(shù)據(jù)源的耦合。
結(jié)論??
在具有數(shù)十個服務(wù)的大型分布式系統(tǒng)中,并不會有一個適用于所有場景的方式,我們需要將其中的幾個方法結(jié)合起來,應(yīng)用于不同的環(huán)境中。我們可能會將幾個服務(wù)部署在一個共享的運行時上,以滿足對數(shù)據(jù)一致性的特殊需求。我們可能會選擇兩階段的提交來與支持 JTA 的遺留系統(tǒng)進(jìn)行集成。我們可能會編排復(fù)雜的業(yè)務(wù)流程,并讓其余的服務(wù)使用協(xié)同式模式和并行處理??偠灾氵x擇什么策略并不重要,重要的是基于正確的原因,精心選擇一個策略,并執(zhí)行它。
— 本文結(jié)束 —

●?漫談設(shè)計模式在 Spring 框架中的良好實踐
關(guān)注我,回復(fù) 「加群」 加入各種主題討論群。
對「服務(wù)端思維」有期待,請在文末點個在看
喜歡這篇文章,歡迎轉(zhuǎn)發(fā)、分享朋友圈


