API 網(wǎng)關(guān)性能比較:Nginx vs. Zuul vs. Spring Cloud Gateway vs. Linkerd
API 網(wǎng)關(guān)
API 網(wǎng)關(guān)出現(xiàn)的原因是微服務(wù)架構(gòu)的出現(xiàn),不同的微服務(wù)一般會有不同的網(wǎng)絡(luò)地址,而外部客戶端可能需要調(diào)用多個服務(wù)的接口才能完成一個業(yè)務(wù)需求,如果讓客戶端直接與各個微服務(wù)通信,會有以下的問題:
- 客戶端會多次請求不同的微服務(wù),增加了客戶端的復(fù)雜性。
- 存在跨域請求,在一定場景下處理相對復(fù)雜。
- 認(rèn)證復(fù)雜,每個服務(wù)都需要獨立認(rèn)證。
- 難以重構(gòu),隨著項目的迭代,可能需要重新劃分微服務(wù)。例如,可能將多個服務(wù)合并成一個或者將一個服務(wù)拆分成多個。如果客戶端直接與微服務(wù)通信,那么重構(gòu)將會很難實施。
- 某些微服務(wù)可能使用了防火墻 / 瀏覽器不友好的協(xié)議,直接訪問會有一定的困難。
以上這些問題可以借助 API 網(wǎng)關(guān)解決。API 網(wǎng)關(guān)是介于客戶端和服務(wù)器端之間的中間層,所有的外部請求都會先經(jīng)過 API 網(wǎng)關(guān)這一層。也就是說,API 的實現(xiàn)方面更多的考慮業(yè)務(wù)邏輯,而安全、性能、監(jiān)控可以交由 API 網(wǎng)關(guān)來做,這樣既提高業(yè)務(wù)靈活性又不缺安全性,典型的架構(gòu)圖如圖所示:
使用 API 網(wǎng)關(guān)后的優(yōu)點如下:
- 易于監(jiān)控??梢栽诰W(wǎng)關(guān)收集監(jiān)控數(shù)據(jù)并將其推送到外部系統(tǒng)進(jìn)行分析。
- 易于認(rèn)證。可以在網(wǎng)關(guān)上進(jìn)行認(rèn)證,然后再將請求轉(zhuǎn)發(fā)到后端的微服務(wù),而無須在每個微服務(wù)中進(jìn)行認(rèn)證。
- 減少了客戶端與各個微服務(wù)之間的交互次數(shù)。
NGINX 服務(wù)
Nginx 由內(nèi)核和模塊組成,內(nèi)核的設(shè)計非常微小和簡潔,完成的工作也非常簡單,僅僅通過查找配置文件與客戶端請求進(jìn)行 URL 匹配,用于啟動不同的模塊去完成相應(yīng)的工作。
下面這張圖反應(yīng)的是 HTTP 請求的常規(guī)處理流程:
Nginx 的模塊直接被編譯進(jìn) Nginx,因此屬于靜態(tài)編譯方式。啟動 Nginx 后,Nginx 的模塊被自動加載,不像 Apache,首先將模塊編譯為一個 so 文件,然后在配置文件中指定是否進(jìn)行加載。在解析配置文件時,Nginx 的每個模塊都有可能去處理某個請求,但是同一個處理請求只能由一個模塊來完成。
Nginx 在啟動后,會有一個 Master 進(jìn)程和多個 Worker 進(jìn)程,Master 進(jìn)程和 Worker 進(jìn)程之間是通過進(jìn)程間通信進(jìn)行交互的,如圖所示。Worker 工作進(jìn)程的阻塞點是在像 select()、epoll_wait() 等這樣的 I/O 多路復(fù)用函數(shù)調(diào)用處,以等待發(fā)生數(shù)據(jù)可讀 / 寫事件。Nginx 采用了異步非阻塞的方式來處理請求,也就是說,Nginx 是可以同時處理成千上萬個請求的。一個 Worker 進(jìn)程可以同時處理的請求數(shù)只受限于內(nèi)存大小,而且在架構(gòu)設(shè)計上,不同的 Worker 進(jìn)程之間處理并發(fā)請求時幾乎沒有同步鎖的限制,Worker 進(jìn)程通常不會進(jìn)入睡眠狀態(tài),因此,當(dāng) Nginx 上的進(jìn)程數(shù)與 CPU 核心數(shù)相等時(***每一個 Worker 進(jìn)程都綁定特定的 CPU 核心),進(jìn)程間切換的代價是最小的。
Zuul
Zuul 是 Netflix 開源的微服務(wù)網(wǎng)關(guān)組件,它可以和 Eureka、Ribbon、Hystrix 等組件配合使用。Zuul 的核心是一系列的過濾器,這些過濾器可以完成以下功能:
- 身份認(rèn)證與安全:識別每個資源的驗證要求,并拒絕那些與要求不符的請求。
- 審查與監(jiān)控:與邊緣位置追蹤有意義的數(shù)據(jù)和統(tǒng)計結(jié)果,從而帶來精確的生產(chǎn)視圖。
- 動態(tài)路由:動態(tài)地將請求路由到不同的后端集群。
- 壓力測試:逐漸增加指向集群的流量,以了解性能。
- 負(fù)載分配:為每一種負(fù)載類型分配對應(yīng)容量,并棄用超出限定值的請求。
- 靜態(tài)響應(yīng)處理:在邊緣位置直接建立部分響應(yīng),從而避免其轉(zhuǎn)發(fā)到內(nèi)部集群。
- 多區(qū)域彈性:跨越 AWS Region 進(jìn)行請求路由,旨在實現(xiàn) ELB(Elastic Load Balancing,彈性負(fù)載均衡)使用的多樣化,以及讓系統(tǒng)的邊緣更貼近系統(tǒng)的使用者。
上面提及的這些特性是 Nigix 所沒有的,這是因為 Netflix 公司創(chuàng)造 Zuul 是為了解決云端的諸多問題(特別是幫助 AWS 解決跨 Region 情況下的這些特性實現(xiàn)),而不僅僅是做一個類似于 Nigix 的反向代理,當(dāng)然,我們可以僅使用反向代理功能,這里不多做描述。
Zuul1 是基于 Servlet 框架構(gòu)建,如圖所示,采用的是阻塞和多線程方式,即一個線程處理一次連接請求,這種方式在內(nèi)部延遲嚴(yán)重、設(shè)備故障較多情況下會引起存活的連接增多和線程增加的情況發(fā)生。
Zuul2 的巨大區(qū)別是它運行在異步和無阻塞框架上,每個 CPU 核一個線程,處理所有的請求和響應(yīng),請求和響應(yīng)的生命周期是通過事件和回調(diào)來處理的,這種方式減少了線程數(shù)量,因此開銷較小。又由于數(shù)據(jù)被存儲在同一個 CPU 里,可以復(fù)用 CPU 級別的緩存,前面提及的延遲和重試風(fēng)暴問題也通過隊列存儲連接數(shù)和事件數(shù)方式減輕了很多(較線程切換來說輕量級很多,自然消耗較小)。這一變化一定會大大提升性能,我們在后面的測試環(huán)節(jié)看看結(jié)果。
我們今天談的是 API 網(wǎng)關(guān)性能,這一點也涉及到高可用,簡單介紹 Zuul 的高可用特性,高可用是非常關(guān)鍵的,因為外部請求到后端微服務(wù)的流量都會經(jīng)過 Zuul,所以在生產(chǎn)環(huán)境中一般都需要部署高可用的 Zuul 來避免單點故障。一般我們有兩種部署方案:
1. Zuul 客戶端注冊到 Eureka Server
這種情況是比較簡單的情況,只需要將多個 Zuul 節(jié)點注冊到 Eureka Server 上,就可以實現(xiàn) Zuul 的高可用。事實上,這種情況下的高可用和其他服務(wù)做高可用的方案沒有什么區(qū)別。我們來看下面這張圖,當(dāng) Zuul 客戶端注冊到 Eureka Server 上時,只需要部署多個 Zuul 節(jié)點就可以實現(xiàn)高可用。Zuul 客戶端會自動從 Eureka Server 查詢 Zuul Server 列表,然后使用負(fù)載均衡組件(例如 Ribbon)請求 Zuul 集群。
2. Zuul 客戶端不能注冊到 Eureka Server
假如說我們的客戶端是手機(jī)端 APP,那么不可能通過方案 1 的方式注冊到 Eureka Server 上。這種情況下,我們可以通過額外的負(fù)載均衡器來實現(xiàn) Zuul 的高可用,例如 Nginx、HAProxy、F5 等。
如圖所示,Zuul 客戶端將請求發(fā)送到負(fù)載均衡器,負(fù)載均衡器將請求轉(zhuǎn)發(fā)到其代理的其中一個 Zuul 節(jié)點,這樣就可以實現(xiàn) Zuul 的高可用。
Spring Cloud
雖然 Spring Cloud 帶有“Cloud”,但是它并不是針對云計算的解決方案,而是在 Spring Boot 基礎(chǔ)上構(gòu)建的,用于快速構(gòu)建分布式系統(tǒng)的通用模式的工具集。
使用 Spring Cloud 開發(fā)的應(yīng)用程序非常適合在 Docker 或者 PaaS 上部署,所以又叫云原生應(yīng)用。云原生可以簡單理解為面向云環(huán)境的軟件架構(gòu)。
既然是工具集,那么它一定包含很多工具,我們來看下面這張圖:
這里由于僅涉及到 API 網(wǎng)關(guān)的對比,因此我不逐一介紹其他工具了。
Spring Cloud 對 Zuul 進(jìn)行了整合,但從 Zuul 來看,沒有大變化,但是 Spring Cloud 整個框架經(jīng)過了組件的集成,提供的功能遠(yuǎn)多于 Netflix Zuul,可能對比時會出現(xiàn)差異。
Service Mesh 之 Linkerd
我想 Turgay Celik 博士把 Linkerd 作為對比對象之一,可能是因為 Linkerd 為云原生應(yīng)用提供彈性的 Service Mesh,而 Service Mesh 能夠提供輕量級高性能網(wǎng)絡(luò)代理,并且也提供微服務(wù)框架支撐。
從介紹來看,linkerd 是我們面向微服務(wù)的開源 RPC 代理,它直接立足于 Finagle(Twitter 的內(nèi)部核心庫,負(fù)責(zé)管理不同服務(wù)間之通信流程。事實上,Twitter 公司的每一項在線服務(wù)都立足于 Finagle 構(gòu)建而成,而且其支持著每秒發(fā)生的成百上千萬條 RPC 調(diào)用)構(gòu)建而成,設(shè)計目標(biāo)在于幫助用戶簡化微服務(wù)架構(gòu)下的運維,它是專用于處理時間敏感的服務(wù)到服務(wù)的通信基礎(chǔ)設(shè)施層。
和 Spring Cloud 類似,Linkerd 也提供了負(fù)載均衡、熔斷機(jī)器、服務(wù)發(fā)現(xiàn)、動態(tài)請求路由、重試和離線、TLS、HTTP 網(wǎng)關(guān)集成、透明代理、gRPC、分布式跟蹤、運維等諸多功能,功能是相當(dāng)全了,為微服務(wù)框架的技術(shù)選型又增加了一個。由于沒有接觸過 Linkerd,所以暫時無法從架構(gòu)層面進(jìn)行分析,后續(xù)會補(bǔ)充這方面的內(nèi)容,自己來做一次技術(shù)選型。
性能測試結(jié)果
Turgay Çelik 博士的那篇文章里使用了 Apache 的 HTTP 服務(wù)器性能評估工具 AB 作為測試工具。注意,由于他是基于亞馬遜(AWS)公有云的進(jìn)行的測試,可能和你實際物理機(jī)上的測試結(jié)果有出入。
實驗中啟動了客戶端和服務(wù)端兩臺機(jī)器,分別安裝多個待測試服務(wù),客戶端通過幾種方式分別訪問,嘗試獲取資源。測試方案如下圖所示:
測試選擇了三個環(huán)境,分別是:
- 單 CPU 核,1GB 內(nèi)存:用于比較 Nginx 反向代理和 Zuul(去除***次運行后的平均結(jié)果);
- 雙 CPU 核,8GB 內(nèi)存:用于比較 Nginx 反向代理和 Zuul(去除***次運行后的平均結(jié)果);
- 8 個核 CPU,32GB 內(nèi)存:用于比較 Nginx 反向代理、Zuul(去除***次運行后的平均結(jié)果)、Spring Cloud Zuul、Linkerd。
測試過程均采用 200 個并行線程發(fā)送總共 1 萬次請求,命令模板如下所示:
- ab -n 10000 -c 200 HTTP://<server-address>/<path to resource>
注意:由于 Turgay Çelik 博士的測試過程中是基于 Zuul 1 進(jìn)行的測試,所以性能上較差,不能真實反映當(dāng)前 Zuul 版本的性能狀況。
從上面的結(jié)果來看,單核環(huán)境下,Zuul 的性能最差(950.57 次 /s),直接訪問方式性能***(6519.68 次 /s),采用 Nginx 反向代理方式較直接訪問方式損失 26% 的性能(4888.24 次 /s)。在雙核環(huán)境下,Nginx 的性能較 Zuul 性能強(qiáng)接近 3 倍(分別是 6187.14 次 /s 和 2099.93 次 /s)。在較強(qiáng)的測試環(huán)境下(8 核),直接訪問、Nginx、Zuul 差距不大,但是 Spring Cloud Zuul 可能由于內(nèi)部整體消耗,導(dǎo)致每秒的請求數(shù)只有 873.14。
最終結(jié)論
從產(chǎn)品思維來看,API 網(wǎng)關(guān)負(fù)責(zé)服務(wù)請求路由、組合及協(xié)議轉(zhuǎn)換??蛻舳说乃姓埱蠖际紫冉?jīng)過 API 網(wǎng)關(guān),然后由它將請求路由到合適的微服務(wù)。API 網(wǎng)關(guān)經(jīng)常會通過調(diào)用多個微服務(wù)并合并結(jié)果來處理一個請求,它可以在 Web 協(xié)議(如 HTTP 與 WebSocket)與內(nèi)部使用的非 Web 友好協(xié)議之間轉(zhuǎn)換,所以說作用還是很大的,因此技術(shù)方案選型對于整個系統(tǒng)來說也有一定重要性。
從我所理解的這四款組件的設(shè)計原理來看,Zuul1 的設(shè)計模式和 Nigix 較像,每次 I/O 操作都是從工作線程中選擇一個執(zhí)行,請求線程被阻塞直到工作線程完成,但是差別是 Nginx 用 C++ 實現(xiàn),Zuul 用 Java 實現(xiàn),而 JVM 本身有***次加載較慢的情況。Zuul2 的性能肯定會較 Zuul1 有較大的提升,此外,Zuul 的***次測試性能較差,但是從第二次開始就好了很多,可能是由于 JIT(Just In Time)優(yōu)化造成的吧。而對于 Linkerd,它本身是對于資源比較敏感的一種網(wǎng)關(guān)設(shè)計,所以在通用環(huán)境下拿它和其他網(wǎng)關(guān)實現(xiàn)相比較,可能會出現(xiàn)不準(zhǔn)確的結(jié)果。