在單體應用程式中,組件可通過語言級方法或者函數相互調用。相比之下,基於微服務的應用程式是一個運行在多台機器上的分散式系統。通常,每個服務實例是一個進程。因此,服務必須使用進程間通信(IPC)機制進行交互。稍後我們將瞭解到多種 IPC 技術,但在此之前,我們先來探討一下涉及到的各種設計問題。 ...
鏈接:https://github.com/oopsguy/microservices-from-design-to-deployment-chinese
譯者:Oopsguy
本書的第三章主要是關於使用微服務架構構建應用程式。第一章介紹了微服務架構模式,將其與單體架構模式進行對比,並討論了使用微服務的優點和缺點。第二章描述了應用程式客戶端通過扮演中間人角色的 API 網關與微伺服器進行通信。在本章中,我們來瞭解一下系統中的服務是如何相互通信的。第四章將詳細探討服務發現方面的內容。
3.1、簡介
在單體應用程式中,組件可通過語言級方法或者函數相互調用。相比之下,基於微服務的應用程式是一個運行在多台機器上的分散式系統。通常,每個服務實例是一個進程。
因此,如圖 3-1 所示,服務必須使用進程間通信(IPC)機制進行交互。
稍後我們將瞭解到多種 IPC 技術,但在此之前,我們先來探討一下涉及到的各種設計問題。
3.2、交互方式
當為服務選擇一種 IPC 機制時,首先需要考慮服務如何交互。有很多種客戶端-服務交互方式。它們可以分為兩個類。第一類是一對一交互與一對多交互:
- 一對一 - 每個客戶端請求都由一個服務實例處理。
- 一對多 - 每個請求由多個服務實例處理。
第二類是同步交互與非同步交互:
- 同步 - 客戶端要求服務及時響應,在等待過程中可能會發生阻塞。
- 非同步 - 客戶端在等待響應時不會發生阻塞,但響應(如果有)不一定立即返回。
下表展示了各種交互方式。
- | 一對一 | 一對多 |
---|---|---|
同步 | 請求/響應 | - |
非同步 | 通知 | 發佈/訂閱 |
非同步 | 請求/非同步響應 | 發佈/非同步響應 |
表 3-1、進程間通信方式
一對一交互分為以下列舉的類型,包括同步(請求/響應)和非同步(通知與請求/非同步響應):
- 請求/響應 - 客戶端向服務發出請求並等待響應。客戶端要求響應及時到達。在基於線程的應用程式中,發出請求的線程可能在等待時發生阻塞。
- 通知(又名單向請求) - 客戶端向服務發送請求,但不要求響應。
- 請求/非同步響應 - 客戶端向服務發送請求,服務非同步響應。客戶端在等待時不發生阻止,適用於假設響應可能不會立即到達的場景。
一對多交互可分為以下列舉的類型,它們都是非同步的:
- 發佈/訂閱 - 客戶端發佈通知消息,由零個或多個感興趣的服務消費。
- 發佈/非同步響應 - 客戶端發佈請求消息,然後等待一定時間來接收消費者的響應。
通常,每個服務都組合著使用這些交互方式。對於一些服務,單一的 IPC 機制就足夠了,但其他服務可能需要組合多個 IPC 機制。
圖 3-2 顯示了當用戶請求打車時,打車應用中的服務可能會發生交互。
服務使用了通知、請求/響應和發佈/訂閱組合。例如,乘客的智能手機向 Trip Management 微服務發送一條通知以請求一輛車。Trip Management 服務通過使用請求/響應來調用 Passenger Management 服務以驗證乘客的帳戶是否可用。之後,Trip Management 服務創建路線,並使用發佈/訂閱通知其他服務,包括用於定位可用司機的 Dispatcher。
現在我們來看一下交互方式,我們先來看看如何定義 API。
3.3、定義 API
服務 API 是服務與客戶端之間的契約。無論您選擇何種 IPC 機制,使用介面定義語言(interface definition language,IDL)嚴格定義服務 API 都是非常有必要的。有論據證明使用 API 優先(API‑first)法定義服務更加合適。在對您需要實現的服務的 API 定義進行迭代之後,您可以通過編寫介面定義並與客戶端開發人員進行審閱來開始開發服務。這樣設計可以增加您構建出符合客戶端需求的服務的機率。
正如您將會在後面看到,定義 API 的方式取決於您使用何種 IPC 機制。如果您正在使用消息傳遞,則 API 由消息通道和消息類型組成。如果您使用的是 HTTP,則 API 由 URL、請求和響應格式組成。稍後我們將詳細地介紹關於 IDL 方面的內容。
3.4、演化 API
服務 API 總是隨著時間而變化。在單體應用程式中,更改 API 和更新所有調用者通常是一件直截了當的事。但在基於微服務的應用程式中,即使您的 API 的所有消費者都是同一應用程式中的其他服務,要想完成這些工作也是非常困難的。通常,您無法強制所有客戶端與服務升級的節奏一致。此外,您可能需要逐步部署新版本服務,以便新舊版本的服務同時運行。制定這些問題的處理策略還是很重要的。
處理 API 變更的方式取決於變更的程度。某些更改是次要或需要向後相容以前的版本。例如,您可能會向請求或響應添加屬性。這時設計客戶與服務時遵守魯棒性原則就顯得很有意義了。使用較舊 API 的客戶端應繼續使用新版本的服務。該服務為缺少的請求屬性提供預設值,並且客戶端忽略任何多餘的響應屬性。使用 IPC 機制和消息格式非常重要,可以讓您輕鬆地演化 API。
但有時候,您必須對 API 作出大量不相容的更改。由於您無法強制客戶端立即升級,服務也必須支持較舊版本的 API 一段時間。如果您使用了基於 HTTP 的機制(如 REST),則一種方法是將版本號嵌入 URL 中。每個服務實例可能同時處理多個版本。或者,您可以部署多個不同的實例,每個實例用於處理特定版本。
3.5、處理局部故障
正如第二章中關於 API 網關所述,在分散式系統中存在局部故障風險。由於客戶端進程與服務進程是分開的,服務可能無法及時響應客戶端的請求。由於故障或者維護,服務可能需要關閉。也有可能因服務過載,造成響應速度變得極慢。
例如,請回想第二章中的產品詳細信息場景。我們假設 Recommendation Service 沒有響應。客戶端天真般的實現可能會無限期地阻塞以等待響應。這不僅會導致用戶體驗糟糕,而且在許多應用程式中,它將消耗如線程之類等寶貴資源。以致最終,在運行時將線程用完,造成無法響應,如圖 3-3 所示。
為了防止這個問題出現,您必須設計您的服務以處理局部故障。以下是一個由 Netflix 給出的好方法。處理局部故障的策略包括:
- 網路超時 - 在等待響應時,不要無限期地阻塞,始終使用超時方案。使用超時方案確保資源不被無限地消耗。
- 限制未完成的請求數量 - 對客戶端擁有特定服務的未完成請求的數量設置上限。如果達到了上限,發出的額外請求可能是毫無意義的,因此這些嘗試需要立即失敗。
- 斷路器模式 - 追蹤成功和失敗請求的數量。如果錯誤率超過配置閾值,則斷開斷路器,以便後續的嘗試能立即失敗。如果出現大量請求失敗,則表明服務不可用,發送請求將是無意義的。發生超時後,客戶端應重新嘗試,如果成功,則關閉斷路器。
- 提供回退 - 請求失敗時執行回退邏輯。例如,返回緩存數據或者預設值,如一組空白的推薦數據。
Netflix Hystrix 是一個實現上述和其他模式的開源庫。如果您正在使用 JVM,那麼您一定要考慮使用 Hystrix。如果您在非 JVM 環境中運行,則應使用相等作用的庫。
3.6、IPC 技術
有多種 IPC 技術可供選擇。服務可以使用基於同步請求/響應的通信機制,比如基於 HTTP 的 REST 或 Thrift。或者,可以使用非同步、基於消息的通信機制,如 AMQP 或 STOMP。
還有各種不同的消息格式。服務可以使用人類可讀的、基於文本的格式,如 JSON 或 XML。或者,可以使用如 Avro 或 Protocol Buffers 等二進位格式(更加高效)。稍後我們將討論同步 IPC 機制,但在此之前讓我們先來討論一下非同步 IPC 機制。
3.7、非同步、基於消息的通信
當使用消息傳遞時,進程通過非同步交換消息進行通信。客戶端通過發送消息向服務發出請求。如果服務需要回覆,則通過向客戶端發送一條單獨的消息來實現。由於通信是非同步的,因此客戶端不會阻塞等待回覆。相反,客戶端被假定不會立即收到回覆。
一條消息由頭部(如發件人之類的元數據)和消息體組成。消息通過通道進行交換。任何數量的生產者都可以向通道發送消息。類似地,任何數量的消費者都可以從通道接收消息。有兩種通道類型,分別是點對點(point‑to‑point)與發佈訂閱(publish‑subscribe):
- 點對點通道發送一條消息給一個切確的、正在從通道讀取消息的消費者。服務使用點對點通道,就是上述的一對一交互方式。
- 發佈訂閱通道將每條消息傳遞給所有訂閱的消費者。服務使用發佈訂閱通道,就是上述的一對多交互方式。
圖 3-4 展示了打車應用程式如何使用發佈訂閱通道。
Trip Management 服務通過向發佈訂閱通道寫入 Trip Created 消息來通知已訂閱的服務,如 Dispatcher。 Dispatcher 找到可用的司機並通過向發佈訂閱通道寫入 Driver Proposed 消息來通知其他服務。
有許多消息系統可供選擇。您應該選擇一個支持多種編程語言的。
一些消息系統支持標準協議,如 AMQP 和 STOMP。其他消息系統有專有的文檔化協議。
有大量的開源消息系統可供選擇,包括 RabbitMQ、Apache Kafka、Apache ActiveMQ 和 NSQ。在高層上,他們都支持某種形式的消息和通道。他們都力求做到可靠、高性能和可擴展。然而,每個代理的消息傳遞模型細節上都存在著很大差異。
使用消息傳遞有很多優點:
- 將客戶端與服務分離 - 客戶端通過向相應的通道發送一條消息來簡單地發出一個請求。服務實例對客戶端而言是透明的。客戶端不需要使用發現機制來確定服務實例的位置。
- 消息緩衝 - 使用如 HTTP 的同步請求/響應協議,客戶端和服務在交換期間必須可用。相比之下,消息代理會將消息寫入通道入隊,直到消費者處理它們。這意味著,例如,即使訂單執行系統出現緩慢或不可用的情況,線上商店還是可以接受客戶的訂單。訂單消息只需要簡單地排隊。
- 靈活的客戶端-服務交互 - 消息傳遞支持前面提到的所有交互方式。
- 毫無隱瞞的進程間通信 - 基於 RPC 的機制試圖使調用遠程服務看起來與調用本地服務相同。然而,由於物理因素和局部故障的可能性,他們實際上是完全不同的。消息傳遞使這些差異變得非常明顯,所以開發人員不會被這些虛假的安全感所欺騙。
然而,消息傳遞也存在一些缺點:
- 額外的複雜操作 - 消息傳遞系統是一個需要安裝、配置和操作的系統組件。消息代理程式必須高度可用,否則系統的可靠性將受到影響。
- 實施基於請求/響應的交互的複雜性 - 請求/響應式交互需要做些工作來實現。每個請求消息必須包含應答通道標識符和相關標識符。該服務將包含相關 ID 的響應消息寫入應答通道。客戶端使用相關 ID 將響應與請求相匹配。通常使用直接支持請求/響應的 IPC 機制更加容易。
現在我們已經瞭解了使用基於消息的 IPC,讓我們來看看請求/響應的 IPC。
3.8、同步的請求/響應 IPC
當使用基於同步、基於請求/響應的 IPC 機制時,客戶端向伺服器發送請求。該服務處理該請求並返迴響應。
在許多客戶端中,請求的線程在等待響應時被阻塞。其他客戶端可能會使用非同步、事件驅動的客戶端代碼,這些代碼可能是由 Futures 或 Rx Observables 封裝的。然而,與使用消息傳遞不同,客戶端假定響應能及時到達。
有許多協議可供選擇。有兩種流行協議分別是 REST 和 Thrift。我們先來看一下 REST。
3.8.1、REST
如今,開發 RESTful 風格的 API 是很流行的。REST 是一種使用了 HTTP (幾乎總是)的 IPC 機制。
資源是 REST 中的一個關鍵概念,它通常表示業務對象,如客戶、產品或這些業務對象的集合。REST 使用 HTTP 動詞(謂詞)來操縱資源,這些資源通過 URL 引用。例如,GET 請求返回一個資源的表示形式,可能是 XML 文檔或 JSON 對象形式。POST 請求創建一個新資源,PUT 請求更新一個資源。
引用 REST 創建者 Roy Fielding:
“REST 提供了一套架構約束,當作為整體應用時,其強調組件交互的可擴展性、介面的通用性、組件的獨立部署以及中間組件,以減少交互延遲、實施安全性和封裝傳統系統。” - Roy Fielding,《架構風格與基於網路的軟體架構設計》
圖 3-5 展示了打車應用程式可能使用 REST 的方式之一。
乘客的智能手機通過向 Trip Management 服務的 /trips
資源發出一個 POST 請求來請求旅程。該服務通過向 Passenger Management 服務發送一個獲取乘客信息的 GET 請求來處理該請求。在驗證乘客被授權創建旅程後,Trip Management 服務將創建旅程,並向智能手機返回 201 響應。
許多開發人員聲稱其基於 HTTP 的 API 就是 RESTful。然而,正如 Fielding 在這篇博文中所描述的那樣,並不是所有的都是這樣。
Leonard Richardson 定義了一個非常有用的 REST 成熟度模型,包括以下層次:
- 級別 0 - 級別 0 的 API 的客戶端通過向其唯一的 URL 端點發送 HTTP POST 請求來調用該服務。每個請求被指定要執行的操作、操作的目標(如業務對象)以及參數。
- 級別 1 - 級別 1 的 API 支持資源概念。要對資源執行操作,客戶端會創建一個 POST 請求,指定要執行的操作和參數。
- 級別 2 - 級別 2 的 API 使用 HTTP 動詞(謂詞)執行操作:使用 GET 檢索、使用 POST 創建和使用 PUT 進行更新。請求查詢參數和請求體(如果有)指定操作的參數。這使服務能夠利用 Web 基礎特性,如緩存 GET 請求。
- 級別 3級 - 級別 3 的 API 基於非常規命名原則設計,HATEOAS(超文本作為應用程式狀態引擎)。基本思想是 GET 請求返回的資源的表示,包含用於執行該資源上允許的操作的鏈接。例如,客戶端可以使用發送 GET 請求檢索訂單返回的訂單響應中的鏈接來取消訂單。HATEOAS 的一個好處是不再需要將 URL 硬編碼在客戶端代碼中。另一個好處是,由於資源的表示包含可允許操作的鏈接,所以客戶端不必猜測可以對當前狀態的資源執行什麼操作。
使用基於 HTTP 的協議有很多好處:
- HTTP 簡單易懂。
- 您可以使用瀏覽器中的擴展(如 Postman)測試 HTTP API,或者使用 curl 命令行測試 HTTP API(假設使用了 JSON 或其他一些文本格式)。
- 它直接支持請求/響應式通信。
- HTTP 是防火牆友好。
- 它不需要中間代理,簡化了系統架構。
使用 HTTP 也存在一些缺點:
- HTTP 僅直接支持請求/響應的交互方式。您可以使用 HTTP 進行通知,但伺服器必須始終發送 HTTP 響應。
- 因為客戶端和服務直接通信(沒有一個中間者來緩衝消息),所以它們必須在交換期間都運行。
- 客戶端必須知道每個服務實例的位置(即 URL)。如第二章關於 API 網關所述,這是現代應用程式中的一個不簡單的問題。客戶端必須使用服務發現機制來定位服務實例。
開發人員社區最近重新發現了 RESTful API 介面定義語言的價值。有幾個可以選擇,包括 RAML 和 Swagger。一些 IDL(如 Swagger)允許您定義請求和響應消息的格式。其他如 RAML,需要您使用一個單獨的規範,如 JSON 模式。除了用於描述 API 之外,IDL 通常還具有可從介面定義生成客戶端 stub 和伺服器 skeleton 的工具。
3.8.2、Thrift
Apache Thrift 是 REST 的一個有趣的替代方案。它是一個用於編寫跨語言 RPC 客戶端和伺服器 skeleton。Thrift 提供了一個 C 風格的 IDL 來定義您的 API。您可以使用 Thrift 編譯器生成客戶端存根和伺服器端骨架。編譯器可以生成各種語言的代碼,包括 C++、Java、Python、PHP、Ruby、Erlang 和 Node.js。
Thrift 介面由一個或多個服務組成。一個服務定義類似於一個 Java 介面。它是強類型方法的集合。Thrift 方法可以返回一個(可能為 void)值,或者如果它們被定義為單向,則不會返回值。返回值方法實現了請求/響應的交互方式,客戶端等待響應,並可能會拋出異常。單向方式對應通知互動方式,伺服器不發送響應。
Thrift 支持多種消息格式:JSON,二進位和壓縮二進位。二進位比 JSON 更有效率,因為其解碼速度更快。而且,顧名思義,壓縮二進位是一種節省空間的格式。當然,JSON 是人性化和瀏覽器友好的。Thrift 還為您提供了包括原始 TCP 和 HTTP 在內的傳輸協議選擇。原始 TCP 可能比 HTTP 更有效率。然而,HTTP 是防火牆友好的、瀏覽器友好的和人性化的。
3.9、消息格式
我們已經瞭解了 HTTP 和 Thrift,現在讓我們來看看消息格式的問題。如果您使用的是消息系統或 REST,則可以選擇您的消息格式。其他 IPC 機制如 Thrift 可能只支持少量的消息格式,甚至只支持一種。在任一種情況下,使用跨語言消息格式就顯得非常重要了。即使您現在是以單一語言編寫您的微服務,您將來也可能會使用到其他語言。
有兩種主要的消息格式:文本和二進位。基於文本格式的例子有 JSON 和 XML。這些格式的優點在於,它們不僅是人類可讀的,而且是自描述的。在 JSON 中,對象的屬性由鍵值對集合表示。類似地,在 XML 中,屬性由命名元素和值表示。這使得消息消費者能夠挑選其感興趣的值並忽略其餘的值。因此,可以輕鬆地向後相容作出微小更改的消息格式。
XML 文檔的結構由 XML 模式(schema)指定。隨著時間的推移,開發人員社區已經意識到 JSON 也需要一個類似的機制。一個選擇是使用 JSON Schema,獨立或作為 IDL 的一部分,如 Swagger。
使用基於文本的消息格式的缺點是消息往往是冗長的,特別是 XML。因為消息是自描述的,每個消息除了它們的值之外還包含屬性的名稱。另一個缺點是解析文本的開銷。因此,您可能需要考慮使用二進位格式。
有幾種二進位格式可供選擇。如果您使用的是 Thrift RPC,您可以使用二進位 Thrift。如果您選擇的消息格式,包括了流行的 Protocol Buffers 和 Apache Avro。這兩種格式都提供了一種用於定義消息結構的類型 IDL。然而,一個區別是 Protocol Buffers 使用標記欄位,而 Avro 消費者需要知道模式才能解釋消息。因此,Protocol Buffers 的 API 演化比 Avro 更容易使用。這裡有篇博文對 Thrift、Protocol Buffers 和 Avro 作出了極好的比較。
3.10、總結
微服務必須使用進程間通信機制進行通信。在設計服務如何進行通信時,您需要考慮各種問題:服務如何交互、如何為每個服務指定 API、如何演變 API 以及如何處理局部故障。微服務可以使用兩種 IPC 機制:非同步消息傳遞和同步請求/響應。為了進行通信,一個服務必須能夠找到另一個服務。在第四章中,我們將介紹微服務架構中服務發現問題。
微服務實戰:NGINX 與 應用程式架構
by Floyd Smith
NGINX 使您能夠實現各種伸縮和鏡像操作,使您的應用程式更加靈敏和高度可用。您為伸縮和鏡像所作的選擇會影響到您如何進行進程間通信,這是本章的主題。
我們在 NGINX 方面建議您在實現基於微服務的應用程式時考慮使用四層架構。Forrester 在這方面有詳細的報告,您可以從 NGINX 上免費下載。這些層代表客戶端(包括台式機或筆記本電腦、移動、可穿戴或 IoT 客戶端)、交付、聚合(包括數據存儲)和服務,其中包括應用功能和特定服務,而不是共用數據存儲。
四層架構比以前的三層架構更加靈活,具有可擴展、響應靈敏、移動友好,並且內在支持基於微服務的應用程式開發和交付等優點。像 Netflix 和 Uber 這樣的行業引領者能夠通過使用這種架構來實現用戶所需的性能水平。
NGINX 本質上非常適合四層架構,從客戶端層的媒體流,到交付層的負載均衡與緩存、聚合層的高性能和安全的基於 API 的通信的工具,以及服務層中支持靈活管理的短暫服務實例。
同樣的靈活性使得可以實現強大的伸縮和鏡像模式,以處理流量變化,防止安全攻擊,此外還提供可用的故障配置切換,從而實現高可用。
在更為複雜的架構中,包括服務實例實例化和需求不斷的服務發現,解耦的進程間通信往往更受青睞。非同步和一對多通信方式可能比高耦合的通信方式更加靈活,它們最終提供更高的性能和可靠性。
本系列全部譯文
相關鏈接
- 微服務從設計到部署(一)微服務簡介
- 微服務從設計到部署(二)使用 API 網關
- 微服務從設計到部署(三)進程間通信