詳解微服務技術中進程間通信

在單體應用中,一個組件調用其它組組件時,是通過語言級的方法或者函數調用,而一個基於微服務的應用是運行於多個服務器上的分佈式系統,每個服務實例是一個典型的進程。所以,如下圖顯示的,服務必須通過內部進程交互機制(IPC)進行交互。

交互風格

在爲一個服務選擇 IPC 的時候,首先考慮一下這些服務是如何交互的是很有用處的。有多種 client/server 的交互風格,它們可以通過兩個維度分類,第一種維度是交互是一對一,還是一對多的:

下表顯示出各種交互風格:

DNEPh9

有如下幾種一對一的交互形式:

請求 / 響應:客戶端發送一個請求給一個服務,並且等待響應結果,客戶端期望結果能快速的返回,在一個基於線程的應用中,發送請求的線程甚至可能在等待的時候被阻塞。

有如下幾種一對多的交互形式:

每個服務一般都使用這幾種交互風格的組合風格。對於一些服務來說,單一的 IPC 機制就足夠了,而其它的服務可能需要組合使用若干種 IPC 機制。下面的圖給出當客戶請求行程時,在一個打車應用可能出現的一些交互。

這些服務使用了通知,請求 / 響應,發佈 / 訂閱的交互方式。比如說,一個乘客的智能手機向行程管理服務發送了一個上車請求,行程管理服務通過請求 / 響應方式向乘客服務確認乘客的賬戶是否是活躍賬戶,行程管理服務於是創建一個行程訂單,並且用發佈 / 訂閱方式通知其它的服務,包括一個分發服務,用以定位空閒的司機。

定義 API

服務的 API 是服務與它所有的客戶端之間的一種契約,不管選用何種 IPC 機制,使用一些接口定義語言 (IDL),對於精確定義服務 API 是很重要的,甚至已經有一些關於使用 API-first approach 來定義服務的好的討論。

開發一個微服務從書寫接口定義以及與客戶端開發人員一起 review 這些接口定義開始,在不斷的對這些 API 定義進行迭代,最終纔算是實現了一個微服務。這種基於前端的設計方式,增加了構建出符合客戶端需求的機會。

在文章的後面你會看到,API 定義的特性依賴與你使用的 IPC 機制,如果你使用消息機制,API 就會涉及到消息通道和消息類型;如果你選用 HTTP 方式,那麼 API 就會包含一些 URL 和請求 / 響應的格式,之後我們會詳細的介紹 IDL。

API 的演進

一個服務的 API 會隨着時間而經常變化。在單體應用中,通常是很直接的修改 API,再更新所有的調用之處,但在基於微服務的應用中,情況要困難得多,甚至你 API 的所有消費者是同一個應用中的其它服務。你通常不能強迫所有的客戶端步調一致的升級它們的服務。而且你可能會大量的開發服務的新版本,於是新舊版本的服務會同時運行,制定一個處理這種問題的戰略原則顯得很重要。

如何處理一個 API 的變化,取決於這種變化的多少。有的變化很少,可以向後兼容之前的版本,比如,你可能只是在請求或者響應格式中增加一些屬性。設計出具有魯棒原則的客戶端和服務是有意義的,那些使用更舊的 API 的客戶端應該能夠繼續和新版本的服務工作得很好,服務會給請求中沒有的屬性提供默認值,客戶端會忽略那些響應中額外的屬性。使用 IPC 機制和消息格式是重要的,讓你能輕易的演進 API。

有時候,你不得不對 API 做一些主要的、不兼容的改動。既然不能強制客戶端立刻升級,那這個服務必須能夠支持舊版本的 API 一定時期。如果你用的是基於 HTTP 的機制,如 REST,一個好的辦法是在 API 的 URL 中嵌入版本號。每個服務實例應該可以同時處理不同版本的 API 請求,或者是部署不同的服務實例來處理不同的 API 版本。

處理部分失敗

在之前關於 API 網關的文章中曾經提到,在分佈式系統中,總會存在部分失敗的風險,既然客戶端和服務是分開的進程,一個服務可能不能對一個客戶端請求及時的返回結果,服務也可能因爲錯誤或者是維護停止了,亦或是因爲過載而對請求響應緩慢。

比如說,如上篇文章中提到的那個產品詳頁的場景,試想一下如果那個推薦服務失去響應了,客戶端的一個本地實現就可能在無限的等待響應中被阻塞了,這不僅會帶來劣質的用戶體驗,而且在很多應用中,這會消耗寶貴的資源,如一個線程,最終運行時環境會線程耗盡,變成無法響應,正如下圖所示。

爲了避免這種問題,把你的服務設計成能處理部分失敗是很有必要的。

Netfix 給我們提出了一個可以遵循的好辦法,其中處理部分失敗的原則包括:

Netfix Hystrix 是這些模式的一種開源實現,如果你正在使用 JVM,你肯定會考慮使用 Hystrix 的,如果你運行的是一個非 JVM 的環境,同樣需要考慮使用一個類似的庫。

IPC 技術

有許多 IPC 技術可供選擇,如同步的請求 / 響應機制,這裏面有基於 HTTP 方式的 REST 和 Thrift,另外有基於消息的異步通信機制,如 AMQP 和 STOMP。其中消息的格式也是多種多樣的,有一些是人可讀的,比如 JSON 和 XML,有些是二進制格式的(這種更高效),如 Avro 和緩存協議。稍後我們介紹同步的 IPC 機制,但在這之前,先討論異步的 IPC 機制。

