使用API Gateway處理微服務請求轉發、合併

前面兩篇Docker微服務的服務發現以及Docker微服務的服務間通訊機制。主要介紹瞭如何解決微服務的服務發現和通訊問題。
在微服務的架構體系中,為了減少服務間的耦合,在劃分服務間的限界上下文的時候。會盡量減少微服務之間的 呼叫。在實際的需求場景中,往往要同時請求多個微服務資源。

解耦微服務的呼叫

如下面一個場景,”使用者訂單列表“的一個聚合頁面,需要請求”使用者服務“獲取基礎使用者資訊,以及”訂單服務“獲取訂單資訊,再通過請求”商品服務“獲取訂單列表的商品聚合訂單上的商品資訊。如下圖所示的場景 :
微服務Docker-GatewayAPI

如果讓客戶端發出多個請求來解決多個資訊聚合,則會增加客戶端的複雜度。比較合理的方式就是增加API Gateway層。API Gateway跟微服務一樣,也可以部署、執行在Docker容器中,也是一個Springboot應用。如下,通過Gateway API進行轉發後:
Docker微服務-GatewayApi

所有的請求的資訊,由Gateway進行聚合,Gateway也是進入系統的唯一節點。並且Gateway和所有微服務,以及提供給客戶端的也是Restful風格API。Gateway層的引入可以很好的解決資訊的聚合問題。而且可以更好得適配不同的客戶端的請求,比如H5的頁面不需要展示使用者資訊,而iOS客戶端需要展示使用者資訊,則只需要新增一個Gateway API請求資源即可,微服務層的資源不需要進行變更。

API Gateway的特點

API gateway除了可以進行請求的合併、轉發。還需要有其他的特點,才能成為一個完整的Gateway。

響應式程式設計

Gateway是所有客戶端請求的入口。就像Facade模式。為了提高請求的效能,最好選擇一套非阻塞I/O的框架。在一些多客戶端的場景下,前文舉例的“使用者訂單列表”。對於每個微服務請求不一定需要同步的。比如獲取使用者資訊,以及獲取訂單列表,就是兩個獨立請求。然後,獲取訂單的商品詳情,需要等訂單資訊返回之後,根據訂單的商品id列表再去請求商品微服務。為了減少整個請求的響應時間,需要Gateway能夠併發處理相互獨立的請求。一種解決方案就是採用響應式程式設計。
目前使用Java技術棧的響應式程式設計方式有,Java8的CompletableFuture,以及ReactiveX提供的基於JVM的實現-RxJava。

ReactiveX是一個使用可觀察資料流進行非同步程式設計的程式設計介面,ReactiveX結合了觀察者模式、迭代器模式和函數語言程式設計的精華。除了RxJava還有RxJS,RX.NET等多語言的實現。

對於Gateway來說,RxJava提供的Observable可以很好的解決並行的獨立I/O請求,並且如果微服務專案中使用Java8,那對RxJava的函式學習吸收會更快,基於Lambda風格的響應式程式設計,可以使程式碼更加簡潔。中文文件可以閱讀RxJava文件和教程
通過響應式程式設計的Observable模式,可以很簡潔、方便得建立事件流、資料流,以及用簡潔的函式進行資料的組合和轉換,同時可以訂閱任何可觀察的資料流並執行操作。
如下圖所示的“使用者訂單列表”的資源請求時序圖:
RxJava並行流處理-使用者訂單列表時序圖

響應式程式設計可以更好的處理各種執行緒同步、併發請求,通過Observables和Schedulers提供了透明的資料流、事件流的執行緒處理。對於敏捷模式下,響應式程式設計的程式碼更加簡潔,更好維護。作為為了適配不同客戶端以及會頻繁變更的Gateway來說,是一個很好的解決方案。

鑑權

通過Gateway作為系統的唯一入口,那麼所有的鑑權,都可以圍繞Gateway去做。在Springboot工程中,基礎的授權可以使用Spring Security(Spring Security也可以整合在Spring MVC專案中)。Spring Security主要使用AOP對資源請求進行攔截,內部維護了一個角色的Filter Chain。因為微服務都是通過Gateway請求的,所以微服務的@Secured可以根據Gateway的資源請求鑑權級別進行設定。對於各不同客戶端的Token資訊的加密、儲存以及驗證,需要應用自己完成。經過Gateway鑑權之後,解析後的token資訊可以直接傳遞給微服務層。
如果應用需要授權(對資源請求需要管理不同的角色、許可權),也只要在Gateway的Rest API基礎上基於AOP來做即可。統一管理鑑權和授權,這也是使用類似Facade模式的Gateway API的好處之一。

負載均衡

API Gateway跟Microservice一樣,作為Springboot應用,提供Rest api。所以同樣執行在Docker容器中。Gateway和微服務之間的服務發現還是可以採用前篇所述的客戶端發現模式,或者服務端發現模式。
在叢集環境下,API Gateway 可以暴露統一的埠,其例項會執行在不同IP的伺服器上。因為我們是使用阿里雲的ECS作為容器的基礎設施,所以在叢集環境的負載均衡也是使用阿里雲的負載均衡SLB,域名解析也使用AliyunDNS。下圖是一個簡單的網路請求的示意:

Gateway 微服務服務叢集

