聊聊 API Gateway 和 Netflix Zuul
轉自:http://www.scienjus.com/api-gateway-and-netflix-zuul/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
最近參與了公司 API Gateway 的搭建工作,技術選型是 Netflix Zuul,主要聊一聊其中的一些心得和體會。
本文主要是介紹使用 Zuul 且在不強制使用其他 Neflix OSS 組件時,如何搭建生產環境的 Gateway,以及能使用 Gateway 做哪些事。不打算介紹任何關於如何快速搭建 Zuul,或是一些輕易集成 Eureka 之類的的方法,這些在官方文檔上已經介紹的很明確了。
API Gateway
API Gateway 是隨著微服務(Microservice)這個概念一起興起的一種架構模式,它用於解決微服務過於分散,沒有一個統一的出入口進行流量管理的問題。
用 Kong 官網的兩張圖來解釋再合適不過。
當使用微服務構建整個 API 服務時,一般會有許許多多職責不同的應用在運行著,這些應用會需要一些通用的功能,例如鑒權、流控、監控、日誌統計。
在傳統的單體應用中,這些功能一般都是內嵌在應用中,作為一個組件運行。但是在微服務模式下,不同種類且獨立運行的應用可能會有數十甚至數百種,繼續使用這種方式會造成非常高的管理和發布成本。所以就需要在這些應用上抽象出一個統一的流量入口,完成這些功能的實現。
在我看來,API Gateway 的職責主要分為兩部分:
- 對服務應用有感知且重要的功能,例如鑒權。
- 對服務應用無感知的邊緣服務,例如流控、監控、頁面級緩存等。
Netflix Zuul
對於 API Gateway,常見的選型有基於 Openresty 的 Kong、基於 Go 的 Tyk 和基於 Java 的 Zuul。
這三個選型本身沒有什麽明顯的區別,主要還是看技術棧是否能滿足快速應用和二次開發,例如我司原有的技術棧就是使用 Go/Openresty 的平臺組和使用 Java 的後端組,討論後覺得 API Gateway 未來還是處理業務功能的場景更多些,而且後端這邊有很多功能可以直接移植過來,最終就選擇了 Zuul。
關於 Zuul,大部分使用 Java 做微服務的人可能都會或多或少了解 Spring Cloud 和 Netflix 全家桶。而對於完全不了解的人,可以暫時將它想象為一個類似於 Servlet 中過濾器(Filter)的概念。
就像上圖中所描述的一樣,Zuul 提供了四種過濾器的 API,分別為前置(Pre)、後置(Post)、路由(Route)和錯誤(Error)四種處理方式。
一個請求會先按順序通過所有的前置過濾器,之後在路由過濾器中轉發給後端應用,得到響應後又會通過所有的後置過濾器,最後響應給客戶端。在整個流程中如果發生了異常則會跳轉到錯誤過濾器中。
一般來說,如果需要在請求到達後端應用前就進行處理的話,會選擇前置過濾器,例如鑒權、請求轉發、增加請求參數等行為。在請求完成後需要處理的操作放在後置過濾器中完成,例如統計返回值和調用時間、記錄日誌、增加跨域頭等行為。路由過濾器一般只需要選擇 Zuul 中內置的即可,錯誤過濾器一般只需要一個,這樣可以在 Gateway 遇到錯誤邏輯時直接拋出異常中斷流程,並直接統一處理返回結果。
應用場景
以下介紹一些 Zuul 中不同過濾器的應用場景。
前置過濾器
鑒權
一般來說整個服務的鑒權邏輯可以很復雜。
- 客戶端:App、Web、Backend
- 權限組:用戶、後臺人員、其他開發者
- 實現:OAuth、JWT
- 使用方式:Token、Cookie、SSO
而對於後端應用來說,它們其實只需要知道請求屬於誰,而不需要知道為什麽,所以 Gateway 可以友善的幫助後端應用完成鑒權這個行為,並將用戶的唯一標示透傳到後端,而不需要、甚至不應該將身份信息也傳遞給後端,防止某些應用利用這些敏感信息做錯誤的事情。
Zuul 默認情況下在處理後會刪除請求的 Authorization
頭和 Set-Cookie
頭,也算是貫徹了這個原則。
流量轉發
流量轉發的含義就是將指向 /a/xxx.json
的請求轉發到指向 /b/xxx.json
的請求。這個功能可能在一些項目遷移、或是灰度發布上會有一些用處。
在 Zuul 中並沒有一個很好的辦法去修改 Request URI。在某些 Issue 中開發者會建議設置 requestURI
這個屬性,但是實際在 Zuul 自身的 PreDecorationFilter
流程中又會被覆蓋一遍。
不過對於一個基於 Servlet 的應用,使用 HttpServletRequestWrapper
基本可以解決一切問題,在這個場景中只需要重寫其 getRequestURI
方法即可。
class RewriteURIRequestWrapper extends HttpServletRequestWrapper {
|
後置過濾器
跨域
使用 Gateway 做跨域相比應用本身或是 Nginx 的好處是規則可以配置的更加靈活。例如一個常見的規則。
- 對於任意的 AJAX 請求,返回
Access-Control-Allow-Origin
為*
,且Access-Control-Allow-Credentials
為true
,這是一個常用的允許任意源跨域的配置,但是不允許請求攜帶任何 Cookie - 如果一個被信任的請求者需要攜帶 Cookie,那麽將它的
Origin
增加到白名單中。對於白名單中的請求,返回Access-Control-Allow-Origin
為該域名,且Access-Control-Allow-Credentials
為true
,這樣請求者可以正常的請求接口,同時可以在請求接口時攜帶 Cookie - 對於 302 的請求,即使在白名單內也必須要設置
Access-Control-Allow-Origin
為*
,否則重定向後的請求攜帶的Origin
會為null
,有可能會導致 iOS 低版本的某些兼容問題
統計
Gateway 可以統一收集所有應用請求的記錄,並寫入日誌文件或是發到監控系統,相比 Nginx 的 access log,好處主要也是二次開發比較方便,比如可以關註一些業務相關的 HTTP 頭,或是將請求參數和返回值都保存為日誌打入消息隊列中,便於線上故障調試。也可以收集一些性能指標發送到類似 Statsd 這樣的監控平臺。
錯誤過濾器
錯誤過濾器的主要用法就像是 Jersey 中的 ExceptionMapper
或是 Spring MVC 中的 @ExceptionHandler
一樣,在處理流程中認為有問題時,直接拋出統一的異常,錯誤過濾器捕獲到這個異常後,就可以統一的進行返回值的封裝,並直接結束該請求。
配置管理
雖然將這些邏輯都切換到了 Gateway,省去了很多維護和叠代的成本,但是也面臨著一個很大的問題,就是 Gateway 只有邏輯卻沒有配置,它並不知道一個請求要走哪些流程。
例如同樣是後端服務 API,有的可能是給網頁版用的、有的是給客戶端用的,亦或是有的給用戶用、有的給管理人員用,那麽 Gateway 如何知道到底這些 API 是否需要登錄、流控以及緩存呢?
理論上我們可以為 Gateway 編寫一個管理後臺,裏面有當前服務的所有 API,每一個開發者都可以在裏面創建新的 API,以及為它增加鑒權、緩存、跨域等功能。為了簡化使用,也許我們會額外的增加一個權限組,例如 /admin/*
下的所有 API 都應該為後臺接口,它只允許內部來源的鑒權訪問。
但是這樣做依舊太復雜了,而且非常硬編碼,當開發者開發了一個新的 API 之後,即使這個應用已經能正常接收特定 URI 的請求並處理之後,卻還要通過人工的方式去一個管理後臺進行額外的配置,而且可能會因為不謹慎打錯了路徑中的某個單詞而造成不必要的事故,這都是不合理的。
我個人推薦的做法是,在後端應用中依舊保持配置的能力,即使應用裏已經沒有真實處理的邏輯了。例如在 Java 中通過註解聲明式的編寫 API,且在應用啟動時自動註冊 Gateway 就是一種比較好的選擇。
/**
|
這樣 API 的編寫者就會根據業務場景考慮該 API 需要哪些功能,也減少了管理的復雜度。
除此之外還會有一些後端應用無關的配置,有些是自動化的,例如惡意請求攔截,Gateway 會將所有請求的信息通過消息隊列發送給一些實時數據分析的應用,這些應用會對請求分析,發現惡意請求的特征,並通過 Gateway 提供的接口將這些特征上報給 Gateway,Gateway 就可以實時的對這些惡意請求進行攔截。
穩定性
在 Nginx 和後端應用之間又建立了一個 Java 應用作為流量入口,很多人會去擔心它的穩定性,亦或是擔心它能否像 Nginx 一樣和後端的多個 upstream 進行交互,以下主要介紹一下 Zuul 的隔離機制以及重試機制。
隔離機制
在微服務的模式下,應用之間的聯系變得沒那麽強烈,理想中任何一個應用超過負載或是掛掉了,都不應該去影響到其他應用。但是在 Gateway 這個層面,有沒有可能出現一個應用負載過重,導致將整個 Gateway 都壓垮了,已致所有應用的流量入口都被切斷?
這當然是有可能的,想象一個每秒會接受很多請求的應用,在正常情況下這些請求可能在 10 毫秒之內就能正常響應,但是如果有一天它出了問題,所有請求都會 Block 到 30 秒超時才會斷開(例如頻繁 Full GC 無法有效釋放內存)。那麽在這個時候,Gateway 中也會有大量的線程在等待請求的響應,最終會吃光所有線程,導致其他正常應用的請求也受到影響。
在 Zuul 中,每一個後端應用都稱為一個 Route,為了避免一個 Route 搶占了太多資源影響到其他 Route 的情況出現,Zuul 使用 Hystrix 對每一個 Route 都做了隔離和限流。
Hystrix 的隔離策略有兩種,基於線程或是基於信號量。Zuul 默認的是基於線程的隔離機制,這意味著每一個 Route 的請求都會在一個固定大小且獨立的線程池中執行,這樣即使其中一個 Route 出現了問題,也只會是某一個線程池發生了阻塞,其他 Route 不會受到影響。
一般使用 Hystrix 時,只有調用量巨大會受到線程開銷影響時才會使用信號量進行隔離策略,對於 Zuul 這種網絡請求的用途使用線程隔離更加穩妥。
重試機制
一般來說,後端應用的健康狀態是不穩定的,應用列表隨時會有修改,所以 Gateway 必須有足夠好的容錯機制,能夠減少後端應用變更時造成的影響。
Zuul 的路由主要有 Eureka 和 Ribbon 兩種方式,由於我一直使用的都是 Ribbon,所以簡單介紹下 Ribbon 支持哪些容錯配置。
重試的場景分為三種:
okToRetryOnConnectErrors
:只重試網絡錯誤okToRetryOnAllErrors
:重試所有錯誤OkToRetryOnAllOperations
:重試所有操作(這裏不太理解,猜測是 GET/POST 等請求都會重試)
重試的次數有兩種:
MaxAutoRetries
:每個節點的最大重試次數MaxAutoRetriesNextServer
:更換節點重試的最大次數
一般來說我們希望只在網絡連接失敗時進行重試、或是對 5XX 的 GET 請求進行重試(不推薦對 POST 請求進行重試,無法保證冪等性會造成數據不一致)。單臺的重試次數可以盡量小一些,重試的節點數盡量多一些,整體效果會更好。
如果有更加復雜的重試場景,例如需要對特定的某些 API、特定的返回值進行重試,那麽也可以通過實現 RequestSpecificRetryHandler
定制邏輯(不建議直接使用 RetryHandler
,因為這個子類可以使用很多已有的功能)。
聊聊 API Gateway 和 Netflix Zuul