深入微服務架構內部進程間通信

jopen 10年前發布 | 66K 次閱讀 微服務 WEB服務/RPC/SOA


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

簡介

在單體式應用中,各個模塊之間通過編程語言級別的方法或者函數互相喚醒。相對的,一個基于微服務的應用是分布在多臺設備上的應用,每個服務實例一般都是一個進程,因此,如下圖所示,服務必須通過進程間通信(IPC)機制來交互。

后面我們將會看看特定的IPC技術,但是首先要看看各種設計上中的問題。

互操作模式

當為一個服務選擇了一種IPC機制,首先需要考慮服務之間如何交互。C/S模式有很多交互模式,可以從兩個維度進行歸類,第一個維度是交互式一對一還是一對多:

? 一對一 – 每個客戶端請求有一個服務實例來響應。

? 一對多– 每個客戶端請求有多個服務實例來響應

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

? 同步模式:客戶端請求需要服務端即時響應,甚至可能由于等待而阻塞

? 異步模式:客戶端請求不會阻塞進程,服務端的響應不必是即時的

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

一對一 一對多

同步 請求/響應 —

異步 請求 發布/訂閱

異步響應 發布/異步響應

有如下幾種一對一的交互模式

? 請求/響應: 一個客戶端向服務器端發起請求,等待響應。客戶端期望此響應即時到達。在一個基于線程的應用中,等待過程可能造成線程阻塞。

? 通知(也就是常說的 單向請求):一個客戶端請求發到服務端,但是并不期望服務端響應。

? 請求/異步響應:客戶端請求發到服務端,響應異步。客戶端不會阻塞,而且被設計成默認響應不會立刻到達。

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

? 發布/ 訂閱模式:客戶端發布通知消息,客戶被感興趣的服務接收到。

? 發布/異步模式:客戶端發布請求消息,然后等待從感興趣服務發回的響應。

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

這個服務模式是通知,請求/響應和發布/訂閱模式的組合。例如,乘客移動應用端給西城廣利服務發送一個通知,請求一次出租服務。行程管理服務發送請求/響應交互模式給乘客服務確認乘客賬號是有效的,然后創建此次行程,用發布/訂閱交互模式通知其他服務,包括定位可用司機的調度服務。

我們了解了交互模式,下面我們來看看如何定義APIs.

定義APIs

API是服務端和客戶端之間的合約。不管選擇了什么IPC機制,重要的是使用某種交互式語言(IDL)來精確定義一個服務的API。甚至有一些關于使用API第一位的方法(API-first approach)來定義服務的很好的爭論。一般都是先定義接口,然后與客戶端開發者討論之后,才開始服務端開發的。這種方式開發出的服務端,更加容易滿足客戶端請求。

在本文后面將會看到,API定義本質上依賴于IPC機制的選擇。如果使用消息機制,API則由消息頻道和消息類型構成;如果選擇使用HTTP機制,API則由URL和請求、響應格式構成。后面將會詳細描述IDLs。

APIs的演化

服務端API會不斷變化。在一個單體式應用中經常會直接修改API,然后更新給所有的調用者。而在基于微服務架構應用中,這很困難,即使只有一個服務使用這個API,不可能強迫用戶跟服務端保持更新同步。而且,可能會增量式部署新版服務,從而新舊服務同時在運行,必須要考慮如何應對這些場景。

如何處置API改變依賴于改變大小。某些改變是微小的而且和以前版本兼容。設計客戶端和服務端時候應該遵循健壯性原理,這點很關鍵。客戶端使用舊版API應該也能和新版本一起工作。服務端仍然提供默認響應值,客戶端忽略此版本不需要的響應。使用IPC機制和消息格式對于API演化很重要。

但是有時候,API需要有些主版本升級,與以前版本不兼容。因為不可能強制所有客戶端立刻都升級,因此支持老版本客戶端的服務還需要在運行一段時間。如何使用基于HTTP機制的IPC,例如REST,可以采用的一個方法是把版本號嵌入到URL中。每個服務都可以同時處置多個版本,或者,可以部署不同的實例,每個處置不同的版本請求。

處置部分失效

再上一篇關于 API gateway的文章中,在一個分布式系統中部分失效是普片存在的問題。因為客戶端和服務端是獨立進程,一個服務端有可能因為故障或者維護而停止服務,或者此服務因為過載停止或者反應很慢。

考慮一個場景,如Product details scenario 文中描述的。假設推薦服務沒有響應了,最幼稚的客戶端實現會由于等待響應而阻塞,這不僅給客戶帶來很差的體驗,而且在很多應用中還會占用很多資源,比如一個線程,以至于到最后由于等待響應被阻塞的客戶端越來越多,線程資源被耗費完了。如下圖所示:

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

