3-構建微服務-微服務架構下的進程間通信

本文出自Nginx官網,是微服務介紹系列文章的第三篇。原文地址:https://www.nginx.com/blog/building-microservices-inter-process-communication/

1.介紹

在第一篇文章中我們比較了微服務架構的應用和單體應用的差異,討論了微服務架構的優點與缺點;第二篇文章中討論了客戶端如何使用API網關跟微服務通信;在本篇文章中我們討論微服務之間的通信機制;在第四篇文章中我們將討論服務發現機制。

在單體應用中,應用的各個模塊使用語言級的方法調用通信;微服務架構的應用是分佈式系統,服務運行在多臺物理服務器上,每個服務都是一個進程,需要使用進程間通信機制,詳細見下圖:


接下來,我們先討論進程間通信需要考慮的設計問題,再詳細討論進程間通信的主流技術。

2.進程間通信的方式

選擇進程間通信機制,首先要確定微服務的交互方式,交互方式有很多種,可以從兩個維度來分類,一個維度是按照服務端是一個還是多個來分類:

一對一:客戶端的請求只被一個特定的服務端實例處理。

一對多:請求被多個服務端實例處理。

另外一個維度是按照交互是異步還是同步進行分類:

同步調用:客戶端希望立即獲得響應,甚至會在等待響應期間阻塞。

異步調用:響應不一定立即發出,客戶端在等待響應的過程中不會阻塞。

下面的表格展示了不同的進程間通信方式:


         一對一交互分爲以下幾類:

         請求/響應模式:客戶端向服務器發送請求,等待響應;客戶端期望響應能及時到達,在等待過程中客戶端可能阻塞。

         通知模式(也叫單向請求):客戶端向服務器發送請求,不期望有響應。

         請求/異步響應模式:客戶端向服務器發送請求,等待響應;客戶端認定響應不會立即達到,客戶端不會阻塞。

         一對多交互分爲以下幾類:

         發佈/訂閱模式:客戶端發佈通知消息,零個或者多個服務消費該消息。

         發佈/異步響應模式:客戶端發佈請求消息,在一定時間內等待服務端響應。

         服務一般都會組合應用以上多種交互方式。對於一些服務而言,單一的進程間通信機制就能滿足需求;對於另外一些服務,可能需要組合多種通信機制才能滿足需求。以下圖示展示了當用戶發佈行程時,叫車應用的服務如何交互:


         圖中應用了通知模式、請求/響應模式、發佈/訂閱模式等多種方式:用戶使用智能手機向行程管理服務發佈新行程請求通知;行程管理服務使用請求/響應模式調用乘客管理服務來驗證用戶賬號;驗證通過後,行程管理服務創建行程並使用發佈/訂閱模式將新的行程信息通知到分發服務(基於地理位置尋找潛在司機)。接下來,我們討論服務接口的定義。

3. 接口定義

服務接口是服務和客戶端之間的契約,無論你使用哪種進程間通信機制,使用某一種接口定義語言精確定義接口都非常重要。使用接口優先的方法開發服務是個聰明的選擇,通過跟客戶端開發人員討論接口定義,對接口進行迭代之後再開始服務的開發工作會讓你的服務更有可能滿足需求。

服務接口的定義取決於你使用哪一種進程間通信機制:如果使用消息機制,接口定義可能包含消息通道和消息類型;如果使用HTTP機制,接口定義可能包含URL以及請求和響應的消息格式。

4.接口的變化

服務接口會隨着時間不斷變化,當接口變化時,在單體應用中,直接更新接口和所有調用者就可以;在微服務架構下,即使所有調用者都是應用內的其他服務,更新接口和所有調用者也是件困難的事情。你無法強制所有客戶端和服務端同步升級,因此,服務的新舊版本往往同時存在,你需要採取策略應對這種情況。

如何應對接口變化取決於變化的大小。有些變化比較小,能兼容之前的版本。比如說,請求和響應中可能會增加一些屬性。這種情況下,客戶端和服務端遵從健壯性設計就很有用,能保證實現舊接口的客戶端在新接口下依然能工作:服務端爲不存在的屬性設置默認值,客戶端忽略新增加的屬性。考慮到系統健壯性,選用合適的進程間通信機制、使用支持變化的消息格式很重要。

