五分鐘了解微服務架構通信模式
通信是微服務架構中的關鍵要素,人們廣泛討論的焦點是如何選擇最有效的方法進行服務間交互。在這篇文章中,將探討和總結微服務的最佳通信策略,深入探討如何有效利用每種通信方式。
交互方式
要有效理解微服務架構中的服務通信方式,首先必須熟悉可用的交互方式。每種風格都有其獨特的優缺點,在為服務確定最合適的通信機制的明智決定之前,全面了解這些細微差別至關重要。這些基礎知識可確保所選方法完全符合系統的具體要求和挑戰。
交互方式可以分為兩個維度,第一個維度是一對一還是一對多的交互:
- 一對一(One-to-one) - 每個客戶請求由一個服務處理。
- 一對多(One-to-many) - 每個請求由多個服務處理。
第二個維度是同步還是異步交互。
- 同步(Synchronous) - 客戶端希望服務及時做出響應,甚至可能在等待時阻塞。
- 異步(Asynchronous) - 客戶端不會阻塞,即使有響應,也不一定立即發送。
下表顯示了不同維度的組合:
通信維度
下面分別簡要介紹一下。
一對一交互:
- 請求/響應 - 客戶端向服務端提出請求并等待響應。客戶端希望響應能及時到達,甚至可能在等待時阻塞。這種交互方式通常會導致服務緊耦合。
- 異步請求/響應 - 客戶端向服務端發送請求,服務端以異步方式回復。客戶端在等待時不會阻塞,而服務端可能很長時間都不會發送響應。
- 單向通知 - 客戶端向服務端發送請求,但不期望立即獲得回復。
一對多交互:
- 發布/訂閱 - 客戶端發布一條通知消息,由零個或多個感興趣的服務消費。
- 發布/同步響應 - 客戶端發布請求信息,然后等待相關服務的響應。
記住,一種服務可以有多種通信方式!
使用同步遠程過程(Remote Procedure Invocation)調用模式進行通信
客戶端向服務端發送請求,服務處理請求并發回響應。有些客戶端可能會阻塞等待響應,有些客戶端則可能采用反應式非阻塞架構。但與使用消息傳遞不同的是,客戶端假定響應會及時到達。
下圖顯示了 RPI 的工作原理。客戶端中的業務邏輯會調用 PRI 代理適配器類實現的代理接口,RPI 代理向服務發出請求。
請求由 RPI 服務端適配器類處理,該類通過接口調用服務的業務邏輯,然后將回復發送給 RPI 代理,后者將結果返回給客戶端的業務邏輯。
代理接口通常封裝了底層通信協議。我們將重點介紹最流行的 REST 和 gRPC 協議。
REST API
REST 的關鍵概念是資源,通常代表單個業務對象(如客戶或產品)或業務對象集合。REST 通過 HTTP 動詞來操作資源,資源使用 URL 引用。例如,GET 請求返回資源的表示形式,通常是 XML 文檔或 JSON 對象,也可以使用二進制等其他格式。POST 請求創建新資源,PUT 請求更新資源。
1.REST API 的挑戰:
(1) 在一次請求中獲取多個資源:
REST 資源通常以客戶和訂單等業務對象為重點,這給在一次請求中獲取多個相關對象帶來了挑戰。例如,獲取訂單及其關聯的客戶通常需要多次 API 調用。常見的解決方法是增強應用程序接口,使客戶端可以在一次調用中獲取相關資源,例如使用帶有擴展查詢參數的 GET 請求來指定相關資源。雖然這種方法在很多情況下都很有效,但實施起來可能會很復雜、很耗時,這也是 GraphQL 等用于更簡化數據檢索的替代技術興起的原因之一。
(2) 將操作映射到 HTTP 動詞
一個值得注意的 REST API 設計挑戰是如何將業務對象上的特定操作分配給正確的 HTTP 動詞。例如,更新訂單可能涉及取消或修改訂單等各種操作,而且并非所有更新都必須是冪等的,而這正是使用 HTTP PUT 方法所必需的。一種常見的方法是為不同的更新操作創建子資源,例如使用 POST 取消訂單(POST /orders/{orderId}/cancel )或修改訂單(POST /orders/{orderId}/revise )。另一種方法是將操作作為 URL 查詢參數。不過,這些方法可能并不完全符合 REST 原則。將操作映射到 HTTP 動詞上的這種困難,促成了 gRPC 等替代技術的流行。
使用 REST 有很多好處:
- 使用簡單,大部分工程師都比較熟悉。
- 可以在瀏覽器中使用 Postman 插件等工具測試 HTTP API,也可以在命令行中使用 curl 進行測試(假設使用的是 JSON 或其他文本格式)。
- 直接支持請求/響應式通信。
- HTTP 對防火墻是友好的。
- 不需要中間代理,從而簡化了系統架構。
使用 REST 有一些缺點:
- 只支持請求/響應式通信。
- 可用性低。由于客戶端和服務端直接通信,沒有中間組件緩沖信息,因此在交互過程中,客戶端和服務端必須同時運行。
- 客戶端必須知道服務端實例的位置(URL)。在現代應用中,這是一個非同小可的問題。客戶端必須使用所謂的服務發現機制來定位服務實例。
- 在一次請求中獲取多個資源具有挑戰性。
- 有時很難將多個更新操作映射到 HTTP 動詞。
3.使用 gRPC
gRPC 提供了另一種選擇,它使用基于二進制消息的協議,強調 API 優先的方法。gRPC 采用協議緩沖區(Protobuf),一種由谷歌開發的語言中立的序列化系統,允許開發人員在基于Protobuf的接口定義語言(IDL)中定義 API。gRPC API 在 HTTP/2 上運行,支持簡單的請求/響應和流式 RPC,因此服務端可以向客戶端發送消息流,反之亦然。該技術支持創建定義明確的服務接口和強類型方法,為處理微服務架構中各種復雜的通信模式提供了強大的框架。
gRPC 的優點和缺點
gRPC 有幾個好處:
- 設計一個擁有豐富更新操作的應用程序接口非常簡單。
- 具有高效、緊湊的 IPC 機制,尤其是在交換大型信息時。
- 雙向數據流可實現 RPI 和消息傳遞兩種通信方式。
- 可實現客戶端與用多種語言編寫的服務之間的互操作性。
gRPC 也有若干缺點:
- 與基于 REST/JSON 的應用程序接口相比,JavaScript 客戶端在使用基于 gRPC 的應用程序接口時需要花費更多的時間。
- 老式防火墻可能不支持 HTTP/2。
- gRPC 是 REST 的一個令人信服的替代方案,但與 REST 一樣,它也是一種同步通信機制,因此也存在部分失效的問題。
使用異步消息傳遞模式進行通信
使用消息傳遞時,服務通過異步消息進行通信。基于消息傳遞的應用程序通常使用消息代理,作為服務之間的中介。客戶端通過發送消息向服務端發出請求,如果服務端實例需要回復,就會向客戶端發送一條單獨的消息。由于通信是異步的,客戶端不會阻塞等待回復。相反,客戶端在編寫時假定不會立即收到回復。
1.消息傳遞概述
摘自 Gregor Hohpe 和 Bobby Woolf 合著的《企業集成模式》一書:
消息通過信道進行交互。發送方(應用程序或服務)向信道寫入消息,接收方(應用程序或服務)從信道讀取消息。我們先介紹下信息,再了解下信道。
2.關于消息
消息由消息頭(header) 和消息體(message body) 組成。
消息頭是描述發送數據的名-值對和元數據的集合。除了表示消息發送方的名-值對,消息頭還包含其他名-值對,如由發送方或消息基礎設施生成的唯一消息 ID,以及可選的返回地址,該地址指定了應寫入響應的信道。消息體是以文本或二進制格式發送的數據。
有幾種不同的消息:
- 文檔(Document) - 僅包含數據的通用消息,由接收方決定如何解釋。對命令的回復就是文檔消息的一個例子。
- 命令(Command) - 相當于 RPC 請求的消息,指定了要調用的操作及其參數。
- 事件(Event) - 表示發件人發生了值得注意的事情的信息。事件通常是領域事件,表示領域對象(如訂單或客戶)的狀態變化。
本文將主要介紹命令和事件。
3.關于信道
發送方的業務邏輯調用發送端接口,該接口封裝了底層通信機制。發送端由消息發送適配器類實現,該適配器類通過信道向接收者發送消息。信道是消息傳送基礎架構的一個抽象概念。接收器中的消息處理適配器類被調用來處理消息,并調用由消費者業務邏輯實現的接收端接口。任何數量的發送者都可以向同一個信道發送消息。同樣,任何數量的接收者都可以從同一個信道接收消息。
信道基礎設施
了解兩種信道非常重要:點對點(Point-To-Point) 和發布-訂閱(Publish-Subscribe)。
- 點對點信道將消息準確發送給正從信道讀取消息的消費者之一。服務使用點對點信道來實現前面介紹的一對一交互方式。例如,命令消息通常通過點對點信道發送。
- 發布-訂閱信道將每條消息發送給所有訂閱的消費者。服務使用發布-訂閱信道來實現前面介紹的一對多交互方式。例如,事件消息通常通過發布-訂閱信道發送。
既然我們已經清楚了解了異步通信,包括消息和信道的概念,那么接下來就應該探索異步通信框架提供的各種通信機制的實現。
4.實現請求/響應和異步請求/響應
當客戶端和服務端使用請求/響應或異步請求/響應進行交互時,客戶端發送請求,而服務端則返回響應。這兩種交互方式的區別在于,使用請求/響應時,客戶端希望服務立即做出響應,而使用異步請求/響應時則沒有這種期望。消息傳遞本質上是異步的,因此只提供異步請求/響應。但客戶端可以阻塞,直到收到響應為止。
客戶端和服務端通過交換消息來實現異步請求/響應式交互。如圖所示,客戶端向服務端對應的點對點消息傳遞信道發送一條命令消息,其中指定了要執行的操作及其參數。服務處理請求并向客戶端擁有的點對點信道發送包含結果的回復消息。
5.異步請求/響應
從上圖可以看出,客戶端必須告訴服務端將響應發送到哪兒,并且必須將響應與請求匹配起來。幸運的是,解決這兩個問題并不難。客戶端發送的命令報文帶有回復信道頭reply channel header。服務端將響應消息寫入回復信道,響應消息包含與命令消息標識符具有相同值的關聯 ID correlation id。客戶端通過correlation id將響應消息與請求匹配。
由于客戶端和服務端使用消息傳遞進行通信,因此本質上是異步交互。理論上,客戶端可以阻塞直到收到響應,但實際上,客戶端會異步處理。此外,響應通常由客戶端的任意實例進行處理。
6.實現單向通知
使用異步消息傳遞可以直接實現單向通知。客戶端向服務端對應的點對點信道發送消息,通常是命令消息。服務端訂閱該信道并處理消息,但不發送回復。可以復用"異步請求/響應"相同的圖示,但沒有回復信道。
7.實現發布/訂閱
客戶端向發布-訂閱信道發布消息,多個消費者可以讀取該消息。服務通過發布/訂閱來發布域事件,這些事件代表了對域對象的更改。發布域事件的服務擁有一個發布-訂閱信道,該通道的名稱源自域類。對特定域對象事件感興趣的服務只需訂閱相應的信道即可。
發布/訂閱
8.實現發布/同步響應
發布/同步響應交互方式是一種更高級別的交互方式,通過結合發布/訂閱和請求/響應的元素來實現。客戶端向發布-訂閱信道發布一條指定了回復信道頭reply channel header的消息。消費者向回復信道寫入包含correlation id的回復信息。客戶端通過correlation id將回復消息與請求匹配起來。
應用程序中具有異步 API 的每個服務都將使用其中一種或多種實現技術。使用異步 API 調用操作的服務將有一個用于請求的消息信道。同樣,發布事件的服務也會將事件發布到事件消息信道。
使用消息代理
基于消息傳遞的應用程序通常會使用消息代理(一種基礎架構服務,服務通過它進行通信)。但基于代理的架構并不是唯一的消息傳遞架構,也可以使用無代理消息傳遞架構,在這種架構中,服務之間可以直接通信(本文將不涉及這一主題)。
1.基于代理的消息傳遞概述
消息代理是所有消息流動的中介。發送者將消息寫入消息代理,然后由消息代理將消息發送給接收者。使用消息代理的主要好處是,發送者不需要知道消費者的網絡位置。另一個好處是,消息代理可以緩沖消息,直到消費者能夠處理。
有許多消息代理可供選擇。流行的開源消息代理包括以下幾種:
- ActiveMQ
- RabbitMQ
- Apache Kafka
每個代理都會做出不同的權衡。例如,延遲極低的代理可能不保留排序,不保證傳遞消息,只將消息存儲在內存中;而保證傳遞消息并可靠的將消息存儲在磁盤上的代理可能會有更高的延遲。
哪種消息代理最合適取決于應用程序的需求,應用程序的不同部分甚至可能有不同的消息傳遞需求。
2.使用消息代理實現消息信道
每個消息代理都以不同的方式實現消息信道概念。如表所示,JMS 消息代理(如 ActiveMQ)有隊列和主題。基于 AMQP 的消息代理(如 RabbitMQ)有交換和隊列。Apache Kafka 有主題,AWS Kinesis 有流,AWS SQS 有隊列。 此外,一些消息代理提供比本章所述消息和信道抽象更靈活的消息傳遞機制。
消息代理列表
這里介紹的幾乎所有消息代理都支持點對點和發布-訂閱信道。AWS SQS 是個例外,它只支持點對點信道。
消息代理的問題
1.接收競爭和消息排序
挑戰之一保持消息有序的同時擴展消息接收器。為了并發處理消息,通常需要多個服務實例。此外,即使是單個服務實例也可能會使用線程來并發處理多個消息。使用多線程和多服務實例并發處理消息可以提高應用程序吞吐量,但并發處理消息的挑戰在于確保每條消息都能按順序處理。
例如,假設有三個服務實例從同一個點對點信道讀取消息,發送方按順序發布 "創建訂單"、"更新訂單"和"取消訂單"事件消息。簡單的消息傳遞實現可以同時將每條消息傳遞給不同的接收方。由于網絡問題或垃圾回收導致的延遲,消息的處理順序可能會被打亂,從而導致奇怪的行為。理論上,一個服務實例可能會在另一個服務處理"訂單創建"消息之前處理"訂單取消"消息!
Apache Kafka 和 AWS Kinesis 等現代消息代理常用的解決方案是使用分片(分區)信道,下圖顯示了其工作原理。該解決方案分為三個部分:
- 分片信道由兩個或多個分片組成,每個分片的行為都和信道一樣。
- 發送者在信息頭中指定分片key,通常是任意字符串或字節序列。消息代理通過分片key將信息分配給特定分片/分區。例如,可以通過計算分片key的哈希值乘以分片數來選擇分片。
- 消息代理將接收器的多個實例分組,并將它們視為同一個邏輯接收器。例如,Apache Kafka 使用消費者組(consumer group)一詞。消息代理將每個分片分配給一個接收器。當接收器啟動和關閉時,會重新分配分片。
分片信道架構
在本例中,每個訂單事件消息都以orderId作為其分片key。特定訂單的每個事件都發布到同一個分片,由單個消費者實例讀取。因此,可以保證這些消息按順序處理。
2.處理重復消息
使用消息傳遞時必須解決的另一個難題是處理重復消息。理想情況下,消息代理應該每條消息只傳遞一次(exactly-once),但保證準確傳遞一次消息通常成本太高。相反,大多數消息代理都承諾至少傳遞一次消息(at-least-once)。
當系統正常運行時,保證至少交付一次的消息代理只交付一次消息。然而,客戶端、網絡或消息代理的故障可能導致消息被傳送多次。假設一個客戶端處理了消息并更新了數據庫,但在發送確認消息之前崩潰了。消息代理將再次傳送未確認的消息,要么在該客戶端重啟時傳送給它,要么傳送給該客戶端的另一個副本。
理想情況下,應該使用在重新傳遞消息時保留排序的消息代理。
假設客戶端在處理"創建訂單"事件后,又處理了同一訂單的"取消訂單"事件,而 "創建訂單"事件沒有得到確認。消息代理應同時重新傳遞"創建訂單"和"取消訂單"事件。如果只重新傳遞"創建訂單"事件,客戶機可能會取消訂單。
有幾種不同的方法可以處理重復信息:
- 編寫冪等消息處理程序。
- 跟蹤并丟棄重復消息。
我們來簡單了解一下每種方案。
3.編寫冪等消息處理程序
如果處理消息的應用邏輯是冪等的,那么重復報文是無害的。如果用相同的輸入值多次調用應用邏輯不會產生額外的效果,那么應用邏輯就是冪等的。例如,取消一個已經取消的訂單就是一個冪等操作。使用客戶提供的 ID 創建訂單也是如此。
只要消息代理在重新傳遞消息時保留排序,冪等消息處理程序就可以安全執行多次。
遺憾的是,應用邏輯往往不是等效的。或者消息代理在重新傳遞消息時不保留順序,重復或失序的消息可能會導致錯誤。在這種情況下,就必須自己編寫消息處理程序來跟蹤消息并丟棄重復消息。
4.跟蹤并丟棄重復消息
例如,考慮一個對消費者信用卡進行鑒權的消息處理程序,必須對每張訂單的信用卡精確鑒權一次。這個應用程序邏輯示例每次調用都會產生不同的效果。如果重復的消息導致消息處理程序多次執行這一邏輯,應用程序的行為就會不正確。執行這種應用程序邏輯的消息處理程序必須通過檢測和丟棄重復消息來獨立實現。一個簡單的解決方案是,消息消費者使用消息 ID 跟蹤已處理的消息,并丟棄任何重復消息。舉例來說,可以在數據庫表中存儲它處理過的每條消息的ID。下圖顯示了如何使用專用表來實現這一功能。
跟蹤消息流
消費者處理消息時,會在數據庫表中記錄message id,作為創建和更新業務實體的事務的一部分。在本例中,消費者向 PROCESSED_MESSAGES 表中插入一條包含message id的記錄。如果是重復消息,則 INSERT 將失敗,用戶可以丟棄該消息。
另一種方法是讓消息處理程序在應用程序表而不是專用表中記錄message id。這種方法在使用 NoSQL 數據庫時特別有用,因為 NoSQL 數據庫的事務模型有限,不支持將更新兩個表作為數據庫事務的一部分。
結論
總之,微服務架構中通信方式的選擇對于應用程序的整體效率和可擴展性至關重要。在本文中,我們探討了從同步調用到異步消息傳遞等各種通信機制,每種機制都有其獨特的優勢和合適的使用場景。正如我們所看到的,正確的通信策略不僅能提高性能,還能確保服務交互的彈性和靈活性。
在選擇通信方式時,必須考慮服務交互的性質、對實時數據的需求以及所涉及服務的復雜性等因素。請記住,我們的目標是建立一個強大架構,使其能夠隨著組織需求和技術進步而發展。