本文是基于“微服務架構設計模式”這本書的總結和提煉,將其中的關鍵知識點結合個人的開發實踐進行結合提煉,并對部分話題進一步挖深講透,參雜了部分個人理解。
單體服務VS微服務
單體架構也稱之為單體系統或者是單體應用。就是一種把系統中所有的功能、模塊耦合在一個應用中的架構方式。單體架構特點:1)打包成一個獨立的單元(導成一個唯一的 jar 包或者是 war 包);2)以一個進程的方式來運行,MVC架構就是典型的單體架構。
單體架構的優缺點如下:
優點
- 應用的開發很簡單:IDE和其他開發工具只需要構建這一個單獨的應用程序。
- 易于對應用程序進行大規模的更改:可以更改代碼和數據庫模式,然后構建和部署。
- 測試相對簡單直觀:開發者只需要寫幾個端到端的測試。
- 部署簡單明了: 開發者唯一需要做的就是把war文件復制到安裝了Tomacat的服務器上。
缺點
- 隨著業務的迭代,單體系統會逐漸龐大和復雜,以至于任意一個開發都很難理解和cover它的全部。
- 開發速度變慢:IDE工具會變慢,構建部署時間長。多人協作沖突的概率變高,每一次改動影響面會變大。總之從代碼提交到實際部署交付的周期會變長。
- 難以擴展:單體應用多個高并發請求會導致物理資源(如CPU、內存等)出現單點瓶頸。
- 迭代困難:需要長期依賴某個可能已經過時的技術棧。
微服務是一種架構風格。一個大型的復雜軟件應用,由一個或多個微服務組成。系統中的各個微服務可被獨立部署,各個微服務之間是松耦合的。每個微服務僅關注于完成一個業務域的事情。微服務特點:1)系統是由多個服務構成;2)每個服務可以單獨獨立部署;3)每個服務之間是松耦合的。服務內部是高內聚的,外部是低耦合的。
微服務的優缺點如下:
優點
- 使大型的復雜應用程序可以持續交付和持續部署。
- 每個服務都相對較小并容易維護。
- 服務可以獨立部署和獨立擴展,系統迭代容易。
- 微服務架構可以實現團隊的自治,團隊協作容易,每個服務團隊可以獨立于其他團隊開發、部署和擴展。開發速度相對單體應用更快。
- 每個微服務都可以有獨立的存儲和服務器,從而整個系統的吞吐能力會指數增長。
缺點
- 運維成本過高,部署數量較多,需要協調更多的開發團隊。
- 接口需要兼容多版本,
- 一個需要改動的服務工程會比較多
- 分布式系統帶來更高的復雜性,需要處理分布式事務,需要有更好的發布平臺和分布式跟蹤平臺等。
微服務架構 與 SOA的異同
SOA(Service Oriented Architecture,面向服務的架構)是一種設計方法,其中包含多個服務, 服務之間通過相互依賴最終提供一系列的功能。一個服務通常以獨立的形式存在于操作系統進程中。各個服務之間 通過網絡調用。
微服務是SOA發展出來的產物,它是一種比較現代化的細粒度的SOA實現方式。
SOA往往采用全局數據模型并共享數據庫,而每個微服務都有自己的數據模型和數據庫;SOA是較大的單體應用,微服務是較小的服務。SOA之間的通信采用的是類似ESB(Enterprise Service Bus)只能管道,采用例如SOAP、WS等重量級協議,而微服務往往采用RPC或者REST這種輕量級的協議。
討論「微服務和SOA的差別」的意義遠不如討論「微服務和單體系統的差別」更大,因為他們的區別實在有點微妙。
微服務架構其實和 SOA 架構類似,微服務是在 SOA 上做的升華,微服務架構強調的一個重點是“業務需要徹底的組件化和服務化”,原有的單個業務系統會拆分為多個可以獨立開發、設計、運行的小應用。這些小應用之間通過服務完成交互和集成。下面這個公式很好的描述兩者關系:
微服務架構 = 80%的SOA服務架構思想 + 100%的組件化架構思想 + 80%的領域建模思想
微服務如何拆分以及如何設計
微服務的物理拆分--將一個大需求拆分為多個子系統
跟所有的軟件開發過程一樣,一開始我們需要拿到領域專家或者現有應用的需求文檔。跟所有的軟件開發一樣,定義架構也是一項藝術而非技術。下面定義應用程序架構的三步式流程。
- 第一步是識別業務系統操作。將應用程序的需求提煉為各種關鍵請求。描述服務之間協作方式的架構場景。
- 第二步是確定如何分解服務。有幾種策略可供選擇。一種源于業務架構學派的策略是定義與業務能力相對應的服務。另一種策略是圍繞領域驅動設計的子域來分解和設計服務。但這些策略的最終結果都是圍繞業務概念而非技術概念分解和設計的服務。
- 第三步是確定每個服務的API。為此,你將第一步中標識的每個系統操作分配給服務。服務可以完全獨立地實現操作。
識別業務系統操作
定義應用程序架構的第一步是定義業務系統操作。起點是應用程序的需求,包括用戶故事及其相關的用戶場景(請注意,這些與架構場景不同)。第一步創建由關鍵類組成的抽象領域模型,這些關鍵類提供用于描述系統操作的詞匯表。第二步確定系統操作,并根據領域模型描述每個系統操作的行為。
領域模型主要源自用戶故事中提及的名詞,系統操作主要來自用戶故事中提及的動詞。你還可以使用名為事件風暴(Event Storming)的技術定義領域模型,每個系統操作的行為都是根據它對一個或多個領域對象的影響以及它們之間的關系來描述的。
根據業務能力進行服務拆分
創建微服務架構的策略之一就是采用業務能力進行服務拆分。業務能力是一個來自于業務架構建模的術語。業務能力是指一些能夠為公司(或組織)產生價值的商業活動。特定業務的業務能力取決于這個業務的類型。例如,保險公司業務能力通常包括承保、理賠管理、賬務和合規等。在線商店的業務能力包括:訂單管理、庫存管理和發貨,等等。
- 業務能力定義了一個組織的工作。組織的業務能力通常是指這個組織的業務是做什么,它們通常都是穩定的。與之相反,組織采用何種方式來實現它的業務能力,是隨著時間不斷變化的。
- 識別業務能力。一個組織有哪些業務能力,是通過對組織的目標、結構和商業流程的分析得來的。每一個業務能力都可以被認為是一個服務。
- 從業務能力到服務。一旦確定了業務能力,就可以為每個能力或相關能力組定義服務。
根據子域進行服務拆分
Eric Evans在他的經典著作中(Addison-Wesley Professional,2003)提出的領域驅動設計是構建復雜軟件的方法論,這些軟件通常都以面向對象和領域模型為核心。領域模型以解決具體問題的方式包含了一個領域內的知識。它定義了當前領域相關團隊的詞匯表,DDD也稱之為通用語言(Ubiquitous language)。領域模型會被緊密地映射到應用的設計和實現環節。在微服務架構的設計層面,DDD有兩個特別重要的概念,子域和限界上下文。
子域是領域的一部分,領域是DDD中用來描述應用程序問題域的一個術語。識別子域的方式跟識別業務能力一樣:分析業務并識別業務的不同專業領域,分析產出的子域定義結果也會跟業務能力非常接近。
DDD把領域模型的邊界稱為限界上下文(bounded context)。限界上下文包括實現這個模型的代碼集合。當使用微服務架構時,每一個限界上下文對應一個或者一組服務。換一種說法,我們可以通過DDD的方式定義子域,并把子域對應為每一個服務,這樣就完成了微服務架構的設計工作。
關于根據子域進行服務拆分可以參考我的這篇文章,這篇文章是以一個在線問診場景,描述了如何從需求落地到微服務。
醫療場景交易平臺戰略設計&戰術落地思考
微服務的邏輯拆分--架構風格
微服務將一個大型的復雜軟件應用拆分為一個或多個微服務系統 。每一個微服務系統的內部代碼組織方式就是架構風格。常見的架構風格有MVC三層架構以及現在提倡的六邊形架構,下面對這兩種架構進行介紹總結。
分層式架構風格
架構的典型例子是分層架構。分層架構將軟件元素按“層”的方式組織。每個層都有明確定義的職責。分層架構還限制了層之間的依賴關系。每一層只能依賴于緊鄰其下方的層(如果嚴格分層)或其下面的任何層。
可以將分層架構應用于前面討論的四個視圖中的任何一個。流行的三層架構是應用于邏輯視圖的分層架構。它將應用程序的類組織到以下層中:
表現層:包含實現用戶界面或外部API的代碼。
業務邏輯層:包含業務邏輯。
數據持久化層:實現與數據庫交互的邏輯。
分層架構是架構風格的一個很好的例子,但它確實有一些明顯的弊端:
單個表現層:它無法展現應用程序可能不僅僅由單個系統調用的事實。
單一數據持久化層:它無法展現應用程序可能與多個數據庫進行交互的事實。
將業務邏輯層定義為依賴于數據持久化層:理論上,這樣的依賴性會妨礙你在沒有數據庫的情況下測試業務邏輯。
此外,分層架構錯誤地表示了精心設計的應用程序中的依賴關系。業務邏輯通常定義數據訪問方法的接口或接口庫。數據持久化層則定義了實現存儲庫接口的DAO類。換句話說,依賴關系與分層架構所描述的相反。
關于架構風格的六邊形
六邊形架構是分層架構風格的替代品。如下圖所示,六邊形架構風格選擇以業務邏輯為中心的方式組織邏輯視圖。應用程序具有一個或多個入站適配器,而不是表示層,它通過調用業務邏輯來處理來自外部的請求。同樣,應用程序具有一個或多個出站適配器,而不是數據持久化層,這些出站適配器由業務邏輯調用并調用外部應用程序。此架構的一個關鍵特性和優點是業務邏輯不依賴于適配器。相反,各種適配器都依賴業務邏輯。
業務邏輯具有一個或多個端口(port)。端口定義了一組操作,關于業務邏輯如何與外部交互。例如,在Java中,端口通常是Java接口。有兩種端口:入站和出站端口。入站端口是業務邏輯公開的API,它使外部應用程序可以調用它。入站端口的一個實例是服務接口,它定義服務的公共方法。出站端口是業務邏輯調用外部系統的方式。出站端口的一個實例是存儲庫接口,它定義數據訪問操作的集合。
業務邏輯的周圍是適配器。與端口一樣,有兩種類型的適配器:入站和出站。入站適配器通過調用入站端口來處理來自外部世界的請求。入站適配器的一個實例是Spring MVC Controller,它實現一組REST接口(endpoint)或一組Web頁面。另一個實例是訂閱消息的消息代理客戶端。多個入站適配器可以調用相同的入站端口。
出站適配器實現出站端口,并通過調用外部應用程序或服務處理來自業務邏輯的請求。出站適配器的一個實例是實現訪問數據庫的操作的數據訪問對象(DAO)類。另一個實例是調用遠程服務的代理類。出站適配器也可以發布事件。
六邊形架構風格的一個重要好處是它將業務邏輯與適配器中包含的表示層和數據訪問層的邏輯分離開來。業務邏輯不依賴于表示層邏輯或數據訪問層邏輯。
由于這種分離,單獨測試業務邏輯要容易得多。另一個好處是它更準確地反映了現代應用程序的架構。可以通過多個適配器調用業務邏輯,每個適配器實現特定的API或用戶界面。業務邏輯還可以調用多個適配器,每個適配器調用不同的外部系統。六邊形架構是描述微服務架構中每個服務的架構的好方法。
分層架構和六邊形架構都是架構風格的實例。每個都定義了架構的構建塊(元素),并對它們之間的關系施加了約束。六邊形架構和分層架構(三層架構)構成了軟件的邏輯視圖。現在讓我們將微服務架構定義為構成軟件的實現視圖的架構風格。
服務拆分的規范
微服務拆分之后,工程會比較的多,如果沒有一定的規范,將會非常混亂,難以維護。
首先人們經常問的一個問題是,服務拆分之后,原來都在一個進程里面的函數調用,現在變成了A調用B調用C調用D調用E,會不會因為調用鏈路過長而使得相應變慢呢?
服務拆分的規范一:服務拆分最多三層,兩次調用
服務拆分是為了橫向擴展,因而應該橫向拆分,而非縱向拆成一串的。也即應該將商品和訂單拆分,而非下單的十個步驟拆分,然后一個調用一個。
縱向的拆分最多三層:
基礎服務層:用于屏蔽數據庫,緩存層,提供原子的對象查詢接口,有這一層,為了數據層做一定改變的時候,例如分庫分表,數據庫擴容,緩存替換等,對于上層透明,上層僅僅調用這一層的接口,不直接訪問數據庫和緩存。
組合服務層:這一層調用基礎服務層,完成較為復雜的業務邏輯,實現分布式事務也多在這一層
Controller層:接口層,調用組合服務層對外
服務拆分的規范二:僅僅單向調用,嚴禁循環調用
微服務拆分后,服務之間的依賴關系復雜,如果循環調用,升級的時候就很頭疼,不知道應該先升級哪個,后升級哪個,難以維護。
因而層次之間的調用規定如下:
基礎服務層主要做數據庫的操作和一些簡單的業務邏輯,不允許調用其他任何服務。
組合服務層,可以調用基礎服務層,完成復雜的業務邏輯,可以調用組合服務層,不允許循環調用,不允許調用Controller層服務
Controller層,可以調用組合業務層服務,不允許被其他服務調用
如果出現循環調用,例如A調用B,B也調用A,則分成Controller層和組合服務層兩層,A調用B的下層,B調用A的下層。也可以使用消息隊列,將同步調用,改為異步調用。
服務拆分的規范三:將串行調用改為并行調用,或者異步化
如果有的組合服務處理流程的確很長,需要調用多個外部服務,應該考慮如何通過消息隊列,實現異步化和解耦。
例如下單之后,要刷新緩存,要通知倉庫等,這些都不需要再下單成功的時候就要做完,而是可以發一個消息給消息隊列,異步通知其他服務。
而且使用消息隊列的好處是,你只要發送一個消息,無論下游依賴方有一個,還是有十個,都是一條消息搞定,只不過多幾個下游監聽消息即可。
對于下單必須同時做完的,例如扣減庫存和優惠券等,可以進行并行調用,這樣處理時間會大大縮短,不是多次調用的時間之和,而是最長的那個系統調用時間。
服務拆分的規范四:接口應該實現冪等
微服務拆分之后,服務之間的調用當出現錯誤的時候,一定會重試,但是為了不要下兩次單,支付兩次,需要所有的接口實現冪等。
冪等一般需要設計一個冪等表來實現,冪等表中的主鍵或者唯一鍵可以是transaction id,或者business id,可以通過這個id的唯一性標識一個唯一的操作。
也有冪等操作使用狀態機,當一個調用到來的時候,往往觸發一個狀態的變化,當下次調用到來的時候,發現已經不是這個狀態,就說明上次已經調用過了。
狀態的變化需要是一個原子操作,也即并發調用的時候,只有一次可以執行。可以使用分布式鎖,或者樂觀鎖CAS操作實現。
服務拆分的規范五:接口數據定義嚴禁內嵌,透傳
微服務接口之間傳遞數據,往往通過數據結構,如果數據結構透傳,從底層一直到上層使用同一個數據結構,或者上層的數據結構內嵌底層的數據結構,當數據結構中添加或者刪除一個字段的時候,波及的面會非常大。
因而接口數據定義,在每兩個接口之間約定,嚴禁內嵌和透傳,即便差不多,也應該重新定義,這樣接口數據定義的改變,影響面僅僅在調用方和被調用方,當接口需要更新的時候,比較可控,也容易升級。
服務拆分的規范六:規范化工程名
微服務拆分后,工程名非常多,開發人員,開發團隊也非常多,如何讓一個開發人員看到一個工程名,或者jar的名稱,就大概知道是干什么的,需要一個規范化的約定。
例如出現pay就是支付,出現order就是下單,出現account就是用戶。
再如出現compose就是組合層,controller就是接口層,basic就是基礎服務層。
出現api就是接口定義,impl就是實現。
pay-compose-api就是支付組合層接口定義。
account-basic-impl就是用戶基礎服務層的實現。
微服務架構中的業務邏輯設計
代碼模型結構
貧血模型是指使用的領域對象中只有setter和getter方法(POJO),所有的業務邏輯都不包含在領域對象中而是放在業務邏輯層。有人將我們這里說的貧血模型進一步劃分成失血模型(領域對象完全沒有業務邏輯)和貧血模型(領域對象有少量的業務邏輯),我們這里就不對此加以區分了。充血模型將大多數業務邏輯和持久化放在領域對象中,業務邏輯(業務門面)只是完成對業務邏輯的封裝、事務和權限等的處理。
充血模型的層次結構和上面的差不多,不過大多業務邏輯和持久化放在Domain Object里面,Business Logic只是簡單封裝部分業務邏輯以及控制事務、權限等,這樣層次結構就變成Client->(Business Facade)->Business Logic->Domain Object->Data Access。
優點是面向對象,Business Logic符合單一職責,不像在貧血模型里面那樣包含所有的業務邏輯太過沉重。
脹血模型是基于充血模型上取消Service層,只剩下domain object和DAO兩層,在domain object的domain logic上面封裝事務。
在這四種模型當中,失血模型和脹血模型應該是不被提倡的。而貧血模型和充血模型從技術上來說,都已經是可行的了。事務封裝還是盡量放在Service層(我們的manage層)。脹血模型將對象的序列化行為封裝到領域層,即domain object會調用domain acess層,同時domain access層又依賴domain object的結構,所以脹血模型中domain object層會和domain access層雙向依賴。
我們平時做 Web 項目的業務開發,大部分都是基于貧血模型的 MVC 三層架構,稱為傳統的開發模式。之所以稱之為“傳統”,是相對于新興的基于充血模型的DDD 開發模式來說的。基于貧血模型的傳統開發模式,是典型的面向過程的編程風格。相反,基于充血模型的 DDD 開發模式,是典型的面向對象的編程風格。不過,DDD 也并非銀彈。對于業務不復雜的系統開發來說,基于貧血模型的傳統開發模式簡單夠用,基于充血模型的 DDD 開發模式有點大材小用,無法發揮作用。相反,對于業務復雜的系統開發來說,基于充血模型的 DDD 開發模式,因為前期需要在設計上投入更多時間和精力,來提高代碼的復用性和可維護性,所以相比基于貧血模型的開發模式,更加有優勢。基于充血模型的 DDD 開發模式跟基于貧血模型的傳統開發模式相比,主要區別在 Service層。在基于充血模型的開發模式下,我們將部分原來在 Service 類中的業務邏輯移動到了一個充血的 Domain 領域模型中,讓 Service 類的實現依賴這個 Domain 類。不過,Service 類并不會完全移除,而是負責一些不適合放在 Domain 類中的功能。比如,負責與 Repository 層打交道、跨領域模型的業務聚合功能、冪等事務等非功能性的工作。基于充血模型的 DDD 開發模式跟基于貧血模型的傳統開發模式相比,Controller 層和Repository 層的代碼基本上相同。這是因為,Repository 層的 Entity 生命周期有限,Controller 層的 VO 只是單純作為一種 DTO。兩部分的業務邏輯都不會太復雜。業務邏輯主要集中在 Service 層。所以,Repository 層和 Controller 層繼續沿用貧血模型的設計思路是沒有問題的。
事務腳本VS領域建模模式
單業務邏輯比較簡單時,失血模型和貧血模型基本一樣,所有的業務邏輯集中在service層,編寫一個稱為事務腳本的方法來處理來自表示層的每個請求,這種設計風格是高度面向過程的,這種方法適用于簡單的業務邏輯。
采用事務腳本會隨著業務邏輯變得復雜,代碼也會難以維護。就像單體應用程序不斷增長的趨勢一樣,事務腳本也存在同樣的問題。很多類同時包含狀態和行為,通過將用戶的狀態和行為收斂到對象領域模型上,實現邏輯上的高內聚,同時代碼邏輯也會更高復用。
事務腳本模式是實現簡單業務邏輯的好方法。但是在實現復雜的業務邏輯時,應該考慮使用面向對象的領域模型模式。
關于DDD的一些理論基礎參考我的另一篇文章 領域驅動設計理論基礎
發布領域事件
設計服務的業務邏輯的好方法是使用DDD聚合。DDD聚合很有用,因為它們把領域模塊化,消除了服務之間對象的直接引用,并確保每個ACID事務都在服務內。
創建或更新聚合時應發布領域事件。領域事件具有廣泛的用途。可以參考我的另一篇文章分布式事務總結中事件表部分。
微服務之間的交互方式總結
在單體應用中,各模塊之間的調用是通過編程語言級別的方法或者函數來實現的。而基于微服務的分布式應用是運行在多臺機器上的;一般來說,每個服務實例都是一個進程。因此,服務之間的交互必須通過進程間通信(IPC)來實現。
交互模式
當為某個服務選擇 IPC 時,首先需要考慮服務之間的交互問題。客戶端和服務器之間有很多的交互模式,我們可以從兩個維度進行歸類。
第一個維度是這些交互式是同步還是異步:
? 同步模式:客戶端請求需要服務端即時響應,甚至可能由于等待而阻塞。
? 異步模式:客戶端請求不會阻塞進程,服務端的響應可以是非即時的。
第二個維度是一對一還是一對多:
? 一對一:每個客戶端請求有一個服務實例來響應。包括:請求/響應,通知(也就是常說的單向請求)、 請求/異步響應。
? 一對多:每個客戶端請求有多個服務實例來響應。包括:發布/ 訂閱模式,發布/異步響應模式。
IPC 技術
現在有很多不同的 IPC 技術。服務間通信可以使用同步的請求/響應模式,比如基于 HTTP 的 REST 或者 Thrift。另外,也可以選擇異步的、基于消息的通信模式,比如 AMQP 或者 STOMP。此外,還可以選擇 JSON 或者 XML 這種可讀的、基于文本的消息格式。當然,也還有效率更高的二進制格式,比如 Avro 和 Protocol Buffer。在討論同步的 IPC 機制之前,我們先了解異步的 IPC 機制。
基于消息的異步通信
使用消息模式的時候,進程之間通過異步交換消息消息的方式通信。客戶端通過向服務端發送消息提交請求,如果服務端需要回復,則會發送另一條獨立的消息給客戶端。由于異步通信,客戶端不會因為等待而阻塞,相反會認為響應不會被立即收到。
消息通過渠道發送,通過渠道接收。
消息由數據頭(例如發送方這樣的元數據)和消息正文構成。消息通過渠道發送,任何數量的生產者都可以發送消息到渠道,同樣,任何數量的消費者都可以從渠道中接受數據。頻道有兩類,包括點對點渠道和發布/訂閱渠道。點對點渠道會把消息準確的發送到從渠道讀取消息的用戶,服務端使用點對點來實現之前提到的一對一交互模式;而發布/訂閱則把消息投送到所有從渠道讀取數據的用戶,服務端使用發布/訂閱渠道來實現上面提到的一對多交互模式。
基于消息的異步通信的經典實現就是基于MQ,目前互聯網使用的MQ主要是Rocketmq,關于Rockemq的使用參考我另一篇文章Rocketmq原理&最佳實踐
基于請求/響應的同步 IPC
使用同步的、基于請求/響應的 IPC 機制的時候,客戶端向服務端發送請求,服務端處理請求并返回響應。一些客戶端會由于等待服務端響應而被阻塞,而另外一些客戶端可能使用異步的、基于事件驅動的客戶端代碼,這些代碼可能通過 Future 或者 Rx Observable 封裝。然而,與使用消息機制不同,客戶端需要響應及時返回。這個模式中有很多可選的協議,但最常見的兩個協議是 REST 和 RPC。
首先我們來了解 REST。當前很流行開發 RESTful 風格的 API。REST 基于 HTTP 協議,其核心概念是資源典型地代表單一業務對象或者一組業務對象,業務對象包括“消費者”或“產品”。REST 使用 HTTP 協議來控制資源,通過 URL 實現。譬如,GET 請求會返回一個資源的包含信息,可能是 XML 文檔或 JSON 對象格式。POST 請求會創建新資源,而 PUT 請求則會更新資源。REST 之父 Roy Fielding 曾經說過:REST 提供了一系列架構系統參數,作為整體使用,強調組件交互的擴展性、接口的通用性、組件的獨立部署、以及減少交互延遲的中間件,它強化安全,也能封裝遺留系統。使用基于 HTTP 的協議有如下好處:1)HTTP 非常簡單并且大家都很熟悉。2)可以使用瀏覽器擴展(比如 Postman)或者 curl 之類的命令行來測試 API。3)內置支持請求/響應模式的通信。4)HTTP 對防火墻友好。5)不需要中間代理,簡化了系統架構。
不足之處包括:1)只支持請求/響應模式交互。盡管可以使用 HTTP 通知,但是服務端必須一直發送 HTTP 響應。2)由于客戶端和服務端直接通信(沒有代理或者緩沖機制),在交互期間必須都保持在線。3)客戶端必須知道每個服務實例的 URL。
使用REST的一個挑戰是,由于HTTP僅提供有限數量的動詞,因此設計支持多個更新操作的REST API并不總是很容易。避免此問題的進程間通信技術是RPC。RPC有幾個好處:1)設計具有復雜更新操作的API非常簡單。2)它具有高效、緊湊的進程間通信機制,尤其是在交換大量消息時。3)支持客戶端和用各種語言編寫的服務端之間的互操作性。
RPC也有幾個弊端:1)與基于REST/JSON的API機制相比,使用基于RPC的API需要做更多的工作。2)RPC是REST的一個引人注目的替代品,但與REST一樣,它是一種同步通信機制,因此它也存在局部故障的問題。
更多關于RPC的明細參考我的另一篇文章RPC詳解&跨語言RPC實踐
服務發現
假設你正在編寫一些調用具有REST API的服務的代碼。為了發出請求,你的代碼需要知道服務實例的網絡位置(IP地址和端口)。在物理硬件上運行的傳統應用程序中,服務實例的網絡位置通常是靜態的。例如,你的代碼可以從偶爾更新的配置文件中讀取網絡位置。但在現代的基于云的微服務應用程序中,通常不那么簡單,現代應用程序更具動態性。
服務實例具有動態分配的網絡位置。此外,由于自動擴展、故障和升級,服務實例集會動態更改。因此,你的客戶端代碼必須使用服務發現。
由于無法使用服務的IP地址靜態配置客戶端,應用程序必須使用動態服務發現機制。服務發現在概念上非常簡單:其關鍵組件是服務注冊表,它是包含服務實例網絡位置信息的一個數據庫。
服務實例啟動和停止時,服務發現機制會更新服務注冊表。當客戶端調用服務時,服務發現機制會查詢服務注冊表以獲取可用服務實例的列表,并將請求路由到其中一個服務實例。
常見的服務發現中間件有zookeeper和consul,兩者的原理基本類似。關于consul可以參考我的另一篇文章consul入門篇
分布式事務問題
提起微服務架構,不可避免的兩個話題就是服務治理和分布式事務。數據庫和業務模塊的垂直拆分為我們帶來了系統性能、穩定性和開發效率的提升的同時也引入了一些更復雜的問題,例如在數據一致性問題上,我們不再能夠依賴數據庫的本地事務,對于一系列的跨庫寫入操作,如何保證其原子性,是微服務架構下不得不面對的問題。
針對分布式系統的特點,基于不同的一致性需求產生了不同的分布式事務解決方案,追求強一致的兩階段提交、追求最終一致性的柔性事務和事務消息等等。各種方案沒有絕對的好壞,拋開具體場景我們無法評價,更無法能做出合理選擇。在選擇分布式事務方案時,需要我們充分了解各種解決方案的原理和設計初衷,再結合實際的業務場景,從而做出科學合理的選擇。
關于分布式事務問題可以參考我的另一篇文章,里面有對分布式事務進行系統性闡述,分布式事務總結
事件溯源&CQRS
事件溯源
事件溯源是構建業務邏輯和持久化聚合的另一種選擇,它將聚合以一系列事件的方式持久化保存,每個事件代表聚合的一次狀態變化。應用通過重放事件來重新創建聚合的當前狀態。它的好處有:1)保留聚合的歷史記錄(審計和監管);2)可靠地發布領域事件(微服務架構)。它的弊端是:1)有一定學習曲線;2)查詢事件存儲庫通常很困難,這需要CQRS模式。
傳統持久化技術的問題
對象與關系的阻抗失調:關系數據庫的表格結構模式與領域模型及其復雜關系的圖狀結構之間,存在基本的概念不匹配問題。
缺乏聚合的歷史:只存儲聚合的當前狀態,聚合更新后先前的狀態丟失,實現審計功能將非常繁瑣且容易出錯。
事件發布是凌駕于業務邏輯之上:不支持發布領域事件,開發人員必須自己處理事件生成的邏輯。
事件溯源原理
事件溯源通過事件來持久化聚合,事件溯源采用基于領域事件的概念來實現聚合的持久化,將每個聚合持久化為數據庫中的一系列事件。應用程序從事件存儲中檢索并重放事件來加載聚合:
- 加載聚合的事件
- 使用其默認的構造函數創建聚合實例
- 調用apply()方法遍歷事件
事件代表狀態的改變,事件必須包含執行狀態更改所需要的數據,聚合方法都和事件相關。
業務邏輯通過調用聚合根上的命令方法來處理對聚合的更新請求。命令方法通常會驗證其參數,而后更新一個或多個聚合字段。
基于事件溯源的應用程序的命令方法則會生成一系列事件,并應用于聚合以更新其狀態。
使用樂觀鎖處理并發更新
樂觀鎖通常使用版本列來檢測聚合自讀取以來是否已更改。只有當前版本和應用程序讀取聚合時版本一致,此UPDATE語句才會成功。
事件溯源和發布事件
可以將事件溯源作為可靠的事件發布機制。將這些持久化保存的事件傳遞給所有感興趣的消費者。使用輪詢或者日志拖尾技術(binlog監聽)來發布事件
使用快照提升性能
長生命周期的聚合可能有大量事件,可定期持久保存聚合狀態的快照。應用通過加載最新快照以及僅加載快照后發生的事件來快速恢復聚合狀態。
冪等方式的消息處理
基于關系型數據庫事件存儲庫的冪等消息處理:將message ID插入PROCESSED_MESSAGES表,作為插入EVENTS表的事件的事務的一部分,以檢測和丟棄重復消息。
基于非關系數據庫事件存儲庫的冪等消息處理:NOSQL的事件存儲庫事務模型功能有限,簡單的解決方案是消息的ID存儲在處理它時生成的事件中,通過驗證聚合的所有事件中是否有包含該消息的ID來做重復檢測。
領域事件的演化
事件的結構經常隨著時間的推移而變化,應用程序可能需要處理多個事件版本。
服務的領域模型隨著時間的推移而發展,向事件添加字段,不大可能影響接收方,但更改字段名詞等操作不向后兼容。
通過向上轉換來管理結構的變化,事件溯源應用可以使用類似Flyway的方法處理向后兼容的更改。從事件存儲庫加載事件時,將各個事件從舊版本更新為新版本。
事件溯源的好處
- 可靠地發布領域事件
- 保留聚合的歷史
- 最大程度避免對象與關聯的“阻抗失調”問題
- 為開發者提供一個“時光機”
事件溯源的弊端
- 有一定學習曲線
- 基于消息傳遞的應用程序的復雜性(消息代理確保至少一次成功傳遞,這意味著非冪等的事件處理程序必須檢測并丟棄重復事件)
- 處理事件的演化有一定難度
- 刪除數據存在一定難度
- 查詢事件存儲庫很有挑戰性
使用 CQRS 實現查詢
使用API組合模式進行查詢
每個微服務只負責一個業務子域的上下文,只有這個子域的數據,因此很多查詢需要從多個服務中獲取數據。最常用的就是API組合模式進行查詢。涉及兩類角色:API組合器和數據提供方服務。
由誰擔任API組合器角色:
1)客戶端擔任,但這對于防火墻之外客戶以及通過較慢網絡訪問的服務,此選擇不實用。
2)API Gateway中實現,API查詢提供方服務,檢索數據,組合結果并向客戶端返回響應。
3)API組合器,將多個客戶端和服務使用的查詢操作實現為獨立的服務,可實現API Gateway無法完成的復雜的聚合邏輯。應使用響應式編程模式,盡可能并行調用服務,最大限度地縮短查詢操作的響應時間
API組合模式的弊端
- 增加了額外的開銷:需要調用多個服務和查詢多個數據庫,這帶來了額外的開銷。
- 帶來了可用性降低的風險:隨著調用的服務的數量增多,整個查詢鏈路的可用性是所有數據提供服務的可用性相乘。
- 缺乏事務數據一致性:一個寫操作涉及到多個微服務,可能某些微服務還沒有完全結束,此時的查詢可能會出現多個服務之間的數據不一致。
使用CQRS模式
使用API組合模式檢索分散在多個服務中的數據會導致昂貴、低效的內存中連接(如某些服務并不存儲用于過濾的屬性)。
擁有數據的服務將數據存儲在不能有效支持所需查詢的表單或數據庫中(如無法執行有效的地理空間查詢)。
鑒于隔離(避免過多的職責導致過載服務)考慮,擁有數據的服務不一定是會實現查詢操作的服務。
CQRS模式使用事件來維護從多個服務復制數據的只讀視圖,借此實現對來自多個服務的數據的查詢。
CQRS模式將命令和查詢職責隔離。將持久化數據模型和使用數據的模塊分為兩部分:命令端和查詢端。命令端模塊和數據模型實現CUD操作,查詢端模塊和數據模型實現查詢。查詢端通過訂閱命令端發布的事件,使其數據模型與命令端數據模型保持同步。見下圖:
CQRS的利弊
CQRS的優勢:
- 在微服務架構中高效地實現查詢,有效地實現了檢索多個服務所擁有地數據的查詢。
- 高效地實現多個不同的查詢類型,通過寬表避免了多次RPC調用和內存Join。
- 在基于事件溯源技術的應用中實現了查詢,通過訂閱由基于事件溯源的聚合發布的事件流,可以保持最新的聚合的一個或多個視圖。
- 更進一步地實現問題隔離。通過將命令和查詢分離,讓操作更加單純,利于維護。
CQRS的弊端
- 更加復雜的架構
- 處理數據復制導致的延遲,一種解決方案是采用命令端和查詢端API為客戶端提供版本信息,使其能夠判斷查詢端是否過時。
外部API模式
外部API的設計難題
Web應用在防火墻內部運行,它們通過高帶寬、低延遲的局域網訪問服務。其他客戶端在防火墻之外運行,通過較低帶寬、較高延遲的互聯網或移動網路訪問。
應用程序扮演API組合器的角色,調用多個服務并組合結果,存在如下問題:
- 多次客戶端請求導致用戶體驗不佳
- 缺乏封裝導致前端開發做出的代碼修改影響后端
- 服務可能選用對客戶端不友好的進程間通信
- 同樣存在API組合低效的問題,但更大的問題是第三方開發人員需要一個穩定的API,API舊版本可能需要永遠維護。
API Gateway模式
直接訪問服務的API客戶端會導致很多問題,更好的方法是API Gateway,即實現一個服務,該服務是外部API客戶端進入基于微服務應用程序的入口點,它負責:
- 請求路由
- API組合
- 協議轉換
- 能夠為每一個客戶端提供它們專用的API
- 其他邊緣功能(身份驗證、訪問授權、速率限制、緩存、指標收集、請求日志)
API Gateway的架構具有分層模塊化架構,如API層和公共層,API層由一個或多個獨立的API模塊組成。每個API模塊為特定客戶端實現API。公共層實現共享功能,如邊緣功能。
API Gateway若由一個單獨團隊維護,這種集中式的瓶頸與微服務架構理念背道而馳。更好的方法或許是讓客戶端團隊擁有他們的API模塊,而API Gateway團隊負責開發公共模塊和API Gateway的運維。部署流水線必須完全自動化。
API Gateway的職責不明確。后端前置模式為每個客戶端定義一個單獨的API Gateway。每個客戶端團隊都擁有自己的API Gateway。API Gateway團隊擁有并維護共享層。每個端的團隊擁有并維護屬于他們的API。
API Gateway的好處是客戶端不必調用特定服務,而是與API Gateway通信,減少往返次數,簡化了代碼。弊端是存在成為開發瓶頸的風險,開發人員必須更新API Gateway才能對外公開服務的API,更新過程要盡可能輕量化,必要時使用后端前置模式。
開發自己的API Gateway
API Gateway的設計難題
1)性能和可擴展性.所有的外部請求必須首先通過API Gateway。影響性能和可擴展性的關鍵設計決策是API Gateway應用使用同步還是異步I/O
2)使用響應式編程抽象。按順序調用服務,服務響應時間過長,盡可能同時調用所有服務,但編寫可維護的并發代碼存在挑戰。可使用響應式方法,如CompleteFutures、Monos、RxJava等。
3)處理局部故障。通過多實例的負載均衡以及斷路器模式。
目前開源的主流API Gateway有:Netflix Zuul和Spring Cloud Gateway。
使用GraphQL實現API Gateway
實現支持多種客戶端的REST API的API Gateway非常耗時,你可能需要考慮使用基于圖形的API框架,如GraphQL。
API由映射到服務的基于圖形的模式組成,客戶端發出檢索多個圖形節點的查詢。基于查詢的API框架通過從一個或多個服務檢索數據來執行查詢。
基于GraphQL(一種標準)的API Gateway可使用Node.js Express Web 框架和Apollo GraphQL服務器,用js編寫。它可以由三部分組成:
- GraphQL模式:定義服務器端數據模型及其支持的查詢
- 解析器函數:解析函數將模式的元素映射到各種后端服務。
- 代理類:代理類調用應用程序的服務。
執行GraphQL
使用GraphQL的主要好處是它的查詢語言為客戶端提供了對返回數據的令人難以置信的控制。客戶端通過向服務器發出包含查詢文檔的請求來執行查詢。簡單情況下,查詢文檔包含查詢的名稱,參數值及要返回結果的對象字段。
當GraphQL服務器執行查詢時,必須從一個或多個數據存儲中檢索所請求的數據。通過將解析函數附加到模式定義的對象類型字段,可以將GraphQL模式與數據源相關聯。GraphQL通過調用解析器函數檢索數據,以此實現API組合模式。
GraphQL通過遞歸調用Query文檔中指定的字段解析器函數來執行查詢。首先,它執行查詢解析器,然后遞歸調用結果對象層次結構中字段的解析器。
測試
將代碼扔給QA團隊,手動測試,效率很低,在交付流程中才進行測試為時已晚。使用微服務的一個關鍵動機是提高可測試性,微服務架構的復雜性要求編寫自動化測試,以縮短交付(代碼投入生產環境)周期。
什么是測試
測試的目的是驗證被測系統的行為。測試用例是用于特定目標的一組測試輸入、執行條件和預期結果,一組相關的測試用例集構成一個測試套件。
每個自動化測試都是通過測試類中一個測試方法實現。測試包括四個階段:設置——初始化測試環境,這是運行測試的基礎;執行——調用被測系統;驗證——驗證測試的結果;清理——清理測試環境。
被測系統在運行時常會依賴另一些系統,依賴的麻煩在于它們可能把測試復雜化,減慢測試速度。解決方案使用測試替身,該對象負責模擬依賴項的行為。測試替身分為stub(代替依賴項向被測系統發送調用的返回值),mock(用來驗證被測系統是否正確調用來依賴項,也扮演stub的角色)。
根據范圍分類,測試分為以下類型:
- 單元測試:主要測試業務邏輯,測試服務的一小部分,例如類
- 集成測試:驗證服務與它依賴方的通話,驗證服務是否可以與基礎設施服務或其他服務進行交互。
- 組件測試:服務的驗收測試,單個服務的驗收測試
- 端到端測試:應用程序的驗收測試,整個應用程序的測試
微服務帶來的質量挑戰
系統依賴性增加:將單體應用轉成微服務,雖然增加了縮放能力和靈活性,但是引入了更多的依賴,使系統整體變的更復雜,使測試環境的搭建配置以及校驗指標更加難以掌控。
并行開發障礙:系統依賴性的增加還會給微服務的并行開發工作造成影響,需要等待其他微服務測試環境部署完畢,才能實現集成、測試。微服務數量越多,需要考慮的對象就越是廣泛
影響傳統測試方法:傳統測試方法往往通過UI測試進行驗證,而微服務的測試方案更加復雜。不僅需要驗證各獨立微服務,還需要檢查整體業務的執行路徑。
為服務編寫單元測試
單元測試有以下兩種類型:
- 獨立型單元測試: 使用針對類的依賴性的模擬對象隔離測試類,常用于領域服務(Service),控制器類、入站和出站消息網關的測試。對外部依賴項進行測試替身。
- 協作型單元測試: 測試一個類及其依賴項,常用于實體、值對象、Sagas的測試。
類的職責及其在架構中的角色決定了要使用的單元測試類型。控制類和服務類通常使用獨立型單元測試。領域對象(例如實體和值對象)通常使用協作型單元測試。
領域服務的單元測試
領域服務的方法調用實體和存儲庫并發布領域事件,測試這種 類的有效方法是獨立型單元測試,它可以模擬存儲庫和消息傳遞類等依賴項。單元測試分三個階段:
1)配置服務依賴項的模擬對象
2)調用服務方法
3)驗證服務方法返回的值是否正確,以及是否已正確調用依賴項
事件和消息處理程序的單元測試
每個測試實例都是消息適配器,向消息通道發送消息,并驗證是否正確調用了服務模擬。而消息傳遞的基礎設施是基于樁的,因此不涉及消息代理。測試可以使用Eventuate Tram Mock Messaging框架。
單元測試不會驗證服務是否與其他服務正確交互,為了驗證服務是否正確地與其他服務交互,必須編寫集成測試。
集成測試
為了確保服務按預期工作,必須編寫測試來驗證服務是否可以正確地與基礎設施服務和其他服務進行交互。一種方法是啟動所有服務并通過其API進行測試。更有效的策略是編寫集成測試,針對不同類型的適配器采用不同的測試驗證方法,比如:1)針對基于REST的請求/響應,直接驗證http請求和響應。2)針對發布/訂閱適配器,通過測試驗證/模擬對應的領域事件;3)針對異步請求/響應,驗證命令消息和恢復消息。
針對持久化層的集成測試
執行持久化集成測試每個階段的行為如下:
設置:通過創建數據庫結構設置數據庫,并將其初始化為已知狀態。也可能開始執行一些必要的數據庫事務
執行:執行數據庫操作。
驗證:對數據庫的狀態和從數據庫中檢索的對象進行斷言。
拆解:可選階段,可以撤銷對數據庫所作的更改。
關于如何配置在持久化集成測試中的使用的數據庫,可以使用Docker方案解決。
針對基于REST的請求/響應式交互的集成測試
良好的集成測試策略是使用消費者驅動的契約測試。契約用于驗證兩端的適配器類。
針對發布/訂閱式交互的集成測試
與測試REST交互的方式類似,不同的是每個契約都指定了一個領域事件。通過驗證是否觸發生成對應的領域事件,或是否正確調用了其模擬的依賴項來驗證。
針對異步請求/響應式交互的集成契約測試
消費者端測試驗證命令消息代理類是否發送了結構正確的命令消息,并正確處理回復消息。提供者測試由Spring Cloud Contract代碼生成。每種測試方法對應一份契約。它將契約的輸入消息作為命令消息發送,并驗證回復消息是否與契約輸出消息匹配。
組件測試
組件測試指單獨測試服務。驗收測試是針對軟件組件的面向業務的測試。它們從組件客戶端而非內部實現角度描述所需的外部可見行為。這些測試源自用戶故事或用例。
使用Gherkin編寫驗收測試
使用Java編寫驗收測試有挑戰性,更好的方法是使用Gherkin,用類似英語場景定義驗收測試。可自動將場景轉換為可運行的代碼。情景具有given-when-then結構。
使用Cucumber執行Gherkin的測試規范
Cucumber是Gherkin的測試自動化框架。你可以編寫一個步驟定義類,類包含一組方法,方法定義了每個given-when-then步驟的具體含義。
進程內組件測試
使用常駐內存的樁和模擬代替其依賴性運行服務。編寫更簡單,速度更快,但不測試服務的可部署性。
進程外組件測試
將服務打包為生產環境就緒的格式(如Docker容器鏡像),并作為單獨的進程運行。進程外組件測試使用真實的基礎設施服務,如數據庫、消息代理,但對應用程序服務的任何依賴項使用樁。好處是提高測試覆蓋率,測試內容更接近部署的內容;缺點是編寫起來更復雜,執行更慢。
端到端測試
端到端測試位于測試金字塔頂端。開發這類測試緩慢、脆弱且耗時。應盡量控制端到端測試數量。
編寫用戶旅程測試,模擬用戶在應用程序中的旅程,并驗證相對較大的應用程序功能片段的高級行為。如可編寫完成所有若個測試的單個測試,而不是單獨測試這些步驟。這可以顯著減少編寫測試數量并縮短測試執行時間。
端到端測試與組件測試實現類似,使用Gherkin編寫并使用Cucumber執行。