有時候接口必須進行較大的變化,不能向前兼容。既然無法強制客戶端和服務端同時更新,在一定時期內就需要保證新舊接口都能使用。如果你使用基於HTTP的通信機制(REST),一種處理方式是在請求URL中加上服務版本號,這樣,每個服務實例就能同時處理多個版本的請求;另外一種方式是部署多個版本的服務實例,每一種服務實例處理特定版本的請求。

5.處理部分失敗

就像在第二篇文章中描述的,分佈式系統中存在部分失敗的風險。既然客戶端和服務端都是獨立的進程,服務端就可能無法及時響應。服務端有可能因爲維護升級而關閉,也有可能由於過載而導致響應緩慢。

還是以商品詳情的頁面爲例,假設智能推薦的服務無響應,簡單的客戶端實現可能會一直阻塞等待響應,這不但會帶來糟糕的用戶體驗,還會消耗寶貴的線程資源,最終可能會消耗掉運行環境的所有線程資源使得整個應用宕掉。以下是線程資源消耗光的圖示:


爲了阻止此類問題發生,必須要針對部分失敗進行相應設計。Netflix找到了好辦法來應對部分失敗,它的策略主要包括:

         網絡超時:等待響應時用超時機制替代阻塞機制,避免資源的無期限佔用。

         限定未完成的請求數:限定客戶端對同一個服務的最大未完成請求數;如果達到最大值,後續的請求會立即返回失敗。

         斷路器模式:跟蹤成功和失敗的請求數,如果請求失敗的比率超過閾值,斷路器會打開,後續的請求會立即返回失敗。如果大量的請求都失敗,預示着服務不可用,因此後續的服務請求無意義;經過一個超時週期後,客戶端會再次嘗試訪問服務,請求如果成功,則關閉斷路器。

         提供回調:當請求失敗時,執行回調邏輯,返回緩存數據或默認數據。

         NetflixHystrix是一個開源代碼庫,它實現了以上策略,如果你的應用使用JVM,應該考慮使用Hystrix;如果沒有JVM,也需要考慮使用類似的代碼庫。

6.進程間通信技術

         有許多進程間通信的技術可供選擇。可以使用像REST或者Thrift之類的請求/響應同步模式,也可以使用類似AMQP或者STOMP之類基於消息的異步模式。消息格式也有很多種,有容易理解的基於文本的JSON和XML;也有更高效的二進制的Avro和Protocol Buffers。我們先討論異步機制再討論同步機制。

異步的基於消息的通信機制

         當使用基於消息的異步通信機制時,客戶端通過發送消息的形式發送請求,如果服務端需要響應,也發送消息給客戶端。既然是異步通信,客戶端就不用阻塞等待響應。實際上,在異步機制下,客戶端認定響應不會立即到達。

         消息包括消息頭和消息體,消息在通道中傳遞,可以由任意多個生產者向通道發送消息;類似的也可以有任意多個消費者從通道上接收消息。有兩種類型的通道,一種是點對點,一種是發佈/訂閱。點對點模式下,通道將消息分發給某個特定的消費者,之前描述的一對一交互可以使用該模式;發佈/訂閱模式下,通道將消息分發給所有監聽該通道的消費者,一對多交互可以使用該模式。

         下面圖示展示了叫車應用中如何使用發佈/訂閱通道:


         行程管理服務通過向通道寫新的行程消息通知其他相關服務;分發服務定位到可用的駕駛員,通過向發佈/訂閱通道寫一條可用司機的消息通知其他相關服務。

         有許多種消息系統可以選擇,爲了以後擴展方便,要優先選擇支持多語言的消息系統。一些消息系統支持標準的協議,像AMQP和STOMP;另外一些使用專用協議。有很多開源的消息系統,像RabbitMQ、Apache Kafka、Apache ActiveMQ、NSQ等。從更高層次上來說,這些消息系統都支持一些消息格式和通道,都致力於實現可靠的、高性能的、可伸縮的消息傳遞,然而在消息模型的實現細節上還是有很大差異。

         使用消息機制通信有很多好處:

         實現客戶端和服務端解耦:客戶端通過向消息中間件的通道發送消息實現服務請求,完全不用考慮服務端,不需要使用服務發現機制確定服務端地址。

         消息緩存:同步的請求/響應協議(如REST),在信息交換期間,服務端和客戶端必須都可用;在基於消息的異步通信機制下,消息中間件會在隊列中緩存消息,一直到有消費者消費爲止。這就像一家在線商店,雖然訂單處理系統很慢甚至有時候不可用,但不耽誤顧客下單子,訂單會被先緩存下來。

         靈活的客戶端/服務端交互:使用消息機制通信支持上面提到所有交互方式。

         明確的進程間通信:基於RPC的通信機制允許客戶端像調用本地服務一樣調用遠程服務;由於分佈式系統自身的特性和部分失敗的存在,遠程調用和本地調用差別很大。使用消息機制通信使得本地調用和遠程調用的差異明顯化,促使開發人員充分考慮遠程調用的各種問題。

         當然,基於消息通信的機制也有缺點:

         增加額外複雜度:必須安裝、配置和部署消息系統,並且消息中間件必須是高可靠的,否則應用的可靠性就會受到影響。

         實現請求/響應模式比較複雜:每個請求消息中必須包含消息標識和響應消息的通道標識符,攜帶消息標識的響應消息被寫到響應通道中,客戶端接收響應消息後根據請求標識實現請求消息和響應消息的匹配。如果使用其他直接支持請求/響應模式的進程間通信機制,這個過程就簡單多了。

         接下來討論基於請求/響應模式的進程間通信機制。

