網關性能比較:NGINX vs.ZUUL vs.SpringCloudGateway vs.Linkerd

前幾天拜讀了 OpsGenie 公司(一家致力於 Dev & Ops 的公司)的資深工程師 Turgay Çelik 博士寫的一篇文章(鏈接在文末),文中介紹了他們最初也是採用 Nginx 作為單體應用的網關,後來接觸到微服務架構後開始逐漸採用了其他組件。

我對於所做的工作或者感興趣的技術,喜歡刨根問底,所以當讀一篇文章時發現沒有看到我想要看到的設計思想,我就會四處蒐集資料,此外這篇文章涉及了我正在搗鼓的 Spring Cloud,所以我就決定寫一篇文章,爭取能從設計思路上解釋為什麼會有這樣的性能差異。

技術介紹

文中針對 Nginx、ZUUL、Spring Cloud、Linkerd 等技術進行了對比(其實還有 Envoy 和 UnderTow 也是屬於可選的 API 網關,本文不予涉及),那我就分別進行介紹,當然,首先得介紹 API 網關。

API 網關

API 網關出現的原因是微服務架構的出現,不同的微服務一般會有不同的網絡地址,而外部客戶端可能需要調用多個服務的接口才能完成一個業務需求,如果讓客戶端直接與各個微服務通信,會有以下的問題:

客戶端會多次請求不同的微服務,增加了客戶端的複雜性。存在跨域請求,在一定場景下處理相對複雜。認證複雜,每個服務都需要獨立認證。難以重構,隨著項目的迭代,可能需要重新劃分微服務。例如,可能將多個服務合併成一個或者將一個服務拆分成多個。如果客戶端直接與微服務通信,那麼重構將會很難實施。某些微服務可能使用了防火牆 / 瀏覽器不友好的協議,直接訪問會有一定的困難。

以上這些問題可以藉助 API 網關解決。API 網關是介於客戶端和服務器端之間的中間層,所有的外部請求都會先經過 API 網關這一層。也就是說,API 的實現方面更多的考慮業務邏輯,而安全、性能、監控可以交由 API 網關來做,這樣既提高業務靈活性又不缺安全性,典型的架構圖如圖所示:

使用 API 網關後的優點如下:

易於監控。可以在網關收集監控數據並將其推送到外部系統進行分析。易於認證。可以在網關上進行認證,然後再將請求轉發到後端的微服務,而無須在每個微服務中進行認證。減少了客戶端與各個微服務之間的交互次數。

NGINX 服務

Nginx 由內核和模塊組成,內核的設計非常微小和簡潔,完成的工作也非常簡單,僅僅通過查找配置文件與客戶端請求進行 URL 匹配,用於啟動不同的模塊去完成相應的工作。

下面這張圖反應的是 HTTP 請求的常規處理流程:

Nginx 的模塊直接被編譯進 Nginx,因此屬於靜態編譯方式。啟動 Nginx 後,Nginx 的模塊被自動加載,不像 Apache,首先將模塊編譯為一個 so 文件,然後在配置文件中指定是否進行加載。在解析配置文件時,Nginx 的每個模塊都有可能去處理某個請求,但是同一個處理請求只能由一個模塊來完成。

Nginx 在啟動後,會有一個 Master 進程和多個 Worker 進程,Master 進程和 Worker 進程之間是通過進程間通信進行交互的,如圖所示。Worker 工作進程的阻塞點是在像 select()、epoll_wait() 等這樣的 I/O 多路複用函數調用處,以等待發生數據可讀 / 寫事件。Nginx 採用了異步非阻塞的方式來處理請求,也就是說,Nginx 是可以同時處理成千上萬個請求的。一個 Worker 進程可以同時處理的請求數只受限於內存大小,而且在架構設計上,不同的 Worker 進程之間處理併發請求時幾乎沒有同步鎖的限制,Worker 進程通常不會進入睡眠狀態,因此,當 Nginx 上的進程數與 CPU 核心數相等時(最好每一個 Worker 進程都綁定特定的 CPU 核心),進程間切換的代價是最小的。

Netflix 的 Zuul

Zuul 是 Netflix 開源的微服務網關組件,它可以和 Eureka、Ribbon、Hystrix 等組件配合使用。Zuul 的核心是一系列的過濾器,這些過濾器可以完成以下功能:

身份認證與安全:識別每個資源的驗證要求,並拒絕那些與要求不符的請求。審查與監控:與邊緣位置追蹤有意義的數據和統計結果,從而帶來精確的生產視圖。動態路由:動態地將請求路由到不同的後端集群。壓力測試:逐漸增加指向集群的流量,以瞭解性能。負載分配:為每一種負載類型分配對應容量,並棄用超出限定值的請求。靜態響應處理:在邊緣位置直接建立部分響應,從而避免其轉發到內部集群。多區域彈性:跨越 AWS Region 進行請求路由,旨在實現 ELB(Elastic Load Balancing,彈性負載均衡)使用的多樣化,以及讓系統的邊緣更貼近系統的使用者。

上面提及的這些特性是 Nigix 所沒有的,這是因為 Netflix 公司創造 Zuul 是為了解決雲端的諸多問題(特別是幫助 AWS 解決跨 Region 情況下的這些特性實現),而不僅僅是做一個類似於 Nigix 的反向代理,當然,我們可以僅使用反向代理功能,這裡不多做描述。

Zuul1 是基於 Servlet 框架構建,如圖所示,採用的是阻塞和多線程方式,即一個線程處理一次連接請求,這種方式在內部延遲嚴重、設備故障較多情況下會引起存活的連接增多和線程增加的情況發生。

Zuul2 的巨大區別是它運行在異步和無阻塞框架上,每個 CPU 核一個線程,處理所有的請求和響應,請求和響應的生命週期是通過事件和回調來處理的,這種方式減少了線程數量,因此開銷較小。又由於數據被存儲在同一個 CPU 裡,可以複用 CPU 級別的緩存,前面提及的延遲和重試風暴問題也通過隊列存儲連接數和事件數方式減輕了很多(較線程切換來說輕量級很多,自然消耗較小)。這一變化一定會大大提升性能,我們在後面的測試環節看看結果。

我們今天談的是 API 網關性能,這一點也涉及到高可用,簡單介紹 Zuul 的高可用特性,高可用是非常關鍵的,因為外部請求到後端微服務的流量都會經過 Zuul,所以在生產環境中一般都需要部署高可用的 Zuul 來避免單點故障。一般我們有兩種部署方案:

1. Zuul 客戶端註冊到 Eureka Server

這種情況是比較簡單的情況,只需要將多個 Zuul 節點註冊到 Eureka Server 上,就可以實現 Zuul 的高可用。事實上,這種情況下的高可用和其他服務做高可用的方案沒有什麼區別。我們來看下面這張圖,當 Zuul 客戶端註冊到 Eureka Server 上時,只需要部署多個 Zuul 節點就可以實現高可用。Zuul 客戶端會自動從 Eureka Server 查詢 Zuul Server 列表,然後使用負載均衡組件(例如 Ribbon)請求 Zuul 集群。

2. Zuul 客戶端不能註冊到 Eureka Server

假如說我們的客戶端是手機端 APP,那麼不可能通過方案 1 的方式註冊到 Eureka Server 上。這種情況下,我們可以通過額外的負載均衡器來實現 Zuul 的高可用,例如 Nginx、HAProxy、F5 等。

如圖所示,Zuul 客戶端將請求發送到負載均衡器,負載均衡器將請求轉發到其代理的其中一個 Zuul 節點,這樣就可以實現 Zuul 的高可用。

Spring Cloud

雖然 Spring Cloud 帶有“Cloud”,但是它並不是針對雲計算的解決方案,而是在 Spring Boot 基礎上構建的,用於快速構建分佈式系統的通用模式的工具集。

使用 Spring Cloud 開發的應用程序非常適合在 Docker 或者 PaaS 上部署,所以又叫雲原生應用。雲原生可以簡單理解為面向雲環境的軟件架構。

既然是工具集,那麼它一定包含很多工具,我們來看下面這張圖:

這裡由於僅涉及到 API 網關的對比,因此我不逐一介紹其他工具了。

Spring Cloud 對 Zuul 進行了整合,但從 Zuul 來看,沒有大變化,但是 Spring Cloud 整個框架經過了組件的集成,提供的功能遠多於 Netflix Zuul,可能對比時會出現差異。

Service Mesh 之 Linkerd

我想 Turgay Celik 博士把 Linkerd 作為對比對象之一,可能是因為 Linkerd 為雲原生應用提供彈性的 Service Mesh,而 Service Mesh 能夠提供輕量級高性能網絡代理,並且也提供微服務框架支撐。

從介紹來看,linkerd 是我們面向微服務的開源 RPC 代理,它直接立足於 Finagle(Twitter 的內部核心庫,負責管理不同服務間之通信流程。事實上,Twitter 公司的每一項在線服務都立足於 Finagle 構建而成,而且其支持著每秒發生的成百上千萬條 RPC 調用)構建而成,設計目標在於幫助用戶簡化微服務架構下的運維,它是專用於處理時間敏感的服務到服務的通信基礎設施層。

和 Spring Cloud 類似,Linkerd 也提供了負載均衡、熔斷機器、服務發現、動態請求路由、重試和離線、TLS、HTTP 網關集成、透明代理、gRPC、分佈式跟蹤、運維等諸多功能,功能是相當全了,為微服務框架的技術選型又增加了一個。由於沒有接觸過 Linkerd,所以暫時無法從架構層面進行分析,後續會補充這方面的內容,自己來做一次技術選型。

性能測試結果

Turgay Çelik 博士的那篇文章裡使用了 Apache 的 HTTP 服務器性能評估工具 AB 作為測試工具。注意,由於他是基於亞馬遜(AWS)公有云的進行的測試,可能和你實際物理機上的測試結果有出入。

實驗中啟動了客戶端和服務端兩臺機器,分別安裝多個待測試服務,客戶端通過幾種方式分別訪問,嘗試獲取資源。測試方案如下圖所示:

Turgay Çelik 博士的這次測試選擇了三個環境,分別是:

單 CPU 核,1GB 內存:用於比較 Nginx 反向代理和 Zuul(去除第一次運行後的平均結果);雙 CPU 核,8GB 內存:用於比較 Nginx 反向代理和 Zuul(去除第一次運行後的平均結果);8 個核 CPU,32GB 內存:用於比較 Nginx 反向代理、Zuul(去除第一次運行後的平均結果)、Spring Cloud Zuul、Linkerd。

測試過程均採用 200 個並行線程發送總共 1 萬次請求,命令模板如下所示:

ab -n 10000 -c 200 HTTP://<server-address>/<path>/<server-address>

注意:由於 Turgay Çelik 博士的測試過程中是基於 Zuul 1 進行的測試,所以性能上較差,不能真實反映當前 Zuul 版本的性能狀況,後續文章我會自己做實驗併發布結果。

從上面的結果來看,單核環境下,Zuul 的性能最差(950.57 次 /s),直接訪問方式性能最好(6519.68 次 /s),採用 Nginx 反向代理方式較直接訪問方式損失 26% 的性能(4888.24 次 /s)。在雙核環境下,Nginx 的性能較 Zuul 性能強接近 3 倍(分別是 6187.14 次 /s 和 2099.93 次 /s)。在較強的測試環境下(8 核),直接訪問、Nginx、Zuul 差距不大,但是 Spring Cloud Zuul 可能由於內部整體消耗,導致每秒的請求數只有 873.14。

最終結論

從產品思維來看,API 網關負責服務請求路由、組合及協議轉換。客戶端的所有請求都首先經過 API 網關,然後由它將請求路由到合適的微服務。API 網關經常會通過調用多個微服務併合並結果來處理一個請求,它可以在 Web 協議(如 HTTP 與 WebSocket)與內部使用的非 Web 友好協議之間轉換,所以說作用還是很大的,因此技術方案選型對於整個系統來說也有一定重要性。

從我所理解的這四款組件的設計原理來看,Zuul1 的設計模式和 Nigix 較像,每次 I/O 操作都是從工作線程中選擇一個執行,請求線程被阻塞直到工作線程完成,但是差別是 Nginx 用 C++ 實現,Zuul 用 Java 實現,而 JVM 本身有第一次加載較慢的情況。Zuul2 的性能肯定會較 Zuul1 有較大的提升,此外,Zuul 的第一次測試性能較差,但是從第二次開始就好了很多,可能是由於 JIT(Just In Time)優化造成的吧。而對於 Linkerd,它本身是對於資源比較敏感的一種網關設計,所以在通用環境下拿它和其他網關實現相比較,可能會出現不準確的結果。