【CHRIS RICHARDSON 微服務系列】微服務架構中的進程間通信-3

 

編者的話 |本文來自 Nginx 官方博客,是微服務系列文章的第三篇,在第一篇文章中介紹了微服務架構模式,與單體模式進行了比較,並且討論了使用微服務架構的優缺點。第二篇描述了採用微服務架構的應用客戶端之間如何採用 API 網關方式進行通信。在這篇文章中,我們將討論系統服務之間是如何實現通信的。

作者介紹:Chris Richardson,是世界著名的軟件大師,經典技術著作《POJOS IN ACTION》一書的作者,也是 cloudfoundry.com 最初的創始人,Chris Richardson 與 Martin Fowler、Sam Newman、Adrian Cockcroft 等並稱爲世界十大軟件架構師。

Chris Richardson 微服務系列全 7 篇:

1. 微服務架構概念解析

2. 構建微服務架構:使用 API Gateway

3. 深入微服務架構的進程間通信(本篇文章)

4. 服務發現的可行方案以及實踐案例

5. 微服務的事件驅動數據管理

6. 選擇微服務部署策略

7. 將單體應用改造爲微服務(本篇文章)

簡介

在單體應用中,各模塊之間的調用是通過編程語言級別的方法或者函數來實現的。而基於微服務的分佈式應用是運行在多臺機器上的;一般來說,每個服務實例都是一個進程。

因此,如下圖所示,服務之間的交互必須通過進程間通信(IPC)來實現。

後面我們將會詳細介紹 IPC 技術,現在我們先來看下設計相關的問題。

交付模式

當爲某個服務選擇 IPC 時,首先需要考慮服務之間的交互問題。客戶端和服務器之間有很多的交互模式,我們可以從兩個維度進行歸類。

第一個維度是一對一還是一對多:

  • 一對一:每個客戶端請求有一個服務實例來響應。
  • 一對多:每個客戶端請求有多個服務實例來響應。

第二個維度是這些交互式是同步還是異步:

  • 同步模式:客戶端請求需要服務端即時響應,甚至可能由於等待而阻塞。
  • 異步模式:客戶端請求不會阻塞進程,服務端的響應可以是非即時的。

下表顯示了不同交互模式:

一對一一對多
同步 請求/響應
異步 通知 發佈/訂閱
異步 請求/異步響應 發佈/異步響應

一對一的交互模式有以下幾種方式:

  • 請求/響應:一個客戶端向服務器端發起請求,等待響應,客戶端期望此響應即時到達。在一個基於線程的應用中,等待過程可能造成線程阻塞。
  • 通知(也就是常說的單向請求):一個客戶端請求發送到服務端,但是並不期望服務端響應。
  • 請求/異步響應:客戶端發送請求到服務端,服務端異步響應請求。客戶端不會阻塞,而且被設計成默認響應不會立刻到達。

一對多的交互模式有以下幾種方式:

  • 發佈/ 訂閱模式:客戶端發佈通知消息,被零個或者多個感興趣的服務消費。
  • 發佈/異步響應模式:客戶端發佈請求消息,然後等待從感興趣服務發回的響應。

每個服務都是以上這些模式的組合。對某些服務,一個 IPC 機制就足夠了;而對另外一些服務則需要多種 IPC 機制組合。下圖展示了在用戶叫車時,打車應用內的服務是如何交互的。

上圖中的服務通信使用了通知、請求/響應、發佈/訂閱等方式。例如,乘客在移動端向“行程管理”服務發送通知,請求一次接送服務。“行程管理”服務通過使用請求/響應來喚醒“乘客服務”來驗證乘客賬號有效,繼而創建此次行程,並利用發佈/訂閱來通知其它服務,其中包括定位可用司機的調度服務。

現在我們瞭解了交互模式,接下來我們一起來看看如何定義 API。

定義API

