微服務實戰(三):深入微服務架構的程式間通訊

huyg發表於2017-09-28

【編者的話】這是採用微服務架構建立自己應用系列第三篇文章。第一篇介紹了微服務架構模式,和單體式模式進行了比較,並且討論了使用微服務架構的優缺點。第二篇描述了採用微服務架構應用客戶端之間如何採用API Gateway方式進行通訊。在這篇文章中,我們將討論系統服務之間如何通訊。

簡介

在單體式應用中,各個模組之間的呼叫是通過程式語言級別的方法或者函式來實現的。但是一個基於微服務的分散式應用是執行在多臺機器上的。一般來說,每個服務例項都是一個程式。因此,如下圖所示,服務之間的互動必須通過程式間通訊(IPC)來實現。

Richardson-microservices-part3-monolith-vs-microservices-1024x518.png



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

互動模式

當為某一個服務選擇IPC時,首先需要考慮服務之間如何互動。客戶端和伺服器之間有很多的互動模式,我們可以從兩個維度進行歸類。第一個維度是一對一還是一對多:

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

第二個維度是這些互動式同步還是非同步:

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

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

74.pic_.jpg



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

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

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

• 釋出/ 訂閱模式:客戶端釋出通知訊息,被零個或者多個感興趣的服務消費。

• 釋出/非同步響應模式:客戶端釋出請求訊息,然後等待從感興趣服務發回的響應。

每個服務都是以上這些模式的組合,對某些服務,一個IPC機制就足夠了;而對另外一些服務則需要多種IPC機制組合。下圖展示了在一個叫車服務請求中服務之間是如何通訊的。

Richardson-microservices-part3-taxi-service-1024x609.png



上圖中的服務通訊使用了通知、請求/響應、釋出/訂閱等方式。例如,乘客通過移動端給『行程管理服務』傳送通知,希望申請一次出租服務。『行程管理服務』傳送請求/響應訊息給『乘客服務』以確認乘客賬號是有效的。緊接著建立此次行程,並用釋出/訂閱互動模式通知其他服務,包括定位可用司機的排程服務。

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

定義API

API是服務端和客戶端之間的契約。不管選擇了什麼樣的IPC機制,重要的是使用某種互動式定義語言(IDL)來精確定義一個服務的API。甚至有一些關於使用API first的方法(API-first approach)來定義服務的很好的理由。在開發之前,你需要先定義服務的介面,並與客戶端開發者詳細討論確認。這樣的討論和設計會大幅度提到API的可用度以及滿意度。

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

API的演化

服務端API會不斷變化。在一個單體式應用中經常會直接修改API,然後更新給所有的呼叫者。而在基於微服務架構應用中,這很困難,即使只有一個服務使用這個API,不可能強迫使用者跟服務端保持同步更新。另外,開發者可能會嘗試性的部署新版本的服務,這個時候,新舊服務就會同事執行。你需要知道如何處理這些問題。

你如何處理API變化,這依賴於這些變化有多大。某些改變是微小的,並且可以和之前版本相容。比如,你可能只是為某個請求和響應新增了一個屬性。設計客戶端和服務端時候應該遵循健壯性原理,這很重要。客戶端使用舊版API應該也能和新版本一起工作。服務端仍然提供預設響應值,客戶端忽略此版本不需要的響應。使用IPC機制和訊息格式對於API演化很有幫助。

但是有時候,API需要進行大規模的改動,並且可能與之前版本不相容。因為你不可能強制讓所有的客戶端立即升級,所以支援老版本客戶端的服務還需要再執行一段時間。如果你正在使用基於基於HTTP機制的IPC,例如REST,一種解決方案是把版本號嵌入到URL中。每個服務都可能同時處理多個版本的API。或者,你可以部署多個例項,每個例項負責處理一個版本的請求。

處理部分失敗

在上一篇關於API gateway的文章中,我們瞭解到分散式系統中部分失敗是普遍存在的問題。因為客戶端和服務端是都是獨立的程式,一個服務端有可能因為故障或者維護而停止服務,或者此服務因為過載停止或者反應很慢。

考慮這篇文章中描述的部分失敗的場景。假設推薦服務無法響應請求,那客戶端就會由於等待響應而阻塞,這不僅會給客戶帶來很差的體驗,而且在很多應用中還會佔用很多資源,比如執行緒,以至於到最後由於等待響應被阻塞的客戶端越來越多,執行緒資源被耗費完了。如下圖所示:

Richardson-microservices-part3-threads-blocked-1024x383.png



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

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