同步的基於請求/響應的通信機制

當使用基於IPC(進程間通信)的同步請求/響應方式時,客戶端直接向服務端發送請求,服務端處理請求並返回響應。大多數客戶端在等待響應時會阻塞;也可以使用異步的基於事件驅動的方式,客戶端的代碼被封裝在Future或Rx Observables中;與消息機制不同的是,即使使用異步方式,客戶端還是會認定響應將立即到達。有許多同步請求/響應協議可以選擇,REST和Thrift是用的較多的兩種。

REST

現在編寫REST風格的接口很流行,REST是使用HTTP協議的IPC。REST的主要概念是資源,資源代表一個業務對象(像顧客或者產品)或者一組業務對象的集合。REST使用HTTP原語處理URL引用的資源:GET請求返回資源,可能用XML文本表示也可能用JSON表示;POST請求創建資源;PUT請求更新資源。

“REST提供了一組架構約束,強調組件交互的可擴展性、接口的通用性、組件的獨立部署,以及用於降低延遲、實施安全性、封裝遺留系統的中間組件。”在《基於網絡的架構設計》一書中這樣定義。

下圖展示了叫車應用使用REST實現的效果:


         乘客在智能手機上發佈新行程請求,客戶端向行程管理服務的“/trips”資源POST請求;行程管理服務接收請求,通過發送查詢乘客信息的GET請求來處理;行程管理服務驗證乘客賬號有效後,創建新行程,並向智能手機返回201消息。

         許多開發人員聲稱他們的接口是RESTful的,實際上並非如此;Leonard Richardson定義了一個非常有用的REST成熟度模型,它包括以下級別:

         L0:客戶端使用HTTP POST向唯一服務端請求服務,在請求中指定要執行的操作、操作的目標(比如業務對象)以及參數。

         L1:支持資源的概念。客戶端向某個資源發請求,指定要執行的動作和參數。

         L2:使用HTTP原語執行動作:GET獲取數據、POST新建數據、PUT更新數據,參數包含在請求中或者消息體中。使用HTTP原語的好處是可以使用Web基礎設施,比如GET請求的緩存等。

         L3:基於HATEOAS(超文本作爲應用狀態引擎)設計接口。基本思想是在GET請求返回的資源表示中包含在該資源上可執行操作的鏈接,比如:客戶端可以調用一個取消訂單的鏈接來執行取消訂單的操作,而這個鏈接包含在獲取訂單的GET操作所返回的訂單資源中。使用HATEOAS的好處是不用在客戶端代碼中硬編碼URL;還有一個好處是由於返回的資源表達包含了所有能執行的操作,客戶端就不用去猜測當前狀態下服務端能做什麼。

         使用基於HTTP的協議有很多好處:HTTP簡單熟悉;接口容易測試(使用JSON或者其他文本格式),可以在安裝了Postman插件的瀏覽器中調用,也可以在命令行使用curl調用;直接支持請求/響應模式的通信;防火牆友好;不需要中間代理,簡化系統結構。

         使用HTTP也有一些缺點:直接支持的模式只有請求/響應模式,HTTP也可以用於通知模式,但是服務端總是會發送響應消息;由於客戶端和服務端直接通信(沒有中間代理緩存消息),它們在信息交換過程中必須同時可用;客戶端必須知道所有服務端實例的地址,這非常困難,必須依賴於服務發現機制。

         開發者社區最近重新認識到接口定義語言對於RESTful接口開發的意義,可用的接口定義語言有RAML和Swagger。一些接口定義語言(像Swagger)支持定義請求消息和響應消息的格式;一些接口定義語言(像RAML)要求使用單獨的規範(像JSON Schema)。除了定義接口之外,接口定義語言一般還支持根據接口描述生成客戶端的stubs和服務器端的skeletons。

         Thrift

         ApacheThrift是REST一個有趣的替代品,它是用於編寫跨語言RPC客戶端和服務器的框架。Thrift使用C語言風格的接口定義,需要使用Thrift編譯器生成客戶端stub和服務端skeleton。編譯器支持大多數開發語言,包括:C++、Java、Python、PHP、Ruby、Erlang和Node.js。

         Thrift接口包含一個或者多個服務,服務定義類似Java接口,是一些強類型方法的集合。Thrift方法可以是雙向的(有返回),也可以是單向的(無返回);有返回的方法對應於請求/響應模式的實現,客戶端等待響應的過程中可能會拋出異常;無返回的方法對應於通知模式,該模式下服務端不發送響應消息。

         Thrift支持多種類型的消息:JSON、二進制和壓縮二進制。二進制消息比JSON高效,編解碼更快;壓縮二級制消息比二進制節約空間;JSON的優勢是易讀和瀏覽器友好。Thrift還支持傳輸協議的選擇,包括Raw TCP和HTTP;Raw TCP更高效,HTTP易讀、對防火牆和瀏覽器友好。

         消息格式

         如果使用消息機制或者REST,你可以選擇消息格式;其他的IPC機制(比如Thrift),可能只支持一種消息格式。在任一情況下,選擇支持跨語言的消息格式都有必要,即使現在你編寫微服務只用一種編程語言,也不能排除將來會使用其他語言。

         有兩種主要的消息格式:文本和二進制。基於文本的消息格式包括JSON和XML,優勢是易讀、自描述。在JSON中,對象的屬性由名稱-數值對的集合表示;在XML中也類似,屬性由名稱命名元素和值表示;這樣的結構允許消費者獲取感興趣的屬性值,忽略其他屬性值,這樣的接口對屬性的增加和減少都能兼容。

         XML文檔的結構由XML

