本文出自Nginx官網(wǎng),是微服務(wù)介紹系列文章的第三篇。原文地址:https://www.nginx.com/blog/building-microservices-inter-process-communication/
1.介紹
在第一篇文章中我們比較了微服務(wù)架構(gòu)的應(yīng)用和單體應(yīng)用的差異,討論了微服務(wù)架構(gòu)的優(yōu)點與缺點;第二篇文章中討論了客戶端如何使用API網(wǎng)關(guān)跟微服務(wù)通信;在本篇文章中我們討論微服務(wù)之間的通信機(jī)制;在第四篇文章中我們將討論服務(wù)發(fā)現(xiàn)機(jī)制。
在單體應(yīng)用中,應(yīng)用的各個模塊使用語言級的方法調(diào)用通信;微服務(wù)架構(gòu)的應(yīng)用是分布式系統(tǒng),服務(wù)運(yùn)行在多臺物理服務(wù)器上,每個服務(wù)都是一個進(jìn)程,需要使用進(jìn)程間通信機(jī)制,詳細(xì)見下圖:

接下來,我們先討論進(jìn)程間通信需要考慮的設(shè)計問題,再詳細(xì)討論進(jìn)程間通信的主流技術(shù)。
2.進(jìn)程間通信的方式
選擇進(jìn)程間通信機(jī)制,首先要確定微服務(wù)的交互方式,交互方式有很多種,可以從兩個維度來分類,一個維度是按照服務(wù)端是一個還是多個來分類:
一對一:客戶端的請求只被一個特定的服務(wù)端實例處理。
一對多:請求被多個服務(wù)端實例處理。
另外一個維度是按照交互是異步還是同步進(jìn)行分類:
同步調(diào)用:客戶端希望立即獲得響應(yīng),甚至?xí)诘却憫?yīng)期間阻塞。
異步調(diào)用:響應(yīng)不一定立即發(fā)出,客戶端在等待響應(yīng)的過程中不會阻塞。
下面的表格展示了不同的進(jìn)程間通信方式:

? ? ? ? ?一對一交互分為以下幾類:
???????? 請求/響應(yīng)模式:客戶端向服務(wù)器發(fā)送請求,等待響應(yīng);客戶端期望響應(yīng)能及時到達(dá),在等待過程中客戶端可能阻塞。
???????? 通知模式(也叫單向請求):客戶端向服務(wù)器發(fā)送請求,不期望有響應(yīng)。
???????? 請求/異步響應(yīng)模式:客戶端向服務(wù)器發(fā)送請求,等待響應(yīng);客戶端認(rèn)定響應(yīng)不會立即達(dá)到,客戶端不會阻塞。
???????? 一對多交互分為以下幾類:
???????? 發(fā)布/訂閱模式:客戶端發(fā)布通知消息,零個或者多個服務(wù)消費(fèi)該消息。
???????? 發(fā)布/異步響應(yīng)模式:客戶端發(fā)布請求消息,在一定時間內(nèi)等待服務(wù)端響應(yīng)。
???????? 服務(wù)一般都會組合應(yīng)用以上多種交互方式。對于一些服務(wù)而言,單一的進(jìn)程間通信機(jī)制就能滿足需求;對于另外一些服務(wù),可能需要組合多種通信機(jī)制才能滿足需求。以下圖示展示了當(dāng)用戶發(fā)布行程時,叫車應(yīng)用的服務(wù)如何交互:

