編者的話|本文來自 Nginx 官方博客,是微服務系列文章的第三篇,在第一篇文章中介紹了微服務架構模式,與單體模式進行了比較,而且討論了使用微服務架構的優缺點。第二篇描述了採用微服務架構的應用客戶端之間如何採用 API 網關方式進行通訊。在這篇文章中,咱們將討論系統服務之間是如何實現通訊的。web
1.微服務架構的優點與不足
2.使用 API 網關構建微服務
3.微服務架構中的進程間通訊
4.服務發現的可行方案以及實踐案例
5.微服務的事件驅動數據管理
6.選擇微服務部署策略
7.將單體應用改造爲微服務編程
在單體應用中,各模塊之間的調用是經過編程語言級別的方法或者函數來實現的。而基於微服務的分佈式應用是運行在多臺機器上的;通常來講,每一個服務實例都是一個進程。瀏覽器
所以,以下圖所示,服務之間的交互必須經過進程間通訊(IPC)來實現。緩存
當爲某個服務選擇 IPC 時,首先須要考慮服務之間的交互問題。客戶端和服務器之間有不少的交互模式,咱們能夠從兩個維度進行歸類。第一個維度是一對一仍是一對多:安全
第二個維度是這些交互式是同步仍是異步:服務器
下表顯示了不一樣交互模式:
網絡
一對多的交互模式有如下幾種方式:架構
每一個服務都是以上這些模式的組合。對某些服務,一個 IPC 機制就足夠了;而對另一些服務則須要多種 IPC 機制組合。下圖展現了在用戶叫車時,打車應用內的服務是如何交互的。框架
上圖中的服務通訊使用了通知、請求/響應、發佈/訂閱等方式。例如,乘客在移動端向「行程管理」服務發送通知,請求一次接送服務。「行程管理」服務經過使用請求/響應來喚醒「乘客服務」來驗證乘客帳號有效,繼而建立這次行程,並利用發佈/訂閱來通知其它服務,其中包括定位可用司機的調度服務。curl
如今咱們瞭解了交互模式,接下來咱們一塊兒來看看如何定義 API。
API 是服務端和客戶端之間的契約。不管選擇了何種 IPC 機制,重點是使用某種交互定義語言(IDL)來準肯定義服務的 API。對於如何使用 API 優先的方式來定義服務,已經有了一些很好的討論。你在開發服務以前,要定義服務接口並與客戶端開發者共同討論,後續只須要迭代 API 定義。這樣的設計可以大幅提高服務的可用度。
在本文後半部分你將會看到,API 定義實質上依賴於選定的 IPC 機制。若是使用消息機制,API 則由消息頻道(channel)和消息類型構成;若是選擇使用 HTTP 機制,API 則由 URL 和請求、響應格式構成。後面將會詳細描述 IDL。
服務的 API 會隨着時間而不斷變化。在單體應用中,常常會直接修改 API 並更新全部的調用者。可是在基於微服務的應用中,即便全部的 API 的使用者都在同一應用中,這種作法也困難重重,一般不能強制讓全部客戶端都與服務保持同步更新。此外,你可能會增量部署服務的新版本,這時舊版本會與新版本同時運行。瞭解這些問題的處理策略相當重要。
對 API 變化的處理方式與變化的大小有關。有的變化很小,而且能夠兼容以前的版本;好比給請求或響應增長屬性。在設計客戶端和服務時,頗有必要遵循健壯性原則。服務更新版本後,使用舊版 API 的客戶端應該繼續使用。服務爲缺失的請求屬性提供默認值,客戶端則忽略任何額外的響應。使用 IPC 機制和消息格式可以讓你輕鬆改進 API。
然而有時候,API 須要進行大規模改動,而且不兼容舊版本。鑑於不能強制讓全部客戶端當即升級,支持舊版 API 的服務還要再運行一段時間。若是你使用的是諸如 REST 這樣的基於 HTTP 機制的 IPC,一種方法就是將版本號嵌入到 URL 中,每一個服務實例能夠同時處理多個版本。另外一種方法是部署不一樣實例,每一個實例處理一個版本的請求。
在上一篇關於 API 網關的文章中,咱們瞭解到,分佈式系統廣泛存在局部失敗的問題。因爲客戶端和服務端是獨立的進程,服務端可能沒法及時響應客戶端請求。服務端可能會由於故障或者維護而暫時不可用。服務端也可能會因爲過載,致使對請求的響應極其緩慢。
以上篇文章中說起的產品頁爲例,假設推薦服務沒法響應,客戶端可能會因爲無限期等待響應而阻塞。這不只會致使不好的用戶體驗,而且在不少應用中還會佔用以前的資源,好比線程;最終,以下圖所示,運行時耗盡線程資源,沒法響應。
爲了預防這種問題,設計服務時候必需要考慮部分失敗的問題。
Netfilix 提供了一個比較好的解決方案,具體的應對措施包括:
如今有不少不一樣的 IPC 技術。服務間通訊可使用同步的請求/響應模式,好比基於 HTTP 的 REST 或者 Thrift。另外,也能夠選擇異步的、基於消息的通訊模式,好比 AMQP 或者 STOMP。此外,還能夠選擇 JSON 或者 XML 這種可讀的、基於文本的消息格式。固然,也還有效率更高的二進制格式,好比 Avro 和 Protocol Buffer。在討論同步的 IPC 機制以前,咱們先了解異步的 IPC 機制。
基於消息的異步通訊
使用消息模式的時候,進程之間經過異步交換消息消息的方式通訊。客戶端經過向服務端發送消息提交請求,若是服務端須要回覆,則會發送另外一條獨立的消息給客戶端。因爲異步通訊,客戶端不會由於等待而阻塞,相反會認爲響應不會被當即收到。
消息由數據頭(例如發送方這樣的元數據)和消息正文構成。消息經過渠道發送,任何數量的生產者均可以發送消息到渠道,一樣,任何數量的消費者均可以從渠道中接受數據。頻道有兩類,包括點對點渠道和發佈/訂閱渠道。點對點渠道會把消息準確的發送到從渠道讀取消息的用戶,服務端使用點對點來實現以前提到的一對一交互模式;而發佈/訂閱則把消息投送到全部從渠道讀取數據的用戶,服務端使用發佈/訂閱渠道來實現上面提到的一對多交互模式。
下圖展現了打車軟件如何使用發佈/訂閱:
經過向發佈/訂閱渠道寫入一條建立行程的消息,行程管理服務會通知調度服務有新的行程請求。調度服務發現可用的司機後會向發佈/訂閱渠道寫入一條推薦司機的消息,並通知其它服務。
有多種消息系統可供選擇,最好選擇支持多編程語言的。有的消息系統支持 AMQP 和 STOMP 這樣的標準協議,有的則支持專利協議。也有大量的開源消息系統可用,譬如 RabbitMQ、Apache Kafka、Apache ActiveMQ 和 NSQ。宏觀上,它們都支持一些消息和渠道格式,而且努力提高可靠性、高性能和可擴展性。然而,細節上,它們的消息模型卻截然不同。
使用消息機制有不少優勢:
然而,消息機制也有本身的缺點:
使用同步的、基於請求/響應的 IPC 機制的時候,客戶端向服務端發送請求,服務端處理請求並返回響應。一些客戶端會因爲等待服務端響應而被阻塞,而另一些客戶端可能使用異步的、基於事件驅動的客戶端代碼,這些代碼可能經過 Future 或者 Rx Observable 封裝。然而,與使用消息機制不一樣,客戶端須要響應及時返回。這個模式中有不少可選的協議,但最多見的兩個協議是 REST 和 Thrift。首先咱們來了解 REST。
當前很流行開發 RESTful 風格的 API。REST 基於 HTTP 協議,其核心概念是資源典型地表明單一業務對象或者一組業務對象,業務對象包括「消費者」或「產品」。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(注:與本文做者 Chris 無任何關係)爲 REST 定義了一個成熟度模型,具體包含如下四個層次:
HTTP 很是簡單而且你們都很熟悉。
不足之處包括:
開發者社區最近從新認識到了 RESTful API 接口定義語言的價值,因而誕生了包括 RAML 和 Swagger 在內的服務框架。Swagger 這樣的 IDL 容許定義請求和響應消息的格式,而 RAML 容許使用 JSON Schema 這種獨立的規範。對於描述 API,IDL 一般都有工具從接口定義中生成客戶端存根和服務端框架。
Apache Thrift 是一個頗有趣的 REST 的替代品,實現了多語言 RPC 客戶端和服務端調用。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,就須要選擇消息格式。像 Thrift 這樣的 IPC 機制可能只支持少許消息格式,或許只支持一種格式。不管哪一種狀況,使用跨語言的消息格式很是重要。即使你如今使用單一語言實現微服務,但頗有可能將來須要用到其它語言。
目前有文本和二進制這兩種主要的消息格式。文本格式包括 JSON 和 XML。這種格式的優勢在於不只可讀,並且是自描述的。在 JSON 中,對象的屬性是名稱-值對的集合。與此相似,在 XML 中,屬性則表示爲命名的元素和值。消費者可以從中選擇感興趣的值同時忽略其它部分。相應地,對消息格式的小幅度修改也能容易地向後兼容。
XML 的文檔結構由 XML schema 定義。隨着時間發展,開發者社區意識到 JSON 也須要一個相似的機制。方法之一是使用 JSON Schema,要麼獨立使用,要麼做爲 Swagger 這類 IDL 的一部分。
文本消息格式的一大缺點是消息會變得冗長,特別是 XML。因爲消息是自描述的,因此每一個消息都包含屬性和值。另一個缺點是解析文本的負擔過大。因此,你可能須要考慮使用二進制格式。
二進制的格式也有不少。若是使用的是 Thrift RPC,那可使用二進制 Thrift。若是選擇消息格式,經常使用的還包括 Protocol Buffers 和 Apache Avro,兩者都提供類型 IDL 來定義消息結構。差別之處在於 Protocol Buffers 使用添加標記的字段(tagged fields),而 Avro 消費者須要瞭解模式來解析消息。
Martin Kleppmann 的博客文章 對 Thrift、Protocol Buffers 和 Avor 進行了詳細的比較。
微服務必須使用進程間通訊機制來交互。在設計服務的通訊模式時,你須要考慮幾個問題:服務如何交互,每一個服務如何標識 API,如何升級 API,以及如何處理局部失敗。微服務架構異步消息機制和同步請求/響應機制這兩類 IPC 機制可用。在下一篇文章中,咱們將會討論微服務架構中的服務發現問題。