為了不暴露服務的埠和資源地址,也可以在Gateway之上再部署Nginx服務,外部的負載均衡設施比如SLB可以將請求轉發到Nginx伺服器,請求通過Nginx再轉發給Gateway埠。如果是自建機房的叢集,就需要搭建高可用的負載均衡中心。為了跨機器請求,最好使用Consul,Consul(Consul Template)+Registor+Haproxy來做服務發現和負載均衡中心。

快取

對於一些高QPS的請求,可以在API Gateway做多級快取。分散式的快取可以使用Mongo,Redis,Memcached等。如果是一些對實時性要求不高的,變化頻率不高但是高QPS的頁面級請求,也可以在Gateway層做本地快取。而且Gateway可以讓快取方案更靈活和通用。

API Gateway的錯誤處理

在Gateway的具體實現過程中,錯誤處理也是一個很重要的事情。對於Gateway的錯誤處理,可以使用Hystrix來處理請求的熔斷。並且RxJava自帶的onErrorReturn回撥也可以方便得處理錯誤資訊的返回。對於熔斷機制,主要需要處理以下幾個方面:

  • 服務請求的容錯處理
    作為一個合理的Gateway,其應該只負責處理資料流、事件流,而不應該處理業務邏輯。在
    處理多個微服務的請求時,會出現微服務請求的超時、不可用的情況。在一些特定的場景下,
    需要能夠合理得處理部分失敗。比如上例中的“使用者訂單列表”,當“User”微服務出現錯誤時,不應該影響“Order”資料的請求。最好的處理方式就是給當時錯誤的使用者資訊一個預設的返回,比如顯示一個預設頭像,預設使用者暱稱。然後對於正確的訂單,以及商品資訊給與正確的資料返回。如果這種場景下,當“Order”領域的微服務異常時,則應該給客戶端一個錯誤碼,以及合理的錯誤提示資訊。這樣的處理可以儘量在部分系統不可用時提升使用者體驗。使用RxJava時,具體的實現方式就是針對不同的客戶端請求的情況,寫好onErrorReturn,做好錯誤資料相容即可。
  • 異常的捕捉和記錄
    Gateway主要是做請求的轉發、合併。為了能清楚得排查問題,定位到具體哪個服務、甚至是哪個Docker容器的問題,需要Gateway能對不同型別的異常、業務錯誤進行捕捉和記錄。如果使用FeignClinet來請求微服務資源,可以通過對ErrorDecoder介面的實現,來針對Response結果進行進一步的過濾處理,以及在日誌中記錄下所有請求資訊。如果是使用Spring Rest Template,則可以通過定義一個定製化的RestTempate,並對返回的ResponseEntity進行解析。在返回序列化之後的結果物件之前,對錯誤資訊進行日誌記錄。
  • 超時機制
    Gateway執行緒中大多是IO執行緒,為了防止因為某一微服務請求阻塞,導致Gateway過多的等待執行緒,耗盡執行緒池、佇列等系統資源。需要Gateway中提供超時機制,對超時介面能進行優雅的服務降級。
    在SpringCloud的Feign專案中集成了HystrixHystrix提供了比較全面的超時處理的熔斷機制。預設情況下,超時機制是開啟的。除了可以配置超時相關的引數,Netflix還提供了基於Hytrix的實時監控Netflix -Dashboard,並且叢集服務只需再附加部署Netflix-Turbine。通用的Hytrix的配置項可以參考Hystrix-Configuration
    如果是使用RxJava的Observable的響應式程式設計,想對不同的請求設定不同的超時時間,可以直接在Observable的timeout()方法的引數進行設定回撥的方法以及超時時間等。
  • 重試機制
    對於一些關鍵的業務,在請求超時時,為了保證正確的資料返回,需要Gateway能提供重試機制。如果使用SpringCloudFeign,則其內建的Ribbon,會提供的預設的重試配置,可以通過設定spring.cloud.loadbalancer.retry.enabled=false將其關閉。Ribbon提供的重試機制會在請求超時或者socket read timeout觸發,除了設定重試,也可以定製重試的時間閥值以及重試次數等。
    對於除了使用Feign,也使用Spring RestTemplate的應用,可以通過自定義的RestTemplate,對於返回的ResponseEntity物件進行結果解析,如果請求需要重試(比如某個固定格式的error-code的方式識別重試策略),則通過Interceptor進行請求攔截,以及回撥的方式invoke多次請求。

小結

對於微服務的架構,通過一個獨立的API Gateway,可以進行統一的請求轉發、合併以及協議轉換。可以更靈活得適配不同客戶端的請求資料。而且對於不同客戶端的版本相容的請求可以很好地進行遮蔽,讓微服務更加純粹。微服務只要關注內部的領域服務的設計,事件的處理。
API gateway還可以對微服務的請求進行一定的容錯、服務降級。使用響應式程式設計來實現API gateway可以使執行緒同步、併發的程式碼更簡潔,更易於維護。在對於微服務的請求可以統一通過FeignClint。程式碼也會很有層次。如下圖,示例
Gateway rxJava userorders

  • Clint負責服務發現、負載均衡以及發出請求,並獲取ResponseEntity物件。
  • Translator將ResponseEntity轉換成Observable物件,以及對異常進行統一日誌採集,類似於DDD中防腐層的概念。
  • Adapter呼叫各個Translator,使用Observable函式,對請求的資料流進行合併。如果存在多個數據的組裝,可以增加一層Assembler專門處理DTO->Model的轉換。
  • Controller,提供Restful資源的管理,每個Controller只請求唯一的Adapter。