本文是基于“微服務(wù)架構(gòu)設(shè)計模式”這本書的總結(jié)和提煉版确,將其中的關(guān)鍵知識點結(jié)合個人的開發(fā)實踐進(jìn)行結(jié)合提煉反璃,并對部分話題進(jìn)一步挖深講透取董,參雜了部分個人理解盐茎。
單體服務(wù)VS微服務(wù)
單體架構(gòu)也稱之為單體系統(tǒng)或者是單體應(yīng)用者冤。就是一種把系統(tǒng)中所有的功能肤视、模塊耦合在一個應(yīng)用中的架構(gòu)方式。單體架構(gòu)特點:1)打包成一個獨立的單元(導(dǎo)成一個唯一的 jar 包或者是 war 包)涉枫;2)以一個進(jìn)程的方式來運行邢滑,MVC架構(gòu)就是典型的單體架構(gòu)。
單體架構(gòu)的優(yōu)缺點如下:
優(yōu)點
- 應(yīng)用的開發(fā)很簡單:IDE和其他開發(fā)工具只需要構(gòu)建這一個單獨的應(yīng)用程序愿汰。
- 易于對應(yīng)用程序進(jìn)行大規(guī)模的更改:可以更改代碼和數(shù)據(jù)庫模式困后,然后構(gòu)建和部署。
- 測試相對簡單直觀:開發(fā)者只需要寫幾個端到端的測試衬廷。
- 部署簡單明了: 開發(fā)者唯一需要做的就是把war文件復(fù)制到安裝了Tomacat的服務(wù)器上摇予。
缺點
- 隨著業(yè)務(wù)的迭代,單體系統(tǒng)會逐漸龐大和復(fù)雜吗跋,以至于任意一個開發(fā)都很難理解和cover它的全部侧戴。
- 開發(fā)速度變慢:IDE工具會變慢宁昭,構(gòu)建部署時間長。多人協(xié)作沖突的概率變高酗宋,每一次改動影響面會變大积仗。總之從代碼提交到實際部署交付的周期會變長本缠。
- 難以擴(kuò)展:單體應(yīng)用多個高并發(fā)請求會導(dǎo)致物理資源(如CPU斥扛、內(nèi)存等)出現(xiàn)單點瓶頸。
- 迭代困難:需要長期依賴某個可能已經(jīng)過時的技術(shù)棧丹锹。
微服務(wù)是一種架構(gòu)風(fēng)格稀颁。一個大型的復(fù)雜軟件應(yīng)用,由一個或多個微服務(wù)組成楣黍。系統(tǒng)中的各個微服務(wù)可被獨立部署匾灶,各個微服務(wù)之間是松耦合的。每個微服務(wù)僅關(guān)注于完成一個業(yè)務(wù)域的事情租漂。微服務(wù)特點:1)系統(tǒng)是由多個服務(wù)構(gòu)成阶女;2)每個服務(wù)可以單獨獨立部署;3)每個服務(wù)之間是松耦合的哩治。服務(wù)內(nèi)部是高內(nèi)聚的秃踩,外部是低耦合的。
微服務(wù)的優(yōu)缺點如下:
優(yōu)點
- 使大型的復(fù)雜應(yīng)用程序可以持續(xù)交付和持續(xù)部署业筏。
- 每個服務(wù)都相對較小并容易維護(hù)憔杨。
- 服務(wù)可以獨立部署和獨立擴(kuò)展,系統(tǒng)迭代容易蒜胖。
- 微服務(wù)架構(gòu)可以實現(xiàn)團(tuán)隊的自治消别,團(tuán)隊協(xié)作容易,每個服務(wù)團(tuán)隊可以獨立于其他團(tuán)隊開發(fā)台谢、部署和擴(kuò)展寻狂。開發(fā)速度相對單體應(yīng)用更快。
- 每個微服務(wù)都可以有獨立的存儲和服務(wù)器朋沮,從而整個系統(tǒng)的吞吐能力會指數(shù)增長蛇券。
缺點
- 運維成本過高,部署數(shù)量較多樊拓,需要協(xié)調(diào)更多的開發(fā)團(tuán)隊纠亚。
- 接口需要兼容多版本,
- 一個需要改動的服務(wù)工程會比較多
- 分布式系統(tǒng)帶來更高的復(fù)雜性骑脱,需要處理分布式事務(wù)菜枷,需要有更好的發(fā)布平臺和分布式跟蹤平臺等苍糠。
微服務(wù)架構(gòu) 與 SOA的異同
SOA(Service Oriented Architecture叁丧,面向服務(wù)的架構(gòu))是一種設(shè)計方法,其中包含多個服務(wù), 服務(wù)之間通過相互依賴最終提供一系列的功能拥娄。一個服務(wù)通常以獨立的形式存在于操作系統(tǒng)進(jìn)程中蚊锹。各個服務(wù)之間 通過網(wǎng)絡(luò)調(diào)用。
微服務(wù)是SOA發(fā)展出來的產(chǎn)物稚瘾,它是一種比較現(xiàn)代化的細(xì)粒度的SOA實現(xiàn)方式牡昆。
SOA往往采用全局?jǐn)?shù)據(jù)模型并共享數(shù)據(jù)庫,而每個微服務(wù)都有自己的數(shù)據(jù)模型和數(shù)據(jù)庫摊欠;SOA是較大的單體應(yīng)用丢烘,微服務(wù)是較小的服務(wù)。SOA之間的通信采用的是類似ESB(Enterprise Service Bus)只能管道些椒,采用例如SOAP播瞳、WS等重量級協(xié)議,而微服務(wù)往往采用RPC或者REST這種輕量級的協(xié)議免糕。
討論「微服務(wù)和SOA的差別」的意義遠(yuǎn)不如討論「微服務(wù)和單體系統(tǒng)的差別」更大赢乓,因為他們的區(qū)別實在有點微妙。
微服務(wù)架構(gòu)其實和 SOA 架構(gòu)類似石窑,微服務(wù)是在 SOA 上做的升華牌芋,微服務(wù)架構(gòu)強(qiáng)調(diào)的一個重點是“業(yè)務(wù)需要徹底的組件化和服務(wù)化”,原有的單個業(yè)務(wù)系統(tǒng)會拆分為多個可以獨立開發(fā)松逊、設(shè)計躺屁、運行的小應(yīng)用。這些小應(yīng)用之間通過服務(wù)完成交互和集成棺棵。下面這個公式很好的描述兩者關(guān)系:
微服務(wù)架構(gòu) = 80%的SOA服務(wù)架構(gòu)思想 + 100%的組件化架構(gòu)思想 + 80%的領(lǐng)域建模思想
微服務(wù)如何拆分以及如何設(shè)計
微服務(wù)的物理拆分--將一個大需求拆分為多個子系統(tǒng)
跟所有的軟件開發(fā)過程一樣楼咳,一開始我們需要拿到領(lǐng)域?qū)<一蛘攥F(xiàn)有應(yīng)用的需求文檔。跟所有的軟件開發(fā)一樣烛恤,定義架構(gòu)也是一項藝術(shù)而非技術(shù)母怜。下面定義應(yīng)用程序架構(gòu)的三步式流程。
- 第一步是識別業(yè)務(wù)系統(tǒng)操作缚柏。將應(yīng)用程序的需求提煉為各種關(guān)鍵請求苹熏。描述服務(wù)之間協(xié)作方式的架構(gòu)場景。
- 第二步是確定如何分解服務(wù)币喧。有幾種策略可供選擇轨域。一種源于業(yè)務(wù)架構(gòu)學(xué)派的策略是定義與業(yè)務(wù)能力相對應(yīng)的服務(wù)。另一種策略是圍繞領(lǐng)域驅(qū)動設(shè)計的子域來分解和設(shè)計服務(wù)杀餐。但這些策略的最終結(jié)果都是圍繞業(yè)務(wù)概念而非技術(shù)概念分解和設(shè)計的服務(wù)干发。
- 第三步是確定每個服務(wù)的API。為此史翘,你將第一步中標(biāo)識的每個系統(tǒng)操作分配給服務(wù)枉长。服務(wù)可以完全獨立地實現(xiàn)操作冀续。
識別業(yè)務(wù)系統(tǒng)操作
定義應(yīng)用程序架構(gòu)的第一步是定義業(yè)務(wù)系統(tǒng)操作。起點是應(yīng)用程序的需求必峰,包括用戶故事及其相關(guān)的用戶場景(請注意洪唐,這些與架構(gòu)場景不同)。第一步創(chuàng)建由關(guān)鍵類組成的抽象領(lǐng)域模型吼蚁,這些關(guān)鍵類提供用于描述系統(tǒng)操作的詞匯表凭需。第二步確定系統(tǒng)操作,并根據(jù)領(lǐng)域模型描述每個系統(tǒng)操作的行為肝匆。
領(lǐng)域模型主要源自用戶故事中提及的名詞粒蜈,系統(tǒng)操作主要來自用戶故事中提及的動詞。你還可以使用名為事件風(fēng)暴(Event Storming)的技術(shù)定義領(lǐng)域模型旗国,每個系統(tǒng)操作的行為都是根據(jù)它對一個或多個領(lǐng)域?qū)ο蟮挠绊懸约八鼈冎g的關(guān)系來描述的薪伏。
根據(jù)業(yè)務(wù)能力進(jìn)行服務(wù)拆分
創(chuàng)建微服務(wù)架構(gòu)的策略之一就是采用業(yè)務(wù)能力進(jìn)行服務(wù)拆分。業(yè)務(wù)能力是一個來自于業(yè)務(wù)架構(gòu)建模的術(shù)語粗仓。業(yè)務(wù)能力是指一些能夠為公司(或組織)產(chǎn)生價值的商業(yè)活動嫁怀。特定業(yè)務(wù)的業(yè)務(wù)能力取決于這個業(yè)務(wù)的類型。例如借浊,保險公司業(yè)務(wù)能力通常包括承保塘淑、理賠管理、賬務(wù)和合規(guī)等蚂斤。在線商店的業(yè)務(wù)能力包括:訂單管理存捺、庫存管理和發(fā)貨,等等曙蒸。
- 業(yè)務(wù)能力定義了一個組織的工作捌治。組織的業(yè)務(wù)能力通常是指這個組織的業(yè)務(wù)是做什么,它們通常都是穩(wěn)定的纽窟。與之相反肖油,組織采用何種方式來實現(xiàn)它的業(yè)務(wù)能力,是隨著時間不斷變化的臂港。
- 識別業(yè)務(wù)能力森枪。一個組織有哪些業(yè)務(wù)能力,是通過對組織的目標(biāo)审孽、結(jié)構(gòu)和商業(yè)流程的分析得來的县袱。每一個業(yè)務(wù)能力都可以被認(rèn)為是一個服務(wù)。
- 從業(yè)務(wù)能力到服務(wù)佑力。一旦確定了業(yè)務(wù)能力式散,就可以為每個能力或相關(guān)能力組定義服務(wù)。
根據(jù)子域進(jìn)行服務(wù)拆分
Eric Evans在他的經(jīng)典著作中(Addison-Wesley Professional打颤,2003)提出的領(lǐng)域驅(qū)動設(shè)計是構(gòu)建復(fù)雜軟件的方法論暴拄,這些軟件通常都以面向?qū)ο蠛皖I(lǐng)域模型為核心宛畦。領(lǐng)域模型以解決具體問題的方式包含了一個領(lǐng)域內(nèi)的知識。它定義了當(dāng)前領(lǐng)域相關(guān)團(tuán)隊的詞匯表揍移,DDD也稱之為通用語言(Ubiquitous language)。領(lǐng)域模型會被緊密地映射到應(yīng)用的設(shè)計和實現(xiàn)環(huán)節(jié)反肋。在微服務(wù)架構(gòu)的設(shè)計層面那伐,DDD有兩個特別重要的概念,子域和限界上下文石蔗。
子域是領(lǐng)域的一部分罕邀,領(lǐng)域是DDD中用來描述應(yīng)用程序問題域的一個術(shù)語。識別子域的方式跟識別業(yè)務(wù)能力一樣:分析業(yè)務(wù)并識別業(yè)務(wù)的不同專業(yè)領(lǐng)域养距,分析產(chǎn)出的子域定義結(jié)果也會跟業(yè)務(wù)能力非常接近诉探。
DDD把領(lǐng)域模型的邊界稱為限界上下文(bounded context)。限界上下文包括實現(xiàn)這個模型的代碼集合棍厌。當(dāng)使用微服務(wù)架構(gòu)時肾胯,每一個限界上下文對應(yīng)一個或者一組服務(wù)。換一種說法耘纱,我們可以通過DDD的方式定義子域敬肚,并把子域?qū)?yīng)為每一個服務(wù),這樣就完成了微服務(wù)架構(gòu)的設(shè)計工作束析。
關(guān)于根據(jù)子域進(jìn)行服務(wù)拆分可以參考我的這篇文章艳馒,這篇文章是以一個在線問診場景,描述了如何從需求落地到微服務(wù)员寇。
醫(yī)療場景交易平臺戰(zhàn)略設(shè)計&戰(zhàn)術(shù)落地思考
微服務(wù)的邏輯拆分--架構(gòu)風(fēng)格
微服務(wù)將一個大型的復(fù)雜軟件應(yīng)用拆分為一個或多個微服務(wù)系統(tǒng) 弄慰。每一個微服務(wù)系統(tǒng)的內(nèi)部代碼組織方式就是架構(gòu)風(fēng)格。常見的架構(gòu)風(fēng)格有MVC三層架構(gòu)以及現(xiàn)在提倡的六邊形架構(gòu)蝶锋,下面對這兩種架構(gòu)進(jìn)行介紹總結(jié)陆爽。
分層式架構(gòu)風(fēng)格
架構(gòu)的典型例子是分層架構(gòu)。分層架構(gòu)將軟件元素按“層”的方式組織扳缕。每個層都有明確定義的職責(zé)墓陈。分層架構(gòu)還限制了層之間的依賴關(guān)系。每一層只能依賴于緊鄰其下方的層(如果嚴(yán)格分層)或其下面的任何層第献。
可以將分層架構(gòu)應(yīng)用于前面討論的四個視圖中的任何一個贡必。流行的三層架構(gòu)是應(yīng)用于邏輯視圖的分層架構(gòu)。它將應(yīng)用程序的類組織到以下層中:
表現(xiàn)層:包含實現(xiàn)用戶界面或外部API的代碼庸毫。
業(yè)務(wù)邏輯層:包含業(yè)務(wù)邏輯仔拟。
數(shù)據(jù)持久化層:實現(xiàn)與數(shù)據(jù)庫交互的邏輯。
分層架構(gòu)是架構(gòu)風(fēng)格的一個很好的例子飒赃,但它確實有一些明顯的弊端:
單個表現(xiàn)層:它無法展現(xiàn)應(yīng)用程序可能不僅僅由單個系統(tǒng)調(diào)用的事實利花。
單一數(shù)據(jù)持久化層:它無法展現(xiàn)應(yīng)用程序可能與多個數(shù)據(jù)庫進(jìn)行交互的事實科侈。
將業(yè)務(wù)邏輯層定義為依賴于數(shù)據(jù)持久化層:理論上,這樣的依賴性會妨礙你在沒有數(shù)據(jù)庫的情況下測試業(yè)務(wù)邏輯炒事。
此外臀栈,分層架構(gòu)錯誤地表示了精心設(shè)計的應(yīng)用程序中的依賴關(guān)系。業(yè)務(wù)邏輯通常定義數(shù)據(jù)訪問方法的接口或接口庫挠乳。數(shù)據(jù)持久化層則定義了實現(xiàn)存儲庫接口的DAO類权薯。換句話說,依賴關(guān)系與分層架構(gòu)所描述的相反睡扬。
關(guān)于架構(gòu)風(fēng)格的六邊形
六邊形架構(gòu)是分層架構(gòu)風(fēng)格的替代品盟蚣。如下圖所示,六邊形架構(gòu)風(fēng)格選擇以業(yè)務(wù)邏輯為中心的方式組織邏輯視圖卖怜。應(yīng)用程序具有一個或多個入站適配器屎开,而不是表示層,它通過調(diào)用業(yè)務(wù)邏輯來處理來自外部的請求马靠。同樣奄抽,應(yīng)用程序具有一個或多個出站適配器,而不是數(shù)據(jù)持久化層甩鳄,這些出站適配器由業(yè)務(wù)邏輯調(diào)用并調(diào)用外部應(yīng)用程序如孝。此架構(gòu)的一個關(guān)鍵特性和優(yōu)點是業(yè)務(wù)邏輯不依賴于適配器。相反娩贷,各種適配器都依賴業(yè)務(wù)邏輯第晰。
業(yè)務(wù)邏輯具有一個或多個端口(port)。端口定義了一組操作彬祖,關(guān)于業(yè)務(wù)邏輯如何與外部交互茁瘦。例如,在Java中储笑,端口通常是Java接口甜熔。有兩種端口:入站和出站端口。入站端口是業(yè)務(wù)邏輯公開的API突倍,它使外部應(yīng)用程序可以調(diào)用它腔稀。入站端口的一個實例是服務(wù)接口,它定義服務(wù)的公共方法羽历。出站端口是業(yè)務(wù)邏輯調(diào)用外部系統(tǒng)的方式焊虏。出站端口的一個實例是存儲庫接口,它定義數(shù)據(jù)訪問操作的集合秕磷。
業(yè)務(wù)邏輯的周圍是適配器诵闭。與端口一樣,有兩種類型的適配器:入站和出站。入站適配器通過調(diào)用入站端口來處理來自外部世界的請求疏尿。入站適配器的一個實例是Spring MVC Controller瘟芝,它實現(xiàn)一組REST接口(endpoint)或一組Web頁面。另一個實例是訂閱消息的消息代理客戶端褥琐。多個入站適配器可以調(diào)用相同的入站端口锌俱。
出站適配器實現(xiàn)出站端口,并通過調(diào)用外部應(yīng)用程序或服務(wù)處理來自業(yè)務(wù)邏輯的請求敌呈。出站適配器的一個實例是實現(xiàn)訪問數(shù)據(jù)庫的操作的數(shù)據(jù)訪問對象(DAO)類贸宏。另一個實例是調(diào)用遠(yuǎn)程服務(wù)的代理類。出站適配器也可以發(fā)布事件驱富。
六邊形架構(gòu)風(fēng)格的一個重要好處是它將業(yè)務(wù)邏輯與適配器中包含的表示層和數(shù)據(jù)訪問層的邏輯分離開來。業(yè)務(wù)邏輯不依賴于表示層邏輯或數(shù)據(jù)訪問層邏輯匹舞。
由于這種分離褐鸥,單獨測試業(yè)務(wù)邏輯要容易得多。另一個好處是它更準(zhǔn)確地反映了現(xiàn)代應(yīng)用程序的架構(gòu)赐稽〗虚牛可以通過多個適配器調(diào)用業(yè)務(wù)邏輯,每個適配器實現(xiàn)特定的API或用戶界面姊舵。業(yè)務(wù)邏輯還可以調(diào)用多個適配器晰绎,每個適配器調(diào)用不同的外部系統(tǒng)。六邊形架構(gòu)是描述微服務(wù)架構(gòu)中每個服務(wù)的架構(gòu)的好方法括丁。
分層架構(gòu)和六邊形架構(gòu)都是架構(gòu)風(fēng)格的實例荞下。每個都定義了架構(gòu)的構(gòu)建塊(元素),并對它們之間的關(guān)系施加了約束史飞。六邊形架構(gòu)和分層架構(gòu)(三層架構(gòu))構(gòu)成了軟件的邏輯視圖〖饣瑁現(xiàn)在讓我們將微服務(wù)架構(gòu)定義為構(gòu)成軟件的實現(xiàn)視圖的架構(gòu)風(fēng)格。
服務(wù)拆分的規(guī)范
微服務(wù)拆分之后构资,工程會比較的多抽诉,如果沒有一定的規(guī)范,將會非惩旅啵混亂迹淌,難以維護(hù)。
首先人們經(jīng)常問的一個問題是己单,服務(wù)拆分之后唉窃,原來都在一個進(jìn)程里面的函數(shù)調(diào)用,現(xiàn)在變成了A調(diào)用B調(diào)用C調(diào)用D調(diào)用E纹笼,會不會因為調(diào)用鏈路過長而使得相應(yīng)變慢呢句携?
服務(wù)拆分的規(guī)范一:服務(wù)拆分最多三層,兩次調(diào)用
服務(wù)拆分是為了橫向擴(kuò)展允乐,因而應(yīng)該橫向拆分矮嫉,而非縱向拆成一串的削咆。也即應(yīng)該將商品和訂單拆分,而非下單的十個步驟拆分蠢笋,然后一個調(diào)用一個拨齐。
縱向的拆分最多三層:
基礎(chǔ)服務(wù)層:用于屏蔽數(shù)據(jù)庫,緩存層昨寞,提供原子的對象查詢接口瞻惋,有這一層,為了數(shù)據(jù)層做一定改變的時候歼狼,例如分庫分表添瓷,數(shù)據(jù)庫擴(kuò)容搀愧,緩存替換等,對于上層透明,上層僅僅調(diào)用這一層的接口篙贸,不直接訪問數(shù)據(jù)庫和緩存。
組合服務(wù)層:這一層調(diào)用基礎(chǔ)服務(wù)層,完成較為復(fù)雜的業(yè)務(wù)邏輯价说,實現(xiàn)分布式事務(wù)也多在這一層
Controller層:接口層彻磁,調(diào)用組合服務(wù)層對外
服務(wù)拆分的規(guī)范二:僅僅單向調(diào)用薪贫,嚴(yán)禁循環(huán)調(diào)用
微服務(wù)拆分后鞍匾,服務(wù)之間的依賴關(guān)系復(fù)雜梁棠,如果循環(huán)調(diào)用男娄,升級的時候就很頭疼暖混,不知道應(yīng)該先升級哪個收擦,后升級哪個,難以維護(hù)讹剔。
因而層次之間的調(diào)用規(guī)定如下:
基礎(chǔ)服務(wù)層主要做數(shù)據(jù)庫的操作和一些簡單的業(yè)務(wù)邏輯,不允許調(diào)用其他任何服務(wù)狞玛。
組合服務(wù)層软驰,可以調(diào)用基礎(chǔ)服務(wù)層,完成復(fù)雜的業(yè)務(wù)邏輯为居,可以調(diào)用組合服務(wù)層碌宴,不允許循環(huán)調(diào)用,不允許調(diào)用Controller層服務(wù)
Controller層蒙畴,可以調(diào)用組合業(yè)務(wù)層服務(wù)贰镣,不允許被其他服務(wù)調(diào)用
如果出現(xiàn)循環(huán)調(diào)用呜象,例如A調(diào)用B,B也調(diào)用A碑隆,則分成Controller層和組合服務(wù)層兩層恭陡,A調(diào)用B的下層,B調(diào)用A的下層上煤。也可以使用消息隊列休玩,將同步調(diào)用,改為異步調(diào)用劫狠。
服務(wù)拆分的規(guī)范三:將串行調(diào)用改為并行調(diào)用拴疤,或者異步化
如果有的組合服務(wù)處理流程的確很長,需要調(diào)用多個外部服務(wù)独泞,應(yīng)該考慮如何通過消息隊列呐矾,實現(xiàn)異步化和解耦羔飞。
例如下單之后宵统,要刷新緩存,要通知倉庫等硫戈,這些都不需要再下單成功的時候就要做完荞膘,而是可以發(fā)一個消息給消息隊列罚随,異步通知其他服務(wù)。
而且使用消息隊列的好處是羽资,你只要發(fā)送一個消息淘菩,無論下游依賴方有一個,還是有十個削罩,都是一條消息搞定瞄勾,只不過多幾個下游監(jiān)聽消息即可费奸。
對于下單必須同時做完的弥激,例如扣減庫存和優(yōu)惠券等,可以進(jìn)行并行調(diào)用愿阐,這樣處理時間會大大縮短微服,不是多次調(diào)用的時間之和,而是最長的那個系統(tǒng)調(diào)用時間缨历。
服務(wù)拆分的規(guī)范四:接口應(yīng)該實現(xiàn)冪等
微服務(wù)拆分之后以蕴,服務(wù)之間的調(diào)用當(dāng)出現(xiàn)錯誤的時候,一定會重試辛孵,但是為了不要下兩次單丛肮,支付兩次,需要所有的接口實現(xiàn)冪等魄缚。
冪等一般需要設(shè)計一個冪等表來實現(xiàn)宝与,冪等表中的主鍵或者唯一鍵可以是transaction id焚廊,或者business id,可以通過這個id的唯一性標(biāo)識一個唯一的操作习劫。
也有冪等操作使用狀態(tài)機(jī)咆瘟,當(dāng)一個調(diào)用到來的時候,往往觸發(fā)一個狀態(tài)的變化诽里,當(dāng)下次調(diào)用到來的時候袒餐,發(fā)現(xiàn)已經(jīng)不是這個狀態(tài),就說明上次已經(jīng)調(diào)用過了谤狡。
狀態(tài)的變化需要是一個原子操作灸眼,也即并發(fā)調(diào)用的時候,只有一次可以執(zhí)行墓懂〈闭ǎ可以使用分布式鎖,或者樂觀鎖CAS操作實現(xiàn)拒贱。
服務(wù)拆分的規(guī)范五:接口數(shù)據(jù)定義嚴(yán)禁內(nèi)嵌宛徊,透傳
微服務(wù)接口之間傳遞數(shù)據(jù),往往通過數(shù)據(jù)結(jié)構(gòu)逻澳,如果數(shù)據(jù)結(jié)構(gòu)透傳闸天,從底層一直到上層使用同一個數(shù)據(jù)結(jié)構(gòu),或者上層的數(shù)據(jù)結(jié)構(gòu)內(nèi)嵌底層的數(shù)據(jù)結(jié)構(gòu)斜做,當(dāng)數(shù)據(jù)結(jié)構(gòu)中添加或者刪除一個字段的時候苞氮,波及的面會非常大。
因而接口數(shù)據(jù)定義瓤逼,在每兩個接口之間約定笼吟,嚴(yán)禁內(nèi)嵌和透傳,即便差不多霸旗,也應(yīng)該重新定義贷帮,這樣接口數(shù)據(jù)定義的改變,影響面僅僅在調(diào)用方和被調(diào)用方诱告,當(dāng)接口需要更新的時候撵枢,比較可控,也容易升級精居。
服務(wù)拆分的規(guī)范六:規(guī)范化工程名
微服務(wù)拆分后锄禽,工程名非常多,開發(fā)人員靴姿,開發(fā)團(tuán)隊也非常多沃但,如何讓一個開發(fā)人員看到一個工程名,或者jar的名稱佛吓,就大概知道是干什么的宵晚,需要一個規(guī)范化的約定恨旱。
例如出現(xiàn)pay就是支付,出現(xiàn)order就是下單坝疼,出現(xiàn)account就是用戶搜贤。
再如出現(xiàn)compose就是組合層,controller就是接口層钝凶,basic就是基礎(chǔ)服務(wù)層仪芒。
出現(xiàn)api就是接口定義,impl就是實現(xiàn)耕陷。
pay-compose-api就是支付組合層接口定義掂名。
account-basic-impl就是用戶基礎(chǔ)服務(wù)層的實現(xiàn)。
微服務(wù)架構(gòu)中的業(yè)務(wù)邏輯設(shè)計
代碼模型結(jié)構(gòu)
貧血模型是指使用的領(lǐng)域?qū)ο笾兄挥衧etter和getter方法(POJO)哟沫,所有的業(yè)務(wù)邏輯都不包含在領(lǐng)域?qū)ο笾卸欠旁跇I(yè)務(wù)邏輯層饺蔑。有人將我們這里說的貧血模型進(jìn)一步劃分成失血模型(領(lǐng)域?qū)ο笸耆珱]有業(yè)務(wù)邏輯)和貧血模型(領(lǐng)域?qū)ο笥猩倭康臉I(yè)務(wù)邏輯)嗜诀,我們這里就不對此加以區(qū)分了猾警。充血模型將大多數(shù)業(yè)務(wù)邏輯和持久化放在領(lǐng)域?qū)ο笾校瑯I(yè)務(wù)邏輯(業(yè)務(wù)門面)只是完成對業(yè)務(wù)邏輯的封裝隆敢、事務(wù)和權(quán)限等的處理发皿。
充血模型的層次結(jié)構(gòu)和上面的差不多,不過大多業(yè)務(wù)邏輯和持久化放在Domain Object里面拂蝎,Business Logic只是簡單封裝部分業(yè)務(wù)邏輯以及控制事務(wù)穴墅、權(quán)限等,這樣層次結(jié)構(gòu)就變成Client->(Business Facade)->Business Logic->Domain Object->Data Access温自。
優(yōu)點是面向?qū)ο笮酰珺usiness Logic符合單一職責(zé),不像在貧血模型里面那樣包含所有的業(yè)務(wù)邏輯太過沉重悼泌。
脹血模型是基于充血模型上取消Service層松捉,只剩下domain object和DAO兩層,在domain object的domain logic上面封裝事務(wù)券躁。
在這四種模型當(dāng)中惩坑,失血模型和脹血模型應(yīng)該是不被提倡的掉盅。而貧血模型和充血模型從技術(shù)上來說也拜,都已經(jīng)是可行的了。事務(wù)封裝還是盡量放在Service層(我們的manage層)趾痘。脹血模型將對象的序列化行為封裝到領(lǐng)域?qū)勇磀omain object會調(diào)用domain acess層,同時domain access層又依賴domain object的結(jié)構(gòu)永票,所以脹血模型中domain object層會和domain access層雙向依賴卵贱。
我們平時做 Web 項目的業(yè)務(wù)開發(fā)滥沫,大部分都是基于貧血模型的 MVC 三層架構(gòu),稱為傳統(tǒng)的開發(fā)模式键俱。之所以稱之為“傳統(tǒng)”兰绣,是相對于新興的基于充血模型的DDD 開發(fā)模式來說的”嗾瘢基于貧血模型的傳統(tǒng)開發(fā)模式缀辩,是典型的面向過程的編程風(fēng)格。相反踪央,基于充血模型的 DDD 開發(fā)模式臀玄,是典型的面向?qū)ο蟮木幊田L(fēng)格。不過畅蹂,DDD 也并非銀彈健无。對于業(yè)務(wù)不復(fù)雜的系統(tǒng)開發(fā)來說,基于貧血模型的傳統(tǒng)開發(fā)模式簡單夠用液斜,基于充血模型的 DDD 開發(fā)模式有點大材小用累贤,無法發(fā)揮作用。相反少漆,對于業(yè)務(wù)復(fù)雜的系統(tǒng)開發(fā)來說畦浓,基于充血模型的 DDD 開發(fā)模式,因為前期需要在設(shè)計上投入更多時間和精力检疫,來提高代碼的復(fù)用性和可維護(hù)性讶请,所以相比基于貧血模型的開發(fā)模式,更加有優(yōu)勢屎媳《嵋纾基于充血模型的 DDD 開發(fā)模式跟基于貧血模型的傳統(tǒng)開發(fā)模式相比,主要區(qū)別在 Service層烛谊。在基于充血模型的開發(fā)模式下风响,我們將部分原來在 Service 類中的業(yè)務(wù)邏輯移動到了一個充血的 Domain 領(lǐng)域模型中,讓 Service 類的實現(xiàn)依賴這個 Domain 類丹禀。不過状勤,Service 類并不會完全移除,而是負(fù)責(zé)一些不適合放在 Domain 類中的功能双泪。比如持搜,負(fù)責(zé)與 Repository 層打交道、跨領(lǐng)域模型的業(yè)務(wù)聚合功能焙矛、冪等事務(wù)等非功能性的工作葫盼。基于充血模型的 DDD 開發(fā)模式跟基于貧血模型的傳統(tǒng)開發(fā)模式相比村斟,Controller 層和Repository 層的代碼基本上相同贫导。這是因為抛猫,Repository 層的 Entity 生命周期有限,Controller 層的 VO 只是單純作為一種 DTO孩灯。兩部分的業(yè)務(wù)邏輯都不會太復(fù)雜闺金。業(yè)務(wù)邏輯主要集中在 Service 層。所以峰档,Repository 層和 Controller 層繼續(xù)沿用貧血模型的設(shè)計思路是沒有問題的掖看。
事務(wù)腳本VS領(lǐng)域建模模式
單業(yè)務(wù)邏輯比較簡單時,失血模型和貧血模型基本一樣面哥,所有的業(yè)務(wù)邏輯集中在service層哎壳,編寫一個稱為事務(wù)腳本的方法來處理來自表示層的每個請求,這種設(shè)計風(fēng)格是高度面向過程的尚卫,這種方法適用于簡單的業(yè)務(wù)邏輯归榕。
采用事務(wù)腳本會隨著業(yè)務(wù)邏輯變得復(fù)雜,代碼也會難以維護(hù)吱涉。就像單體應(yīng)用程序不斷增長的趨勢一樣刹泄,事務(wù)腳本也存在同樣的問題。很多類同時包含狀態(tài)和行為怎爵,通過將用戶的狀態(tài)和行為收斂到對象領(lǐng)域模型上特石,實現(xiàn)邏輯上的高內(nèi)聚,同時代碼邏輯也會更高復(fù)用鳖链。
事務(wù)腳本模式是實現(xiàn)簡單業(yè)務(wù)邏輯的好方法姆蘸。但是在實現(xiàn)復(fù)雜的業(yè)務(wù)邏輯時,應(yīng)該考慮使用面向?qū)ο蟮念I(lǐng)域模型模式芙委。
關(guān)于DDD的一些理論基礎(chǔ)參考我的另一篇文章 領(lǐng)域驅(qū)動設(shè)計理論基礎(chǔ)
發(fā)布領(lǐng)域事件
設(shè)計服務(wù)的業(yè)務(wù)邏輯的好方法是使用DDD聚合逞敷。DDD聚合很有用,因為它們把領(lǐng)域模塊化灌侣,消除了服務(wù)之間對象的直接引用推捐,并確保每個ACID事務(wù)都在服務(wù)內(nèi)。
創(chuàng)建或更新聚合時應(yīng)發(fā)布領(lǐng)域事件侧啼。領(lǐng)域事件具有廣泛的用途牛柒。可以參考我的另一篇文章分布式事務(wù)總結(jié)中事件表部分痊乾。
微服務(wù)之間的交互方式總結(jié)
在單體應(yīng)用中皮壁,各模塊之間的調(diào)用是通過編程語言級別的方法或者函數(shù)來實現(xiàn)的。而基于微服務(wù)的分布式應(yīng)用是運行在多臺機(jī)器上的符喝;一般來說闪彼,每個服務(wù)實例都是一個進(jìn)程。因此协饲,服務(wù)之間的交互必須通過進(jìn)程間通信(IPC)來實現(xiàn)畏腕。
交互模式
當(dāng)為某個服務(wù)選擇 IPC 時,首先需要考慮服務(wù)之間的交互問題茉稠。客戶端和服務(wù)器之間有很多的交互模式描馅,我們可以從兩個維度進(jìn)行歸類。
第一個維度是這些交互式是同步還是異步:
? 同步模式:客戶端請求需要服務(wù)端即時響應(yīng)而线,甚至可能由于等待而阻塞铭污。
? 異步模式:客戶端請求不會阻塞進(jìn)程,服務(wù)端的響應(yīng)可以是非即時的膀篮。
第二個維度是一對一還是一對多:
? 一對一:每個客戶端請求有一個服務(wù)實例來響應(yīng)嘹狞。包括:請求/響應(yīng),通知(也就是常說的單向請求)誓竿、 請求/異步響應(yīng)磅网。
? 一對多:每個客戶端請求有多個服務(wù)實例來響應(yīng)。包括:發(fā)布/ 訂閱模式筷屡,發(fā)布/異步響應(yīng)模式涧偷。
IPC 技術(shù)
現(xiàn)在有很多不同的 IPC 技術(shù)。服務(wù)間通信可以使用同步的請求/響應(yīng)模式毙死,比如基于 HTTP 的 REST 或者 Thrift燎潮。另外,也可以選擇異步的扼倘、基于消息的通信模式确封,比如 AMQP 或者 STOMP。此外再菊,還可以選擇 JSON 或者 XML 這種可讀的隅肥、基于文本的消息格式。當(dāng)然袄简,也還有效率更高的二進(jìn)制格式腥放,比如 Avro 和 Protocol Buffer。在討論同步的 IPC 機(jī)制之前绿语,我們先了解異步的 IPC 機(jī)制秃症。
基于消息的異步通信
使用消息模式的時候,進(jìn)程之間通過異步交換消息消息的方式通信吕粹≈指蹋客戶端通過向服務(wù)端發(fā)送消息提交請求,如果服務(wù)端需要回復(fù)匹耕,則會發(fā)送另一條獨立的消息給客戶端聚请。由于異步通信,客戶端不會因為等待而阻塞,相反會認(rèn)為響應(yīng)不會被立即收到驶赏。
消息通過渠道發(fā)送炸卑,通過渠道接收。
消息由數(shù)據(jù)頭(例如發(fā)送方這樣的元數(shù)據(jù))和消息正文構(gòu)成煤傍。消息通過渠道發(fā)送盖文,任何數(shù)量的生產(chǎn)者都可以發(fā)送消息到渠道,同樣蚯姆,任何數(shù)量的消費者都可以從渠道中接受數(shù)據(jù)五续。頻道有兩類,包括點對點渠道和發(fā)布/訂閱渠道龄恋。點對點渠道會把消息準(zhǔn)確的發(fā)送到從渠道讀取消息的用戶疙驾,服務(wù)端使用點對點來實現(xiàn)之前提到的一對一交互模式;而發(fā)布/訂閱則把消息投送到所有從渠道讀取數(shù)據(jù)的用戶郭毕,服務(wù)端使用發(fā)布/訂閱渠道來實現(xiàn)上面提到的一對多交互模式它碎。
基于消息的異步通信的經(jīng)典實現(xiàn)就是基于MQ,目前互聯(lián)網(wǎng)使用的MQ主要是Rocketmq铣卡,關(guān)于Rockemq的使用參考我另一篇文章Rocketmq原理&最佳實踐
基于請求/響應(yīng)的同步 IPC
使用同步的链韭、基于請求/響應(yīng)的 IPC 機(jī)制的時候,客戶端向服務(wù)端發(fā)送請求煮落,服務(wù)端處理請求并返回響應(yīng)敞峭。一些客戶端會由于等待服務(wù)端響應(yīng)而被阻塞,而另外一些客戶端可能使用異步的蝉仇、基于事件驅(qū)動的客戶端代碼旋讹,這些代碼可能通過 Future 或者 Rx Observable 封裝。然而轿衔,與使用消息機(jī)制不同沉迹,客戶端需要響應(yīng)及時返回。這個模式中有很多可選的協(xié)議害驹,但最常見的兩個協(xié)議是 REST 和 RPC鞭呕。
首先我們來了解 REST。當(dāng)前很流行開發(fā) RESTful 風(fēng)格的 API宛官。REST 基于 HTTP 協(xié)議葫松,其核心概念是資源典型地代表單一業(yè)務(wù)對象或者一組業(yè)務(wù)對象,業(yè)務(wù)對象包括“消費者”或“產(chǎn)品”底洗。REST 使用 HTTP 協(xié)議來控制資源腋么,通過 URL 實現(xiàn)。譬如亥揖,GET 請求會返回一個資源的包含信息珊擂,可能是 XML 文檔或 JSON 對象格式。POST 請求會創(chuàng)建新資源,而 PUT 請求則會更新資源摧扇。REST 之父 Roy Fielding 曾經(jīng)說過:REST 提供了一系列架構(gòu)系統(tǒng)參數(shù)圣贸,作為整體使用,強(qiáng)調(diào)組件交互的擴(kuò)展性扳剿、接口的通用性旁趟、組件的獨立部署昼激、以及減少交互延遲的中間件庇绽,它強(qiáng)化安全,也能封裝遺留系統(tǒng)橙困。使用基于 HTTP 的協(xié)議有如下好處:1)HTTP 非常簡單并且大家都很熟悉瞧掺。2)可以使用瀏覽器擴(kuò)展(比如 Postman)或者 curl 之類的命令行來測試 API。3)內(nèi)置支持請求/響應(yīng)模式的通信凡傅。4)HTTP 對防火墻友好辟狈。5)不需要中間代理,簡化了系統(tǒng)架構(gòu)夏跷。
不足之處包括:1)只支持請求/響應(yīng)模式交互哼转。盡管可以使用 HTTP 通知,但是服務(wù)端必須一直發(fā)送 HTTP 響應(yīng)槽华。2)由于客戶端和服務(wù)端直接通信(沒有代理或者緩沖機(jī)制)壹蔓,在交互期間必須都保持在線。3)客戶端必須知道每個服務(wù)實例的 URL猫态。
使用REST的一個挑戰(zhàn)是佣蓉,由于HTTP僅提供有限數(shù)量的動詞,因此設(shè)計支持多個更新操作的REST API并不總是很容易亲雪。避免此問題的進(jìn)程間通信技術(shù)是RPC勇凭。RPC有幾個好處:1)設(shè)計具有復(fù)雜更新操作的API非常簡單。2)它具有高效义辕、緊湊的進(jìn)程間通信機(jī)制虾标,尤其是在交換大量消息時。3)支持客戶端和用各種語言編寫的服務(wù)端之間的互操作性灌砖。
RPC也有幾個弊端:1)與基于REST/JSON的API機(jī)制相比璧函,使用基于RPC的API需要做更多的工作。2)RPC是REST的一個引人注目的替代品周崭,但與REST一樣柳譬,它是一種同步通信機(jī)制,因此它也存在局部故障的問題续镇。
更多關(guān)于RPC的明細(xì)參考我的另一篇文章RPC詳解&跨語言RPC實踐
服務(wù)發(fā)現(xiàn)
假設(shè)你正在編寫一些調(diào)用具有REST API的服務(wù)的代碼美澳。為了發(fā)出請求,你的代碼需要知道服務(wù)實例的網(wǎng)絡(luò)位置(IP地址和端口)。在物理硬件上運行的傳統(tǒng)應(yīng)用程序中制跟,服務(wù)實例的網(wǎng)絡(luò)位置通常是靜態(tài)的舅桩。例如,你的代碼可以從偶爾更新的配置文件中讀取網(wǎng)絡(luò)位置雨膨。但在現(xiàn)代的基于云的微服務(wù)應(yīng)用程序中擂涛,通常不那么簡單,現(xiàn)代應(yīng)用程序更具動態(tài)性聊记。
服務(wù)實例具有動態(tài)分配的網(wǎng)絡(luò)位置撒妈。此外,由于自動擴(kuò)展排监、故障和升級狰右,服務(wù)實例集會動態(tài)更改。因此舆床,你的客戶端代碼必須使用服務(wù)發(fā)現(xiàn)棋蚌。
由于無法使用服務(wù)的IP地址靜態(tài)配置客戶端,應(yīng)用程序必須使用動態(tài)服務(wù)發(fā)現(xiàn)機(jī)制挨队。服務(wù)發(fā)現(xiàn)在概念上非常簡單:其關(guān)鍵組件是服務(wù)注冊表谷暮,它是包含服務(wù)實例網(wǎng)絡(luò)位置信息的一個數(shù)據(jù)庫。
服務(wù)實例啟動和停止時盛垦,服務(wù)發(fā)現(xiàn)機(jī)制會更新服務(wù)注冊表湿弦。當(dāng)客戶端調(diào)用服務(wù)時,服務(wù)發(fā)現(xiàn)機(jī)制會查詢服務(wù)注冊表以獲取可用服務(wù)實例的列表情臭,并將請求路由到其中一個服務(wù)實例省撑。
常見的服務(wù)發(fā)現(xiàn)中間件有zookeeper和consul,兩者的原理基本類似俯在。關(guān)于consul可以參考我的另一篇文章consul入門篇
分布式事務(wù)問題
提起微服務(wù)架構(gòu)竟秫,不可避免的兩個話題就是服務(wù)治理和分布式事務(wù)。數(shù)據(jù)庫和業(yè)務(wù)模塊的垂直拆分為我們帶來了系統(tǒng)性能跷乐、穩(wěn)定性和開發(fā)效率的提升的同時也引入了一些更復(fù)雜的問題肥败,例如在數(shù)據(jù)一致性問題上,我們不再能夠依賴數(shù)據(jù)庫的本地事務(wù)愕提,對于一系列的跨庫寫入操作馒稍,如何保證其原子性,是微服務(wù)架構(gòu)下不得不面對的問題浅侨。
針對分布式系統(tǒng)的特點纽谒,基于不同的一致性需求產(chǎn)生了不同的分布式事務(wù)解決方案,追求強(qiáng)一致的兩階段提交如输、追求最終一致性的柔性事務(wù)和事務(wù)消息等等鼓黔。各種方案沒有絕對的好壞央勒,拋開具體場景我們無法評價,更無法能做出合理選擇澳化。在選擇分布式事務(wù)方案時崔步,需要我們充分了解各種解決方案的原理和設(shè)計初衷,再結(jié)合實際的業(yè)務(wù)場景缎谷,從而做出科學(xué)合理的選擇井濒。
關(guān)于分布式事務(wù)問題可以參考我的另一篇文章,里面有對分布式事務(wù)進(jìn)行系統(tǒng)性闡述列林,分布式事務(wù)總結(jié)
事件溯源&CQRS
事件溯源
事件溯源是構(gòu)建業(yè)務(wù)邏輯和持久化聚合的另一種選擇瑞你,它將聚合以一系列事件的方式持久化保存,每個事件代表聚合的一次狀態(tài)變化席纽。應(yīng)用通過重放事件來重新創(chuàng)建聚合的當(dāng)前狀態(tài)捏悬。它的好處有:1)保留聚合的歷史記錄(審計和監(jiān)管)撞蚕;2)可靠地發(fā)布領(lǐng)域事件(微服務(wù)架構(gòu))润梯。它的弊端是:1)有一定學(xué)習(xí)曲線;2)查詢事件存儲庫通常很困難甥厦,這需要CQRS模式纺铭。
傳統(tǒng)持久化技術(shù)的問題
對象與關(guān)系的阻抗失調(diào):關(guān)系數(shù)據(jù)庫的表格結(jié)構(gòu)模式與領(lǐng)域模型及其復(fù)雜關(guān)系的圖狀結(jié)構(gòu)之間,存在基本的概念不匹配問題刀疙。
缺乏聚合的歷史:只存儲聚合的當(dāng)前狀態(tài)舶赔,聚合更新后先前的狀態(tài)丟失,實現(xiàn)審計功能將非常繁瑣且容易出錯谦秧。
事件發(fā)布是凌駕于業(yè)務(wù)邏輯之上:不支持發(fā)布領(lǐng)域事件竟纳,開發(fā)人員必須自己處理事件生成的邏輯。
事件溯源原理
事件溯源通過事件來持久化聚合疚鲤,事件溯源采用基于領(lǐng)域事件的概念來實現(xiàn)聚合的持久化锥累,將每個聚合持久化為數(shù)據(jù)庫中的一系列事件。應(yīng)用程序從事件存儲中檢索并重放事件來加載聚合:
- 加載聚合的事件
- 使用其默認(rèn)的構(gòu)造函數(shù)創(chuàng)建聚合實例
- 調(diào)用apply()方法遍歷事件
事件代表狀態(tài)的改變集歇,事件必須包含執(zhí)行狀態(tài)更改所需要的數(shù)據(jù)桶略,聚合方法都和事件相關(guān)。
業(yè)務(wù)邏輯通過調(diào)用聚合根上的命令方法來處理對聚合的更新請求诲宇。命令方法通常會驗證其參數(shù)际歼,而后更新一個或多個聚合字段。
基于事件溯源的應(yīng)用程序的命令方法則會生成一系列事件姑蓝,并應(yīng)用于聚合以更新其狀態(tài)鹅心。
使用樂觀鎖處理并發(fā)更新
樂觀鎖通常使用版本列來檢測聚合自讀取以來是否已更改。只有當(dāng)前版本和應(yīng)用程序讀取聚合時版本一致,此UPDATE語句才會成功纺荧。
事件溯源和發(fā)布事件
可以將事件溯源作為可靠的事件發(fā)布機(jī)制旭愧。將這些持久化保存的事件傳遞給所有感興趣的消費者溯泣。使用輪詢或者日志拖尾技術(shù)(binlog監(jiān)聽)來發(fā)布事件
使用快照提升性能
長生命周期的聚合可能有大量事件,可定期持久保存聚合狀態(tài)的快照。應(yīng)用通過加載最新快照以及僅加載快照后發(fā)生的事件來快速恢復(fù)聚合狀態(tài)榕茧。
冪等方式的消息處理
基于關(guān)系型數(shù)據(jù)庫事件存儲庫的冪等消息處理:將message ID插入PROCESSED_MESSAGES表垃沦,作為插入EVENTS表的事件的事務(wù)的一部分,以檢測和丟棄重復(fù)消息用押。
基于非關(guān)系數(shù)據(jù)庫事件存儲庫的冪等消息處理:NOSQL的事件存儲庫事務(wù)模型功能有限肢簿,簡單的解決方案是消息的ID存儲在處理它時生成的事件中,通過驗證聚合的所有事件中是否有包含該消息的ID來做重復(fù)檢測蜻拨。
領(lǐng)域事件的演化
事件的結(jié)構(gòu)經(jīng)常隨著時間的推移而變化池充,應(yīng)用程序可能需要處理多個事件版本。
服務(wù)的領(lǐng)域模型隨著時間的推移而發(fā)展,向事件添加字段,不大可能影響接收方缎讼,但更改字段名詞等操作不向后兼容收夸。
通過向上轉(zhuǎn)換來管理結(jié)構(gòu)的變化,事件溯源應(yīng)用可以使用類似Flyway的方法處理向后兼容的更改血崭。從事件存儲庫加載事件時,將各個事件從舊版本更新為新版本卧惜。
事件溯源的好處
- 可靠地發(fā)布領(lǐng)域事件
- 保留聚合的歷史
- 最大程度避免對象與關(guān)聯(lián)的“阻抗失調(diào)”問題
- 為開發(fā)者提供一個“時光機(jī)”
事件溯源的弊端
- 有一定學(xué)習(xí)曲線
- 基于消息傳遞的應(yīng)用程序的復(fù)雜性(消息代理確保至少一次成功傳遞,這意味著非冪等的事件處理程序必須檢測并丟棄重復(fù)事件)
- 處理事件的演化有一定難度
- 刪除數(shù)據(jù)存在一定難度
- 查詢事件存儲庫很有挑戰(zhàn)性
使用 CQRS 實現(xiàn)查詢
使用API組合模式進(jìn)行查詢
每個微服務(wù)只負(fù)責(zé)一個業(yè)務(wù)子域的上下文夹纫,只有這個子域的數(shù)據(jù)咽瓷,因此很多查詢需要從多個服務(wù)中獲取數(shù)據(jù)。最常用的就是API組合模式進(jìn)行查詢舰讹。涉及兩類角色:API組合器和數(shù)據(jù)提供方服務(wù)茅姜。
由誰擔(dān)任API組合器角色:
1)客戶端擔(dān)任,但這對于防火墻之外客戶以及通過較慢網(wǎng)絡(luò)訪問的服務(wù)月匣,此選擇不實用钻洒。
2)API Gateway中實現(xiàn),API查詢提供方服務(wù)锄开,檢索數(shù)據(jù)素标,組合結(jié)果并向客戶端返回響應(yīng)。
3)API組合器院刁,將多個客戶端和服務(wù)使用的查詢操作實現(xiàn)為獨立的服務(wù)糯钙,可實現(xiàn)API Gateway無法完成的復(fù)雜的聚合邏輯。應(yīng)使用響應(yīng)式編程模式退腥,盡可能并行調(diào)用服務(wù)任岸,最大限度地縮短查詢操作的響應(yīng)時間
API組合模式的弊端
- 增加了額外的開銷:需要調(diào)用多個服務(wù)和查詢多個數(shù)據(jù)庫,這帶來了額外的開銷狡刘。
- 帶來了可用性降低的風(fēng)險:隨著調(diào)用的服務(wù)的數(shù)量增多享潜,整個查詢鏈路的可用性是所有數(shù)據(jù)提供服務(wù)的可用性相乘。
- 缺乏事務(wù)數(shù)據(jù)一致性:一個寫操作涉及到多個微服務(wù)嗅蔬,可能某些微服務(wù)還沒有完全結(jié)束剑按,此時的查詢可能會出現(xiàn)多個服務(wù)之間的數(shù)據(jù)不一致疾就。
使用CQRS模式
使用API組合模式檢索分散在多個服務(wù)中的數(shù)據(jù)會導(dǎo)致昂貴、低效的內(nèi)存中連接(如某些服務(wù)并不存儲用于過濾的屬性)艺蝴。
擁有數(shù)據(jù)的服務(wù)將數(shù)據(jù)存儲在不能有效支持所需查詢的表單或數(shù)據(jù)庫中(如無法執(zhí)行有效的地理空間查詢)猬腰。
鑒于隔離(避免過多的職責(zé)導(dǎo)致過載服務(wù))考慮,擁有數(shù)據(jù)的服務(wù)不一定是會實現(xiàn)查詢操作的服務(wù)猜敢。
CQRS模式使用事件來維護(hù)從多個服務(wù)復(fù)制數(shù)據(jù)的只讀視圖姑荷,借此實現(xiàn)對來自多個服務(wù)的數(shù)據(jù)的查詢。
CQRS模式將命令和查詢職責(zé)隔離缩擂。將持久化數(shù)據(jù)模型和使用數(shù)據(jù)的模塊分為兩部分:命令端和查詢端鼠冕。命令端模塊和數(shù)據(jù)模型實現(xiàn)CUD操作,查詢端模塊和數(shù)據(jù)模型實現(xiàn)查詢胯盯。查詢端通過訂閱命令端發(fā)布的事件懈费,使其數(shù)據(jù)模型與命令端數(shù)據(jù)模型保持同步。見下圖:
CQRS的利弊
CQRS的優(yōu)勢:
- 在微服務(wù)架構(gòu)中高效地實現(xiàn)查詢博脑,有效地實現(xiàn)了檢索多個服務(wù)所擁有地數(shù)據(jù)的查詢憎乙。
- 高效地實現(xiàn)多個不同的查詢類型,通過寬表避免了多次RPC調(diào)用和內(nèi)存Join趋厉。
- 在基于事件溯源技術(shù)的應(yīng)用中實現(xiàn)了查詢壳贪,通過訂閱由基于事件溯源的聚合發(fā)布的事件流窑睁,可以保持最新的聚合的一個或多個視圖述呐。
- 更進(jìn)一步地實現(xiàn)問題隔離馏谨。通過將命令和查詢分離慕蔚,讓操作更加單純物舒,利于維護(hù)晚唇。
CQRS的弊端
- 更加復(fù)雜的架構(gòu)
- 處理數(shù)據(jù)復(fù)制導(dǎo)致的延遲净赴,一種解決方案是采用命令端和查詢端API為客戶端提供版本信息金度,使其能夠判斷查詢端是否過時。
外部API模式
外部API的設(shè)計難題
Web應(yīng)用在防火墻內(nèi)部運行勘高,它們通過高帶寬、低延遲的局域網(wǎng)訪問服務(wù)洞慎。其他客戶端在防火墻之外運行焦人,通過較低帶寬花椭、較高延遲的互聯(lián)網(wǎng)或移動網(wǎng)路訪問忽匈。
應(yīng)用程序扮演API組合器的角色,調(diào)用多個服務(wù)并組合結(jié)果矿辽,存在如下問題:
- 多次客戶端請求導(dǎo)致用戶體驗不佳
- 缺乏封裝導(dǎo)致前端開發(fā)做出的代碼修改影響后端
- 服務(wù)可能選用對客戶端不友好的進(jìn)程間通信
- 同樣存在API組合低效的問題丹允,但更大的問題是第三方開發(fā)人員需要一個穩(wěn)定的API,API舊版本可能需要永遠(yuǎn)維護(hù)袋倔。
API Gateway模式
直接訪問服務(wù)的API客戶端會導(dǎo)致很多問題雕蔽,更好的方法是API Gateway,即實現(xiàn)一個服務(wù)奕污,該服務(wù)是外部API客戶端進(jìn)入基于微服務(wù)應(yīng)用程序的入口點萎羔,它負(fù)責(zé):
- 請求路由
- API組合
- 協(xié)議轉(zhuǎn)換
- 能夠為每一個客戶端提供它們專用的API
- 其他邊緣功能(身份驗證、訪問授權(quán)碳默、速率限制贾陷、緩存缘眶、指標(biāo)收集、請求日志)
API Gateway的架構(gòu)具有分層模塊化架構(gòu)髓废,如API層和公共層巷懈,API層由一個或多個獨立的API模塊組成。每個API模塊為特定客戶端實現(xiàn)API慌洪。公共層實現(xiàn)共享功能顶燕,如邊緣功能。
API Gateway若由一個單獨團(tuán)隊維護(hù)冈爹,這種集中式的瓶頸與微服務(wù)架構(gòu)理念背道而馳涌攻。更好的方法或許是讓客戶端團(tuán)隊擁有他們的API模塊,而API Gateway團(tuán)隊負(fù)責(zé)開發(fā)公共模塊和API Gateway的運維频伤。部署流水線必須完全自動化恳谎。
API Gateway的職責(zé)不明確。后端前置模式為每個客戶端定義一個單獨的API Gateway憋肖。每個客戶端團(tuán)隊都擁有自己的API Gateway因痛。API Gateway團(tuán)隊擁有并維護(hù)共享層。每個端的團(tuán)隊擁有并維護(hù)屬于他們的API岸更。
API Gateway的好處是客戶端不必調(diào)用特定服務(wù)鸵膏,而是與API Gateway通信,減少往返次數(shù)怎炊,簡化了代碼谭企。弊端是存在成為開發(fā)瓶頸的風(fēng)險,開發(fā)人員必須更新API Gateway才能對外公開服務(wù)的API结胀,更新過程要盡可能輕量化赞咙,必要時使用后端前置模式。
開發(fā)自己的API Gateway
API Gateway的設(shè)計難題
1)性能和可擴(kuò)展性.所有的外部請求必須首先通過API Gateway糟港。影響性能和可擴(kuò)展性的關(guān)鍵設(shè)計決策是API Gateway應(yīng)用使用同步還是異步I/O
2)使用響應(yīng)式編程抽象。按順序調(diào)用服務(wù)院仿,服務(wù)響應(yīng)時間過長秸抚,盡可能同時調(diào)用所有服務(wù),但編寫可維護(hù)的并發(fā)代碼存在挑戰(zhàn)歹垫“溃可使用響應(yīng)式方法,如CompleteFutures排惨、Monos吭敢、RxJava等。
3)處理局部故障暮芭。通過多實例的負(fù)載均衡以及斷路器模式鹿驼。
目前開源的主流API Gateway有:Netflix Zuul和Spring Cloud Gateway欲低。
使用GraphQL實現(xiàn)API Gateway
實現(xiàn)支持多種客戶端的REST API的API Gateway非常耗時,你可能需要考慮使用基于圖形的API框架畜晰,如GraphQL砾莱。
API由映射到服務(wù)的基于圖形的模式組成,客戶端發(fā)出檢索多個圖形節(jié)點的查詢凄鼻±吧基于查詢的API框架通過從一個或多個服務(wù)檢索數(shù)據(jù)來執(zhí)行查詢。
基于GraphQL(一種標(biāo)準(zhǔn))的API Gateway可使用Node.js Express Web 框架和Apollo GraphQL服務(wù)器块蚌,用js編寫闰非。它可以由三部分組成:
- GraphQL模式:定義服務(wù)器端數(shù)據(jù)模型及其支持的查詢
- 解析器函數(shù):解析函數(shù)將模式的元素映射到各種后端服務(wù)。
- 代理類:代理類調(diào)用應(yīng)用程序的服務(wù)峭范。
執(zhí)行GraphQL
使用GraphQL的主要好處是它的查詢語言為客戶端提供了對返回數(shù)據(jù)的令人難以置信的控制财松。客戶端通過向服務(wù)器發(fā)出包含查詢文檔的請求來執(zhí)行查詢虎敦。簡單情況下游岳,查詢文檔包含查詢的名稱,參數(shù)值及要返回結(jié)果的對象字段其徙。
當(dāng)GraphQL服務(wù)器執(zhí)行查詢時胚迫,必須從一個或多個數(shù)據(jù)存儲中檢索所請求的數(shù)據(jù)。通過將解析函數(shù)附加到模式定義的對象類型字段唾那,可以將GraphQL模式與數(shù)據(jù)源相關(guān)聯(lián)访锻。GraphQL通過調(diào)用解析器函數(shù)檢索數(shù)據(jù),以此實現(xiàn)API組合模式闹获。
GraphQL通過遞歸調(diào)用Query文檔中指定的字段解析器函數(shù)來執(zhí)行查詢期犬。首先,它執(zhí)行查詢解析器避诽,然后遞歸調(diào)用結(jié)果對象層次結(jié)構(gòu)中字段的解析器龟虎。
測試
將代碼扔給QA團(tuán)隊,手動測試沙庐,效率很低鲤妥,在交付流程中才進(jìn)行測試為時已晚。使用微服務(wù)的一個關(guān)鍵動機(jī)是提高可測試性拱雏,微服務(wù)架構(gòu)的復(fù)雜性要求編寫自動化測試棉安,以縮短交付(代碼投入生產(chǎn)環(huán)境)周期。
什么是測試
測試的目的是驗證被測系統(tǒng)的行為铸抑。測試用例是用于特定目標(biāo)的一組測試輸入贡耽、執(zhí)行條件和預(yù)期結(jié)果,一組相關(guān)的測試用例集構(gòu)成一個測試套件。
每個自動化測試都是通過測試類中一個測試方法實現(xiàn)蒲赂。測試包括四個階段:設(shè)置——初始化測試環(huán)境阱冶,這是運行測試的基礎(chǔ);執(zhí)行——調(diào)用被測系統(tǒng)凳宙;驗證——驗證測試的結(jié)果熙揍;清理——清理測試環(huán)境。
被測系統(tǒng)在運行時常會依賴另一些系統(tǒng)氏涩,依賴的麻煩在于它們可能把測試復(fù)雜化届囚,減慢測試速度。解決方案使用測試替身是尖,該對象負(fù)責(zé)模擬依賴項的行為意系。測試替身分為stub(代替依賴項向被測系統(tǒng)發(fā)送調(diào)用的返回值),mock(用來驗證被測系統(tǒng)是否正確調(diào)用來依賴項饺汹,也扮演stub的角色)蛔添。
根據(jù)范圍分類,測試分為以下類型:
- 單元測試:主要測試業(yè)務(wù)邏輯兜辞,測試服務(wù)的一小部分迎瞧,例如類
- 集成測試:驗證服務(wù)與它依賴方的通話,驗證服務(wù)是否可以與基礎(chǔ)設(shè)施服務(wù)或其他服務(wù)進(jìn)行交互逸吵。
- 組件測試:服務(wù)的驗收測試凶硅,單個服務(wù)的驗收測試
- 端到端測試:應(yīng)用程序的驗收測試,整個應(yīng)用程序的測試
微服務(wù)帶來的質(zhì)量挑戰(zhàn)
系統(tǒng)依賴性增加:將單體應(yīng)用轉(zhuǎn)成微服務(wù)扫皱,雖然增加了縮放能力和靈活性足绅,但是引入了更多的依賴,使系統(tǒng)整體變的更復(fù)雜韩脑,使測試環(huán)境的搭建配置以及校驗指標(biāo)更加難以掌控氢妈。
并行開發(fā)障礙:系統(tǒng)依賴性的增加還會給微服務(wù)的并行開發(fā)工作造成影響,需要等待其他微服務(wù)測試環(huán)境部署完畢段多,才能實現(xiàn)集成首量、測試。微服務(wù)數(shù)量越多进苍,需要考慮的對象就越是廣泛
影響傳統(tǒng)測試方法:傳統(tǒng)測試方法往往通過UI測試進(jìn)行驗證蕾总,而微服務(wù)的測試方案更加復(fù)雜。不僅需要驗證各獨立微服務(wù)琅捏,還需要檢查整體業(yè)務(wù)的執(zhí)行路徑。
為服務(wù)編寫單元測試
單元測試有以下兩種類型:
- 獨立型單元測試: 使用針對類的依賴性的模擬對象隔離測試類递雀,常用于領(lǐng)域服務(wù)(Service)柄延,控制器類、入站和出站消息網(wǎng)關(guān)的測試。對外部依賴項進(jìn)行測試替身搜吧。
- 協(xié)作型單元測試: 測試一個類及其依賴項市俊,常用于實體、值對象滤奈、Sagas的測試摆昧。
類的職責(zé)及其在架構(gòu)中的角色決定了要使用的單元測試類型⊙殉蹋控制類和服務(wù)類通常使用獨立型單元測試绅你。領(lǐng)域?qū)ο螅ɡ鐚嶓w和值對象)通常使用協(xié)作型單元測試。
領(lǐng)域服務(wù)的單元測試
領(lǐng)域服務(wù)的方法調(diào)用實體和存儲庫并發(fā)布領(lǐng)域事件昭躺,測試這種 類的有效方法是獨立型單元測試忌锯,它可以模擬存儲庫和消息傳遞類等依賴項。單元測試分三個階段:
1)配置服務(wù)依賴項的模擬對象
2)調(diào)用服務(wù)方法
3)驗證服務(wù)方法返回的值是否正確领炫,以及是否已正確調(diào)用依賴項
事件和消息處理程序的單元測試
每個測試實例都是消息適配器偶垮,向消息通道發(fā)送消息,并驗證是否正確調(diào)用了服務(wù)模擬帝洪。而消息傳遞的基礎(chǔ)設(shè)施是基于樁的似舵,因此不涉及消息代理。測試可以使用Eventuate Tram Mock Messaging框架葱峡。
單元測試不會驗證服務(wù)是否與其他服務(wù)正確交互砚哗,為了驗證服務(wù)是否正確地與其他服務(wù)交互,必須編寫集成測試族沃。
集成測試
為了確保服務(wù)按預(yù)期工作频祝,必須編寫測試來驗證服務(wù)是否可以正確地與基礎(chǔ)設(shè)施服務(wù)和其他服務(wù)進(jìn)行交互。一種方法是啟動所有服務(wù)并通過其API進(jìn)行測試脆淹。更有效的策略是編寫集成測試常空,針對不同類型的適配器采用不同的測試驗證方法,比如:1)針對基于REST的請求/響應(yīng)盖溺,直接驗證http請求和響應(yīng)漓糙。2)針對發(fā)布/訂閱適配器,通過測試驗證/模擬對應(yīng)的領(lǐng)域事件烘嘱;3)針對異步請求/響應(yīng)昆禽,驗證命令消息和恢復(fù)消息。
針對持久化層的集成測試
執(zhí)行持久化集成測試每個階段的行為如下:
設(shè)置:通過創(chuàng)建數(shù)據(jù)庫結(jié)構(gòu)設(shè)置數(shù)據(jù)庫蝇庭,并將其初始化為已知狀態(tài)醉鳖。也可能開始執(zhí)行一些必要的數(shù)據(jù)庫事務(wù)
執(zhí)行:執(zhí)行數(shù)據(jù)庫操作。
驗證:對數(shù)據(jù)庫的狀態(tài)和從數(shù)據(jù)庫中檢索的對象進(jìn)行斷言哮内。
拆解:可選階段盗棵,可以撤銷對數(shù)據(jù)庫所作的更改。
關(guān)于如何配置在持久化集成測試中的使用的數(shù)據(jù)庫,可以使用Docker方案解決纹因。
針對基于REST的請求/響應(yīng)式交互的集成測試
良好的集成測試策略是使用消費者驅(qū)動的契約測試喷屋。契約用于驗證兩端的適配器類。
針對發(fā)布/訂閱式交互的集成測試
與測試REST交互的方式類似瞭恰,不同的是每個契約都指定了一個領(lǐng)域事件屯曹。通過驗證是否觸發(fā)生成對應(yīng)的領(lǐng)域事件,或是否正確調(diào)用了其模擬的依賴項來驗證惊畏。
針對異步請求/響應(yīng)式交互的集成契約測試
消費者端測試驗證命令消息代理類是否發(fā)送了結(jié)構(gòu)正確的命令消息恶耽,并正確處理回復(fù)消息。提供者測試由Spring Cloud Contract代碼生成陕截。每種測試方法對應(yīng)一份契約驳棱。它將契約的輸入消息作為命令消息發(fā)送,并驗證回復(fù)消息是否與契約輸出消息匹配农曲。
組件測試
組件測試指單獨測試服務(wù)社搅。驗收測試是針對軟件組件的面向業(yè)務(wù)的測試。它們從組件客戶端而非內(nèi)部實現(xiàn)角度描述所需的外部可見行為乳规。這些測試源自用戶故事或用例形葬。
使用Gherkin編寫驗收測試
使用Java編寫驗收測試有挑戰(zhàn)性,更好的方法是使用Gherkin暮的,用類似英語場景定義驗收測試笙以。可自動將場景轉(zhuǎn)換為可運行的代碼冻辩。情景具有g(shù)iven-when-then結(jié)構(gòu)猖腕。
使用Cucumber執(zhí)行Gherkin的測試規(guī)范
Cucumber是Gherkin的測試自動化框架。你可以編寫一個步驟定義類恨闪,類包含一組方法倘感,方法定義了每個given-when-then步驟的具體含義蜡豹。
進(jìn)程內(nèi)組件測試
使用常駐內(nèi)存的樁和模擬代替其依賴性運行服務(wù)。編寫更簡單溉苛,速度更快,但不測試服務(wù)的可部署性愚战。
進(jìn)程外組件測試
將服務(wù)打包為生產(chǎn)環(huán)境就緒的格式(如Docker容器鏡像)视乐,并作為單獨的進(jìn)程運行逢倍。進(jìn)程外組件測試使用真實的基礎(chǔ)設(shè)施服務(wù),如數(shù)據(jù)庫趁怔、消息代理随抠,但對應(yīng)用程序服務(wù)的任何依賴項使用樁矗积。好處是提高測試覆蓋率百匆,測試內(nèi)容更接近部署的內(nèi)容;缺點是編寫起來更復(fù)雜男图,執(zhí)行更慢。
端到端測試
端到端測試位于測試金字塔頂端逛球。開發(fā)這類測試緩慢屋厘、脆弱且耗時。應(yīng)盡量控制端到端測試數(shù)量月而。
編寫用戶旅程測試汗洒,模擬用戶在應(yīng)用程序中的旅程,并驗證相對較大的應(yīng)用程序功能片段的高級行為父款。如可編寫完成所有若個測試的單個測試溢谤,而不是單獨測試這些步驟。這可以顯著減少編寫測試數(shù)量并縮短測試執(zhí)行時間憨攒。
端到端測試與組件測試實現(xiàn)類似世杀,使用Gherkin編寫并使用Cucumber執(zhí)行。