一個很好地方法可以參考 Netflix 案例,預防部分失效的策略包括:

? 網絡超時:當等待響應時,不要無限期的阻塞,而是采用超時策略。使用超時確保資源不會無限期的占用。

? 限制請求的次數:對客戶端對某特定服務設置一個訪問請求上限。如果限制快到了,而且還要申請服務,就要立刻終止服務請求。

? Circuit breaker pattern:跟蹤成功和失敗請求的數量。如果失效率超過一個閾值,觸發斷路器使得后續的請求立刻失敗。如果大量的請求失敗,就可能是這個服務不可用,再發請求無意義。在一個失效期后,客戶端可以再試,如果成功,關閉此斷路器。

? 提供回滾:當一個請求失敗后可以進行回滾邏輯。例如,返回緩存數據或者一個系統默認值。

Netflix Hystrix 是一個實現上述和其他模式的開源庫。如果使用JVM,絕對應該考慮采用 Hystrix。而如果使用非JVM環境,則需要考慮同樣的庫實現。

IPC技術

有許多可用的IPC技術。服務端可以采用同步申請/響應通信機制,例如基于HTTP的REST或者Thrift;可選的,也可以采用異步的,基于消息的通信機制,例如AMQP或者STOMP;除此之外還有其它很多不同的消息格式。服務端可以使用易讀的,基于字符的格式,例如JSON或者XML。除此之外,也可以采用二進制格式(更加有效),例如Avro或者Protocal Buffers。后面我們將討論同步IPC機制,先來看看異步IPC機制。

異步的,基于消息通信

當使用基于異步交換消息的進程通信方式時,一個客戶端通過向服務端發送消息提交請求。如果服務端需要回復,則發還另外一個獨立的消息給客戶端。因為通信是異步的,客戶端不會因為等待而阻塞,相反,客戶端可以理解響應不會立刻接收到。

一個message (消息)由頭(元數據例如發送方)和一個消息體構成。消息通過 channels (渠道)發送,任何數量的生產者都可以發送消息到渠道,同樣的,任何數量的消費者都可以從渠道中接受數據。有兩類渠道,point to point (點對點)和 publish subscribe (發布-訂閱)。一個點對點渠道會把消息準確的發送到某個從渠道讀取消息的消費者,服務端使用點對點渠道來實現之前提到的一對一交互模式;而發布-訂閱渠道則把消息投送到所有從渠道讀取數據的消費者,服務端使用發布-訂閱渠道來實現下面提到的一對多交互模式。

下圖展示了打車軟件如何使用發布-訂閱渠道:

行程管理服務在發布-訂閱渠道內創建一個新行程消息,來通知調度服務有一個新的行程請求,調度服務發現一個可用的司機然后想一個發布-訂閱渠道寫入司機建議消息(Driver Proposed message)來通知其他服務。

有很多消息系統可以選擇,最好選擇一種支持多編程語言的;一些消息系統支持標準協議,例如AMQP和STOMP。其他消息系統使用獨有的協議,有大量開源消息系統可選,包括RabbitMQ, Apache Kafka,Apache ActiveMQ, 和 NSQ。他們都支持某種形式的消息和渠道,都是可靠的,高性能和可擴展的;然而,他們他們在消息模式(messaging model)方面確實完全不同的。

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

? 將客戶端和服務端解耦:客戶端發送請求只需要將消息發送到正確的渠道。客戶端完全不需要知道服務實例,不需要一個發現機制來決定服務實例的位置。

? Message Buffering:在一個同步請求/響應協議中,例如HTTP,所有的客戶單和服務端必須在交互期間保持可用。相比較的,消息代理將所有寫入渠道的消息按照隊列方式管理,知道被消費者處理。也就意味著,例如,在線商店可以接受客戶訂單,即使下單系統很慢或者不可用,只要保持下單消息如隊列就好了。

? 彈性客戶端-服務端交互:消息機制支持以上說的所有交互模式。

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

然而,消息機制還是有缺點:

? 額外的操作復雜性:消息系統式額外被安裝,配置和部署的模塊。消息代理必須高可用,否則系統可靠性將會受到影響。

? 實現基于請求/響應交互模式的復雜性:請求/響應交互模式需要完成額外的工作。每個請求消息必須包含一個回復渠道ID和相關ID。服務端發送一個包含相關 ID的響應消息到渠道中,使用相關ID來將響應對應到發出請求的客戶端。使用一個直接支持請求/響應的IPC機制更容易些。

下面我們來看看如何使用基于消息的IPC,我們檢查一下基于請求/響應的IPC。

同步,請求/響應IPC

