最近參與了公司 API Gateway 的搭建工做,技術選型是 Netflix Zuul,主要聊一聊其中的一些心得和體會。json
本文主要是介紹使用 Zuul 且在不強制使用其餘 Neflix OSS 組件時,如何搭建生產環境的 Gateway,以及能使用 Gateway 作哪些事。不打算介紹任何關於如何快速搭建 Zuul,或是一些輕易集成 Eureka 之類的的方法,這些在官方文檔上已經介紹的很明確了。後端
API Gateway 是隨着微服務(Microservice)這個概念一塊兒興起的一種架構模式,它用於解決微服務過於分散,沒有一個統一的出入口進行流量管理的問題。跨域
用 Kong 官網的兩張圖來解釋再合適不過。緩存
當使用微服務構建整個 API 服務時,通常會有許許多多職責不一樣的應用在運行着,這些應用會須要一些通用的功能,例如鑑權、流控、監控、日誌統計。微信
在傳統的單體應用中,這些功能通常都是內嵌在應用中,做爲一個組件運行。可是在微服務模式下,不一樣種類且獨立運行的應用可能會有數十甚至數百種,繼續使用這種方式會形成很是高的管理和發佈成本。因此就須要在這些應用上抽象出一個統一的流量入口,完成這些功能的實現。網絡
在我看來,API Gateway 的職責主要分爲兩部分:架構
對服務應用有感知且重要的功能,例如鑑權。app
對服務應用無感知的邊緣服務,例如流控、監控、頁面級緩存等。負載均衡
對於 API Gateway,常見的選型有基於 Openresty 的 Kong、基於 Go 的 Tyk 和基於 Java 的 Zuul。ide
這三個選型自己沒有什麼明顯的區別,主要仍是看技術棧是否能知足快速應用和二次開發,例如我司原有的技術棧就是使用 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
,由於這個子類可使用不少已有的功能)。
---------------------------------------------
推薦閱讀: