1. 程式人生 > >使用 API 閘道器構建微服務 & 微服務架構中的程序間通訊

使用 API 閘道器構建微服務 & 微服務架構中的程序間通訊

本期內容

微服務系列文章的第一篇介紹了微服務架構模式,討論了使用微服務的優缺點,以及為什麼微服務雖然複雜度高卻是複雜應用程式的理想選擇。

在決定以一組微服務來構建自己的應用時,你需要確定應用客戶端如何與微服務互動。

在單體式程式中,通常只有一組冗餘的或者負載均衡的服務提供點。在微服務架構中,每一個微服務暴露一組細粒度的服務提供點。在本篇文章中,我們來看它如何影響客戶端到服務端通訊,並提出一種使用 API 閘道器的方法。

簡要概述

讓我們想象一下,你要為一個購物應用程式開發一個原生移動客戶端。你很可能需要實現一個產品詳情頁面,展示任何指定商品的資訊。

下圖展示了 Amazon Android 應用在商品詳情頁顯示的內容。

Richardson-microservices-part2-1_amazon-apps

即使只是個智慧手機應用,產品詳情頁面也顯示了大量的資訊。該頁面不僅包含基本的產品資訊(如名稱、描述、價格),而且還顯示瞭如下內容:

  • 購物車中的商品數量
  • 歷史訂單
  • 客戶評論
  • 低庫存預警
  • 送貨選項
  • 各種推薦,包括經常與該商品一起購買的其它商品、購買該商品的客戶購買的其它商品、購買該商品的客戶看過的其它商品
  • 其它的購物選擇

使用單體應用程式架構時,移動客戶端通過嚮應用程式發起一次 REST 呼叫(GET api.company.com/productdetails/)來獲取這些資料。負載均衡器將請求路由給 N 個相同的應用程式例項中的其中之一。然後,應用程式會查詢各種資料庫表,並將響應返回給客戶端。

相反,若是採用微服務架構,顯示在產品頁上的資料會分佈在不同的微服務上。下面列舉了可能與產品詳情頁資料有關的一些微服務:

  • 購物車服務——購物車中的件數
  • 訂單服務——歷史訂單
  • 目錄服務——商品基本資訊,如名稱、圖片和價格
  • 評論服務——客戶的評論
  • 庫存服務——低庫存預警
  • 送貨服務——送貨選項、期限和費用,這些資訊單獨從送貨方 API 獲取
  • 推薦服務——推薦商品

Richardson-microservices-part2-2_microservices-client

我們需要決定移動客戶端如何訪問這些服務。讓我們看看有哪些方法。

客戶端與微服務直接通訊

從理論上講,客戶端可以直接向每個微服務傳送請求。每個微服務都有一個公開的端點(https ://.api.company.name)。該 URL 對映到微服務的負載均衡器,由後者負責在可用例項之間分發請求。為了獲取產品詳情,移動客戶端將逐一向上文列出的 N 個服務傳送請求。

遺憾的是,這種方法存在挑戰和侷限。問題之一是客戶端需求和每個微服務暴露的細粒度 API 不匹配。在這個例子中,客戶端需要傳送 7 個獨立請求。在更復雜的應用程式中,可能要傳送更多的請求;按照 Amazon 的說法,他們在顯示他們的產品頁面時就呼叫了數百個服務。然而,客戶端通過 LAN 傳送許多請求,這在公網上可能會很低效,在行動網路上就根本不可行。這種方法還使得客戶端程式碼非常複雜。

客戶端直接呼叫微服務的另一個問題是,部分服務使用的協議對 web 並不友好。一個服務可能使用 Thrift 二進位制 RPC,而另一個服務可能使用 AMQP 訊息傳遞協議。不管哪種協議對於瀏覽器或防火牆都不夠友好,最好是內部使用。在防火牆之外,應用程式應該使用諸如 HTTP 和 WebSocket 之類的協議。

這種方法的另一個缺點是,它會使得微服務難以重構。隨著時間推移,我們可能想要更改系統拆分成服務的方式。例如,我們可能合併兩個服務,或者將一個服務拆分成兩個或更多服務。然而,如果客戶端與微服務直接通訊,那麼執行這類重構就非常困難了。

由於上述三種問題的原因,客戶端直接與伺服器端通訊的方式很少在實際中使用。

使用 API 閘道器構建微服務

通常來說,使用 API 閘道器是更好的解決方式。API 閘道器是一個伺服器,也可以說是進入系統的唯一節點。這與面向物件設計模式中的 Facade 模式很像。API 閘道器封裝內部系統的架構,並且提供 API 給各個客戶端。它還可能還具備授權、監控、負載均衡、快取、請求分片和管理、靜態響應處理等功能。下圖展示了一個適應當前架構的 API 閘道器。

Richardson-microservices-part2-3_api-gateway

API 閘道器負責服務請求路由、組合及協議轉換。客戶端的所有請求都首先經過 API 閘道器,然後由它將請求路由到合適的微服務。API 閘道器經常會通過呼叫多個微服務併合並結果來處理一個請求。它可以在 web 協議(如 HTTP 與 WebSocket)與內部使用的非 web 友好協議之間轉換。

API 閘道器還能為每個客戶端提供一個定製的 API。通常,它會向移動客戶端暴露一個粗粒度的 API。以產品詳情的場景為例,API 閘道器可以提供一個端點(/productdetails?productid=xxx),使移動客戶端可以通過一個請求獲取所有的產品詳情。API 閘道器通過呼叫各個服務(產品資訊、推薦、評論等等)併合並結果來處理請求。

Netflix API 閘道器是一個很好的 API 閘道器例項。Netflix 流媒體服務提供給成百上千種類型的裝置使用,包括電視、機頂盒、智慧手機、遊戲系統、平板電腦等等。

最初,Netflix 試圖為他們的流媒體服務提供一個通用的 API。然而他們發現,由於各種各樣的裝置都有自己獨特的需求,這種方式並不能很好地工作。如今,他們使用一個 API 閘道器,通過執行與針對特定裝置的介面卡程式碼,來為每種裝置提供定製的 API。通常,一個介面卡通過呼叫平均 6 到 7 個後端服務來處理每個請求。Netflix API 閘道器每天處理數十億請求。

API 閘道器的優點和缺點

如你所料,使用 API 閘道器有優點也有不足。使用 API 閘道器的最大優點是,它封裝了應用程式的內部結構。客戶端只需要同閘道器互動,而不必呼叫特定的服務。API 閘道器為每一類客戶端提供了特定的 API,這減少了客戶端與應用程式間的互動次數,還簡化了客戶端程式碼。

API 閘道器也有一些不足。它增加了一個我們必須開發、部署和維護的高可用元件。還有一個風險是,API 閘道器變成了開發瓶頸。為了暴露每個微服務的端點,開發人員必須更新 API 閘道器。API閘道器的更新過程要儘可能地簡單,這很重要;否則,為了更新閘道器,開發人員將不得不排隊等待。不過,雖然有這些不足,但對於大多數現實世界的應用程式而言,使用 API 閘道器是合理的。

實現 API 閘道器

到目前為止,我們已經探討了使用 API 閘道器的動力及其優缺點。下面讓我們看一下需要考慮的各種設計問題。

效能和可擴充套件性

只有少數公司擁有 Netflix 這樣的規模,需要每天處理每天需要處理數十億請求。不管怎樣,對於大多數應用程式而言,API 閘道器的效能和可擴充套件性都非常重要。因此,將 API 閘道器構建在一個支援非同步、I/O 非阻塞的平臺上是合理的。有多種不同的技術可以實現一個可擴充套件的 API 閘道器。在 JVM 上,可以使用一種基於 NIO 的框架,比如 Netty、Vertx、Spring Reactor 或 JBoss Undertow 中的一種。一個非常流行的非 JVM 選項是 Node.js,它是一個基於 Chrome JavaScript 引擎構建的平臺。

另一個方法是使用 NGINX Plus。NGINX Plus 提供了一個成熟的、可擴充套件的、高效能 web 伺服器和一個易於部署的、可配置可程式設計的反向代理。NGINX Plus 可以管理身份驗證、訪問控制、負載均衡請求、快取響應,並提供應用程式可感知的健康檢查和監控。

使用響應式程式設計模型

API 閘道器通過簡單地將請求路由給合適的後端服務來處理部分請求,而通過呼叫多個後端服務併合並結果來處理其它請求。對於部分請求,比如產品詳情相關的多個請求,它們對後端服務的請求是獨立於其它請求的。為了最小化響應時間,API 閘道器應該併發執行獨立請求。

然而,有時候,請求之間存在依賴。在將請求路由到後端服務之前,API 閘道器可能首先需要呼叫身份驗證服務驗證請求的合法性。類似地,為了獲取客戶心願單中的產品資訊,API 閘道器必須首先獲取包含這些資訊的客戶資料,然後再獲取每個產品的資訊。關於 API 組合,另一個有趣的例子是 Netflix Video Grid。

使用傳統的非同步回撥方法編寫 API 組合程式碼會讓你迅速墜入回撥地獄。程式碼會變得混亂、難以理解且容易出錯。一個更好的方法是使用響應式方法,以一種宣告式樣式編寫 API 閘道器程式碼。響應式抽象概念的例子有 Scala 中的 Future、Java 8 中的 CompletableFuture 和 JavaScript 中的P romise,還有最初微軟為 .NET 平臺開發的 Reactive Extensions(RX)。Netflix 建立了 RxJava for JVM,專門用於他們的 API 閘道器。此外,還有 RxJS for JavaScript,它既可以在瀏覽器中執行,也可以在 Node.js 中執行。使用響應式方法能讓你編寫簡單但高效的 API 閘道器程式碼。

服務呼叫

基於微服務的應用程式是一個分散式系統,必須使用一種程序間通訊機制。有兩種型別的程序間通訊機制可供選擇。一種是使用非同步的、基於訊息傳遞的機制。有些實現使用諸如 JMS 或 AMQP 那樣的訊息代理,而其它的實現(如 Zeromq)則沒有代理,服務間直接通訊。

另一種程序間通訊型別是諸如 HTTP 或 Thrift 那樣的同步機制。通常,一個系統會同時使用非同步和同步兩種型別。它甚至還可能使用同一型別的多種實現。總之,API 閘道器需要支援多種通訊機制。

服務發現

API 閘道器需要知道它與之通訊的每個微服務的位置(IP 地址和埠)。在傳統的應用程式中,或許可以硬連線這個位置,但在現代的、基於雲的微服務應用程式中,這並不是一個容易解決的問題。基礎設施服務(如訊息代理)通常會有一個靜態位置,可以通過 OS 環境變數指定。但是,確定一個應用程式服務的位置沒有這麼簡單。應用程式服務的位置是動態分配的,而且,單個服務的一組例項也會隨著自動擴充套件或升級而動態變化。

總之,像系統中的其它服務客戶端一樣,API 閘道器需要使用系統的服務發現機制,可以是伺服器端發現,也可以是客戶端發現。下一篇文章將更詳細地描述服務發現。現在,需要注意的是,如果系統使用客戶端發現,那麼 API 閘道器必須能夠查詢服務註冊中心,這是一個包含所有微服務例項及其位置的資料庫。

處理區域性失敗

在實現 API 閘道器時,還需要處理區域性失敗的問題。該問題出現在所有的分散式系統中。當一個服務呼叫另一個服務,而後者響應慢或不可用的時候,就會出現這個問題。API 閘道器不能因為無限期地等待下游服務而阻塞。不過,如何處理失敗取決於特定的場景以及哪個服務失敗。例如,在產品詳情場景下,如果推薦服務無響應,那麼 API 閘道器應該向客戶端返回產品詳情的其它內容,因為它們對使用者依然有用。推薦內容可以為空,也可以用一個固定的 TOP 10 列表取代。不過,如果產品資訊服務無響應,那麼 API 閘道器應該向客戶端返回一個錯誤資訊。

如果快取資料可用,那麼 API 閘道器還可以返回快取資料。例如,鑑於產品價格不會頻繁變動,如果價格服務不可用,API 閘道器可以返回快取的價格資料。資料可以由 API 閘道器自己快取,也可以儲存在像 Redis 或 Memcached 之類的外部快取中。通過返回預設資料或者快取資料,API 閘道器可以確保系統故障不影響使用者體驗。

在編寫程式碼呼叫遠端服務方面,Netflix Hystrix 是一個格外有用的庫。Hystrix 會暫停超出特定閾限的呼叫。它實現了一個“斷路器(circuit breaker)”模式,可以防止客戶端對無響應的服務進行不必要的等待。如果服務的錯誤率超出了設定的閾值,那麼 Hystrix 會啟動斷路器,所有請求會立即失敗並持續一定時間。Hystrix 允許使用者定義一個請求失敗後的後援操作,比如從快取讀取資料,或者返回一個預設值。如果你正在使用 JVM,那麼你應該考慮使用 Hystrix;如果你正在使用一個非 JVM 環境,那麼可以使用一個功能相同的庫。

總結

對於大多數基於微服務的應用程式而言,實現 API 閘道器,將其作為系統的唯一入口很有必要。API 閘道器負責服務請求路由、組合及協議轉換。它為每個應用程式客戶端提供一個定製的 API。API 閘道器還可以通過返回快取資料或預設資料遮蔽後端服務失敗。在本系列的下一篇文章中,我們將探討服務間通訊。

簡介

在單體應用中,各模組之間的呼叫是通過程式語言級別的方法或者函式來實現的。而基於微服務的分散式應用是執行在多臺機器上的;一般來說,每個服務例項都是一個程序。

因此,如下圖所示,服務之間的互動必須通過程序間通訊(IPC)來實現。

Richardson-microservices-part3-monolith-vs-microservices-1024x518-1

後面我們將會詳細介紹 IPC 技術,現在我們先來看下設計相關的問題。

交付模式

當為某個服務選擇 IPC 時,首先需要考慮服務之間的互動問題。客戶端和伺服器之間有很多的互動模式,我們可以從兩個維度進行歸類。第一個維度是一對一還是一對多:

• 一對一:每個客戶端請求有一個服務例項來響應。

• 一對多:每個客戶端請求有多個服務例項來響應。

第二個維度是這些互動式是同步還是非同步:

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

• 非同步模式:客戶端請求不會阻塞程序,服務端的響應可以是非即時的。

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

Screen Shot 2016-05-18 at 10.17.41 AM

一對一的互動模式有以下幾種方式:

請求/響應:一個客戶端向伺服器端發起請求,等待響應,客戶端期望此響應即時到達。在一個基於執行緒的應用中,等待過程可能造成執行緒阻塞。
通知(也就是常說的單向請求):一個客戶端請求傳送到服務端,但是並不期望服務端響應。
請求/非同步響應:客戶端傳送請求到服務端,服務端非同步響應請求。客戶端不會阻塞,而且被設計成預設響應不會立刻到達。
一對多的互動模式有以下幾種方式:

釋出/ 訂閱模式:客戶端釋出通知訊息,被零個或者多個感興趣的服務消費。

釋出/非同步響應模式:客戶端釋出請求訊息,然後等待從感興趣服務發回的響應。

每個服務都是以上這些模式的組合。對某些服務,一個 IPC 機制就足夠了;而對另外一些服務則需要多種 IPC 機制組合。下圖展示了在使用者叫車時,打車應用內的服務是如何互動的。

Richardson-microservices-part3-taxi-service-1024x609-2

上圖中的服務通訊使用了通知、請求/響應、釋出/訂閱等方式。例如,乘客在移動端向“行程管理”服務傳送通知,請求一次接送服務。“行程管理”服務通過使用請求/響應來喚醒“乘客服務”來驗證乘客賬號有效,繼而建立此次行程,並利用釋出/訂閱來通知其它服務,其中包括定位可用司機的排程服務。

現在我們瞭解了互動模式,接下來我們一起來看看如何定義 API。

定義 API

API 是服務端和客戶端之間的契約。無論選擇了何種 IPC 機制,重點是使用某種互動定義語言(IDL)來準確定義服務的 API。對於如何使用 API 優先的方式來定義服務,已經有了一些很好的討論。你在開發服務之前,要定義服務介面並與客戶端開發者共同討論,後續只需要迭代 API 定義。這樣的設計能夠大幅提升服務的可用度。

在本文後半部分你將會看到,API 定義實質上依賴於選定的 IPC 機制。如果使用訊息機制,API 則由訊息頻道(channel)和訊息型別構成;如果選擇使用 HTTP 機制,API 則由 URL 和請求、響應格式構成。後面將會詳細描述 IDL。

不斷進化的 API

服務的 API 會隨著時間而不斷變化。在單體應用中,經常會直接修改 API 並更新所有的呼叫者。但是在基於微服務的應用中,即使所有的 API 的使用者都在同一應用中,這種做法也困難重重,通常不能強制讓所有客戶端都與服務保持同步更新。此外,你可能會增量部署服務的新版本,這時舊版本會與新版本同時執行。瞭解這些問題的處理策略至關重要。
對 API 變化的處理方式與變化的大小有關。有的變化很小,並且可以相容之前的版本;比如給請求或響應增加屬性。在設計客戶端和服務時,很有必要遵循健壯性原則。服務更新版本後,使用舊版 API 的客戶端應該繼續使用。服務為缺失的請求屬性提供預設值,客戶端則忽略任何額外的響應。使用 IPC 機制和訊息格式能夠讓你輕鬆改進 API。

然而有時候,API 需要進行大規模改動,並且不相容舊版本。鑑於不能強制讓所有客戶端立即升級,支援舊版 API 的服務還要再執行一段時間。如果你使用的是諸如 REST 這樣的基於 HTTP 機制的 IPC,一種方法就是將版本號嵌入到 URL 中,每個服務例項可以同時處理多個版本。另一種方法是部署不同例項,每個例項處理一個版本的請求。

處理區域性失敗

在上一篇關於 API 閘道器的文章中,我們瞭解到,分散式系統普遍存在區域性失敗的問題。由於客戶端和服務端是獨立的程序,服務端可能無法及時響應客戶端請求。服務端可能會因為故障或者維護而暫時不可用。服務端也可能會由於過載,導致對請求的響應極其緩慢。

以上篇文章中提及的產品頁為例,假設推薦服務無法響應,客戶端可能會由於無限期等待響應而阻塞。這不僅會導致很差的使用者體驗,並且在很多應用中還會佔用之前的資源,比如執行緒;最終,如下圖所示,執行時耗盡執行緒資源,無法響應。

Richardson-microservices-part3-threads-blocked-1024x383-3

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

Netfilix 提供了一個比較好的解決方案,具體的應對措施包括:

  • 網路超時:在等待響應時,不設定無限期阻塞,而是採用超時策略。使用超時策略可以確保資源不被無限期佔用。
  • 限制請求的次數:可以為客戶端對某特定服務的請求設定一個訪問上限。如果請求已達上限,就要立刻終止請求服務。
  • 斷路器模式(Circuit Breaker Pattern):記錄成功和失敗請求的數量。如果失效率超過一個閾值,觸發斷路器使得後續的請求立刻失敗。如果大量的請求失敗,就可能是這個服務不可用,再發請求也無意義。在一個失效期後,客戶端可以再試,如果成功,關閉此斷路器。
  • 提供回滾:當一個請求失敗後可以進行回滾邏輯。例如,返回快取資料或者一個系統預設值。
    Netflix Hystrix 是一個實現相關模式的開源庫。如果使用 JVM,推薦使用Hystrix。而如果使用非 JVM 環境,你可以使用類似功能的庫。

IPC 技術

現在有很多不同的 IPC 技術。服務間通訊可以使用同步的請求/響應模式,比如基於 HTTP 的 REST 或者 Thrift。另外,也可以選擇非同步的、基於訊息的通訊模式,比如 AMQP 或者 STOMP。此外,還可以選擇 JSON 或者 XML 這種可讀的、基於文字的訊息格式。當然,也還有效率更高的二進位制格式,比如 Avro 和 Protocol Buffer。在討論同步的 IPC 機制之前,我們先了解非同步的 IPC 機制。

基於訊息的非同步通訊

使用訊息模式的時候,程序之間通過非同步交換訊息訊息的方式通訊。客戶端通過向服務端傳送訊息提交請求,如果服務端需要回復,則會發送另一條獨立的訊息給客戶端。由於非同步通訊,客戶端不會因為等待而阻塞,相反會認為響應不會被立即收到。

訊息由資料頭(例如傳送方這樣的元資料)和訊息正文構成。訊息通過渠道傳送,任何數量的生產者都可以傳送訊息到渠道,同樣,任何數量的消費者都可以從渠道中接受資料。頻道有兩類,包括點對點渠道和釋出/訂閱渠道。點對點渠道會把訊息準確的傳送到從渠道讀取訊息的使用者,服務端使用點對點來實現之前提到的一對一互動模式;而釋出/訂閱則把訊息投送到所有從渠道讀取資料的使用者,服務端使用釋出/訂閱渠道來實現上面提到的一對多互動模式。

下圖展示了打車軟體如何使用釋出/訂閱:

Richardson-microservices-part3-pub-sub-channels-1024x639-4

通過向釋出/訂閱渠道寫入一條建立行程的訊息,行程管理服務會通知排程服務有新的行程請求。排程服務發現可用的司機後會向釋出/訂閱渠道寫入一條推薦司機的訊息,並通知其它服務。

有多種訊息系統可供選擇,最好選擇支援多程式語言的。有的訊息系統支援 AMQP 和 STOMP 這樣的標準協議,有的則支援專利協議。也有大量的開源訊息系統可用,譬如 RabbitMQ、Apache Kafka、Apache ActiveMQ 和 NSQ。巨集觀上,它們都支援一些訊息和渠道格式,並且努力提升可靠性、高效能和可擴充套件性。然而,細節上,它們的訊息模型卻大相徑庭。

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

  • 解耦客戶端和服務端:客戶端只需要將訊息傳送到正確的渠道。客戶端完全不需要了解具體的服務例項,更不需要一個發現機制來確定服務例項的位置。
  • 訊息緩衝:在 HTTP 這樣的同步請求/響應協議中,所有的客戶端和服務端必須在互動期間保持可用。而在訊息模式中,訊息中間人將所有寫入渠道的訊息按照佇列方式管理,直到被消費者處理。也就是說,線上商店可以接受客戶訂單,即使下單系統很慢或者不可用,只要保持下單訊息進入佇列就好了。
  • 客戶端-服務端的靈活互動:訊息機制支援以上說的所有互動模式。
  • 清晰的程序間通訊:基於 RPC 的通訊機制試圖讓喚醒遠端服務端像呼叫本地服務一樣,然而,囿於物理定律和可能的區域性失敗,這二者大不相同。訊息機制能讓這些差異直觀明確,開發者不會產生安全錯覺。

然而,訊息機制也有自己的缺點:

  • 額外的操作複雜性:訊息系統需要單獨安裝、配置和部署。訊息broker(代理)必須高可用,否則系統可靠性將會受到影響。
  • 實現基於請求/響應互動模式的複雜性:請求/響應互動模式需要完成額外的工作。每個請求訊息必須包含一個回覆渠道 ID 和相關 ID。服務端傳送一個包含相關 ID 的響應訊息到渠道中,使用相關 ID 來將響應對應到發出請求的客戶端。這種情況下,使用一個直接支援請求/響應的 IPC 機制會更容易些。

現在我們已經瞭解了基於訊息的 IPC,接下來我們來看看基於請求/響應模式的 IPC。

基於請求/響應的同步 IPC

使用同步的、基於請求/響應的 IPC 機制的時候,客戶端向服務端傳送請求,服務端處理請求並返回響應。一些客戶端會由於等待服務端響應而被阻塞,而另外一些客戶端可能使用非同步的、基於事件驅動的客戶端程式碼,這些程式碼可能通過 Future 或者 Rx Observable 封裝。然而,與使用訊息機制不同,客戶端需要響應及時返回。這個模式中有很多可選的協議,但最常見的兩個協議是 REST 和 Thrift。首先我們來了解 REST。

REST

當前很流行開發 RESTful 風格的 API。REST 基於 HTTP 協議,其核心概念是資源典型地代表單一業務物件或者一組業務物件,業務物件包括“消費者”或“產品”。REST 使用 HTTP 協議來控制資源,通過 URL 實現。譬如,GET 請求會返回一個資源的包含資訊,可能是 XML 文件或 JSON 物件格式。POST 請求會建立新資源,而 PUT 請求則會更新資源。REST 之父 Roy Fielding 曾經說過:

REST 提供了一系列架構系統引數,作為整體使用,強調元件互動的擴充套件性、介面的通用性、元件的獨立部署、以及減少互動延遲的中介軟體,它強化安全,也能封裝遺留系統。

— Fielding, Architectural Styles and the Design of Network-based Software Architectures
下圖展示了打車軟體如何使用 REST。

Richardson-microservices-part3-rest-1024x397-5

乘客通過移動端向行程管理服務的 /trips 資源提交了一個 POST請求。行程管理服務收到請求之後,會發送一個 GET 請求到乘客管理服務以獲取乘客資訊。當確認乘客資訊之後,隨即建立一個行程,並向移動端返回 201 響應。

很多開發者都表示他們基於 HTTP 的 API 是 RESTful 風格。但是,如同 Fielding 在他的部落格中所說,並非所有這些 API 都是 RESTful。Leonard Richardson(注:與本文作者 Chris 無任何關係)為 REST 定義了一個成熟度模型,具體包含以下四個層次:

  • Level 0:本層級的 Web 服務只是使用 HTTP 作為傳輸方式,實際上只是遠端方法呼叫(RPC)的一種具體形式。SOAP 和 XML-RPC 都屬於此類。
  • Level 1:Level 1 層級的 API 引入了資源的概念。要執行對資源的操作,客戶端發出指定要執行的操作和任何引數的 POST 請求。
  • Level 2:Level 2 層級的 API 使用 HTTP 語法來執行操作,譬如 GET 表示獲取、POST 表示建立、PUT 表示更新。如有必要,請求引數和主體指定操作的引數。這能夠讓服務影響 web 基礎設施服務,如快取 GET 請求。
  • Level 3:Level 3 層級的 API 基於 HATEOAS(Hypertext As The Engine Of Application State)原則設計,基本思想是在由 GET請求返回的資源資訊中包含連結,這些連結能夠執行該資源允許的操作。例如,客戶端通過訂單資源中包含的連結取消某一訂單,GET 請求被髮送去獲取該訂單。HATEOAS 的優點包括無需在客戶端程式碼中寫入硬連結的 URL。此外,由於資源資訊中包含可允許操作的連結,客戶端無需猜測在資源的當前狀態下執行何種操作。

使用基於 HTTP 的協議有如下好處:

  • HTTP 非常簡單並且大家都很熟悉。
  • 可以使用瀏覽器擴充套件(比如 Postman)或者 curl 之類的命令列來測試 API。
  • 內建支援請求/響應模式的通訊。
  • HTTP 對防火牆友好。
  • 不需要中間代理,簡化了系統架構。

不足之處包括:

  • 只支援請求/響應模式互動。儘管可以使用 HTTP 通知,但是服務端必須一直髮送 HTTP 響應。
  • 由於客戶端和服務端直接通訊(沒有代理或者緩衝機制),在互動期間必須都保持線上。
  • 客戶端必須知道每個服務例項的 URL。如前篇文章“API 閘道器”所述,這也是個煩人的問題。客戶端必須使用服務例項發現機制。

開發者社群最近重新認識到了 RESTful API 介面定義語言的價值,於是誕生了包括 RAML 和 Swagger 在內的服務框架。Swagger 這樣的 IDL 允許定義請求和響應訊息的格式,而 RAML 允許使用 JSON Schema 這種獨立的規範。對於描述 API,IDL 通常都有工具從介面定義中生成客戶端存根和服務端框架。

Thrift

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 機制可用。在下一篇文章中,我們將會討論微服務架構中的服務發現問題。