API 是服務端和客戶端之間的契約。無論選擇了何種 IPC 機制,重點是使用某種交互定義語言(IDL)來準確定義服務的 API。對於如何使用 API 優先的方式來定義服務,已經有了一些很好的討論。你在開發服務之前,要定義服務接口並與客戶端開發者共同討論,後續只需要迭代 API 定義。這樣的設計能夠大幅提升服務的可用度。

在本文後半部分你將會看到,API 定義實質上依賴於選定的 IPC 機制。如果使用消息機制,API 則由消息頻道(channel)和消息類型構成;如果選擇使用 HTTP 機制,API 則由 URL 和請求、響應格式構成。後面將會詳細描述 IDL。

不斷進化的API

服務的 API 會隨着時間而不斷變化。在單體應用中,經常會直接修改 API 並更新所有的調用者。但是在基於微服務的應用中,即使所有的 API 的使用者都在同一應用中,這種做法也困難重重,通常不能強制讓所有客戶端都與服務保持同步更新。此外,你可能會增量部署服務的新版本,這時舊版本會與新版本同時運行。瞭解這些問題的處理策略至關重要。

對 API 變化的處理方式與變化的大小有關。有的變化很小,並且可以兼容之前的版本;比如給請求或響應增加屬性。在設計客戶端和服務時,很有必要遵循健壯性原則。服務更新版本後,使用舊版 API 的客戶端應該繼續使用。服務爲缺失的請求屬性提供默認值,客戶端則忽略任何額外的響應。使用 IPC 機制和消息格式能夠讓你輕鬆改進 API。

然而有時候,API 需要進行大規模改動,並且不兼容舊版本。鑑於不能強制讓所有客戶端立即升級,支持舊版 API 的服務還要再運行一段時間。如果你使用的是諸如 REST 這樣的基於 HTTP 機制的 IPC,一種方法就是將版本號嵌入到 URL 中,每個服務實例可以同時處理多個版本。另一種方法是部署不同實例,每個實例處理一個版本的請求。

處理局部失敗

在上一篇關於 API 網關的文章中,我們瞭解到,分佈式系統普遍存在局部失敗的問題。由於客戶端和服務端是獨立的進程,服務端可能無法及時響應客戶端請求。服務端可能會因爲故障或者維護而暫時不可用。服務端也可能會由於過載,導致對請求的響應極其緩慢。

以上篇文章中提及的產品頁爲例,假設推薦服務無法響應,客戶端可能會由於無限期等待響應而阻塞。這不僅會導致很差的用戶體驗,並且在很多應用中還會佔用之前的資源,比如線程;最終,如下圖所示,運行時耗盡線程資源,無法響應。

爲了預防這種問題,設計服務時候必須要考慮部分失敗的問題。

Netfilix 提供了一個比較好的解決方案,具體的應對措施包括:

  • 網絡超時:在等待響應時,不設置無限期阻塞,而是採用超時策略。使用超時策略可以確保資源不被無限期佔用。
  • 限制請求的次數:可以爲客戶端對某特定服務的請求設置一個訪問上限。如果請求已達上限,就要立刻終止請求服務。
  • 斷路器模式(Circuit Breaker Pattern):記錄成功和失敗請求的數量。如果失效率超過一個閾值,觸發斷路器使得後續的請求立刻失敗。如果大量的請求失敗,就可能是這個服務不可用,再發請求也無意義。在一個失效期後,客戶端可以再試,如果成功,關閉此斷路器。
  • 提供回滾:當一個請求失敗後可以進行回滾邏輯。例如,返回緩存數據或者一個系統默認值。Netflix Hystrix 是一個實現相關模式的開源庫。如果使用 JVM,推薦使用Hystrix。而如果使用非 JVM 環境,你可以使用類似功能的庫。

IPC技術