• 網路超時:當等待響應時,不要無限期的阻塞,而是採用超時策略。使用超時策略可以確保資源不會無限期的佔用。
• 限制請求的次數:可以為客戶端對某特定服務的請求設定一個訪問上限。如果請求已達上限,就要立刻終止請求服務。
• 斷路器模式(Circuit Breaker Pattern):記錄成功和失敗請求的數量。如果失效率超過一個閾值,觸發斷路器使得後續的請求立刻失敗。如果大量的請求失敗,就可能是這個服務不可用,再發請求也無意義。在一個失效期後,客戶端可以再試,如果成功,關閉此斷路器。
• 提供回滾:當一個請求失敗後可以進行回滾邏輯。例如,返回快取資料或者一個系統預設值。

Netflix Hystrix是一個實現相關模式的開源庫。如果使用JVM,推薦考慮使用Hystrix。而如果使用非JVM環境,你可以使用類似功能的庫。

IPC技術

現在有很多不同的IPC技術。服務之間的通訊可以使用同步的請求/響應模式,比如基於HTTP的REST或者Thrift。另外,也可以選擇非同步的、基於訊息的通訊模式,比如AMQP或者STOMP。除以之外,還有其它的訊息格式供選擇,比如JSON和XML,它們都是可讀的,基於文字的訊息格式。當然,也還有二進位制格式(效率更高)的,比如Avro和Protocol Buffer。接下來我們將會討論非同步的IPC模式和同步的IPC模式,首先來看非同步的。

非同步的,基於訊息通訊

當使用基於非同步交換訊息的程式通訊方式時,一個客戶端通過向服務端傳送訊息提交請求。如果服務端需要回復,則會傳送另外一個獨立的訊息給客戶端。因為通訊是非同步的,客戶端不會因為等待而阻塞,相反,客戶端理所當然的認為響應不會立刻接收到。

一個訊息由頭部(後設資料例如傳送方)和訊息體構成。訊息通過channel傳送,任何數量的生產者都可以傳送訊息到channel,同樣的,任何數量的消費者都可以從渠道中接受資料。有兩類channel,點對點釋出/訂閱。點對點channel會把訊息準確的傳送到某個從channel讀取訊息的消費者,服務端使用點對點來實現之前提到的一對一互動模式;而釋出/訂閱則把訊息投送到所有從channel讀取資料的消費者,服務端使用釋出/訂閱channel來實現上面提到的一對多互動模式。

下圖展示了叫車軟體如何使用釋出/訂閱:

Richardson-microservices-part3-pub-sub-channels-1024x639.png



行程管理服務在釋出-訂閱channel內建立一個行程訊息,並通知排程服務有一個新的行程請求,排程服務發現一個可用的司機然後向釋出-訂閱channel寫入司機建議訊息(Driver Proposed message)來通知其他服務。

有很多訊息系統可以選擇,最好選擇一種支援多程式語言的。一些訊息系統支援標準協議,例如AMQP和STOMP。其他訊息系統則使用獨有的協議,有大量開源訊息系統可選,比如RabbitMQApache KafkaApache ActiveMQNSQ。它們都支援某種形式的訊息和channel,並且都是可靠的、高效能和可擴充套件的;然而,它們的訊息模型完全不同。

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

• 解耦客戶端和服務端:客戶端只需要將訊息傳送到正確的channel。客戶端完全不需要了解具體的服務例項,更不需要一個發現機制來確定服務例項的位置。

• Message Buffering:在一個同步請求/響應協議中,例如HTTP,所有的客戶端和服務端必須在互動期間保持可用。而在訊息模式中,訊息broker將所有寫入channel的訊息按照佇列方式管理,直到被消費者處理。也就是說,線上商店可以接受客戶訂單,即使下單系統很慢或者不可用,只要保持下單訊息進入佇列就好了。

• 彈性客戶端-服務端互動:訊息機制支援以上說的所有互動模式。

• 直接程式間通訊:基於RPC機制,試圖喚醒遠端服務看起來跟喚醒本地服務一樣。然而,因為物理定律和部分失敗可能性,他們實際上非常不同。訊息使得這些不同非常明確,開發者不會出現問題。

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

• 額外的操作複雜性:訊息系統需要單獨安裝、配置和部署。訊息broker(代理)必須高可用,否則系統可靠性將會受到影響。

• 實現基於請求/響應互動模式的複雜性:請求/響應互動模式需要完成額外的工作。每個請求訊息必須包含一個回覆渠道ID和相關ID。服務端傳送一個包含相關ID的響應訊息到channel中,使用相關ID來將響應對應到發出請求的客戶端。也許這個時候,使用一個直接支援請求/響應的IPC機制會更容易些。

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

同步的,基於請求/響應的IPC

當使用一個同步的,基於請求/響應的IPC機制,客戶端向服務端傳送一個請求,服務端處理請求,返回響應。一些客戶端會由於等待服務端響應而被阻塞,而另外一些客戶端也可能使用非同步的、基於事件驅動的客戶端程式碼(Future或者Rx Observable的封裝)。然而,不像使用訊息機制,客戶端需要響應及時返回。這個模式中有很多可選的協議,但最常見的兩個協議是REST和Thrift。首先我們來看下REST。