異步 (基於消息的通信)

當使用消息時,進程間通過異步的交換消息來通信。客戶端通過向服務發送消息來發送請求,如果期望服務返回應答,那麼它發送回一個獨立的消息給客戶端。由於通信是異步的,客戶端不會阻塞在等待返回結果上,客戶端應該是基於不會立刻收到返回結果的假設來實現。

消息包含消息頭(如發送者這樣的元數據)和消息體,各種消息在通道上交換,任意數量的生產者都能往通道上發送消息,同樣,任意數量的消費者也能從這個通道接收消息。有兩種類型的通道:點對點通道和發佈 / 訂閱通道。點對點的通道只給連接到這個通道上的衆多消費者中的一個發送消息,服務使用這種通道往往是採用前面提到的一對一的交互風格。發佈 / 訂閱這種通道,是給連接到它之上的所有消費者發送消息,這種通道往往被一對多風格的服務採用。

下圖描述的是,在打車應用中,發佈 / 訂閱的通道是如何使用的

行程管理服務向發佈 / 訂閱通道發送一個行程創建的消息,以此告訴那些對此感興趣的服務(比如說分發器服務),一個新行程創建了。分發器服務找到一個可用的司機,將一個需要提名司機的消息寫入發佈 / 訂閱通道,這樣其它的服務就能得到這個通知。

有許多消息系統可供選擇,你應該選擇那些能支持多種開發語言的。一些消息系統支持 AMQP 和 STOMP 這些標準協議,其它的系統是一些專有而且文檔化的協議。現在有不少開源的消息系統,其中包括 RabbitMQ,Apache Kafka,Apache ActiveMQ 和 NSQ。總體上看,他們都支持消息格式和通道,都是可靠的、高性能的和可擴展的,但它們在消息模型細節方面有着巨大的差異。

使用消息有諸多優點:

當然,消息機制也有缺點:

現在我們已經討論完了基於消息的 IPC,接下來探討一下基於請求 / 響應的 IPC

同步的請求 / 響應 IPC

在同步的、基於請求 / 響應的 IPC 機制中,客戶端向服務發送一個請求,服務處理這個請求,並將響應發回。在許多客戶端的實現中,發送請求的線程會在等待響應的時候阻塞。

而另一些客戶端的實現,可能使用異步的、事件驅動的方式,請求相關的代碼會被封裝在 Futrues 或者 Rx Observables 這樣的庫中。和前面介紹的消息機制不同,在這種 IPC 裏客戶端是假設響應會及時返回。有很多協議可供選擇,其中有兩種很流行:REST 和 Thrift。我們先來看看 REST

REST

目前,使用 RESTful 風格來開發 API 是很流行的做法,REST 是使用 HTTP 的 IPC 機制,REST 的一個關鍵概念是資源,資源代表一個業務對象,比如說一個客戶,一個產品,或者是一些業務對象的集合。REST 使用 HTTP 的方法來操作資源,通過 URL 來引用資源。比如,GET 請求會返回一個資源的信息,返回結果用 XML 文檔或者 JSON 對象來表示,POST 請求創建一個資源,PUT 請求是更新一個資源。REST 的創建者 Roy Fielding 的描述如下:

“REST 提供一個架構約束的集合,當被整體應用時,強調組件交互的擴展性、接口的普遍性,組件的獨立部署,減少交互延時的中間組件,增強的安全性以及對遺留系統的封裝。”

下圖展示了打車應用中使用 REST 的一個場景。

乘客的智能手機向行程管理服務發送創建行程的請求,這個時候一個 POST 請求發送到服務端,請求創建一個 / trips 資源,行程管理服務隨後發送一個 GET 請求到乘客管理服務,來獲取乘客的信息,在確認了這個乘客是一個授權過可以創建行程的用戶之後,行程管理服務正式的創建出行程,並且返回一個 201 結果給智能手機。

很多開發者都聲稱他們的 HTTP API 都是 RESTful 的,但如 Fielding 在他的這篇博客裏描述的,其實他們不一定都是。Leonard Richardson 給出了一個很有用的 REST 成熟度模型,包含如下一些級別:

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

使用 HTTP 也有缺點:

開發者社區最近發現了接口定義語言對 RESTful API 的新價值,這方面有一些選擇,包括 RAML 和 Swagger。一些諸如 Swagger 的 IDL 允許定義出請求和響應消息的格式,其它一些諸如 RAML 的 IDL 則要求使用獨立的規範,如 JSON schema。在描述 API 的同時,IDL 一般也有工具來給接口定義生成客戶端樁和服務端骨架。

Thrift

Apache Thrift 是 REST 的一種有趣的替代方案,它是開發跨語言 RPC 客戶端和服務端的框架,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 是對防火牆、瀏覽器和人友好的。

消息格式

前面已經討論過 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 使用標籤域,而 Avro,它的消費者在翻譯消息前,需要提前知道消息的 schema。這篇博客完美解釋了 Thrift, Protocol Buffers 和 Avro 的異同之處。

總結

微服務必須使用一種進程間通信機制,當設計你的服務如何通信時,需要考慮各種問題:服務如何交互,如何爲每個服務設計 API,如何演進 API,以及如何處理部分失敗問題。有兩種微服務可用的 IPC 機制,異步的消息機制和同步的請求 / 響應機制。在這一系列文章的下一篇文章中,我們會研究在微服務架構中的服務發現問題。

英文原文:

https://www.nginx.com/blog/building-microservices-inter-process-communication/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/wJpC5i0NoeuZNASb4ha4xA