現在有很多不同的 IPC 技術。服務間通信可以使用同步的請求/響應模式,比如基於 HTTP 的 REST 或者 Thrift。另外,也可以選擇異步的、基於消息的通信模式,比如 AMQP 或者 STOMP。此外,還可以選擇 JSON 或者 XML 這種可讀的、基於文本的消息格式。當然,也還有效率更高的二進制格式,比如 Avro 和 Protocol Buffer。在討論同步的 IPC 機制之前,我們先了解異步的 IPC 機制。

基於消息的異步通信

使用消息模式的時候,進程之間通過異步交換消息消息的方式通信。客戶端通過向服務端發送消息提交請求,如果服務端需要回復,則會發送另一條獨立的消息給客戶端。由於異步通信,客戶端不會因爲等待而阻塞,相反會認爲響應不會被立即收到。

消息由數據頭(例如發送方這樣的元數據)和消息正文構成。消息通過渠道發送,任何數量的生產者都可以發送消息到渠道,同樣,任何數量的消費者都可以從渠道中接受數據。頻道有兩類,包括點對點渠道和發佈/訂閱渠道。點對點渠道會把消息準確的發送到從渠道讀取消息的用戶,服務端使用點對點來實現之前提到的一對一交互模式;而發佈/訂閱則把消息投送到所有從渠道讀取數據的用戶,服務端使用發佈/訂閱渠道來實現上面提到的一對多交互模式。

下圖展示了打車軟件如何使用發佈/訂閱:

通過向發佈/訂閱渠道寫入一條創建行程的消息,行程管理服務會通知調度服務有新的行程請求。調度服務發現可用的司機後會向發佈/訂閱渠道寫入一條推薦司機的消息,並通知其它服務。

有多種消息系統可供選擇,最好選擇支持多編程語言的。有的消息系統支持 AMQP 和 STOMP 這樣的標準協議,有的則支持專利協議。也有大量的開源消息系統可用,譬如 RabbitMQ、Apache Kafka、Apache ActiveMQ 和 NSQ。宏觀上,它們都支持一些消息和渠道格式,並且努力提升可靠性、高性能和可擴展性。然而,細節上,它們的消息模型卻大相徑庭。

使用消息機制有很多優點:

  • 解耦客戶端和服務端:客戶端只需要將消息發送到正確的渠道。客戶端完全不需要了解具體的服務實例,更不需要一個發現機制來確定服務實例的位置。
  • 消息緩衝:在 HTTP 這樣的同步請求/響應協議中,所有的客戶端和服務端必須在交互期間保持可用。而在消息模式中,消息中間人將所有寫入渠道的消息按照隊列方式管理,直到被消費者處理。也就是說,在線商店可以接受客戶訂單,即使下單系統很慢或者不可用,只要保持下單消息進入隊列就好了。
  • 客戶端-服務端的靈活交互:消息機制支持以上說的所有交互模式。
  • 清晰的進程間通信:基於 RPC 的通信機制試圖讓喚醒遠程服務端像調用本地服務一樣,然而,囿於物理定律和可能的局部失敗,這二者大不相同。消息機制能讓這些差異直觀明確,開發者不會產生安全錯覺。

然而,消息機制也有自己的缺點:

  • 額外的操作複雜性:消息系統需要單獨安裝、配置和部署。消息broker(代理)必須高可用,否則系統可靠性將會受到影響。
  • 實現基於請求/響應交互模式的複雜性:請求/響應交互模式需要完成額外的工作。每個請求消息必須包含一個回覆渠道 ID 和相關 ID。服務端發送一個包含相關 ID 的響應消息到渠道中,使用相關 ID 來將響應對應到發出請求的客戶端。這種情況下,使用一個直接支持請求/響應的 IPC 機制會更容易些。

現在我們已經瞭解了基於消息的 IPC,接下來我們來看看基於請求/響應模式的 IPC。

基於請求/響應的同步 IPC

