本節著重於開發假設的伺服器端企業應用程式。
應用程式規格
假設的應用程式會藉由執行商業規則、存取資料庫,然後傳回 HTML、JSON 或 XML 回應來處理要求。 我們將表示應用程式必須支援各種用戶端,包括執行單頁應用程式(SPA)、傳統 Web 應用程式、行動 Web 應用程式和原生行動應用程式的桌面瀏覽器。 應用程式也可能公開供第三方取用的 API。 它也應該能夠以異步方式整合其微服務或外部應用程式,如此一來,在部分失敗的情況下,該方法將有助於微服務的復原能力。
應用程式將包含下列類型的元件:
簡報元件。 這些元件負責處理UI和取用遠端服務。
網域或商業規則。 此元件是應用程式的網域邏輯。
數據庫存取邏輯。 此元件包含負責存取資料庫的數據存取元件(SQL 或 NoSQL)。
應用程式整合邏輯。 此元件包含以訊息代理程序為基礎的傳訊通道。
應用程式需要高延展性,同時允許其垂直子系統自主相應放大,因為某些子系統需要比其他子系統更多的延展性。
應用程式必須能夠部署在多個基礎結構環境中(多個公用雲端和內部部署),在理想情況下應該是跨平臺,能夠輕鬆地從Linux移至Windows(反之亦然)。
開發團隊背景
我們也假設以下幾點關於應用程式的開發過程:
您有多個開發小組專注於應用程式的不同商務領域。
新的小組成員必須快速提高生產力,而且應用程式必須易於瞭解和修改。
應用程式會有長期演進和不斷變化的商務規則。
您需要良好的長期可維護性,這表示未來實作新變更時具有靈活度,同時能夠更新多個子系統,對其他子系統的影響最小。
您想要練習應用程式的持續整合和持續部署。
您想要利用新興技術(架構、程序設計語言等),同時不斷演進應用程式。 您不想在移至新技術時完整移轉應用程式,因為這會導致高成本並影響應用程式的可預測性和穩定性。
選擇架構
應用程式部署架構應該是什麼? 應用程式的規格以及開發內容,強烈建議您將應用程式分解成以共同作業 微服務 和容器的形式,將應用程式分解成自發子系統,其中微服務是容器。
在這個方法中,每個服務 (container) 都會執行一組聚合且狹義相關的函式。 例如,應用程式可能包含目錄服務、訂購服務、購物籃服務、使用者配置檔服務等服務。
微服務會使用 HTTP(REST)等通訊協議進行通訊,但也盡可能以異步方式(例如,使用AMQP)進行通訊,尤其是在使用整合事件傳播更新時。
微服務會作為相互獨立的容器開發並部署。 這種方法表示開發小組可以開發及部署特定微服務,而不會影響其他子系統。
每個微服務都有自己的資料庫,允許它與其他微服務完全分離。 必要時,使用應用層級整合事件(透過邏輯事件總線)來達成來自不同微服務的資料庫之間的一致性,如 命令和查詢責任隔離 (CQRS) 中所處理。 因此,業務限制需求多個微服務和相關資料庫之間的最終一致性。
eShopOnContainers:使用容器部署之 .NET 和微服務的參考應用程式
因此,您可以專注於架構和技術,而不是考慮您可能不知道的假設商務領域,我們選取了一個已知的商務網域,也就是呈現產品類別目錄的簡化電子商務(e-shop)應用程式、從客戶取得訂單、驗證庫存,以及執行其他商務功能。 此容器型應用程式原始程式碼可在 eShopOnContainers GitHub 存放庫中取得。
此應用程式包含多個子系統,包括數個市集 UI 前端(Web 應用程式和原生行動應用程式),以及後端微服務和容器,用於所有必要伺服器端作業的後端微服務和容器,其中具有數個 API 閘道做為內部微服務的合併進入點。 圖 6-1 顯示參考應用程式的架構。
圖 6-1。 適用於開發環境的 eShopOnContainers 參考應用程式架構
上圖顯示行動和 SPA 用戶端會與單一 API 閘道端點通訊,然後與微服務通訊。 傳統 Web 用戶端會與 MVC 微服務通訊,透過 API 閘道與微服務通訊。
主機環境。 在圖 6-1 中,您會看到數個容器部署在單一 Docker 主機內。 使用 docker-compose up 命令部署到單一 Docker 主機時,情況就是這樣。 不過,如果您使用協調器或容器叢集,則每個容器都可以在不同的主機(節點)中執行,而且任何節點都可以執行任意數目的容器,如我們稍早在架構一節中所述。
通訊架構。 eShopOnContainers 應用程式會使用兩種通訊類型,視功能動作的類型而定(查詢與更新和交易):
透過 API 閘道的 Http 用戶端對微服務通訊。 此方法用於查詢,以及從用戶端應用程式接受更新或交易式命令時。 後續各節會詳細說明使用 API 閘道的方法。
異步事件型通訊。 此通訊會透過事件總線進行,以跨微服務傳播更新,或與外部應用程式整合。 事件總線可以使用 RabbitMQ 之類的任何傳訊代理程式基礎結構技術實作,或使用更上層(抽象層級)的服務總線,例如 Azure 服務總線、 NServiceBus、 MassTransit 或 Brighter。
應用程式會以容器形式部署為一組微服務。 用戶端應用程式可以透過 API 閘道所發佈的公用 URL,與以容器身分執行的微服務通訊。
每個微服務的數據主權
在範例應用程式中,雖然所有 SQL Server 資料庫都部署為單一容器,但每個微服務都會擁有自己的資料庫或數據源。 此設計決策只是為了讓開發人員輕鬆地從 GitHub 取得程式代碼、複製程式代碼,並在 Visual Studio 或 Visual Studio Code 中加以開啟。 或者,它可讓您輕鬆地使用 .NET CLI 和 Docker CLI 編譯自定義 Docker 映射,然後在 Docker 開發環境中部署和執行它們。 無論哪種方式,使用數據源的容器可讓開發人員在幾分鐘內建置和部署,而不需要布建外部資料庫或任何其他與基礎結構(雲端或內部部署)有硬式相依性的數據源。
在實際生產環境中,為了達到高可用性和延展性,資料庫應該以雲端或內部部署中的資料庫伺服器為基礎,而不是在容器中。
因此,微服務的部署單位(甚至此應用程式中的資料庫)是 Docker 容器,而參考應用程式是採用微服務原則的多容器應用程式。
其他資源
- eShopOnContainers GitHub 儲存庫。 參考應用程式的原始程式碼
https://aka.ms/eShopOnContainers/
微服務型解決方案的優點
這類微服務型解決方案有許多優點:
每個微服務相對較小,易於管理及演進。 具體說來:
開發人員可以輕易理解並迅速著手工作,提高工作效率。
容器會快速啟動,讓開發人員更具生產力。
Visual Studio 之類的 IDE 可以快速載入較小的專案,讓開發人員提高生產力。
每個微服務都可以獨立於其他微服務設計、開發及部署,以提供靈活度,因為較容易經常部署新版本的微服務。
可以橫向擴展應用程式的個別區域。 例如,目錄服務或購物籃服務可能需要擴展,但不需要調整訂單處理流程。 微服務基礎結構在擴展時所使用的資源會比單體架構更有效率。
您可以在多個小組之間劃分開發工作。 每個服務都可以由單一開發小組所擁有。 每個小組都可以獨立於其他小組,管理、開發、部署及擴展其服務。
問題更為局部化。 如果某個服務發生問題,則只有該服務一開始受到影響(除非使用錯誤的設計,且微服務之間有直接相依性),而其他服務可以繼續處理要求。 相反地,整合型部署架構中的一個故障元件可能會降低整個系統,特別是當它牽涉到資源時,例如記憶體流失。 此外,當微服務中的問題解決時,您可以只部署受影響的微服務,而不會影響應用程式的其餘部分。
您可以使用最新的技術。 因為您可以獨立開始開發服務,並排執行服務(感謝容器和 .NET),因此您可以立即開始使用最新的技術和架構,而不是停留在整個應用程式的較舊堆疊或架構上。
微服務型解決方案的缺點
這類微服務型解決方案也有一些缺點:
分散式應用程式。 散發應用程式會增加開發人員在設計和建置服務時的複雜性。 例如,開發人員必須使用 HTTP 或 AMQP 等通訊協定來實作服務間通訊,這會增加測試和例外狀況處理的複雜性。 它也會為系統增加延遲。
部署複雜度。 具有數十種微服務類型且需要高延展性的應用程式(它必須能夠為每個服務建立許多實例,並在多部主機之間平衡這些服務)表示 IT 作業和管理的高度部署複雜度。 如果您未使用微服務導向的基礎結構(例如協調器和排程器),則額外的複雜度可能需要比商務應用程式本身更多的開發工作。
原子交易。 多個微服務之間通常無法進行原子性交易。 業務需求必須包含多個微服務之間的最終一致性。 如需詳細資訊,請參閱 等冪訊息處理的挑戰。
增加全域資源需求 (所有伺服器或主機的總記憶體、磁碟驅動器和網路資源)。 在許多情況下,當您使用微服務方法取代整合型應用程式時,新微服務型應用程式所需的初始全域資源數量將會大於原始整合型應用程式的基礎結構需求。 這種方法是因為數據粒度和分散式服務的程度較高,因此需要更多全域資源。 不過,鑒於資源成本普遍較低,以及能夠在某些應用領域進行擴展的好處,相較於進化單體應用程式的長期成本,資源使用量的增加通常是大型長期應用程式的良好取捨。
直接用戶端對微服務通訊的問題。 當應用程式很大時,有數十個微服務,如果應用程式需要直接的用戶端對微服務通訊,就會有挑戰和限制。 其中一個問題是用戶端需求與每個微服務所公開的 API 之間可能不符。 在某些情況下,用戶端應用程式可能需要提出許多個別的要求來撰寫UI,這在因特網上可能沒有效率,而且在行動網路上是不切實際的。 因此,應儘量減少用戶端應用程式向後端系統發出的要求。
直接客戶端對微服務通訊的另一個問題是,有些微服務可能使用不適合網路的通訊協定。 一個服務可能會使用二進位通訊協定,而另一個服務可能會使用AMQP傳訊。 這些通訊協定不方便防火牆使用,且最適合在內部使用。 通常,應用程式應該使用 HTTP 和 WebSocket 等通訊協定在防火牆外部進行通訊。
然而,這種直接客戶端對服務方法的另一個缺點是,難以重構這些微服務的合約。 一段時間后,開發人員可能會想要變更系統分割成服務的方式。 例如,它們可能會合併兩個服務,或將服務分割成兩個或多個服務。 不過,如果用戶端直接與服務通訊,則執行這種重構可能會中斷與用戶端應用程式的相容性。
如架構一節所述,在根據微服務設計和建置複雜的應用程式時,您可能會考慮使用多個精細的 API 閘道,而不是更簡單的直接用戶端對微服務通訊方法。
分割微服務。 最後,無論您針對微服務架構採取哪種方法,另一個挑戰是決定如何將端對端應用程式分割成多個微服務。 如本指南的架構一節所述,您可以採用數種技術和方法。 基本上,您必須識別與其他區域分離的應用程式區域,以及具有少量硬式相依性的區域。 在許多情況下,此方法是依據使用案例來分割服務的。 例如,在我們的電子商店應用程式中,我們有一個訂購服務,負責與訂單程式相關的所有商業規則。 我們也有實作其他功能的目錄服務和購物籃服務。 在理想情況下,每個服務應該只有少量責任。 這種方法類似於套用至類別的單一責任原則(SRP),其中指出類別應該只有一個變更的理由。 但在此情況下,其與微服務有關,因此範圍會大於單一類別。 最重要的是,微服務必須是自主的,端到端,並包括對其自身數據源的責任歸屬。
外部與內部架構和設計模式
外部架構是由多個服務所組成的微服務架構,遵循本指南架構一節中所述的原則。 不過,根據每個微服務的性質,不論您選擇何種高階微服務架構,通常且有時建議針對不同的微服務採用不同的內部架構,而每個架構都是以不同的模式為基礎。 微服務甚至可以使用不同的技術和程式設計語言。 圖 6-2 說明這種多樣性。
圖 6-2。 外部與內部架構和設計
例如,在我們的 eShopOnContainers 範例中,目錄、購物籃和使用者配置檔微服務很簡單(基本上是 CRUD 子系統)。 因此,其內部架構和設計很簡單。 不過,您可能有其他微服務,例如排序微服務,其較為複雜,而且代表具有高度網域複雜性的不斷變更商務規則。 在這類情況下,您可能會想要在特定微服務內實作更進階的模式,例如使用網域驅動設計 (DDD) 方法定義的模式,就像我們在 eShopOnContainers 訂購微服務中所做的一樣。 (我們將在稍後的章節中檢閱這些 DDD 模式,說明 eShopOnContainers 訂購微服務的實作。
每個微服務的不同技術的另一個原因可能是每個微服務的性質。 例如,如果您使用 F# 之類的功能程式設計語言,或甚至是 R 之類的語言,如果您是以 AI 和機器學習領域為目標,而不是更面向物件的程式設計語言,例如 C#,則可能會比較好。
底線是每個微服務可以根據不同的設計模式,有不同的內部架構。 並非所有微服務都應該使用進階 DDD 模式來實作,因為這將會過度工程。 同樣地,具有不斷變化的商業規則的複雜微服務不應該實作為 CRUD 元件,或者您最終可以使用低品質的程式代碼。
新世界:多種架構模式和多語言微服務
軟體架構設計人員和開發人員使用許多架構模式。 以下是幾個混合架構風格和架構模式的例子:
簡單的 CRUD、單層架構。
全新架構 (與 eShopOnWeb 搭配使用)
命令和查詢責任隔離 (CQRS)。
Event-Driven 架構 (EDA) 。
您也可以使用許多技術和語言來建置微服務,例如 ASP.NET Core Web API、NancyFx、ASP.NET Core SignalR(適用於 .NET Core 2 或更新版本)、F#、Node.js、Python、Java、C++、GoLang 等等。
重點是,沒有任何特定的架構模式或樣式,也沒有任何特定技術適合所有情況。 圖 6-3 顯示一些方法和技術(雖然並非以任何特定順序)可用於不同的微服務。
圖 6-3。 多架構模式和 Polyglot 微服務世界
多架構模式和多邊形微服務表示您可以混合語言和技術,以符合每個微服務的需求,並讓它們彼此交談。 如圖 6-3 所示,在由許多微服務組成的應用程式中(領域驅動設計術語中的限定內容,或只是「子系統」作為自主微服務),您可以以不同的方式實作每個微服務。 每個架構模式可能都有不同的架構模式,並根據應用程式的性質、商務需求和優先順序,使用不同的語言和資料庫。 在某些情況下,微服務可能很類似。 但這種情況通常不是這樣,因為每個子系統的內容界限和需求通常不同。
例如,對於簡單的 CRUD 維護應用程式,設計及實作 DDD 模式可能沒有意義。 但對於核心領域或核心業務,您可能需要套用更進階的模式,以使用不斷變化的商務規則來處理商務複雜性。
特別是當您處理由多個子系統組成的大型應用程式時,您不應該根據單一架構模式套用單一最上層架構。 例如,CQRS 不應該套用為整個應用程式的最上層架構,但對於一組特定的服務可能很有用。
沒有萬能藥或適合所有案例的合適架構模式。 您無法有「一個架構模式來全部加以規則」。根據每個微服務的優先順序,您必須為每個微服務選擇不同的方法,如下列各節所述。