如何基于 DDD 构建微服务?
本文將討論微服務(wù)與 DDD 涉及到的概念、策劃和設(shè)計(jì)方法,并且嘗試將一個(gè)單體應(yīng)用拆分成多個(gè)基于 DDD 的微服務(wù)。
微服務(wù)的定義
微服務(wù)中的“微”雖然表示服務(wù)的規(guī)模,但它并不是使應(yīng)用程序成為微服務(wù)的唯一標(biāo)準(zhǔn)。當(dāng)團(tuán)隊(duì)轉(zhuǎn)向基于微服務(wù)的架構(gòu)時(shí),他們的目標(biāo)是提高敏捷性,即自主且頻繁地部署功能。
因此,很難給微服務(wù)架構(gòu)風(fēng)格下一個(gè)簡單的定義。我喜歡 Adrian Cockcroft 關(guān)于微服務(wù)的簡短定義:“面向服務(wù)的架構(gòu)由具有界限上下文、松散耦合的元素組成。”
盡管這定義了一種高級的設(shè)計(jì)啟發(fā)式方法,但微服務(wù)架構(gòu)具有的特性,使其有別于以往的面向服務(wù)架構(gòu)。根據(jù)以往的文章,我們總結(jié)了微服務(wù)架構(gòu)應(yīng)具備的一些特征:
服務(wù)以業(yè)務(wù)上下文為中心定義了良好的邊界,而不是以任意的技術(shù)抽象為中心;
隱藏實(shí)現(xiàn)細(xì)節(jié),并通過意圖接口暴露功能;
服務(wù)不會共享超出其邊界的內(nèi)部結(jié)構(gòu),例如不共享數(shù)據(jù)庫;
服務(wù)具有故障快速恢復(fù)能力;
團(tuán)隊(duì)職能獨(dú)立,能夠自主發(fā)布變更;
團(tuán)隊(duì)擁護(hù)自動化文化,例如自動化測試、持續(xù)集成和持續(xù)交付。
簡而言之,我們可以將這種架構(gòu)風(fēng)格總結(jié)如下:
松散耦合的面向服務(wù)的架構(gòu),其中每個(gè)服務(wù)都封裝在定義良好的界限上下文中,支持應(yīng)用程序快速、頻繁且可靠的交付。
領(lǐng)域驅(qū)動設(shè)計(jì)和界限上下文
微服務(wù)的強(qiáng)大之處在于清晰地定義了它們的職責(zé)并劃定了它們之間的邊界。它的目的是在邊界內(nèi)建立高內(nèi)聚,在邊界外建立低耦合。也就是說,傾向于一起改變的事物應(yīng)該放在一起。正如現(xiàn)實(shí)生活中的許多問題一樣,但這說起來容易做起來難,業(yè)務(wù)在不斷發(fā)展,設(shè)想也隨之改變。因此,重構(gòu)能力是設(shè)計(jì)系統(tǒng)時(shí)考慮的另一項(xiàng)關(guān)鍵問題。
在我們看來,領(lǐng)域驅(qū)動設(shè)計(jì) (DDD) 是關(guān)鍵,它是設(shè)計(jì)微服務(wù)時(shí)必不可少的工具,無論是對單體應(yīng)用進(jìn)行拆分還是從頭開始構(gòu)建一個(gè)新項(xiàng)目。領(lǐng)域驅(qū)動設(shè)計(jì)因 Eric Evans 的著作而出名,它是一組思想、原則和模式,可以幫助我們基于業(yè)務(wù)領(lǐng)域的底層模型設(shè)計(jì)軟件系統(tǒng)。開發(fā)人員和領(lǐng)域?qū)<乙黄鹗褂媒y(tǒng)一的通用語言創(chuàng)建業(yè)務(wù)模型。然后將這些模型綁定到有意義的系統(tǒng)上,在這些系統(tǒng)和處理這些服務(wù)的團(tuán)隊(duì)之間建立協(xié)作協(xié)議。更重要的是,它們設(shè)計(jì)了系統(tǒng)之間的概念輪廓或邊界。
微服務(wù)設(shè)計(jì)從這些概念中汲取了靈感,因?yàn)樗羞@些原理都有助于構(gòu)建可以獨(dú)立變更和發(fā)展的模塊化系統(tǒng)。
在繼續(xù)深入之前,讓我們快速瀏覽一下 DDD 的一些基本術(shù)語。對領(lǐng)域驅(qū)動設(shè)計(jì)的完整概述不在本文的討論范圍之內(nèi)。
領(lǐng)域(Domain): 代表組織所做的工作。例如零售或電子商務(wù)。
子域(Subdomain): 組織或組織內(nèi)的業(yè)務(wù)部門。一個(gè)領(lǐng)域由多個(gè)子域組成。
統(tǒng)一語言(Ubiquitous language):這是用于表達(dá)模型的語言。在下面的例子中(圖 1),Item 是一個(gè)模型,它是每個(gè)子域的統(tǒng)一語言。開發(fā)人員、產(chǎn)品經(jīng)理、領(lǐng)域?qū)<液蜆I(yè)務(wù)各涉眾方都能就使用這種語言達(dá)成一致,并在他們的工件(代碼、產(chǎn)品文檔等)中使用該語言。
圖 1:電子商務(wù)領(lǐng)域中的子域和界限上下文界限上下文(Bounded Contexts):領(lǐng)域驅(qū)動設(shè)計(jì)將界限上下文定義為“一個(gè)單詞或語句出現(xiàn)時(shí)確定其含義的設(shè)置”。簡而言之,這意味著模型在邊界內(nèi)是有含義的。在上面的例子中(圖 1),“Item”在每個(gè)上下文中都有不同的含義。在 Catalog 上下文中,Item 表示可出售的產(chǎn)品,而在 Cart 上下文中,它表示客戶已添加到購物車中的商品選項(xiàng)。在 Fulfillment 上下文中,它表示將要運(yùn)送給客戶的倉庫物料。這些模型各不相同,每個(gè)模型都有不同的含義,并且可能包含不同的屬性。通過將這些模型分離并將其隔離在各自的邊界內(nèi),我們就可以自由地表達(dá)這些模型,而不會產(chǎn)生歧義。
注意: 必須理解子域和界限上下文之間的區(qū)別。子域?qū)儆趩栴}空間,即我們的業(yè)務(wù)要如何看待問題,而界限上下文屬于解決方案空間,即我們將如何實(shí)施問題的解決方案。理論上,每個(gè)子域可能有多個(gè)界限上下文,盡管我們努力每個(gè)子域只提供一個(gè)界限上下文。
微服務(wù)和界限上下文如何關(guān)聯(lián)
現(xiàn)在,微服務(wù)適用于哪些地方?每個(gè)界限上下文都能映射到對應(yīng)的微服務(wù)嗎?不一定。我們來看看原因。在某些情況下,界限上下文的邊界或輪廓可能會非常大。
圖 2:界限上下文和微服務(wù)考慮上面的例子。定價(jià)界限上下文有三個(gè)不同的模型:價(jià)格(Price)、定價(jià)項(xiàng)(Priced items) 和折扣(Discounts),分別負(fù)責(zé)目錄項(xiàng)的價(jià)格、計(jì)算列表項(xiàng)的總價(jià)以及各自使用的折扣。我們可以創(chuàng)建一個(gè)包含上述所有模型的單一系統(tǒng),但它可能是一個(gè)不合理的大型應(yīng)用程序。如前所述,每個(gè)數(shù)據(jù)模型都有其不變性和業(yè)務(wù)規(guī)則。隨著時(shí)間的推移,如果我們不小心的話,這個(gè)系統(tǒng)就可能會變成一個(gè)大泥球,界限模糊,職責(zé)重疊,甚至很可能會回到我們開始的地方——單體應(yīng)用。
對這個(gè)系統(tǒng)建模的另一種方法是將相關(guān)模型分離或分組到單獨(dú)的微服務(wù)中。在 DDD 中,這些模型(價(jià)格、定價(jià)項(xiàng)和折扣)被稱為聚合(Aggregates)。聚合是由相關(guān)模型組成的自包含模型。我們只能通過已發(fā)布的接口來變更聚合的狀態(tài),并且聚合可以確保一致性,而且不變量可以始終保持良好狀態(tài)。
在形式上,聚合是關(guān)聯(lián)對象的集群,被視為數(shù)據(jù)變更的單元。外部引用僅限于指定聚合的一個(gè)成員,即聚合根。在聚合的邊界內(nèi)需應(yīng)用一組一致性規(guī)則。
圖 3:定價(jià)上下文中的微服務(wù)同樣,沒有必要將每個(gè)聚合都建模為一個(gè)不同的微服務(wù)。事實(shí)證明,圖 3 中的服務(wù)(聚合)就是如此,但這不一定是一個(gè)規(guī)則。在某些情況下,在單個(gè)服務(wù)中托管多個(gè)聚合可能是有意義的,特別是在我們不完全了解業(yè)務(wù)領(lǐng)域的情況下。需要注意的一點(diǎn)是,一致性只在單個(gè)聚合中才能得到保證,并且聚合只能通過已發(fā)布的接口進(jìn)行修改。任何違反這些規(guī)則的行為都有增加應(yīng)用程序變成一個(gè)大泥球的風(fēng)險(xiǎn)。
上下文映射:一種精確劃分微服務(wù)邊界的方法
另一個(gè)基本工具是上下文映射,同樣,它也是來自領(lǐng)域驅(qū)動設(shè)計(jì)。一個(gè)單體應(yīng)用通常由不同的模型組成,這些模型大多是緊密耦合的,模型之間可能知道彼此的實(shí)現(xiàn)細(xì)節(jié),變更一個(gè)模型可能造成另一個(gè)模型的副作用等等。當(dāng)你分解單體應(yīng)用時(shí),確定這些模型(在這里是聚合)及其關(guān)系是至關(guān)重要的。上下文映射可以幫助我們做到這一點(diǎn)。它們用于識別和定義各種界限上下文和聚合之間的關(guān)系。在上面的例子中,界限上下文定義了模型的邊界(價(jià)格、折扣等等)。而上下文映射定義了這些模型之間以及不同上下文之間的關(guān)系。在確定了這些依賴關(guān)系之后,我們就可以確定下來實(shí)現(xiàn)這些服務(wù)的團(tuán)隊(duì)之間的正確協(xié)作模型了。
對上下文映射的完整探索不在本文的討論范圍之內(nèi),但我們將用一個(gè)示例來說明。下圖顯示了處理電子商務(wù)訂單支付的各種應(yīng)用程序。
購物車上下文負(fù)責(zé)訂單的在線授權(quán);訂單上下文處理訂單履行完成后的支付流程,如結(jié)算;聯(lián)絡(luò)中心處理任何異常情況,如支付重試和變更訂單使用的支付方式。為了簡單起見,我們假設(shè)所有這些上下文都是作為單獨(dú)的服務(wù)實(shí)現(xiàn)的,所有這些上下文封裝了同一個(gè)模型。請注意,這些模型在邏輯上是相同的。也就是說,它們都遵循相同的統(tǒng)一領(lǐng)域語言——支付方式、授權(quán)和結(jié)算。只是它們是不同上下文的一部分。
另一個(gè)跡象表明,同一個(gè)模型在不同的上下文中傳播,所有這些模型都直接與單個(gè)支付網(wǎng)關(guān)相集成,并且彼此執(zhí)行相同的操作。
圖 4:定義錯(cuò)誤的上下文映射重新定義服務(wù)邊界:將聚合映射到正確的上下文
在上面的設(shè)計(jì)中有一些非常明顯的問題(圖 4)。支付聚合是多個(gè)上下文的一部分。在各種服務(wù)之間強(qiáng)制執(zhí)行不變性和一致性是不可能的,更不用說這些服務(wù)之間的并發(fā)問題了。例如,如果在訂單服務(wù)嘗試按之前提交的付款方式進(jìn)行結(jié)算的過程中,聯(lián)絡(luò)中心更改了與訂單關(guān)聯(lián)的付款方式會發(fā)生什么情況。另外,請注意,支付網(wǎng)關(guān)中的任何更改都將迫使對多個(gè)服務(wù)進(jìn)行更改,可能會涉及到多個(gè)團(tuán)隊(duì),因?yàn)樗鼈児餐瑩碛羞@些上下文。
通過一些調(diào)整并將聚合與正確的上下文對齊,我們就可以更好地表示這些子域了(圖 5)。需要進(jìn)行很多的更改。
我們來看下更改的點(diǎn):
支付聚合有了一個(gè)新家——支付服務(wù)。該服務(wù)還從其他需要支付服務(wù)的服務(wù)中提取了支付網(wǎng)關(guān)。由于單個(gè)界限上下文現(xiàn)在擁有了單個(gè)聚合,所以不變量很容易管理;所有事務(wù)都在同一個(gè)服務(wù)的邊界內(nèi)進(jìn)行,從而避免了任何并發(fā)問題。
支付聚合使用了反腐層(ACL)將核心領(lǐng)域模型與支付網(wǎng)關(guān)的數(shù)據(jù)模型隔離開來,后者通常是由第三方提供的,可能會發(fā)生變化。在以后的文章中,我們將深入研究基于“端口和適配器”模式的應(yīng)用程序設(shè)計(jì)。ACL 層通常包含將支付網(wǎng)關(guān)的數(shù)據(jù)模型轉(zhuǎn)換為支付聚合數(shù)據(jù)模型的適配器。
購物車服務(wù)通過直接調(diào)用 API 的方式來調(diào)用支付服務(wù),因?yàn)楫?dāng)客戶在網(wǎng)站上購物時(shí),購物車服務(wù)需要完成支付授權(quán)。
記錄訂單和支付服務(wù)之間的交互作用。訂單服務(wù)發(fā)出一個(gè)域事件(稍后會在本文中對此進(jìn)行詳細(xì)介紹)。支付服務(wù)監(jiān)聽此事件并完成訂單的結(jié)算
聯(lián)絡(luò)中心服務(wù)可能有許多聚合,但我們只對該用例中的訂單聚合感興趣。當(dāng)更改付款方式時(shí),此服務(wù)發(fā)出一個(gè)事件,支付服務(wù)將通過以下方式對此事件做出響應(yīng):將先前使用的信用卡撤銷,再處理新的信用卡。
通常,單體或遺留應(yīng)用程序有許多聚合,且邊界重疊。創(chuàng)建這些聚合及其依賴關(guān)系的上下文映射,將有助于我們理解從這些單體應(yīng)用中獲取任何新微服務(wù)的輪廓。請記住,微服務(wù)架構(gòu)的成敗取決于聚合之間的低耦合以及聚合之內(nèi)的高內(nèi)聚。
還需要注意的是,界限上下文本身就是合適的內(nèi)聚單元。即使上下文有多個(gè)聚合,也可以將整個(gè)上下文及其聚合組成單個(gè)微服務(wù)。我們發(fā)現(xiàn)這種啟發(fā)式方法對于有些模糊的領(lǐng)域特別有用,比如組織正在涉足的新業(yè)務(wù)領(lǐng)域。我們可能對分離的正確邊界沒有足夠的了解,并且任何過早的聚合分解都可能導(dǎo)致昂貴的重構(gòu)。試想一下,由于數(shù)據(jù)遷移,不得不將兩個(gè)數(shù)據(jù)庫合并為一個(gè),因?yàn)槲覀兣既话l(fā)現(xiàn)兩個(gè)聚合屬于同一類。但是要確保這些聚合通過接口是充分隔離的,這樣它們就不知道彼此的復(fù)雜細(xì)節(jié)了。
事件風(fēng)暴:另一種識別服務(wù)邊界的技術(shù)
事件風(fēng)暴(Event Storming)是識別系統(tǒng)中聚合(以及微服務(wù))的另一種必不可少的技術(shù)。它是一個(gè)非常有用的工具,既可用于分解單體應(yīng)用,也可用于設(shè)計(jì)復(fù)雜的微服務(wù)生態(tài)系統(tǒng)。我們已經(jīng)使用這種技術(shù)分解了一個(gè)復(fù)雜的應(yīng)用程序,并打算寫一篇單獨(dú)的文章來介紹我們的事件風(fēng)暴經(jīng)驗(yàn)。在本文中,我們只給出一個(gè)快速的高層概述。
簡而言之,事件風(fēng)暴是在應(yīng)用程序團(tuán)隊(duì)(這里,指單體)中進(jìn)行的頭腦風(fēng)暴,以識別系統(tǒng)中發(fā)生的各種領(lǐng)域事件和流程。團(tuán)隊(duì)還需確定這些事件影響的總和或模型,以及由此產(chǎn)生的任何后續(xù)影響。當(dāng)團(tuán)隊(duì)做這個(gè)頭腦風(fēng)暴時(shí),他們將識別不同的重疊概念、模棱兩可的領(lǐng)域語言和相互沖突的業(yè)務(wù)流程。他們對相關(guān)的模型進(jìn)行分組,重新定義聚合并識別重復(fù)的流程。隨著這些工作的進(jìn)行,這些聚合所屬的界限上下文變得清晰起來。如果所有團(tuán)隊(duì)都在同一個(gè)房間(物理或虛擬)里,并開始在 Scrum 風(fēng)格的白板上繪制事件、命令和流程的映射,那么事件風(fēng)暴研討就會非常有用。在本練習(xí)結(jié)束時(shí),通常會產(chǎn)出如下成果:
重新定義的聚合列表。這些可能會成為新的微服務(wù)
需要在這些微服務(wù)之間流動的領(lǐng)域事件
其他應(yīng)用程序或用戶直接調(diào)用的命令
下面是我們在一次事件風(fēng)暴研討會結(jié)束時(shí)產(chǎn)生的一個(gè)示例樣板。對于團(tuán)隊(duì)來說,就正確的聚合和界限上下文達(dá)成一致是一次很棒的協(xié)作活動。此外,團(tuán)隊(duì)在本次會議結(jié)束時(shí)還對領(lǐng)域、統(tǒng)一語言和精確的服務(wù)邊界有著共同的理解。
圖 6:事件風(fēng)暴板微服務(wù)之間的通信
快速回顧一下,一個(gè)單體應(yīng)用在單個(gè)流程邊界內(nèi)擁有多個(gè)聚合。因此,可以在這個(gè)邊界內(nèi)管理各個(gè)聚合的一致性。例如,如果客戶下了訂單,我們可以減少商品庫存,并向客戶發(fā)送電子郵件,所有這些都在一個(gè)事務(wù)中完成。所有操作要么都成功,要么都會失敗。但是,當(dāng)我們分解了單體并將聚合分散到不同的上下文中時(shí),我們將擁有數(shù)十個(gè)甚至數(shù)百個(gè)微服務(wù)。但目前為止,存在于單體應(yīng)用單一邊界內(nèi)的流程,現(xiàn)在被分散到了多個(gè)分布式系統(tǒng)中。要在所有這些分布式系統(tǒng)中實(shí)現(xiàn)事務(wù)的完整性和一致性是非常困難的,而且要以系統(tǒng)的可用性為代價(jià)。
微服務(wù)也是分布式系統(tǒng)。因此,CAP 定理也適用于它們:“一個(gè)分布式系統(tǒng)只能提供三個(gè)所需特性中的兩個(gè):一致性、可用性和分區(qū)容錯(cuò)(CAP 中的‘C’——Consistency、‘A’——Availability 和 ‘P’——Partition Tolerance)。”在現(xiàn)實(shí)世界的系統(tǒng)中,分區(qū)容錯(cuò)是不可協(xié)商的——網(wǎng)絡(luò)是不可靠的、虛擬機(jī)可以宕機(jī)、區(qū)域之間的延遲可能會變得更糟等等。
因此,我們可以選擇“可用性”或“一致性”。現(xiàn)在,我們知道,在任何現(xiàn)代應(yīng)用程序中,犧牲“可用性”也不是一個(gè)好主意。
圖 7:CAP 定理圍繞最終一致性設(shè)計(jì)應(yīng)用程序
如果我們想要跨多個(gè)分布式系統(tǒng)構(gòu)建事務(wù),那么我們將再次陷入單體應(yīng)用的困境。但這一次它會是最糟糕的一種單體:一個(gè)分散的單體應(yīng)用。如果這些系統(tǒng)中的任何一個(gè)變得不可用,則整個(gè)流程不可用,通常會導(dǎo)致極差的客戶體驗(yàn)、承諾的失敗等等。此外,對一個(gè)服務(wù)的變更通常會導(dǎo)致另一個(gè)服務(wù)的變更,從而引起復(fù)雜和昂貴的部署。因此,我們最好根據(jù)自己的用例來設(shè)計(jì)應(yīng)用程序,容忍稍微的不一致,以提高可用性。對于上面的例子,我們可以使所有流程異步,從而達(dá)到最終的一致性。我們可以獨(dú)立于其他流程,異步發(fā)送電子郵件;如果已經(jīng)承諾的商品以后在倉庫中不可用,那么該商品可能需要補(bǔ)貨,或者我們可以停止接受超出某個(gè)閾值的該商品的訂單。
有時(shí),我們可能會遇到這樣的場景:可能需要跨越不同流程邊界的兩個(gè)聚合的強(qiáng) ACID 式的事務(wù)。這是一個(gè)重新審視這些聚合并將它們組合成一個(gè)聚合的極好跡象。開始在不同流程邊界中分解這些聚合之前,事件風(fēng)暴和上下文映射將有助于我們及早識別這些依賴關(guān)系。將兩個(gè)微服務(wù)合并為一個(gè)的成本很高,這是我們應(yīng)該努力避免的。
支持事件驅(qū)動的架構(gòu)
微服務(wù)可以將發(fā)生在其聚合上的基本更改發(fā)出來,這些稱為領(lǐng)域事件(Domain Event),并且任何對這些更改感興趣的服務(wù)都可以監(jiān)聽這些事件并在其領(lǐng)域內(nèi)執(zhí)行相應(yīng)的操作。這種方法避免了任何行為上的耦合(一個(gè)領(lǐng)域無需規(guī)定其他領(lǐng)域應(yīng)該做什么)以及時(shí)間上的耦合(一個(gè)流程的成功完成不依賴于所有系統(tǒng)同時(shí)可用)。當(dāng)然,這將意味著系統(tǒng)最終是一致的。
圖 8:事件驅(qū)動架構(gòu)在上面的示例中,訂單服務(wù)發(fā)布一個(gè)事件:訂單已取消。訂閱了該事件的其他服務(wù)處理各自的領(lǐng)域功能:支付服務(wù)退款,庫存服務(wù)調(diào)整商品的庫存,等等。為確保此集成的可靠性和彈性,需要注意以下幾點(diǎn):
生產(chǎn)者應(yīng)確保至少發(fā)出了一次事件。如果過程中出現(xiàn)失敗,則應(yīng)確保存在回退機(jī)制以重新觸發(fā)事件
消費(fèi)者應(yīng)該確保以冪等的方式消費(fèi)事件。如果再次發(fā)生同一事件,不會對消費(fèi)者產(chǎn)生任何副作用。事件也可能不是順序到達(dá)的。消費(fèi)者可以使用時(shí)間戳或版本號字段來保證事件的唯一性。
由于某些用例的特性,不一定總是可以使用基于事件的集成。請看一下購物車服務(wù)和支付服務(wù)之間的集成。這是一個(gè)同步集成,因此我們需要注意一些事項(xiàng)。這是一個(gè)行為耦合的例子——購物車服務(wù)可能會從支付服務(wù)調(diào)用一個(gè) REST API,并指示它授權(quán)訂單的支付,而時(shí)間耦合——支付服務(wù)需要對購物車服務(wù)可用,它才能接受訂單。這種耦合降低了這些上下文的自治性,也可能會引入不必要的依賴。有幾種方法可以避免這種耦合,但是如果使用了所有這些選項(xiàng),我們將失去向客戶提供即時(shí)反饋的能力。
將 REST API 轉(zhuǎn)換為基于事件的集成。但是,如果支付服務(wù)僅公開 REST API,則此選項(xiàng)可能不可用
購物車服務(wù)立即接受訂單,并且有一個(gè)批處理作業(yè)來接管訂單并調(diào)用支付服務(wù) API
購物車服務(wù)生成一個(gè)本地事件,然后調(diào)用支付服務(wù) API
在出現(xiàn)失敗和上游依賴項(xiàng)(支付服務(wù))不可用的情況下,將上述方法與重試相結(jié)合,可以產(chǎn)生更具彈性的設(shè)計(jì)。例如,在發(fā)生故障的情況下,可以通過基于事件或批處理的重試來備份購物車和支付服務(wù)之間的同步集成。這種方法會對客戶體驗(yàn)產(chǎn)生額外的影響:客戶可能輸入了不正確的支付詳細(xì)信息,當(dāng)我們離線處理支付時(shí),無法強(qiáng)制他們在線。或者,收回失敗的支付可能會增加企業(yè)的成本。但在所有可能的情況下,讓購物車服務(wù)對支付服務(wù)的不可用性或故障具有彈性,利大于弊。例如,如果我們無法離線收款,我們可以通知客戶。簡而言之,在用戶體驗(yàn)、彈性和運(yùn)營成本之間存在著權(quán)衡,在設(shè)計(jì)系統(tǒng)時(shí),明智的做法是充分考慮這些折衷。
避免為了滿足調(diào)用者的特定數(shù)據(jù)需求而編排服務(wù)
存在于任何面向服務(wù)架構(gòu)的一個(gè)反模式是:服務(wù)迎合調(diào)用者的特定訪問模式。通常,當(dāng)調(diào)用者團(tuán)隊(duì)與服務(wù)提供者團(tuán)隊(duì)緊密合作時(shí),就會發(fā)生這種情況。如果團(tuán)隊(duì)正在開發(fā)一個(gè)單體應(yīng)用程序,它們通常會創(chuàng)建一個(gè)跨越不同聚合邊界的 API,從而使這些聚合緊密耦合在一起。我們來看一個(gè)例子。比如說 Web 中的訂單詳情頁面,移動應(yīng)用程序需要在單個(gè)頁面上顯示訂單詳情和訂單退款處理詳情。在一個(gè)單體應(yīng)用程序中,訂單獲取 API(Order-GET-API,假設(shè)它是 REST API)需要同時(shí)查詢訂單和退款,合并兩個(gè)聚合并向調(diào)用方發(fā)送一個(gè)復(fù)合響應(yīng)。由于聚合屬于同一流程邊界,因此可以在沒有太多開銷的情況下實(shí)現(xiàn)這一點(diǎn)。調(diào)用者可以在一次會話中獲得所需的所有數(shù)據(jù)。
如果訂單和退款是不同上下文的一部分,那么數(shù)據(jù)不再出現(xiàn)在單個(gè)微服務(wù)或聚合邊界內(nèi)。為調(diào)用者保留相同功能的一個(gè)選項(xiàng)是,讓訂單服務(wù)負(fù)責(zé)調(diào)用退款服務(wù)并創(chuàng)建一個(gè)復(fù)合響應(yīng)。這種方法會引起以下幾個(gè)問題:
訂單服務(wù)現(xiàn)在與另一個(gè)服務(wù)集成,純粹是為了支持那些需要退款數(shù)據(jù)和訂單數(shù)據(jù)的調(diào)用者。現(xiàn)在訂單服務(wù)的自治性降低了,因?yàn)橥丝罹酆系娜魏胃亩紩?dǎo)致訂單聚合的更改。
訂單服務(wù)有另一個(gè)集成,因此需要考慮另一個(gè)故障點(diǎn):如果退款服務(wù)出現(xiàn)故障,訂單服務(wù)是否仍可以發(fā)送部分?jǐn)?shù)據(jù),并且調(diào)用者是否可以優(yōu)雅地處理故障呢?
如果調(diào)用者需要變更,以從退款聚合中獲取更多的數(shù)據(jù),那么現(xiàn)在需要兩個(gè)團(tuán)隊(duì)同時(shí)進(jìn)行變更
如果跨平臺都遵循這種模式,則可能會導(dǎo)致各種域服務(wù)之間形成復(fù)雜的依賴關(guān)系網(wǎng),這都是因?yàn)檫@些服務(wù)迎合了調(diào)用者特定的訪問模式。
專門服務(wù)于前端的后端(BFFs)
一種減輕這種風(fēng)險(xiǎn)的方法是讓調(diào)用者團(tuán)隊(duì)管理各種域服務(wù)之間的編排。畢竟,調(diào)用方更了解訪問模式,并且可以完全控制對這些模式的任何更改。這種方法將域服務(wù)從表示層解耦出來,讓它們專注于核心業(yè)務(wù)流程。但是,如果 Web 和移動應(yīng)用程序開始直接調(diào)用不同的服務(wù),而不是從單體中調(diào)用一個(gè)復(fù)合 API,這可能會給這些應(yīng)用程序帶來性能開銷——在較低帶寬的網(wǎng)絡(luò)上進(jìn)行多次調(diào)用,處理和合并來自不同 API 的數(shù)據(jù),等等。
相反,可以使用另一種稱為用于前端的后端模式(Backend for Front-ends)。在這種設(shè)計(jì)模式中,由消費(fèi)者創(chuàng)建和管理的后端服務(wù),在本例中是 Web 和移動團(tuán)隊(duì),它負(fù)責(zé)對多個(gè)域服務(wù)進(jìn)行集成,純粹是為了向客戶提供前端體驗(yàn)。Web 和移動團(tuán)隊(duì)現(xiàn)在可以根據(jù)它們所需要的用例來設(shè)計(jì)數(shù)據(jù)契約。它們甚至可以使用 GraphQL 而不是 REST API 來靈活地查詢并獲取所需的內(nèi)容。需要注意的是,該服務(wù)是由消費(fèi)者團(tuán)隊(duì)擁有和維護(hù)的,而不是提供域服務(wù)的團(tuán)隊(duì)。前端團(tuán)隊(duì)現(xiàn)在可以根據(jù)它們的需求進(jìn)行優(yōu)化——移動應(yīng)用程序可以請求更小的有效負(fù)載,減少來自移動應(yīng)用程序的會話次數(shù),等等。下面是修訂后的業(yè)務(wù)流程圖。BFF 服務(wù)現(xiàn)在為其用例調(diào)用“訂單”和“退款”域服務(wù)。
圖 9:用于前端的后端盡早構(gòu)建 BFF 服務(wù)也很有用,這樣可以避免從單體系統(tǒng)中分解出過多的服務(wù)。否則,要么域服務(wù)必須支持域間編排,要么 Web 和移動應(yīng)用程序必須直接從前端調(diào)用多個(gè)服務(wù)。這兩個(gè)選項(xiàng)都會導(dǎo)致性能開銷、一次性工作以及團(tuán)隊(duì)之間缺乏自治。
相關(guān)閱讀:
1. ?Eric Evans’ Domain Driven Design
2.? Vaughn Vernon’s Implementing Domain Driven Design
3.? Martin Fowler’s article on Microservices
4.? Sam Newman’s Building Microservices
5.? Event storming
7.? Backend for Frontends
8.? Fallacies of distributed computing
總結(jié)
以上是生活随笔為你收集整理的如何基于 DDD 构建微服务?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 最全.Net学习资料库上线,今日可免费下
- 下一篇: .NET Core ResponseCa