從軟體開發早期(1960 年代)開始,應對大型軟體系統中的復雜性一直是一項令人生畏的任務。多年來為了應對軟體系統的復雜性,軟體工程師和架構師們做了許多嘗試:David Parnas 的模組化和封裝 (1972), Edsger W. Dijkstra (1974)的關註點分離以及 SOA(1988)。
他們都是使用分而治之這項成熟的傳統技術來應對大型系統的復雜性。自 2010 年開始,這些技術被證實無法繼續應對 Web 級套用或者現代大型企業級套用的復雜性。因此架構師和工程師們發展出了一種全新的現代方式來解決這個問題,就是微服務架構。它雖然延續了分而治之的思想,但卻是以全新的方式來實作的。
軟體設計模式是解決軟體設計中常見問題的通用、可復用的解決方案。設計模式讓我們可以分享通用詞匯並使用經實戰檢驗的方案,以免重復造輪子。我先簡單介紹下微服務架構。
透過閱讀這篇文章,你會學到:
微服務架構
微服務架構的優勢
微服務架構的劣勢
何時使用微服務架構
最重要的微服務架構設計模式,包括其優缺點、用例、上下文、技術棧範例及可用資源。
「 請註意,本清單中的大部份設計模式常出現在多種語境中,並且可以在非微服務架構中使用。而我將在微服務這個特定語境中介紹它們。
# 微服務架構
我在之前的部落格【微服務架構概述及為什麽要套用在下個計畫】和【單體軟體架構真的終結了嗎?】中對微服務架構有非常詳盡的介紹。如果你感興趣,可以閱讀這兩篇部落格來深入了解。
https://towardsdatascience.com/microservice-architecture-a-brief-overview-and-why-you-should-use-it-in-your-next-project-a17b6e19adfd
https://towardsdatascience.com/looking-beyond-the-hype-is-modular-monolithic-software-architecture-really-dead-e386191610f8
那到底什麽是微服務架構?有很多種定義方法。我的定義是這這樣的:
微服務架構指的是將大型復雜系統按功能或者業務需求垂直切分成更小的子系統,這些子系統以獨立部署的子行程存在,它們之間透過輕量級的、跨語言的同步(比如 REST,gRPC)或者異步(訊息)網路呼叫進行通訊。
下面是基於微服務架構的商業 Web 套用的元件檢視:
整個應用程式被拆分成相互獨立但包含多個內部模組的子行程
與模組化的單體套用(Modular Monoliths)或 SOA 相反,微服務應用程式根據業務範圍或領域垂直拆分。
微服務邊界是外部的,微服務之間透過網路呼叫(RPC 或訊息)相互通訊。
微服務是獨立的行程,它們可以獨立部署。
它們以輕量級的方式進行通訊,不需要任何智慧通訊通道。
微服務架構的優點
更好的開發規模
更快的開發速度
支持叠代開發或現代化增量開發
充分利用現代軟體開發生態系的優勢(雲、容器、 DevOps、Serverless)
支持水平縮放和細粒度縮放
小體量,較低了開發人員的認知復雜性
微服務架構的缺點
更高數量級的活動元件(服務、資料庫、行程、容器、框架)
復雜性從程式碼轉移到基礎設施
RPC 呼叫和網路通訊的大量增加
整個系統的安全性管理更具有挑戰性
整個系統的設計變得更加困難
引入了分布式系統的復雜性
何時使用微服務架構
大規模 Web 套用開發
跨團隊企業級套用協作開發
長期收益優先於短期收益
團隊擁有能夠設計微服務架構的軟體架構師或高級工程師
# 微服務架構的設計模式
獨享資料庫(Database per Microservice)
當一家公司將大型單體系統替換成一組微服務,首先要面臨的最重要決策是關於資料庫。單體架構會使用大型中央資料庫。即使轉移到微服務架構許多架構師仍傾向於保持資料庫不變。雖然有一些短期收益,但它卻是反模式的,特別是在大規模系統中,微服務將在資料庫層嚴重耦合,整個遷移到微服務的目標都將面臨失敗(例如,團隊授權、獨立開發等問題)。
更好的方法是為每個微服務提供自己的數據儲存,這樣服務之間在資料庫層就不存在強耦合。這裏我使用資料庫這一術語來表示邏輯上的數據隔離,也就是說微服務可以共享物理資料庫,但應該使用分開的數據結構、集合或者表,這還將有助於確保微服務是按照領域驅動設計的方法正確拆分的。
優點
數據由服務完全所有
服務的開發團隊之間耦合度降低
缺點
服務間的數據共享變得更有挑戰性
在套用範圍的保證 ACID 事務變得困難許多
細心設計如何拆分單體資料庫是一項極具挑戰的任務
何時使用獨享資料庫
在大型企業應用程式中
當團隊需要完全把控微服務以實作開發規模擴充套件和速度提升
何時不宜使用獨享資料庫
在小規模套用中
如果是單個團隊開發所有微服務
可用技術範例
所有 SQL、 NoSQL 資料庫都提供數據的邏輯分離(例如,單獨的表、集合、結構、資料庫)。
事件源(Event Sourcing)
在微服務架構中,特別使用獨享資料庫時,微服務之間需要進行數據交換。對於彈性高可伸縮的和可容錯的系統,它們應該透過交換事件進行異步通訊。在這種情況,您可能希望進行類似更新資料庫並行送訊息這樣的原子操作,如果在大數據量的分布式場景使用關聯式資料庫,您將無法使用兩階段鎖協定(2PL),因為它無法伸縮。而 NoSQL 資料庫因為大多不支持兩階段鎖協定甚至無法實作分布式事務。
在這些場景,可以基於事件的架構使用事件源模式。在傳統資料庫中,直接儲存的是業務實體的當前「狀態」,而在事件源中任何「狀態」更新事件或其他重要事件都會被儲存起來,而不是直接儲存實體本身。這意味著業務實體的所有更改將被保存為一系列不可變的事件。因為數據是作為一系列事件儲存的,而非直接更新儲存,所以各項服務可以透過重放事件儲存中的事件來計算出所需的數據狀態。
優點
為高可伸縮系統提供原子性操作
自動記錄實體變更歷史,包括時序回溯功能
松耦合和事件驅動的微服務
缺點
從事件儲存中讀取實體成為新的挑戰,通常需要額外的數據儲存(CQRS 模式)。
系統整體復雜性增加了,通常需要領域驅動設計。
系統需要處理事件重復(冪等)或遺失
變更事件結構成為新的挑戰。
何時使用事件源
使用關聯式資料庫的、高可伸縮的事務型系統
使用 NoSQL 資料庫的事務型系統
彈性高可伸縮微服務架構
典型的訊息驅動或事件驅動系統(電子商務、預訂和預約系統)
何時不宜使用事件源
使用 SQL 資料庫的低可伸縮性事務型系統
在服務可以同步交換數據(例如,透過 API)的簡單微服務架構中。
可用技術範例
事件儲存:EventStoreDB、Apache Kafka、Confluent Cloud、AWS Kinesis、Azure Event Hub、GCP Pub/Sub、Azure Cosmos DB、MongoDB、Cassandra、Amazon DynamoDB
框架:Lagom、Akka、Spring、akkatecture、Axon、Eventuate
命令和查詢職責分離(CQRS)
如果我們使用事件源,那麽從事件儲存中讀取數據就變得困難了。要從數據儲存中獲取實體,我們需要處理所有的實體事件。有時我們對讀寫操作還會有不同的一致性和吞吐量要求。
這種情況,我們可以使用 CQRS 模式。在該模式中,系統的數據修改部份(命令)與數據讀取部份(查詢)是分離的。而 CQRS 模式有兩種容易令人混淆的模式,分別是簡單的和高級的。
在其簡單形式中,不同實體或 ORM 模型被用於讀寫操作,如下所示:
它有助於強化單一職責原則和分離關註點,從而實作更簡潔的設計。
在其高級形式中,會有不同的數據儲存用於讀寫操作。高級的 CQRS 通常結合事件源模式。根據不同情況,會使用不同型別的寫數據儲存和讀數據儲存。寫數據儲存是「記錄的系統」,也就是整個系統的核心源頭。
對於讀頻繁的應用程式或微服務架構,OLTP 資料庫(任何提供 ACID 事務保證的關系或非關聯式資料庫)或分布式訊息系統都可以被用作寫儲存。對於寫頻繁的應用程式(寫操作高可伸縮性和大吞吐量),需要使用寫可水平伸縮的資料庫(如全球托管的公共雲資料庫)。標準化的數據則保存在寫數據儲存中。
對搜尋(例如 Apache Solr、Elasticsearch)或讀操作(KV 資料庫、文件資料庫)進行最佳化的非關聯式資料庫常被用作讀儲存。許多情況會在需要 SQL 查詢的地方使用讀可伸縮的關聯式資料庫。非標準化和特殊最佳化過的數據則保存在讀儲存中。
數據是從寫儲存異步復制到讀儲存中的,所以讀儲存和寫儲存之間會有延遲,但最終是一致的。
優點
在事件驅動的微服務中數據讀取速度更快
數據的高可用性
讀寫系統可獨立擴充套件
缺點
讀數據儲存是弱一致性的(最終一致性)
整個系統的復雜性增加了,混亂的 CQRS 會顯著危害整個計畫。
何時使用 CQRS
在高可延伸的微服務架構中使用事件源
在復雜領域模型中,讀操作需要同時查詢多個數據儲存。
在讀寫操作負載差異明顯的系統中
何時不宜使用 CQRS
在沒有必要儲存大量事件的微服務架構中,用事件儲存快照來計算實體狀態是一個更好的選擇。
在讀寫操作負載相近的系統中。
可用技術範例
寫儲存:EventStoreDB, Apache Kafka, Confluent Cloud, AWS Kinesis, Azure Event Hub, GCP Pub/Sub, Azure Cosmos DB, MongoDB, Cassandra. Amazon DynamoDB
讀儲存:Elastic Search, Solr, Cloud Spanner, Amazon Aurora, Azure Cosmos DB, Neo4j
框架:Lagom, Akka, Spring, akkatecture, Axon, Eventuate
Saga
如果微服務使用獨享資料庫,那麽透過分布式事務管理一致性是一個巨大的挑戰。你無法使用傳統的兩階段送出協定,因為它要麽不可伸縮(關聯式資料庫),要麽不被支持(多數非關聯式資料庫)。
但您還是可以在微服務架構中使用 Saga 模式實作分布式事務。Saga 是 1987 年開發的一種古老模式,是關聯式資料庫中關於大事務的一個替代概念。但這種模式的一種現代變種對分布式事務也非常有效。Saga 模式是一個本地事務序列,其每個事務在一個單獨的微服務內更新數據儲存並釋出一個事件或訊息。
Saga 中的第一個事務是由外部請求(事件或動作)初始化的,一旦本地事務完成(數據已保存在數據儲存且訊息或事件已釋出),那麽釋出的訊息或事件則會觸發 Saga 中的下一個本地事務。
如果本地事務失敗,Saga 將執行一系列補償事務來回滾前面本地事務的更改。
Saga 事務協調管理主要有兩種形式:
事件編排 Choreography:分散協調,每個微服務生產並監聽其他微服務的事件或訊息然後決定是否執行某個動作。
命令編排 Orchestration:集中協調,由一個協調器告訴參與的微服務哪個本地事務需要執行。
優點
為高可伸縮或松耦合的、事件驅動的微服務架構提供一致性事務。
為使用了不支持 2PC 的非關聯式資料庫的微服務架構提供一致性事務。
缺點
需要處理瞬時故障,並且提供等冪性。
難以偵錯,而且復雜性隨著微服務數量增加而增加。
何時使用 Saga
在使用了事件源的高可伸縮、松耦合的微服務中。
在使用了分布式非關聯式資料庫的系統中。
何時不宜使用 Saga
使用關聯式資料庫的低可伸縮性事務型系統。
在服務間存在迴圈依賴的系統中。
可用技術範例
Axon, Eventuate, Narayana
面向前端的後端 (BFF)
在現代商業套用開發,特別是微服務架構中,前後端套用是分離和獨立的服務,它們透過 API 或 GraphQL 連線。如果應用程式還有移動 App 客戶端,那麽 Web 端和移動客戶端使用相同的後端微服務就會出現問題。因為移動客戶端和 Web 客戶端有不同的螢幕尺寸、顯示器、效能、能耗和網路頻寬,它們的 API 需求不同。
面向前端的後端模式適用於需要為特殊 UI 客製單獨後端的場景。它還提供了其他優勢,比如作為下遊微服務的封裝,從而減少 UI 和下遊微服務之間的頻繁通訊。此外,在高安全要求的場景中,BFF 為部署在 DMZ 網路中的下遊微服務提供了更高的安全性。
優點
分離 BFF 之間的關註點,使得我們可以為具體的 UI 最佳化他們。
提供更高的安全性
減少 UI 和下遊微服務之間頻繁的通訊
缺點
BFF 之間程式碼重復
大量的 BFF 用於其他使用者介面(例如,智慧型電視,Web,移動端,PC 桌面版)
需要仔細的設計和實作,BFF 不應該包含任何業務邏輯,而應只包含特定客戶端邏輯和行為。
何時使用 BFF
如果應用程式有多個含不同 API 需求的 UI
出於安全需要,UI 和下遊微服務之間需要額外的層。
如果在 UI 開發中使用微前端。
何時不宜使用 BFF
如果應用程式雖有多個 UI,但使用的 API 相同。
如果核心微服務不是部署在 DMZ 網路中。
可用技術範例
任何後端框架(Node.js,Spring,Django,Laravel,Flask,Play,…)都能支持。
API 閘道器
在微服務架構中,UI 通常連線多個微服務。如果微服務是細粒度的(FaaS) ,那麽客戶端可能需要連線非常多的微服務,這將變得繁雜和具有挑戰性。此外,這些服務包括它們的 API 還將不斷前進演化。大型企業還希望能有其他橫切關註點(SSL 終止、身份驗證、授權、節流、日誌記錄等)。
一個解決這些問題的可行方法是使用 API 閘道器。API 閘道器位於客戶端 APP 和後端微服務之間充當 facade,它可以是反向代理,將客戶端請求路由到適當的後端微服務。它還支持將客戶端請求扇出到多個微服務,然後將響應聚合後返回給客戶端。它還支持必要的橫切關註點。
優點
在前端和後端服務之間提供松耦合
減少客戶端和微服務之間的呼叫次數
透過 SSL 終端、身份驗證和授權實作高安全性
集中管理的橫切關註點,例如,日誌記錄和監視、節流、負載平衡。
缺點
可能導致微服務架構中的單點故障
額外的網路呼叫帶來的延遲增加
如果不進行擴充套件,它們很容易成為整個企業套用的瓶頸。
額外的維護和開發費用
何時使用 API 閘道器
在復雜的微服務架構中,它幾乎是必須的。
在大型企業中,API 閘道器是中心化安全性和橫切關註點的必要工具。
何時不宜使用 API 閘道器
在安全和集中管理不是最優先要素的私人計畫或小公司中。
如果微服務的數量相當少。
可用技術範例
Amazon API 閘道器, Azure API 管理, Apigee, Kong, WSO2 API 管理器
Strangler
如果想在執行中的計畫中使用微服務架構,我們需要將遺留的或現有的單體套用遷移到微服務。將現有的大型線上單體應用程式遷移到微服務是相當有挑戰性的,因為這可能破壞應用程式的可用性。
一個解決方案是使用 Strangler 模式。Strangler 模式意味著透過使用新的微服務逐步替換特定功能,將單體應用程式增量地遷移到微服務架構。此外,新功能只在微服務中添加,而不再添加到遺留的單體套用中。
然後配置一個 Facade (API 閘道器)來路由遺留單體套用和微服務間的請求。當某個功能從單體套用遷移到微服務,Facade 就會攔截客戶端請求並路由到新的微服務。一旦遷移了所有的功能,遺留單體應用程式就會被「扼殺(Strangler)」,即退休。
優點
安全的遷移單體應用程式到微服務
可以並列地遷移已有功能和開發新功能
遷移過程可以更好把控節奏
缺點
在現有的單體套用服務和新的微服務之間共享數據儲存變得具有挑戰性
添加 Facade (API 閘道器)將增加系統延遲
端到端測試變得困難
何時使用 Strangler
將大型後端單體應用程式的增量遷移到微服務
何時不宜使用 Strangler
如果後端單體套用很小,那麽全量替換會更好。
如果無法攔截客戶端對遺留的單體應用程式的請求。
可用技術範例
API 閘道器後端套用框架。
斷路器
在微服務架構中,微服務透過同步呼叫其他服務來滿足業務需求。服務呼叫會由於瞬時故障(網路連線緩慢、超時或暫時不可用) 導致失敗,這種情況重試可以解決問題。然而,如果出現了嚴重問題(微服務完全失敗),那麽微服務將長時間不可用,這時重試沒有意義且浪費寶貴的資源(執行緒被阻塞,CPU 周期被浪費)。此外,一個服務的故障還會引發整個套用系統的級聯故障。這時快速失敗是一種更好的方法。
在這種情況,可以使用斷路器模式挽救。一個微服務透過代理請求另一個微服務,其工作原理類似於電氣斷路器,代理透過統計最近發生的故障數量,並使用它來決定是繼續請求還是簡單的直接返回異常。
斷路器可以有以下三種狀態:
關閉: 斷路器將請求路由到微服務,並統計給定時段內的故障數量,如果超過閾值,它就會觸發並進入開啟狀態。
開啟: 來自微服務的請求會快速失敗並返回異常。在超時後,斷路器進入半開啟狀態。
半開: 只有有限數量的微服務請求被允許透過並進行呼叫。如果這些請求成功,斷路器將進入閉合狀態。如果任何請求失敗,斷路器則會進入開啟狀態。
優點
提高微服務架構的容錯性和彈性
阻止引發其他微服務的級聯故障
缺點
需要復雜的例外處理
日誌和監控
應該支持人工復位
何時使用斷路器
在微服務間使用同步通訊的緊耦合的微服務架構中
如果微服務依賴多個其他微服務
何時不宜使用斷路器
松耦合、事件驅動的微服務架構
如果微服務不依賴於其他微服務
可用技術範例
API 閘道器,服務網格,各種斷路器庫(Hystrix, Reselience4J, Polly)。
外部化配置
每個業務套用都有許多用於各種基礎設施的配置參數(例如,資料庫、網路、連線的服務地址、憑據、證書路徑)。此外在企業應用程式通常部署在各種執行環境(Local、 Dev、 Prod)中,實作這些的一個方法是透過內部配置。這是一個致命糟糕實踐,它會導致嚴重的安全風險,因為生產憑證很容易遭到破壞。此外,配置參數的任何更改都需要重新構建應用程式,這在在微服務架構中會更加嚴峻,因為我們可能擁有數百個服務。
更好的方法是將所有配置外部化,使得構建過程與執行環境分離,生產的配置檔只在執行時或透過環境變量使用,從而最小化了安全風險。
優點
生產配置不屬於程式碼庫,因而最小化了安全漏洞。
修改配置參數不需要重新構建應用程式。
缺點
我們需要選擇一個支持外部化配置的框架。
何時使用外部化配置
任何重要的生產應用程式都必須使用外部化配置。
何時不宜使用外部化配置
在驗證概念的開發中。
可用技術範例
幾乎所有企業級的現代框架都支持外部化配置。
消費端驅動的契約測試
在微服務架構中,通常有許多有不同團隊開發的微服務。這些微型服務協同工作來滿足業務需求(例如,客戶請求),並相互進行同步或異步通訊。消費端微服務的整合測試具有挑戰性,通常用 TestDouble 以獲得更快、更低成本的測試執行。但是 TestDouble 通常並不能代表真正的微服務提供者,而且如果微服務提供者更改了它的 API 或 訊息,那麽 TestDouble 將無法確認這些。另一種選擇是進行端到端測試,盡管它在生產之前是強制性的,但卻是脆弱的、緩慢的、昂貴的且不能替代整合測試(Test Pyramid)。
在這方面消費端驅動的契約測試可以幫助我們。在這裏,負責消費端微服務的團隊針對特定的伺服端微服務,編寫一套包含了其請求和預期響應(同步)或訊息(異步)的測試套件,這些測試套件稱為顯式的約定。對於微服務伺服端,將其消費端所有約定的測試套件都添加到其自動化測試中。當特定伺服端微服務的自動化測試執行時,它將一起執行自己的測試和約定的測試並進行驗證。透過這種方式,契約測試可以自動的幫助維護微服務通訊的完整性。
優點
如果提供程式意外更改 API 或訊息,可以被快速的自動發現。
更少意外、更健壯,特別是包含大量微服務的企業應用程式。
改善團隊自主性。
缺點
需要額外的工作來開發和整合微服務伺服端的契約測試,因為他們可能使用完全不同的測試工具。
如果契約測試與真實服務情況不匹配,將可能導致生產故障。
何時使用需求驅動的契約測試
在大型企業業務應用程式中,通常由不同的團隊開發不同服務。
何時不宜使用消費端驅動的契約測試
所有微服務由同一團隊負責開發的小型簡單的應用程式。
如果伺服端微服務是相對穩定的,並且不處在活躍的開發狀態。
可用技術範例
Pact, Postman, Spring Cloud Contract
# 總結
在現代大規模企業軟體開發中,微服務架構能夠幫助開發擴充套件規模並帶來很多長期收益。但是微服務架構並不是隨處可用的銀彈,如果套用在錯誤的應用程式型別,微服務架構將弊大於利。希望采用微服務架構的開發團隊應該遵循最佳實踐,並使用一系列可重用的、久經錘煉的設計模式。
微服務架構中至關重要的設計模式是獨享資料庫。實作這種設計模式具有挑戰性,需要其他幾種密切相關的設計模式(事件驅動、 CQRS、 Saga)來支持。在具有多個客戶端(Web、 Mobile、 Desktop、 Smart Devices)的典型業務應用程式中,客戶端和微服務之間的通訊量可能是很大的,並且需要統一的安全控制,在這種情況面向前端的後端和 API 閘道器的設計非常有用。
此外,斷路器模式可以大大地幫助應對這類應用程式的錯誤處理場景。遷移遺留的單體套用到微服務是極具挑戰性的,而 Strangler 模式可以幫助做到這點。消費端驅動的契約測試是微服務整合測試的基礎模式。另外外部化配置是任何現代化應用程式開發中的一種必備模式。
這個系列並不全面,在實際情況中您可能需要其他的設計模式,但這個系列能為您提供一個關於微服務架構設計模式的極好介紹。
熱門推薦