1. 程式人生 > >Chris Richardson微服務翻譯:構建微服務之微服務架構的進程通訊

Chris Richardson微服務翻譯:構建微服務之微服務架構的進程通訊

標記 pac blog ural action 客戶端 靈活 dso 不兼容

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微服務翻譯:構建微服務之微服務架構的進程通訊