REST

現在很流行使用RESTful風格的API。REST是基於HTTP協議的。另外,一個需要理解的比較重要的概念是,REST是一個資源,一般代表一個業務物件,比如一個客戶或者一個產品,或者一組商業物件。REST使用HTTP語法協議來修改資源,一般通過URL來實現。舉個例子,GET請求返回一個資源的簡單資訊,響應格式通常是XML或者JSON物件格式。POST請求會建立一個新資源,PUT請求更新一個資源。這裡引用下REST之父Roy Fielding說的:

當需要一個整體的、重視模組互動可擴充套件性、介面概括性、元件部署獨立性和減小延遲、提供安全性和封裝性的系統時,REST可以提供這樣一組滿足需求的架構。

下圖展示了叫車軟體是如何使用REST的。

Richardson-microservices-part3-rest-1024x397.png



乘客通過移動端向行程管理服務的/trips資源提交了一個POST請求。行程管理服務收到請求之後,會傳送一個GET請求到乘客管理服務以獲取乘客資訊。當確認乘客資訊之後,緊接著會建立一個行程,並向移動端返回201(譯者注:狀態碼)響應。

很多開發者都表示他們基於HTTP的API是RESTful的。但是,如同Fielding在他的部落格中所說,這些API可能並不都是RESTful的。Leonard Richardson為REST定義了一個成熟度模型,具體包含以下4個層次(摘自IBM):

  • 第一個層次(Level 0)的 Web 服務只是使用 HTTP 作為傳輸方式,實際上只是遠端方法呼叫(RPC)的一種具體形式。SOAP 和 XML-RPC 都屬於此類。
  • 第二個層次(Level 1)的 Web 服務引入了資源的概念。每個資源有對應的識別符號和表達。
  • 第三個層次(Level 2)的 Web 服務使用不同的 HTTP 方法來進行不同的操作,並且使用 HTTP 狀態碼來表示不同的結果。如 HTTP GET 方法來獲取資源,HTTP DELETE 方法來刪除資源。
  • 第四個層次(Level 3)的 Web 服務使用 HATEOAS。在資源的表達中包含了連結資訊。客戶端可以根據連結來發現可以執行的動作。


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

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

不足之處包括:

• 只支援請求/響應模式互動。可以使用HTTP通知,但是服務端必須一直髮送HTTP響應才行。
• 因為客戶端和服務端直接通訊(沒有代理或者buffer機制),在互動期間必須都線上。
• 客戶端必須知道每個服務例項的URL。如之前那篇關於API Gateway的文章所述,這也是個煩人的問題。客戶端必須使用服務例項發現機制。

開發者社群最近重新發現了RESTful API介面定義語言的價值。於是就有了一些RESTful風格的服務框架,包括RAMLSwagger。一些IDL,例如Swagger允許定義請求和響應訊息的格式。其它的,例如RAML,需要使用另外的標識,例如JSON Schema。對於描述API,IDL一般都有工具來定義客戶端和服務端骨架介面。

Thrift

Apache Thrift是一個很有趣的REST的替代品。它是Facebook實現的一種高效的、支援多種程式語言的遠端服務呼叫的框架。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,就可以選擇訊息格式。其它的IPC機制,例如Thrift可能只支援部分訊息格式,也許只有一種。無論哪種方式,我們必須使用一個跨語言的訊息格式,這非常重要。因為指不定哪天你會使用其它語言。

有兩類訊息格式:文字和二進位制。文字格式的例子包括JSON和XML。這種格式的優點在於不僅可讀,而且是自描述的。在JSON中,一個物件就是一組鍵值對。類似的,在XML中,屬性是由名字和值構成。消費者可以從中選擇感興趣的元素而忽略其它部分。同時,小幅度的格式修改可以很容器向後相容。

XML文件結構是由XML schema定義的。隨著時間發展,開發者社群意識到JSON也需要一個類似的機制。一個選擇是使用JSON Schema,要麼是獨立的,要麼是例如Swagger的IDL。

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

二進位制的格式也有很多。如果使用的是Thrift RPC,那可以使用二進位制Thrift。如果選擇訊息格式,常用的還包括Protocol BuffersApache Avro。它們都提供典型的IDL來定義訊息架構。一個不同點在於Protocol Buffers使用的是加標記(tag)的欄位,而Avro消費者需要知道模式(schema)來解析訊息。因此,使用前者,API更容易演進。這篇部落格很好的比較了Thrift、Protocol Buffers、Avro三者的區別。

總結

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

原文連結:Building Microservices: Inter-Process Communication in a Microservices Architecture(翻譯:楊峰 校對:李穎傑)

相關文章