【編者的話】這是採用微服務架構建立本身應用系列第三篇文章。第一篇介紹了微服務架構模式,和單體式模式進行了比較,而且討論了使用微服務架構的優缺點。第二篇描述了採用微服務架構應用客戶端之間如何採用API Gateway方式進行通訊。在這篇文章中,咱們將討論系統服務之間如何通訊。html
在單體式應用中,各個模塊之間的調用是經過編程語言級別的方法或者函數來實現的。可是一個基於微服務的分佈式應用是運行在多臺機器上的。通常來講,每一個服務實例都是一個進程。所以,以下圖所示,服務之間的交互必須經過進程間通訊(IPC)來實現。java
後面咱們將會詳細介紹IPC技術,如今咱們先來看下設計相關的問題。git
當爲某一個服務選擇IPC時,首先須要考慮服務之間如何交互。客戶端和服務器之間有不少的交互模式,咱們能夠從兩個維度進行歸類。第一個維度是一對一仍是一對多:
•一對一:每一個客戶端請求有一個服務實例來響應。
•一對多:每一個客戶端請求有多個服務實例來響應
第二個維度是這些交互式同步仍是異步:
• 同步模式:客戶端請求須要服務端即時響應,甚至可能因爲等待而阻塞。
• 異步模式:客戶端請求不會阻塞進程,服務端的響應能夠是非即時的。
下表顯示了不一樣交互模式:github
一對一的交互模式有如下幾種方式:
• 請求/響應:一個客戶端向服務器端發起請求,等待響應。客戶端指望此響應即時到達。在一個基於線程的應用中,等待過程可能形成線程阻塞。
• 通知(也就是常說的單向請求):一個客戶端請求發送到服務端,可是並不指望服務端響應。
• 請求/異步響應:客戶端發送請求到服務端,服務端異步響應請求。客戶端不會阻塞,並且被設計成默認響應不會馬上到達。
一對多的交互模式有如下幾種方式:
• 發佈/ 訂閱模式:客戶端發佈通知消息,被零個或者多個感興趣的服務消費。
• 發佈/異步響應模式:客戶端發佈請求消息,而後等待從感興趣服務發回的響應。
每一個服務都是以上這些模式的組合,對某些服務,一個IPC機制就足夠了;而對另一些服務則須要多種IPC機制組合。下圖展現了在一個打車服務請求中服務之間是如何通訊的。web
上圖中的服務通訊使用了通知、請求/響應、發佈/訂閱等方式。例如,乘客經過移動端給『行程管理服務』發送通知,但願申請一次出租服務。『行程管理服務』發送請求/響應消息給『乘客服務』以確認乘客帳號是有效的。緊接着建立這次行程,並用發佈/訂閱交互模式通知其餘服務,包括定位可用司機的調度服務。
如今咱們瞭解了交互模式,接下來咱們一塊兒來看看如何定義API。apache
API是服務端和客戶端之間的契約。無論選擇了什麼樣的IPC機制,重要的是使用某種交互式定義語言(IDL)來精肯定義一個服務的API。甚至有一些關於使用API first的方法(API-first approach)來定義服務的很好的理由。在開發以前,你須要先定義服務的接口,並與客戶端開發者詳細討論確認。這樣的討論和設計會大幅度提到API的可用度以及滿意度。
在本文後半部分你將會看到,API定義實質上依賴於選擇哪一種IPC。若是使用消息機制,API則由消息頻道(channel)和消息類型構成;若是選擇使用HTTP機制,API則由URL和請求、響應格式構成。後面將會詳細描述IDL。編程
服務端API會不斷變化。在一個單體式應用中常常會直接修改API,而後更新給全部的調用者。而在基於微服務架構應用中,這很困難,即便只有一個服務使用這個API,不可能強迫用戶跟服務端保持同步更新。另外,開發者可能會嘗試性的部署新版本的服務,這個時候,新舊服務就會同事運行。你須要知道如何處理這些問題。
你如何處理API變化,這依賴於這些變化有多大。某些改變是微小的,而且能夠和以前版本兼容。好比,你可能只是爲某個請求和響應添加了一個屬性。設計客戶端和服務端時候應該遵循健壯性原理,這很重要。客戶端使用舊版API應該也能和新版本一塊兒工做。服務端仍然提供默認響應值,客戶端忽略此版本不須要的響應。使用IPC機制和消息格式對於API演化頗有幫助。
可是有時候,API須要進行大規模的改動,而且可能與以前版本不兼容。由於你不可能強制讓全部的客戶端當即升級,因此支持老版本客戶端的服務還須要再運行一段時間。若是你正在使用基於基於HTTP機制的IPC,例如REST,一種解決方案是把版本號嵌入到URL中。每一個服務均可能同時處理多個版本的API。或者,你能夠部署多個實例,每一個實例負責處理一個版本的請求。json
在上一篇關於API gateway的文章中,咱們瞭解到分佈式系統中部分失敗是廣泛存在的問題。由於客戶端和服務端是都是獨立的進程,一個服務端有可能由於故障或者維護而中止服務,或者此服務由於過載中止或者反應很慢。
考慮這篇文章中描述的部分失敗的場景。假設推薦服務沒法響應請求,那客戶端就會因爲等待響應而阻塞,這不只會給客戶帶來不好的體驗,並且在不少應用中還會佔用不少資源,好比線程,以致於到最後因爲等待響應被阻塞的客戶端愈來愈多,線程資源被耗費完了。以下圖所示:api
爲了預防這種問題,設計服務時候必需要考慮部分失敗的問題。
Netfilix提供了一個比較好的解決方案,具體的應對措施包括:
• 網絡超時:當等待響應時,不要無限期的阻塞,而是採用超時策略。使用超時策略能夠確保資源不會無限期的佔用。
• 限制請求的次數:能夠爲客戶端對某特定服務的請求設置一個訪問上限。若是請求已達上限,就要馬上終止請求服務。
•斷路器模式(Circuit Breaker Pattern):記錄成功和失敗請求的數量。若是失效率超過一個閾值,觸發斷路器使得後續的請求馬上失敗。若是大量的請求失敗,就多是這個服務不可用,再發請求也無心義。在一個失效期後,客戶端能夠再試,若是成功,關閉此斷路器。
• 提供回滾:當一個請求失敗後能夠進行回滾邏輯。例如,返回緩存數據或者一個系統默認值。
Netflix Hystrix是一個實現相關模式的開源庫。若是使用JVM,推薦考慮使用Hystrix。而若是使用非JVM環境,你可使用相似功能的庫。瀏覽器
如今有不少不一樣的IPC技術。服務之間的通訊可使用同步的請求/響應模式,好比基於HTTP的REST或者Thrift。另外,也能夠選擇異步的、基於消息的通訊模式,好比AMQP或者STOMP。除以以外,還有其它的消息格式供選擇,好比JSON和XML,它們都是可讀的,基於文本的消息格式。固然,也還有二進制格式(效率更高)的,好比Avro和Protocol Buffer。接下來咱們將會討論異步的IPC模式和同步的IPC模式,首先來看異步的。
當使用基於異步交換消息的進程通訊方式時,一個客戶端經過向服務端發送消息提交請求。若是服務端須要回覆,則會發送另一個獨立的消息給客戶端。由於通訊是異步的,客戶端不會由於等待而阻塞,相反,客戶端理所固然的認爲響應不會馬上接收到。
一個消息由頭部(元數據例如發送方)和消息體構成。消息經過channel發送,任何數量的生產者均可以發送消息到channel,一樣的,任何數量的消費者均可以從渠道中接受數據。有兩類channel,點對點和發佈/訂閱。點對點channel會把消息準確的發送到某個從channel讀取消息的消費者,服務端使用點對點來實現以前提到的一對一交互模式;而發佈/訂閱則把消息投送到全部從channel讀取數據的消費者,服務端使用發佈/訂閱channel來實現上面提到的一對多交互模式。
下圖展現了打車軟件如何使用發佈/訂閱:
行程管理服務在發佈-訂閱channel內建立一個行程消息,並通知調度服務有一個新的行程請求,調度服務發現一個可用的司機而後向發佈-訂閱channel寫入司機建議消息(Driver Proposed message)來通知其餘服務。
有不少消息系統能夠選擇,最好選擇一種支持多編程語言的。一些消息系統支持標準協議,例如AMQP和STOMP。其餘消息系統則使用獨有的協議,有大量開源消息系統可選,好比RabbitMQ、Apache Kafka、Apache ActiveMQ和NSQ。它們都支持某種形式的消息和channel,而且都是可靠的、高性能和可擴展的;然而,它們的消息模型徹底不一樣。
使用消息機制有不少優勢:
•解耦客戶端和服務端:客戶端只須要將消息發送到正確的channel。客戶端徹底不須要了解具體的服務實例,更不須要一個發現機制來肯定服務實例的位置。
•Message Buffering:在一個同步請求/響應協議中,例如HTTP,全部的客戶端和服務端必須在交互期間保持可用。而在消息模式中,消息broker將全部寫入channel的消息按照隊列方式管理,直到被消費者處理。也就是說,在線商店能夠接受客戶訂單,即便下單系統很慢或者不可用,只要保持下單消息進入隊列就行了。
• 彈性客戶端-服務端交互:消息機制支持以上說的全部交互模式。
•直接進程間通訊:基於RPC機制,試圖喚醒遠程服務看起來跟喚醒本地服務同樣。然而,由於物理定律和部分失敗可能性,他們實際上很是不一樣。消息使得這些不一樣很是明確,開發者不會出現問題。
然而,消息機制也有本身的缺點:
•額外的操做複雜性:消息系統須要單獨安裝、配置和部署。消息broker(代理)必須高可用,不然系統可靠性將會受到影響。
•實現基於請求/響應交互模式的複雜性:請求/響應交互模式須要完成額外的工做。每一個請求消息必須包含一個回覆渠道ID和相關ID。服務端發送一個包含相關ID的響應消息到channel中,使用相關ID來將響應對應到發出請求的客戶端。也許這個時候,使用一個直接支持請求/響應的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的。
乘客經過移動端向行程管理服務的/trips
資源提交了一個POST請求。行程管理服務收到請求以後,會發送一個GET請求到乘客管理服務以獲取乘客信息。當確認乘客信息以後,緊接着會建立一個行程,並向移動端返回201(譯者注:狀態碼)響應。
不少開發者都表示他們基於HTTP的API是RESTful的。可是,如同Fielding在他的博客中所說,這些API可能並不都是RESTful的。Leonard Richardson爲REST定義了一個成熟度模型,具體包含如下4個層次(摘自IBM):
使用基於HTTP的協議有以下好處:
• HTTP很是簡單而且你們都很熟悉。
• 可使用瀏覽器擴展(好比Postman)或者curl之類的命令行來測試API。
• 內置支持請求/響應模式的通訊。
• HTTP對防火牆友好的。
• 不須要中間代理,簡化了系統架構。
不足之處包括:
• 只支持請求/響應模式交互。可使用HTTP通知,可是服務端必須一直髮送HTTP響應才行。
• 由於客戶端和服務端直接通訊(沒有代理或者buffer機制),在交互期間必須都在線。
• 客戶端必須知道每一個服務實例的URL。如以前那篇關於API Gateway的文章所述,這也是個煩人的問題。客戶端必須使用服務實例發現機制。
開發者社區最近從新發現了RESTful API接口定義語言的價值。因而就有了一些RESTful風格的服務框架,包括RAML和Swagger。一些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 Buffers和Apache Avro。它們都提供典型的IDL來定義消息架構。一個不一樣點在於Protocol Buffers使用的是加標記(tag)的字段,而Avro消費者須要知道模式(schema)來解析消息。所以,使用前者,API更容易演進。這篇博客很好的比較了Thrift、Protocol Buffers、Avro三者的區別。
微服務必須使用進程間通訊機制來交互。當設計服務的通訊模式時,你須要考慮幾個問題:服務如何交互,每一個服務如何標識API,如何升級API,以及如何處理部分失敗。微服務架構有兩類IPC機制可選,異步消息機制和同步請求/響應機制。在下一篇文章中,咱們將會討論微服務架構中的服務發現問題。