當使用一個基于同步,請求/響應的IPC機制,客戶端向服務端發送一個請求,服務端處理請求,發還一個響應。很多客戶端,由于等待服務端反饋而被阻塞,另外一些客戶端可能使用異步,事件驅動的客戶單代碼(用Futures或者Rx Observables封裝)。然而,不像使用消息機制,客戶端認為響應會很及時。也有很多可選的協議,兩個最常見的協議是REST和Thrift。

我們先看REST:

REST

今天用RESTful 風格開發應用是很時尚的事情。REST是一個(基本上全部)使用HTTP的IPC機制,一個關鍵概念,REST是一個資源,一般代表這一個商業對象,比如一個客戶或者一個產品,或者一組商業對象。REST使用HTTP語言來修改資源,一般通過URL來實現。舉個例子,GET請求返回一個資源的代表,一般采用XML文檔或者JSON對象格式。POST請求創建一個新資源,PUT請求更新一個資源。

Roy Fielding, REST之父說:

“當需要提供一個整體的,強調模塊交互可擴展性,接口概括性,組件部署獨立性和減小延遲,提供安全性和封裝性時,REST提供了一組滿足需求的架構”

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

下圖展示了打車軟件如何使用REST的一種方式。

乘客移動端請求一個行程,提交一個POST請求到行程管理服務的/trips資源,此服務處置這條請求,發送一個GET請求到乘客管理服務或者此乘客信息,確認乘客創建行程的權限后,行程管理服務創建此行程,返回一個201響應給乘客移動端。

許多開發者聲稱他們的基于HTTP的應用是RESTful的,如同Fielding在博客中描述的:blog post, 其實并不都是。Leonard Richardson 定義了,一個成熟RESTmaturity model for REST 模塊包括如下構成元素:

? Level0:0級API客戶端通過創建來激活服務

? HTTP POST請求到唯一URL服務點。每個請求都標注了動作,對象和參數。

? Level1:1級API支持資源的概念。對一個資源做某個動作,客戶端發出包括動作和參數的POST請求

? Leve2:2級API使用HTTP動作:Get接收,POST創建,PUT更新。請求參數和數據體,標識動作參數。使得服務端能夠整合web架構下資源,例如為GET使用緩存資源。

? Level3:3級API是基于HATEOAS(Hypertext As The Engine Of Application State)定律的。關鍵點是GET請求返回的資源代表中包含允許動作的鏈接。例如,一個客戶端,使用GET返回中的鏈接,可以取消訂單。 Benefits of HATEOAS 包括不用在客戶端使用寫死的URL。另外一個好處是客戶不需要猜測針對某資源可以做什么操作,因為這些操作都會被以鏈接的方式返回。

使用基于HTTP的協議額還有很多其他好處:

? HTTP簡單常見。

? 可是使用瀏覽器或者簡單命令行就可以測試HTTP API

? 內置支持請求/響應模式通信

? HTTP是對防火墻友好的

? 不需要中間代理,簡化了系統架構

不足之處包括:

? 只支持請求/響應模式交互。可以使用HTTP通知,但是服務端必須一致發送HTTP響應才行。

? 因為客戶端和服務端直接通信(沒有代理或者buffer機制),在交互期間必須都在線

? 客戶端必須知道每個服務實例的URL。如previous article about the API gateway所述,還有一個不致命的問題,客戶端必須使用服務實例發現機制。

開發者社區最近重新發現了RESTful APIs接口定義語言的價值。有一些選擇,包括RAML 和Swagger. 一些IDL,例如Swagger允許定義請求和響應消息的格式。其他的,例如RAML,需要使用另外的標識,例如JSON Schema. 對于描述APIs,IDLs一般都有工具來定義客戶端票根和服務端骨架接口。

Thrift

Apache Thrift 是一個很有趣的REST替代。它是一個跨語言的RPC客戶端和服務端框架。Thrift提供了一個C風格的IDL定義APIs。使用Thrift編譯器創建客戶端票根和服務端骨架。編譯器可以產生多種語言代碼,包括C++, Java, Python, PHP, Ruby, Erlang, and 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 Buffers 和 Apache Avro. 它們都提供典型的IDL來定義消息架構。一個不同點在于Protocol Buffers使用tagged域,而Avro消費者需要知道剛要來解讀消息。因此,用前者,API更容易演進。這篇博客blog post比較了 Thrift, Protocol Buffers, 和Avro.

總結

微服務必須使用進程間通信機制。當設計服務端如何通信是,必須考慮多種可能的問題:服務如何交互,每個服務如何標識API,如何演進API,以及如何處置部分失效。微服務架構有兩類IPC機制可選,異步消息機制和同步請求/響應機制。下一篇文章中,我們將會討論微服務架構中服務發現問題。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!