使用同步的、基於請求/響應的 IPC 機制的時候,客戶端向服務端發送請求,服務端處理請求並返回響應。一些客戶端會由於等待服務端響應而被阻塞,而另外一些客戶端可能使用異步的、基於事件驅動的客戶端代碼,這些代碼可能通過 Future 或者 Rx Observable 封裝。然而,與使用消息機制不同,客戶端需要響應及時返回。這個模式中有很多可選的協議,但最常見的兩個協議是 REST 和 Thrift。首先我們來了解 REST。

REST當前很流行開發 RESTful 風格的 API。REST 基於 HTTP 協議,其核心概念是資源典型地代表單一業務對象或者一組業務對象,業務對象包括“消費者”或“產品”。REST 使用 HTTP 協議來控制資源,通過 URL 實現。譬如,GET 請求會返回一個資源的包含信息,可能是 XML 文檔或 JSON 對象格式。POST 請求會創建新資源,而 PUT 請求則會更新資源。REST 之父 Roy Fielding 曾經說過:

REST 提供了一系列架構系統參數,作爲整體使用,強調組件交互的擴展性、接口的通用性、組件的獨立部署、以及減少交互延遲的中間件,它強化安全,也能封裝遺留系統。

— Fielding, Architectural Styles and the Design of Network-based Software Architectures

下圖展示了打車軟件如何使用 REST。

乘客通過移動端向行程管理服務的 /trips 資源提交了一個 POST請求。行程管理服務收到請求之後,會發送一個 GET 請求到乘客管理服務以獲取乘客信息。當確認乘客信息之後,隨即創建一個行程,並向移動端返回 201 響應。

很多開發者都表示他們基於 HTTP 的 API 是 RESTful 風格。但是,如同 Fielding 在他的博客中所說,並非所有這些 API 都是 RESTful。Leonard Richardson(注:與本文作者 Chris 無任何關係)爲 REST 定義了一個成熟度模型,具體包含以下四個層次:

  • Level 0:本層級的 Web 服務只是使用 HTTP 作爲傳輸方式,實際上只是遠程方法調用(RPC)的一種具體形式。SOAP 和 XML-RPC 都屬於此類。
  • Level 1:Level 1 層級的 API 引入了資源的概念。要執行對資源的操作,客戶端發出指定要執行的操作和任何參數的 POST 請求。
  • Level 2:Level 2 層級的 API 使用 HTTP 語法來執行操作,譬如 GET 表示獲取、POST 表示創建、PUT 表示更新。如有必要,請求參數和主體指定操作的參數。這能夠讓服務影響 web 基礎設施服務,如緩存 GET 請求。
  • Level 3:Level 3 層級的 API 基於 HATEOAS(Hypertext As The Engine Of Application State)原則設計,基本思想是在由 GET請求返回的資源信息中包含鏈接,這些鏈接能夠執行該資源允許的操作。例如,客戶端通過訂單資源中包含的鏈接取消某一訂單,GET 請求被髮送去獲取該訂單。HATEOAS 的優點包括無需在客戶端代碼中寫入硬鏈接的 URL。此外,由於資源信息中包含可允許操作的鏈接,客戶端無需猜測在資源的當前狀態下執行何種操作。

使用基於 HTTP 的協議有如下好處:

  • HTTP 非常簡單並且大家都很熟悉。
  • 可以使用瀏覽器擴展(比如 Postman)或者 curl 之類的命令行來測試 API。
  • 內置支持請求/響應模式的通信。
  • HTTP 對防火牆友好。
  • 不需要中間代理,簡化了系統架構。

不足之處包括:

  • 只支持請求/響應模式交互。儘管可以使用 HTTP 通知,但是服務端必須一直髮送 HTTP 響應。
  • 由於客戶端和服務端直接通信(沒有代理或者緩衝機制),在交互期間必須都保持在線。
  • 客戶端必須知道每個服務實例的 URL。如前篇文章“API 網關”所述,這也是個煩人的問題。客戶端必須使用服務實例發現機制。