???????? 圖中應(yīng)用了通知模式、請求/響應(yīng)模式、發(fā)布/訂閱模式等多種方式:用戶使用智能手機(jī)向行程管理服務(wù)發(fā)布新行程請求通知;行程管理服務(wù)使用請求/響應(yīng)模式調(diào)用乘客管理服務(wù)來驗證用戶賬號;驗證通過后,行程管理服務(wù)創(chuàng)建行程并使用發(fā)布/訂閱模式將新的行程信息通知到分發(fā)服務(wù)(基于地理位置尋找潛在司機(jī))。接下來,我們討論服務(wù)接口的定義。
3.?接口定義
服務(wù)接口是服務(wù)和客戶端之間的契約,無論你使用哪種進(jìn)程間通信機(jī)制,使用某一種接口定義語言精確定義接口都非常重要。使用接口優(yōu)先的方法開發(fā)服務(wù)是個聰明的選擇,通過跟客戶端開發(fā)人員討論接口定義,對接口進(jìn)行迭代之后再開始服務(wù)的開發(fā)工作會讓你的服務(wù)更有可能滿足需求。
服務(wù)接口的定義取決于你使用哪一種進(jìn)程間通信機(jī)制:如果使用消息機(jī)制,接口定義可能包含消息通道和消息類型;如果使用HTTP機(jī)制,接口定義可能包含URL以及請求和響應(yīng)的消息格式。
4.接口的變化
服務(wù)接口會隨著時間不斷變化,當(dāng)接口變化時,在單體應(yīng)用中,直接更新接口和所有調(diào)用者就可以;在微服務(wù)架構(gòu)下,即使所有調(diào)用者都是應(yīng)用內(nèi)的其他服務(wù),更新接口和所有調(diào)用者也是件困難的事情。你無法強(qiáng)制所有客戶端和服務(wù)端同步升級,因此,服務(wù)的新舊版本往往同時存在,你需要采取策略應(yīng)對這種情況。
如何應(yīng)對接口變化取決于變化的大小。有些變化比較小,能兼容之前的版本。比如說,請求和響應(yīng)中可能會增加一些屬性。這種情況下,客戶端和服務(wù)端遵從健壯性設(shè)計就很有用,能保證實現(xiàn)舊接口的客戶端在新接口下依然能工作:服務(wù)端為不存在的屬性設(shè)置默認(rèn)值,客戶端忽略新增加的屬性。考慮到系統(tǒng)健壯性,選用合適的進(jìn)程間通信機(jī)制、使用支持變化的消息格式很重要。
有時候接口必須進(jìn)行較大的變化,不能向前兼容。既然無法強(qiáng)制客戶端和服務(wù)端同時更新,在一定時期內(nèi)就需要保證新舊接口都能使用。如果你使用基于HTTP的通信機(jī)制(REST),一種處理方式是在請求URL中加上服務(wù)版本號,這樣,每個服務(wù)實例就能同時處理多個版本的請求;另外一種方式是部署多個版本的服務(wù)實例,每一種服務(wù)實例處理特定版本的請求。
5.處理部分失敗
就像在第二篇文章中描述的,分布式系統(tǒng)中存在部分失敗的風(fēng)險。既然客戶端和服務(wù)端都是獨(dú)立的進(jìn)程,服務(wù)端就可能無法及時響應(yīng)。服務(wù)端有可能因為維護(hù)升級而關(guān)閉,也有可能由于過載而導(dǎo)致響應(yīng)緩慢。
還是以商品詳情的頁面為例,假設(shè)智能推薦的服務(wù)無響應(yīng),簡單的客戶端實現(xiàn)可能會一直阻塞等待響應(yīng),這不但會帶來糟糕的用戶體驗,還會消耗寶貴的線程資源,最終可能會消耗掉運(yùn)行環(huán)境的所有線程資源使得整個應(yīng)用宕掉。以下是線程資源消耗光的圖示:

為了阻止此類問題發(fā)生,必須要針對部分失敗進(jìn)行相應(yīng)設(shè)計。Netflix找到了好辦法來應(yīng)對部分失敗,它的策略主要包括:
???????? 網(wǎng)絡(luò)超時:等待響應(yīng)時用超時機(jī)制替代阻塞機(jī)制,避免資源的無期限占用。
???????? 限定未完成的請求數(shù):限定客戶端對同一個服務(wù)的最大未完成請求數(shù);如果達(dá)到最大值,后續(xù)的請求會立即返回失敗。
???????? 斷路器模式:跟蹤成功和失敗的請求數(shù),如果請求失敗的比率超過閾值,斷路器會打開,后續(xù)的請求會立即返回失敗。如果大量的請求都失敗,預(yù)示著服務(wù)不可用,因此后續(xù)的服務(wù)請求無意義;經(jīng)過一個超時周期后,客戶端會再次嘗試訪問服務(wù),請求如果成功,則關(guān)閉斷路器。
???????? 提供回調(diào):當(dāng)請求失敗時,執(zhí)行回調(diào)邏輯,返回緩存數(shù)據(jù)或默認(rèn)數(shù)據(jù)。
???????? NetflixHystrix是一個開源代碼庫,它實現(xiàn)了以上策略,如果你的應(yīng)用使用JVM,應(yīng)該考慮使用Hystrix;如果沒有JVM,也需要考慮使用類似的代碼庫。
6.進(jìn)程間通信技術(shù)
???????? 有許多進(jìn)程間通信的技術(shù)可供選擇??梢允褂孟馬EST或者Thrift之類的請求/響應(yīng)同步模式,也可以使用類似AMQP或者STOMP之類基于消息的異步模式。消息格式也有很多種,有容易理解的基于文本的JSON和XML;也有更高效的二進(jìn)制的Avro和Protocol Buffers。我們先討論異步機(jī)制再討論同步機(jī)制。
異步的基于消息的通信機(jī)制
???????? 當(dāng)使用基于消息的異步通信機(jī)制時,客戶端通過發(fā)送消息的形式發(fā)送請求,如果服務(wù)端需要響應(yīng),也發(fā)送消息給客戶端。既然是異步通信,客戶端就不用阻塞等待響應(yīng)。實際上,在異步機(jī)制下,客戶端認(rèn)定響應(yīng)不會立即到達(dá)。
???????? 消息包括消息頭和消息體,消息在通道中傳遞,可以由任意多個生產(chǎn)者向通道發(fā)送消息;類似的也可以有任意多個消費(fèi)者從通道上接收消息。有兩種類型的通道,一種是點對點,一種是發(fā)布/訂閱。點對點模式下,通道將消息分發(fā)給某個特定的消費(fèi)者,之前描述的一對一交互可以使用該模式;發(fā)布/訂閱模式下,通道將消息分發(fā)給所有監(jiān)聽該通道的消費(fèi)者,一對多交互可以使用該模式。
???????? 下面圖示展示了叫車應(yīng)用中如何使用發(fā)布/訂閱通道:

? ? ? ? ?行程管理服務(wù)通過向通道寫新的行程消息通知其他相關(guān)服務(wù);分發(fā)服務(wù)定位到可用的駕駛員,通過向發(fā)布/訂閱通道寫一條可用司機(jī)的消息通知其他相關(guān)服務(wù)。
???????? 有許多種消息系統(tǒng)可以選擇,為了以后擴(kuò)展方便,要優(yōu)先選擇支持多語言的消息系統(tǒng)。一些消息系統(tǒng)支持標(biāo)準(zhǔn)的協(xié)議,像AMQP和STOMP;另外一些使用專用協(xié)議。有很多開源的消息系統(tǒng),像RabbitMQ、Apache Kafka、Apache ActiveMQ、NSQ等。從更高層次上來說,這些消息系統(tǒng)都支持一些消息格式和通道,都致力于實現(xiàn)可靠的、高性能的、可伸縮的消息傳遞,然而在消息模型的實現(xiàn)細(xì)節(jié)上還是有很大差異。
???????? 使用消息機(jī)制通信有很多好處:
???????? 實現(xiàn)客戶端和服務(wù)端解耦:客戶端通過向消息中間件的通道發(fā)送消息實現(xiàn)服務(wù)請求,完全不用考慮服務(wù)端,不需要使用服務(wù)發(fā)現(xiàn)機(jī)制確定服務(wù)端地址。
???????? 消息緩存:同步的請求/響應(yīng)協(xié)議(如REST),在信息交換期間,服務(wù)端和客戶端必須都可用;在基于消息的異步通信機(jī)制下,消息中間件會在隊列中緩存消息,一直到有消費(fèi)者消費(fèi)為止。這就像一家在線商店,雖然訂單處理系統(tǒng)很慢甚至有時候不可用,但不耽誤顧客下單子,訂單會被先緩存下來。
???????? 靈活的客戶端/服務(wù)端交互:使用消息機(jī)制通信支持上面提到所有交互方式。
???????? 明確的進(jìn)程間通信:基于RPC的通信機(jī)制允許客戶端像調(diào)用本地服務(wù)一樣調(diào)用遠(yuǎn)程服務(wù);由于分布式系統(tǒng)自身的特性和部分失敗的存在,遠(yuǎn)程調(diào)用和本地調(diào)用差別很大。使用消息機(jī)制通信使得本地調(diào)用和遠(yuǎn)程調(diào)用的差異明顯化,促使開發(fā)人員充分考慮遠(yuǎn)程調(diào)用的各種問題。
???????? 當(dāng)然,基于消息通信的機(jī)制也有缺點:
???????? 增加額外復(fù)雜度:必須安裝、配置和部署消息系統(tǒng),并且消息中間件必須是高可靠的,否則應(yīng)用的可靠性就會受到影響。
???????? 實現(xiàn)請求/響應(yīng)模式比較復(fù)雜:每個請求消息中必須包含消息標(biāo)識和響應(yīng)消息的通道標(biāo)識符,攜帶消息標(biāo)識的響應(yīng)消息被寫到響應(yīng)通道中,客戶端接收響應(yīng)消息后根據(jù)請求標(biāo)識實現(xiàn)請求消息和響應(yīng)消息的匹配。如果使用其他直接支持請求/響應(yīng)模式的進(jìn)程間通信機(jī)制,這個過程就簡單多了。
???????? 接下來討論基于請求/響應(yīng)模式的進(jìn)程間通信機(jī)制。
同步的基于請求/響應(yīng)的通信機(jī)制
當(dāng)使用基于IPC(進(jìn)程間通信)的同步請求/響應(yīng)方式時,客戶端直接向服務(wù)端發(fā)送請求,服務(wù)端處理請求并返回響應(yīng)。大多數(shù)客戶端在等待響應(yīng)時會阻塞;也可以使用異步的基于事件驅(qū)動的方式,客戶端的代碼被封裝在Future或Rx Observables中;與消息機(jī)制不同的是,即使使用異步方式,客戶端還是會認(rèn)定響應(yīng)將立即到達(dá)。有許多同步請求/響應(yīng)協(xié)議可以選擇,REST和Thrift是用的較多的兩種。
REST
現(xiàn)在編寫REST風(fēng)格的接口很流行,REST是使用HTTP協(xié)議的IPC。REST的主要概念是資源,資源代表一個業(yè)務(wù)對象(像顧客或者產(chǎn)品)或者一組業(yè)務(wù)對象的集合。REST使用HTTP原語處理URL引用的資源:GET請求返回資源,可能用XML文本表示也可能用JSON表示;POST請求創(chuàng)建資源;PUT請求更新資源。
“REST提供了一組架構(gòu)約束,強(qiáng)調(diào)組件交互的可擴(kuò)展性、接口的通用性、組件的獨(dú)立部署,以及用于降低延遲、實施安全性、封裝遺留系統(tǒng)的中間組件?!痹凇痘诰W(wǎng)絡(luò)的架構(gòu)設(shè)計》一書中這樣定義。
下圖展示了叫車應(yīng)用使用REST實現(xiàn)的效果:

? ? ? ? ?乘客在智能手機(jī)上發(fā)布新行程請求,客戶端向行程管理服務(wù)的“/trips”資源POST請求;行程管理服務(wù)接收請求,通過發(fā)送查詢乘客信息的GET請求來處理;行程管理服務(wù)驗證乘客賬號有效后,創(chuàng)建新行程,并向智能手機(jī)返回201消息。
???????? 許多開發(fā)人員聲稱他們的接口是RESTful的,實際上并非如此;Leonard Richardson定義了一個非常有用的REST成熟度模型,它包括以下級別:
???????? L0:客戶端使用HTTP POST向唯一服務(wù)端請求服務(wù),在請求中指定要執(zhí)行的操作、操作的目標(biāo)(比如業(yè)務(wù)對象)以及參數(shù)。
???????? L1:支持資源的概念。客戶端向某個資源發(fā)請求,指定要執(zhí)行的動作和參數(shù)。
???????? L2:使用HTTP原語執(zhí)行動作:GET獲取數(shù)據(jù)、POST新建數(shù)據(jù)、PUT更新數(shù)據(jù),參數(shù)包含在請求中或者消息體中。使用HTTP原語的好處是可以使用Web基礎(chǔ)設(shè)施,比如GET請求的緩存等。
???????? L3:基于HATEOAS(超文本作為應(yīng)用狀態(tài)引擎)設(shè)計接口?;舅枷胧窃贕ET請求返回的資源表示中包含在該資源上可執(zhí)行操作的鏈接,比如:客戶端可以調(diào)用一個取消訂單的鏈接來執(zhí)行取消訂單的操作,而這個鏈接包含在獲取訂單的GET操作所返回的訂單資源中。使用HATEOAS的好處是不用在客戶端代碼中硬編碼URL;還有一個好處是由于返回的資源表達(dá)包含了所有能執(zhí)行的操作,客戶端就不用去猜測當(dāng)前狀態(tài)下服務(wù)端能做什么。
???????? 使用基于HTTP的協(xié)議有很多好處:HTTP簡單熟悉;接口容易測試(使用JSON或者其他文本格式),可以在安裝了Postman插件的瀏覽器中調(diào)用,也可以在命令行使用curl調(diào)用;直接支持請求/響應(yīng)模式的通信;防火墻友好;不需要中間代理,簡化系統(tǒng)結(jié)構(gòu)。
???????? 使用HTTP也有一些缺點:直接支持的模式只有請求/響應(yīng)模式,HTTP也可以用于通知模式,但是服務(wù)端總是會發(fā)送響應(yīng)消息;由于客戶端和服務(wù)端直接通信(沒有中間代理緩存消息),它們在信息交換過程中必須同時可用;客戶端必須知道所有服務(wù)端實例的地址,這非常困難,必須依賴于服務(wù)發(fā)現(xiàn)機(jī)制。
???????? 開發(fā)者社區(qū)最近重新認(rèn)識到接口定義語言對于RESTful接口開發(fā)的意義,可用的接口定義語言有RAML和Swagger。一些接口定義語言(像Swagger)支持定義請求消息和響應(yīng)消息的格式;一些接口定義語言(像RAML)要求使用單獨(dú)的規(guī)范(像JSON Schema)。除了定義接口之外,接口定義語言一般還支持根據(jù)接口描述生成客戶端的stubs和服務(wù)器端的skeletons。
???????? Thrift
???????? ApacheThrift是REST一個有趣的替代品,它是用于編寫跨語言RPC客戶端和服務(wù)器的框架。Thrift使用C語言風(fēng)格的接口定義,需要使用Thrift編譯器生成客戶端stub和服務(wù)端skeleton。編譯器支持大多數(shù)開發(fā)語言,包括:C++、Java、Python、PHP、Ruby、Erlang和Node.js。
???????? Thrift接口包含一個或者多個服務(wù),服務(wù)定義類似Java接口,是一些強(qiáng)類型方法的集合。Thrift方法可以是雙向的(有返回),也可以是單向的(無返回);有返回的方法對應(yīng)于請求/響應(yīng)模式的實現(xiàn),客戶端等待響應(yīng)的過程中可能會拋出異常;無返回的方法對應(yīng)于通知模式,該模式下服務(wù)端不發(fā)送響應(yīng)消息。
???????? Thrift支持多種類型的消息:JSON、二進(jìn)制和壓縮二進(jìn)制。二進(jìn)制消息比JSON高效,編解碼更快;壓縮二級制消息比二進(jìn)制節(jié)約空間;JSON的優(yōu)勢是易讀和瀏覽器友好。Thrift還支持傳輸協(xié)議的選擇,包括Raw TCP和HTTP;Raw TCP更高效,HTTP易讀、對防火墻和瀏覽器友好。
???????? 消息格式
???????? 如果使用消息機(jī)制或者REST,你可以選擇消息格式;其他的IPC機(jī)制(比如Thrift),可能只支持一種消息格式。在任一情況下,選擇支持跨語言的消息格式都有必要,即使現(xiàn)在你編寫微服務(wù)只用一種編程語言,也不能排除將來會使用其他語言。
???????? 有兩種主要的消息格式:文本和二進(jìn)制?;谖谋镜南⒏袷桨↗SON和XML,優(yōu)勢是易讀、自描述。在JSON中,對象的屬性由名稱-數(shù)值對的集合表示;在XML中也類似,屬性由名稱命名元素和值表示;這樣的結(jié)構(gòu)允許消費(fèi)者獲取感興趣的屬性值,忽略其他屬性值,這樣的接口對屬性的增加和減少都能兼容。
???????? XML文檔的結(jié)構(gòu)由XML
Schema指定,長期以來,開發(fā)者社區(qū)認(rèn)為JSON也應(yīng)該有類似的機(jī)制,一個選項是JSON Schema,它可以獨(dú)立存在也可以作為接口定義語言的一部分(像Swagger)。
???????? 文本消息的一個缺點是消息太臃腫,特別是XML,由于消息是自描述的,每一條消息除了包含屬性值還包含屬性名;另外一個缺點是解析文本的開銷。你可能會考慮選用二級制消息。
???????? 有幾種二級制消息格式可以選擇。如果你使用Thrift,你能使用binary Thrift;如果你可以選擇消息格式,流行的二進(jìn)制消息格式有Protocol Buffers和Apache Avro。它們都提供了消息定義語言來定義消息結(jié)構(gòu),一個區(qū)別是Protocol Buffers使用標(biāo)記語言,而Apache Avro需要知道schema才能解析;因此Protocol Buffers比Apache Avro更能適應(yīng)接口的變化。要詳細(xì)了解Thrift、Protocol Buffers和Avro的對比,可以參考這個鏈接:http://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html。
7.總結(jié)
微服務(wù)必須使用進(jìn)程間通信的機(jī)制進(jìn)行交互,當(dāng)設(shè)計通信方式時,有幾點需要考慮:服務(wù)如何交互、怎么定義服務(wù)接口、服務(wù)接口如何變化、怎樣處理部分失敗。進(jìn)程間機(jī)制可分為兩大類:異步消息模式和同步請求/響應(yīng)模式。在下篇文章,我們討論微服務(wù)架構(gòu)下服務(wù)發(fā)現(xiàn)機(jī)制面臨的問題。