基於Netty的四層和七層代理效能方面的一些壓力測試

lovecindywang發表於2019-07-01

本文我們主要是想測試和研究幾點:

  • 基於Netty寫的最簡單的轉發HTTP請求的程式,四層和七層效能的差異
  • 三種代理執行緒模型效能的差異,下文會詳細解釋三種執行緒模型
  • 池和非池化ByteBuffer效能的差異

本文測試使用的程式碼在:
https://github.com/JosephZhu1983/proxytest

在程式碼裡我們實現了兩套代理程式:
image_1demdig32ia6184m64sppm8vp90.png-55.9kB

測試使用的機器配置是(阿里雲ECS):
image_1dembkev02d2sll1ijc18fl4r48j.png-91.9kB
一共三臺機器:

  • server 伺服器安裝了nginx,作為後端
  • client 伺服器安裝了wrk,作為壓測客戶端
  • proxy 伺服器安裝了我們的測試程式碼(代理)

Nginx後端

nginx 配置的就是預設的測試頁(刪了點內容,減少內網頻寬):
image_1dembfnk81i9m19tkvli148c13h86.png-122.8kB
直接對著nginx壓測下來的qps是26.6萬:
image_1delvmebjcpe39n1hdni41hss13.png-55.2kB

有關四層和七層

四層的代理,我們僅僅是使用Netty來轉發ByteBuf。
七層的代理,會有更多額外的開銷,主要是Http請求的編碼解碼以及Http請求的聚合,服務端:

image_1demdm2m82vg1i6b4ng1uitjcp9d.png-136.8kB

客戶端:
image_1demdoius2ekjds1kbr5a1vld9q.png-63.2kB

這裡我們可以想到,四層代理因為少了Http資料的編解碼過程,效能肯定比七層好很多,好多少我們可以看看測試結果。

有關執行緒模型

我們知道作為一個代理,我們需要開啟服務端從上游來獲取請求,然後再作為客戶端把請求轉發到下游,從下游獲取到響應後,返回給上游。我們的服務端和客戶端都需要Worker執行緒來處理IO請求,有三種做法;

  • A:客戶端Bootstrap和服務端ServerBootstrap獨立的執行緒池NioEventLoopGroup,簡稱IndividualGroup
  • B:客戶端和服務端共享一套執行緒池,簡稱ReuseServerGroup
  • C:客戶端直接複用服務端執行緒EventLoop,簡稱ReuseServerThread

以七層代理的程式碼為例:
image_1demdqavbn5i19ff1g1hrp2gbsan.png-98.4kB

接下去的測試我們會來測試這三種執行緒模型,這裡想當然的猜測是方案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)
image_1delvsom6v03e5pngacv714901g.png-54kB

四層代理 + 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)
image_1dem04l2alqs1l4u1ripg9a1fcu1t.png-54.8kB

四層代理 + 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)
image_1dem0br3r1rr3qmj1mk519nn111v2a.png-55.2kB

看到這裡其實已經有結果了,ReuseServerThread效能是最好的,其次是ReuseServerGroup,最差是IndividualGroup,和我們猜的不一致。

四層系統監控圖

從網路頻寬上可以看到,先測試的ReuseServerThread跑到了最大的頻寬(後面三個高峰分別代表了三次測試):
image_1dem0chjrimkn5va5810dk1vk62n.png-52.8kB
從CPU監控上可以看到,效能最好的ReuseServerThread使用了最少的CPU資源(後面三個高峰分別代表了三次測試):
image_1dem0ekoq1l59ju1vvn1lp575u34.png-32.5kB

七層代理 + 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)
image_1dem0mduhkdc11hc2ue12rd433h.png-55kB

七層代理 + 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)
image_1dem0tgtv13ev3h9sl51appi083u.png-55.2kB

七層代理 + 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)
image_1dem14prr1e7kr0gi1ggiqu7l4b.png-55kB

結論一樣,ReuseServerThread效能是最好的,其次是ReuseServerGroup,最差是IndividualGroup。我覺得是這麼一個道理:

  • 複用IO執行緒的話,上下文切換會比較少,效能是最好的,後來我也通過pidstat觀察驗證了這個結論,但是當時忘記截圖
  • 複用執行緒池,客戶端有機會能複用到服務端執行緒,避免部分上下文切換,效能中等
  • 獨立執行緒池,大量上下文切換(觀察下來是複用IO執行緒的4x),效能最差

七層系統監控圖

下面分別是網路頻寬和CPU監控圖:
image_1dem1fh7m1f0cl8s1d1ic7563765.png-39.3kB
image_1dem1e3g01asrq8r9u16ce5e94r.png-60.1kB
可以看到明顯七層代理消耗更多的資源,但是頻寬相比四層少了一些(QPS少了很多)。
出流量比入流量多一點,應該是程式碼裡多加的請求頭導致:
image_1demf0bhrikp1rh0r5i1q3c1iltc1.png-150.8kB

試試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)
image_1dem1qe4v1ddd1c2311pjej81bf16v.png-54.9kB

試試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)
image_1dem1ifds1hoi1lkka691vekmlt6i.png-54.8kB

可以看到Netty 4.1中已經把預設的分配器設定為了PooledByteBufAllocator
image_1demg35il1ambhdb1o3m42c1j9ce.png-43.9kB

總結

這裡總結了一個表格,效能損失比例都以第一行直接壓Nginx為參照:
image_1demepcbume4eoacntrb11mh2b4.png-39.1kB

結論是:

  • Nginx很牛,其實機器配置不算太好,在配置比較好的物理機伺服器上跑的化,Nginx單機百萬沒問題
  • Netty很牛,畢竟是Java的服務端,四層轉發僅損失3% QPS
  • 不管是七層還是四層,複用執行緒的方式明顯效能最好,佔用CPU最少
  • 因為上下文切換的原因,使用Netty開發網路代理應該複用IO執行緒
  • 七層的消耗比四層大很多,即使是Netty也避免不了,這是HTTP協議的問題
  • PooledByteBufAllocator效能比UnpooledByteBufAllocator有一定提升(接近3%)
  • HttpObjectAggregator如果設定較大的最大內容長度,會略微影響點效能

之所以寫這個文章做這個分析的原因是因為最近在做我們自研閘道器的效能優化和壓力測試https://github.com/spring-avengers/tesla。
我發現有一些其它開源的基於Netty的代理專案並不是複用連線的,可能作者沒有意識到這個問題,我看了下Zuul的程式碼,它也是複用的。

相關文章