開發者社區最近重新認識到了 RESTful API 接口定義語言的價值,於是誕生了包括 RAML 和 Swagger 在內的服務框架。Swagger 這樣的 IDL 允許定義請求和響應消息的格式,而 RAML 允許使用 JSON Schema 這種獨立的規範。對於描述 API,IDL 通常都有工具從接口定義中生成客戶端存根和服務端框架。

ThriftApache Thrift 是一個很有趣的 REST 的替代品,實現了多語言 RPC 客戶端和服務端調用。Thrift 提供了一個 C 風格的 IDL 定義 API。通過 Thrift 編譯器能夠生成客戶端存根和服務端框架。編譯器可以生成多種語言的代碼,包括 C++、Java、Python、PHP、Ruby, Erlang 和 Node.js。

Thrift 接口由一個或多個服務組成,服務定義與 Java 接口類似,是一組強類型方法的集合。Thrift 能夠返回(可能無效)值,也可以被定義爲單向。返回值的方法能夠實現交互的請求/響應模式。客戶端等待響應,可能會拋出異常。單向方法與交互的通知模式相對應。服務端不會發送響應。

Thrift 支持 JSON、二進制和壓縮二進制等多種消息格式。由於解碼更快,二進制比 JSON 更高效;如名稱所稱,壓縮二進制格式可以提供更高級別的壓縮效率;同時 JSON 則易讀。Thrift 也能夠讓你選擇傳輸協議,包括原始 TCP 和 HTTP。原始 TCP 比 HTTP 更高效,然而 HTTP 對於防火牆、瀏覽器和使用者來說更友好。

消息格式

瞭解 HTTP 和 Thrift 後,我們要考慮消息格式的問題。如果使用消息系統或者 REST,就需要選擇消息格式。像 Thrift 這樣的 IPC 機制可能只支持少量消息格式,或許只支持一種格式。無論哪種情況,使用跨語言的消息格式非常重要。即便你現在使用單一語言實現微服務,但很有可能未來需要用到其它語言。

目前有文本和二進制這兩種主要的消息格式。文本格式包括 JSON 和 XML。這種格式的優點在於不僅可讀,而且是自描述的。在 JSON 中,對象的屬性是名稱-值對的集合。與此類似,在 XML 中,屬性則表示爲命名的元素和值。消費者能夠從中選擇感興趣的值同時忽略其它部分。相應地,對消息格式的小幅度修改也能容易地向後兼容。

XML 的文檔結構由 XML schema 定義。隨着時間發展,開發者社區意識到 JSON 也需要一個類似的機制。方法之一是使用 JSON Schema,要麼獨立使用,要麼作爲 Swagger 這類 IDL 的一部分。

文本消息格式的一大缺點是消息會變得冗長,特別是 XML。由於消息是自描述的,所以每個消息都包含屬性和值。另外一個缺點是解析文本的負擔過大。所以,你可能需要考慮使用二進制格式。

二進制的格式也有很多。如果使用的是 Thrift RPC,那可以使用二進制 Thrift。如果選擇消息格式,常用的還包括 Protocol Buffers 和 Apache Avro,二者都提供類型 IDL 來定義消息結構。差異之處在於 Protocol Buffers 使用添加標記的字段(tagged fields),而 Avro 消費者需要了解模式來解析消息。

Martin Kleppmann 的博客文章 對 Thrift、Protocol Buffers 和 Avor 進行了詳細的比較。

總結

微服務必須使用進程間通信機制來交互。在設計服務的通信模式時,你需要考慮幾個問題:服務如何交互,每個服務如何標識 API,如何升級 API,以及如何處理局部失敗。微服務架構異步消息機制和同步請求/響應機制這兩類 IPC 機制可用。在下一篇文章中,我們將會討論微服務架構中的服務發現問題。

文章轉載自:http://blog.daocloud.io/microservices-3/

查看英文原文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章