Schema指定,長期以來,開發者社區認爲JSON也應該有類似的機制,一個選項是JSON Schema,它可以獨立存在也可以作爲接口定義語言的一部分(像Swagger)。

         文本消息的一個缺點是消息太臃腫,特別是XML,由於消息是自描述的,每一條消息除了包含屬性值還包含屬性名;另外一個缺點是解析文本的開銷。你可能會考慮選用二級制消息。

         有幾種二級制消息格式可以選擇。如果你使用Thrift,你能使用binary Thrift;如果你可以選擇消息格式,流行的二進制消息格式有Protocol Buffers和Apache Avro。它們都提供了消息定義語言來定義消息結構,一個區別是Protocol Buffers使用標記語言,而Apache Avro需要知道schema才能解析;因此Protocol Buffers比Apache Avro更能適應接口的變化。要詳細瞭解Thrift、Protocol Buffers和Avro的對比,可以參考這個鏈接:http://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html

7.總結

微服務必須使用進程間通信的機制進行交互,當設計通信方式時,有幾點需要考慮:服務如何交互、怎麼定義服務接口、服務接口如何變化、怎樣處理部分失敗。進程間機制可分爲兩大類:異步消息模式和同步請求/響應模式。在下篇文章,我們討論微服務架構下服務發現機制面臨的問題。

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