Chris Richardson微服務翻譯:構建微服務之微服務架構的進程通訊
Chris Richardson 微服務系列翻譯全7篇鏈接:
- 微服務介紹
- 構建微服務之使用API網關
- 構建微服務之微服務架構的進程通訊(本文)
- 微服務架構中的服務發現
- 微服務之事件驅動的數據管理
- 微服務部署
- 重構單體應用為微服務
原文鏈接:Building Microservices: Inter-Process Communication in a Microservices Architecture
簡介
在單體應用中,模塊間使用編程語言級別的方法或函數彼此調用。而基於微服務架構的本質是是運行在多臺機器上的分布式應用,每個服務都是一個進程。如下圖所示,微服務之間必須使用進程間通信(IPC)的機制實現交互:
稍後我們將討論 IPC 技術,先看下設計相關的問題。
交互模式
當為某個服務選擇 IPC 機制時,首先要考慮服務間如何交互。client 和 server 端有很多交互的方式,可以按兩個維度分類:
第一個維度是一對一還是一對多:
- 一對一:每個 client 請求只會被一個 server 處理
- 一對多:每個 client 請求會被多個 server 處理
第二個維度是交互是同步還是異步:
- 同步模式:client 期望來自 server 的及時響應,甚至可能由於等待而阻塞
- 異步模式:client 等待響應時不會阻塞,不需要及時響應
下面表格展示了兩種方式的不同:
一對一 | 一對多 | |
同步 | 請求/響應 | |
異步異步 | 通知 | 發布/訂閱 |
請求/異步響應 | 發布/異步響應 |
下面有幾種一對一的交互模式:
- 請求/響應:client 向 server 發送請求並等待響應,client 期望響應能及時到達。在一個基於線程的應用中,請求的線程可能在等待時阻塞線程的執行。
- 通知(單向請求):client 往 server 發送請求,但不期望響應。
- 請求/異步響應:client 往 server 發送請求,server 異步響應。client 不會阻塞,因為設計時就默認請求不會立即返回。
下面有幾種一對多的交互模式:
- 發布/訂閱模式:client 發布一個通知消息,消息會被 0 或多個感興趣的服務消費。
- 發布/異步響應模式:client 發布一個請求消息,在一定時間內等待感興趣服務的響應。
每個服務都是以上幾種模式的組合,對某些服務來說,一個 IPC 機制就能滿足了,另外一些服務可能需要多個 IPC 機制的組合。下圖展示了用戶叫車應用中,用戶請求行程時,服務是如何交互的:
上圖服務使用了通知、請求/響應、發布/訂閱的方式。例如:乘客在移動端向『行程管理服務』發送接送需求的通知;『行程管理服務』使用 請求/響應 模式 調用『乘客服務』來驗證乘客賬號是否有效;然後『行程管理服務』創建行程並使用 發布/訂閱 模式來通知其他服務(定位可用司機的『調度服務』等)。
我們討論了交互風格,下面看下如何定義 API。
定義API
API 是服務端和客戶端的契約。無論選擇選擇哪種 IPC 機制,都需要使用接口定義語言(IDL)來定義 服務的API。開發服務前,先定義服務接口,並與 client端開發者一起 review,後續再對 API 進行叠代。這樣設計能幫助你構建更符合客戶需求的服務。
文章後半段你會發現,API 的定義依賴選擇的 IPC 機制。如果使用消息機制,API 則由消息頻道和消息類型組成。如果使用 HTTP, API 則是由 URL 和 request/response 格式組成。後面我們將討論 IDL 的細節。
API進化
服務的 API 不可避免的隨著時間進化。單體應用中,可以直接修改 API 並更新所有的調用者。但在微服務應用中,即時 API 的所有調用者都在一個應用中,去更新其他服務也是很困難的,通常不能強制讓所有 client 升級來保持和 server 端一致。此外,你可能還會增加部署新的服務版本,與老版本同時運行。了解處理這些問題的策略是非常重要的。
如何根據更改的大小來處理 API 呢?有的變化很小,通常可以與舊版本做到向後兼容,例如:為請求或響應添加了一個屬性。對此,設計服務時考慮魯棒性是很有必要的:使用舊版本 API 的 client 在新版本的 API 下能正常工作;server 為缺失的屬性提供默認值;client 忽略響應中額外添加的屬性。
有時候 API 不得不做一些大的、不兼容的變動,此時又不能強制讓所有 client 立即升級,因此,舊版本 API 還需要運行一段時間。如果使用的是基於 HTTP 的 IPC,可以在 URL 裏嵌入服務版本,每個服務實例可以同時處理多個版本。另一種方式也可以選擇為每個版本單獨部署。
處理局部故障
分布式系統普遍存在局部失敗的問題,由於 client 和 server 是運行在獨立的進程中,server 可能因為掛了或維護而暫時不可用,不能及時響應 client 的請求,或者因為過載而導致響應很慢。
以上篇文章提到的商品詳情頁場景為例,假設推薦服務沒有響應,client 可能無限期的等待服務響應而導致阻塞,這不僅導致用戶體驗很糟糕,而且會占用線程等寶貴資源,就像下圖所示,運行時線程耗盡,而無法響應任何請求:
為解決此類問題,設計時需要考慮局部故障的問題:
Netfilix 提供了較好的解決方案:
- 網絡超時:等待響應時不設置無期限阻塞,而采用超時策略,保證資源不會無限被占用。
- 限制請求數量:為 client 對某個服務的請求設置訪問上限,如果請求達到上限,則不再處理任何請求,做到快速失敗。
- 熔斷器模式:記錄成功和失敗的請求數量,如果失敗率超過一個閥值,觸發熔斷器使得後面的請求立刻失敗。如果大量請求失敗,那這個服務可認為不可用,繼續請求也沒有意義。一段時間後,client 可以再次重試,如果成功,則關閉熔斷器。
- 提供 fallback 機制:請求失敗時提供 fallback,例如:返回緩存或一個默認值
Netflix Hystrix 是一個實現相關模式的開源庫。如果使用 JVM,那麽推薦使用 Hystrix。如果使用的非 JVM 環境,也可以使用類似的庫。
IPC 技術
現在有不同的 IPC 技術可選擇:基於 請求/響應 的同步通信模式,例如基於 HTTP 的 Rest 或 Thrift;也可以選擇異步的、基於消息的通信模式,例如AMQP、STOMP。這些通信有著不同的消息格式,服務可以選擇基於文本、方便閱讀的 JSON 或 XML格式,或者效率更高的二進制格式(例如 Avro、Protocol Buffers)。
異步,基於消息的通信
使用消息模式時,進程間通過異步消息的方式來通信,client 發送消息來請求 server,如果期望 server 響應,則 server 會發送另外一條消息給 client。由於通信是異步的,client 不會因為等待響應而阻塞,同時 client 編程時也以服務不會立即響應來處理。
消息由消息頭(元數據和發送者)和消息體組成,消息通過頻道進行交換,任意數量的生產者都可以往頻道裏發送消息,同樣,任意數量的消費者都可以從頻道裏消費消息。頻道分為點對點、訂閱/發布兩種:
- 點對點模式:頻道中的消息只會被交付給某個消費者,這種適用於前面提到的一對一的交互方式
- 訂閱/發布模式:頻道中的消息會被交付到所有感興趣的消費者,這種適用於一對多的交互方式
下圖展示了打車軟件中如何使用 發布/訂閱 模式:
行程管理服務向『訂閱-發布』頻道寫入『創建行程』的消息,通知調度服務有新的行程請求。調度服務查找空閑的司機,並通過『發布-訂閱』頻道寫入『推薦司機』的消息,通知其他服務。
有多種消息系統供我們選擇,當然我們盡可能選擇支持多種編程語言的。一些消息系統支持 AMQP和 STOMP 這樣的標準協議,有的則支持專有的協議。開源的消息系統例如:RabbitMQ、Apacha Kafka、Apache ActiveMQ 和 NSQ。統一來看,他們都支持一些消息和頻道,都致力於高可用、高性能和高可擴展性。
使用消息系統有很多優點:
- client 和 server 解耦,client 只需要將消息發送到合適的頻道,完全不需要感知 server 的存在,因此不需要再去使用服務發現機制來確定服務實例的位置。
- 消息緩沖:在 HTTP 這樣的請求/響應協議下,client 和 server 交互期間需要保證雙方的可用性。然而在消息模式中,消息組件會將消息按照隊列方式進行管理,直到消息被消費者消費。例如:即使訂單系統很慢或不可用,在線商店仍舊可以接受客戶的下單請求,只需要將下單消息放入隊列即可。
- 靈活的 client-server 交互方式:消息支持前面提到的所有交互風格。
- 清晰的進程間通信:基於 RPC 的通信機制視圖使調用遠程服務像調用本地服務一樣,然而,由於局部故障的可能,他們大不相同。消息機制使這些差異直觀明顯,開發者不會產生安全錯覺。
當然,消息系統也有缺點:
- 額外的運維復雜度:消息系統組件的安裝、部署、運維等工作,消息系統的高可用保障,否則會影響到系統的可用性。
- 實現 請求/響應 交互模式的復雜度:每條請求消息需要包含一個 回復渠道ID 和 關聯ID,server 發送包含關聯ID的響應消息到渠道中,client 使用關聯ID 去匹配對應的響應。這種情況下,使用支持請求/響應的 IPC 機制會更容易些。
同步,請求/響應 IPC
使用同步、請求/響應的 IPC 時,client 請求 server 時有可能由於等待 server 響應而被阻塞。另外一些client 會使用異步、事件驅動的代碼,例如封裝好的 Future 或者 Rx Observable。這個模式最常見的協議是 Rest 和Thrift。
Rest
當前流行開發 RESTful 風格的 API。 Rest 是基於 HTTP 的 IPC 機制,其核心概念是使用 URL 來表示資源(用戶或產品的一組業務對象)。例如:GET 請求會返回一個資源的信息,可能是 XML 文檔 或 JSON 對象格式;POST 請求會創建新的資源;PUT 請求會更新資源。REST 之父 Roy Fielding 曾經說過:
REST provides a set of architectural constraints that, when applied as a whole, emphasizes scalability of component interactions, generality of interfaces, independent deployment of components, and intermediary components to reduce interaction latency, enforce security, and encapsulate legacy systems.
Rest 提供了一些列架構系統參數作為整體使用,強調組件交互的擴展性、接口的通用性、組件的獨立部署、減少交互延遲的中間件,他強化安全,也能封裝遺留系統。
下面展示打車軟件使用 Rest 的場景:
乘客向行程管理服務的 /trips 資源發送了 POST 請求,行程管理服務然後向乘客管理服務發送 GET 請求獲取乘客信息,當乘客認證完成後,創建一個行程,並返回 201 響應。Leonard Richardson 為 REST 定義了一個成熟度模型,分為如下四個層次:
- Level 0:web 服務使用 HTTP 作為傳輸方式,調用固定的 URL,每次請求指定方法和參數
- Level 1:引入了資源的概念,要執行對資源的操作,請求通過 POST,指定要執行的操作和參數
- Level 2:使用 HTTP 的語法來執行操作,例如:GET 表示獲取,POST 表示創建,PUT 表示更新
- Level 3:API 定義按照 HATEOAS(Hypertext As The Engine Of Application State)設計原則,基本思想 GET 請求返回資源的一些對資源允許操作的鏈接。例如:client 使用 GET 訂單資源中包含的鏈接取消某一訂單。HATEOAS 的一個優點就是無需在 client 代碼中寫入硬鏈接的 URL。此外,返回的資源信息中包含了對資源允許操作的鏈接,client 無需再猜測當前資源下所能做哪些操作了
基於 HTTP 協議的優點:
- 簡單,為大家所熟悉
- 可使用瀏覽器、postman,curl 之類的命令行測試 API
- 支持 請求/響應 模式的通信
- 不需要中間代理,減價系統架構
HTTP 不足之處:
- 只支持 請求/響應的交互
- client 和 server 之間沒有消息緩沖機制,要求交互時雙方必須同時運行
- client 需要知道每個 server實例 的url
Thrift
Apache Thrift 是 REST 的一個有趣的替代品,實現了跨語言的客戶端和服務端RPC通信的框架,Thrift 提供了 C 語言風格的接口定義語言來定義 API,可以通過編譯生成客戶端Stub 和 服務端的骨架,可以生成多種語言的代碼(包括 C++、Java、Python、PHP、Ruby、Erlang、Node.js)。
Thrift 接口通常包含一個或多個服務,服務定義與 Java 接口類似,是一組強類型方法的集合。Thrift 能返回值,也可以定義為單向通信。如果需要返回值就需要實現 請求/響應風格的交互,客戶端等待響應時可以拋出異常;單向通信就是通知模式,服務端不需要返回響應。
Thrift 支持 JSON、二進制、壓縮二進制等不同的消息格式。二進制解碼比 JSON 更快,更為高效;壓縮二進制比 JSON 空間利用率更高; JSON 則更易讀。Thrift 也支持不同的通信協議:TCP 或 HTTP,TCP 比 HTTP 更加高效,而 HTTP 對防火墻、人及瀏覽器更加友好。
消息格式
選擇一種支持多語言的消息格式非常重要,哪怕你只用一種語言實現微服務,誰又能保證以後不會使用新的語言呢?
目前有文本和二進制兩種格式。文本格式包括 JSON 和 XML。這種格式優點不僅可讀,而且是自描述的。JSON中,對象的屬性是鍵值對的集合;XML中,屬性表示為命名的元素和值。消費者能選擇感興趣的值而忽略其他部分,對格式的修改也能容易的向後兼容。
XML文檔的結構是 XML Schema 定義的,隨著時間的發展,開發者意識到 JSON 也需要一個類似的機制,方法一是使用 JSON Schema,要麽獨立使用,要麽作為 Swagger 這類 IDL的一部分使用。
文本格式的一大缺點是消息會變的冗長,尤其是 XML:因為消息是自描述的,每條消息除了值之外還包括屬性的名稱。另一大缺點是解析文本的開銷略大,此時可以考慮二進制格式。
二進制格式也很多,如果使用 Thrift,那麽可以用二進制Thrift;如果使用其他消息格式,常用的還包括 Protocol Buffers 和 Apache Avro,兩者都提供了 IDL 來定義消息結構。差異之處在於 Protocol Buffers 使用標記字段,而 Avro 消費者需要了解 Schema 來解析消息,使用 Protocol Buffers 時,API進化比 Avro 更容易。Martin Kleppmann 的 博客文章 對Thrift、Protocol Buffers 和 Avor 進行了詳細的比較。
總結
微服務需要使用進程間消息通信機制來交互,設計服務的通信模式時,需要考慮一下幾個問題:服務如何交互、如何定義 API、如何升級 API,如何處理局部故障。微服務架構有兩種 IPC 機制可用:異步消息機制和同步請求/響應機制。下篇文章中,我們會討論微服務架構中的服務發現問題。
Chris Richardson微服務翻譯:構建微服務之微服務架構的進程通訊