本文我們主要是想測試和研究幾點:
- 基於Netty寫的最簡單的轉發HTTP請求的程序,四層和七層性能的差異
- 三種代理線程模型性能的差異,下文會詳細解釋三種線程模型
- 池和非池化ByteBuffer性能的差異
本文測試使用的代碼在:
https://github.com/JosephZhu1983/proxytest
在代碼里我們實現了兩套代理程序:
測試使用的機器配置是(阿里雲ECS):
一共三台機器:
- server 服務器安裝了nginx,作為后端
- client 服務器安裝了wrk,作為壓測客戶端
- proxy 服務器安裝了我們的測試代碼(代理)
Nginx后端
nginx 配置的就是默認的測試頁(刪了點內容,減少內網帶寬):
直接對着nginx壓測下來的qps是26.6萬:
有關四層和七層
四層的代理,我們僅僅是使用Netty來轉發ByteBuf。
七層的代理,會有更多額外的開銷,主要是Http請求的編碼解碼以及Http請求的聚合,服務端:
客戶端:
這里我們可以想到,四層代理因為少了Http數據的編解碼過程,性能肯定比七層好很多,好多少我們可以看看測試結果。
有關線程模型
我們知道作為一個代理,我們需要開啟服務端從上游來獲取請求,然后再作為客戶端把請求轉發到下游,從下游獲取到響應后,返回給上游。我們的服務端和客戶端都需要Worker線程來處理IO請求,有三種做法;
- A:客戶端Bootstrap和服務端ServerBootstrap獨立的線程池NioEventLoopGroup,簡稱IndividualGroup
- B:客戶端和服務端共享一套線程池,簡稱ReuseServerGroup
- C:客戶端直接復用服務端線程EventLoop,簡稱ReuseServerThread
以七層代理的代碼為例:
接下去的測試我們會來測試這三種線程模型,這里想當然的猜測是方案A的性能是最好的,因為獨立了線程池不相互影響,我們接下去看看結果
四層代理 + ReuseServerThread線程模型
Layer4ProxyServer Started with config: ServerConfig(type=Layer4ProxyServer, serverIp=172.26.5.213, serverPort=8888, backendIp=172.26.5.214, backendPort=80, backendThreadModel=ReuseServerThread, receiveBuffer=10240, sendBuffer=10240, allocatorType=Unpooled, maxContentLength=2000)
四層代理 + IndividualGroup線程模型
Layer4ProxyServer Started with config: ServerConfig(type=Layer4ProxyServer, serverIp=172.26.5.213, serverPort=8888, backendIp=172.26.5.214, backendPort=80, backendThreadModel=IndividualGroup, receiveBuffer=10240, sendBuffer=10240, allocatorType=Unpooled, maxContentLength=2000)
四層代理 + ReuseServerGroup線程模型
Layer4ProxyServer Started with config: ServerConfig(type=Layer4ProxyServer, serverIp=172.26.5.213, serverPort=8888, backendIp=172.26.5.214, backendPort=80, backendThreadModel=ReuseServerGroup, receiveBuffer=10240, sendBuffer=10240, allocatorType=Unpooled, maxContentLength=2000)
看到這里其實已經有結果了,ReuseServerThread性能是最好的,其次是ReuseServerGroup,最差是IndividualGroup,和我們猜的不一致。
四層系統監控圖
從網絡帶寬上可以看到,先測試的ReuseServerThread跑到了最大的帶寬(后面三個高峰分別代表了三次測試):
從CPU監控上可以看到,性能最好的ReuseServerThread使用了最少的CPU資源(后面三個高峰分別代表了三次測試):
七層代理 + ReuseServerThread線程模型
Layer7ProxyServer Started with config: ServerConfig(type=Layer7ProxyServer, serverIp=172.26.5.213, serverPort=8888, backendIp=172.26.5.214, backendPort=80, backendThreadModel=ReuseServerThread, receiveBuffer=10240, sendBuffer=10240, allocatorType=Unpooled, maxContentLength=2000)
七層代理 + IndividualGroup線程模型
Layer7ProxyServer Started with config: ServerConfig(type=Layer7ProxyServer, serverIp=172.26.5.213, serverPort=8888, backendIp=172.26.5.214, backendPort=80, backendThreadModel=IndividualGroup, receiveBuffer=10240, sendBuffer=10240, allocatorType=Unpooled, maxContentLength=2000)
七層代理 + ReuseServerGroup線程模型
Layer7ProxyServer Started with config: ServerConfig(type=Layer7ProxyServer, serverIp=172.26.5.213, serverPort=8888, backendIp=172.26.5.214, backendPort=80, backendThreadModel=ReuseServerGroup, receiveBuffer=10240, sendBuffer=10240, allocatorType=Unpooled, maxContentLength=2000)
結論一樣,ReuseServerThread性能是最好的,其次是ReuseServerGroup,最差是IndividualGroup。我覺得是這么一個道理:
- 復用IO線程的話,上下文切換會比較少,性能是最好的,后來我也通過pidstat觀察驗證了這個結論,但是當時忘記截圖
- 復用線程池,客戶端有機會能復用到服務端線程,避免部分上下文切換,性能中等
- 獨立線程池,大量上下文切換(觀察下來是復用IO線程的4x),性能最差
七層系統監控圖
下面分別是網絡帶寬和CPU監控圖:
可以看到明顯七層代理消耗更多的資源,但是帶寬相比四層少了一些(QPS少了很多)。
出流量比入流量多一點,應該是代碼里多加的請求頭導致:
試試HttpObjectAggregator設置較大maxContentLength
Layer7ProxyServer Started with config: ServerConfig(type=Layer7ProxyServer, serverIp=172.26.5.213, serverPort=8888, backendIp=172.26.5.214, backendPort=80, backendThreadModel=ReuseServerThread, receiveBuffer=10240, sendBuffer=10240, allocatorType=Pooled, maxContentLength=100000000)
試試PooledByteBufAllocator
Layer7ProxyServer Started with config: ServerConfig(type=Layer7ProxyServer, serverIp=172.26.5.213, serverPort=8888, backendIp=172.26.5.214, backendPort=80, backendThreadModel=ReuseServerThread, receiveBuffer=10240, sendBuffer=10240, allocatorType=Pooled, maxContentLength=2000)
可以看到Netty 4.1中已經把默認的分配器設置為了PooledByteBufAllocator
總結
這里總結了一個表格,性能損失比例都以第一行直接壓Nginx為參照:
結論是:
- Nginx很牛,其實機器配置不算太好,在配置比較好的物理機服務器上跑的化,Nginx單機百萬沒問題
- Netty很牛,畢竟是Java的服務端,四層轉發僅損失3% QPS
- 不管是七層還是四層,復用線程的方式明顯性能最好,占用CPU最少
- 因為上下文切換的原因,使用Netty開發網絡代理應該復用IO線程
- 七層的消耗比四層大很多,即使是Netty也避免不了,這是HTTP協議的問題
- PooledByteBufAllocator性能比UnpooledByteBufAllocator有一定提升(接近3%)
- HttpObjectAggregator如果設置較大的最大內容長度,會略微影響點性能
之所以寫這個文章做這個分析的原因是因為最近在做我們自研網關的性能優化和壓力測試https://github.com/spring-avengers/tesla。
我發現有一些其它開源的基於Netty的代理項目並不是復用連接的,可能作者沒有意識到這個問題,我看了下Zuul的代碼,它也是復用的。