Servlet 3 早在 2011 年推出,但很多 web 項目還是使用 Servlet 2.5。這些 web 項目業務相對較重、單機負載相對較低,Servlet 2.5 由于更少切換線程比 Servlet 3 更加穩定。
隨著微服務的發展,web 層不斷演進,涌現出越來越多的 API 網關。API 網關將 web 層的業務剝離開,專注于安全防護和路由轉發。這種重 IO 輕 CPU 的場景非常適合使用 Servlet 3。
通常異步 Servlet 項目中有 3 類線程池:Tomcat IO 工作線程池、業務線程池、RPC 線程池。
下面介紹 Servlet 優化的五個階段,這五個階段大致分為三類優化手段:線程隔離、無鎖處理和更換容器。
第一階段:純同步
第一階段采用同步 Servlet,Tomcat 線程進入業務代碼并執行預處理、RPC 調用、結果處理
壓測到 1000 QPS 時 AVG(20)、99 線(70)、999 線(75),整體平穩
第二階段:偽異步
異步 Servlet 中通過 servletRequest.startAsync 開啟異步,asyncContext.dispatch 重新分發請求、asyncContext.complete 結束異步。Spring MVC 的異步是先開啟異步、執行業務、重新分發請求。這種方式是為了兼容 Spring MVC 的同步實現,但是由于兩次分發導致代碼更復雜、并可能對 Servlet 帶來更大的壓力。
在第二階段的時候采用類似的實現:
- Tomcat 線程進入業務代碼并預處理請求
- Tomcat 線程開啟異步并異步請求 RPC 服務
- RPC 回調后重新分發請求
- Tomcat 線程進入業務代碼并處理請求結果
壓測到 1000 QPS 時 AVG(18)、99 線(56)、999 線(60),整體平穩,異步占優
壓測到 1400 QPS 時 AVG(23)、99 線(80)、999 線(85),整體平穩,異步占優
壓測到 2000 QPS 時 AVG(32)、99 線(300)、999 線(320),波動較大
第三階段:全異步
項目在第三階段的時候采用全異步:
- Tomcat 線程進入業務代碼并開啟異步
- Tomcat 線程提交業務線程并返回
- 業務線程預處理并調用 RPC 服務
- RPC 回調后提交業務線程
- 業務線程處理請求結果并結束異步
壓測到 2500 QPS 時 AVG(44)、99 線(83)、999 線(85),整體平穩,時有抖動
壓測到 3000 QPS 時 AVG(70)、99 線(180)、999 線(190),整體平穩,時有抖動
第四階段:異步優化
業界 4 核 8 g 異步 Servlet 的機器吞吐量能夠達到 1w+ QPS
1.觀察機器在壓測時的性能指標,QPS 3000 時出現 full gc
2.壓測結束后 Dump 機器內存后發現無大對象,但是有大量 byte [] 和 char [] 對象
3.使用 JProfiler 分析 byte [] 的來源,可能來自 Tomcat 的 IO
在對比 Tomcat 線程數后猜測,可能是 Tomcat 線程池打滿所致。同時在 Tomcat 日志中也出現大量隊列已滿的報錯
4.嘗試調整 Tomcat 線程池配置:增加線程數量、采用 Http11Nio2Protocol 處理協議。調整后壓測吞吐量沒有提升。https://renwole.com/archives/357
<Executor
name="tomcatThreadPool"
namePrefix="catalina-exec-"
maxThreads="500"
minSpareThreads="30"
maxIdleTime="60000"
prestartminSpareThreads = "true"
maxQueueSize = "100"
/>
<Connector
executor="tomcatThreadPool"
port="8080"
protocol="org.apache.coyote.http11.Http11Nio2Protocol"
connectionTimeout="60000"
maxConnections="10000"
redirectPort="8443"
enableLookups="false"
acceptCount="100"
maxPostSize="10485760"
maxHttpHeaderSize="8192"
compression="on"
disableUploadTimeout="true"
compressionMinSize="2048"
acceptorThreadCount="2"
URIEncoding="utf-8"
processorCache="20000"
tcpNoDelay="true"
connectionLinger="5"
server="Server Version 11.0"
/>
5.修改業務線程池大小
壓測到 5000 QPS 時 AVG(59)、99 線(130)、999 線(145),整體平穩
6.修改調用下游服務的方式,基于回調 => 同步
7.對比業務各階段耗時,增加服務調用線程池線程
壓測到 6000 QPS 時高點 AVG(348)、99 線(500)、999 線(1000),整體平穩
8.異步 Servlet 開啟異步后超時自動結束請求,RPC 在回調時如果已經超時會報狀態異常,因此對 onTimeout 和 onComplete 加上同步鎖
9.移除 RPC 調用,mock 服務返回結果
壓測到 10000 QPS 時高點 AVG(12)、99 線(504)、999 線(1000),CPU 幾乎打滿
10.調整業務線程組,預處理和調用服務使用同一線程、RPC 回調和處理返回結果使用同一線程,弱化 CPU 輪轉。壓測到高點時開始出現線程阻塞
壓測到 6000 QPS 時高點 AVG(8)、99 線(35)、999 線(1000),CPU 幾乎打滿
11.縮短 RPC 服務超時時間 200 -> 50
壓測到 4000 時開始出現線程阻塞
12.移除 AsyncContext 監聽中 onTimeout 和 onComplete 上的同步鎖
13.使用 servlet 3.1 nio,注冊 reader listener 和 write listener 異步讀寫
壓測到 7000 QPS 時高點 AVG(10)、99 線(30)、999 線(1000)
第四階段總結:
- 異步 IO 能一定程度上提高并發,在傳輸量不大的情況下提高并不明顯
- 高并發同步調用會阻塞 IO 線程
- Tomcat 內部沒有復用 bytebuffer 導致請求量大時新生代 gc 頻繁,最后引起服務超時,日志阻塞等一系列問題
第五階段:Jetty 容器
Jetty 底層對 ByteBuffer 對象的復用和線程池的處理有更好的優化。
在第五階段,更換 Servlet 容器為 Jetty 進行調優。
壓測到 11000 QPS 時高點 AVG(40)、99 線(90)、999 線(140),整體平穩
第五階段總結:
- 在高并發的場景中,Jetty 的性能比 Tomcat 要強,耗時曲線非常平穩
- 使用 Jetty 容器后,10000 QPS 時 AVG(5)、99 線(20)、999 線(20