—— 4C8G 撐起 5000 併發,maxThreads、acceptCount、keepAlive 一次玩轉
一、Tomcat 性能瓶頸到底卡在哪?
- CPU ?—— 多數 Web 場景 IO 等待 > 計算
- 內存 ?—— 線程對象 + 連接對象 最佔內存
- 網絡 ?—— SYN 隊列 / accept 隊列 / Keep-Alive 超時 決定短鏈接吞吐
結論:先調 線程模型 → 再調 JVM 內存 → 最後 操作系統內核
二、線程池三級模型(一張圖記心裏)
┌--------------┐ 1 線程
│ Acceptor │──┐ 阻塞 accept
└--------------┘ ←┘
│register(NIO)
┌--------------┐ 1~2 線程(可配)
│ Poller │◄───Selector 輪詢
└--------------┘
│submit
┌--------------┐ maxThreads 上限
│ Worker Pool │◄───業務代碼、Filter、Servlet
└--------------┘
關鍵參數:
- acceptorThreadCount = 1(默認)
- pollerThreadCount = 1(默認)
- maxThreads = 200(默認)
- maxConnections = 10000(NIO 默認)
- acceptCount = 100(全連接隊列)
三、參數逐條拆解 + 推薦值
|
參數 |
位置 |
作用 |
默認 |
生產建議(4C8G) |
備註 |
|
acceptorThreadCount |
Connector |
同時 accept 的線程 |
1 |
1~2 |
多核可 2,>2 無提升 |
|
pollerThreadCount |
Connector |
Selector 輪詢線程 |
1 |
2 |
核數≤8 用 2 即可 |
|
maxThreads |
Executor |
工作線程上限 |
200 |
800 |
決定最大併發 |
|
minSpareThreads |
Executor |
核心線程 |
10 |
50 |
啓動即預創建,防瞬峯 |
|
maxIdleTime |
Executor |
線程回收時間(ms) |
60000 |
60000 |
空閒 1 分鐘回收 |
|
maxConnections |
Connector |
同時監控的連接數 |
10000 |
10000 |
含未處理+處理中 |
|
acceptCount |
Connector |
TCP 全連接隊列 |
100 |
200 |
超過後拒絕連接 |
|
connectionTimeout |
Connector |
讀請求頭超時(ms) |
20000 |
15000 |
移動端可 20s |
|
keepAliveTimeout |
Connector |
長連接閒置超時(ms) |
connectionTimeout |
5000 |
短一點快速回收線程 |
|
maxKeepAliveRequests |
Connector |
一條 TCP 最大請求數 |
100 |
200 |
靜態資源多可 500 |
|
compression |
Connector |
gzip |
off |
on |
省 60% 帶寬 |
|
compressionMinSize |
Connector |
壓縮閾值(B) |
2048 |
2048 |
可 512 |
配置樣例(server.xml)
<Executor name="tomcatThreadPool"
maxThreads="800"
minSpareThreads="50"
maxIdleTime="60000"
maxQueueSize="200"
prestartminSpareThreads="true"/>
<Connector port="8080"
protocol="org.apache.coyote.http11.Http11NioProtocol"
executor="tomcatThreadPool"
maxConnections="10000"
acceptCount="200"
connectionTimeout="15000"
keepAliveTimeout="5000"
maxKeepAliveRequests="200"
compression="on"
compressionMinSize="2048"/>
四、現場壓測:默認 200 vs 調優 800
硬件:4C8G 虛擬機 / CentOS 8 / JDK 11
工具:wrk wrk -t4 -c2000 -d30s http://host:8080/api/json
|
場景 |
RPS |
P99(ms) |
錯誤率 |
CPU |
説明 |
|
默認 200 線程 |
1100 |
850 |
5% |
60% |
線程耗盡 |
|
調優 800 線程 |
4800 |
120 |
<0.5% |
75% |
見下圖 |
|
再加 AsyncServlet |
7200 |
65 |
0% |
80% |
非阻塞 IO |
結論:maxThreads 是第一瓶頸;異步化後再提 50%
五、操作系統內核調優(配套)
文件:/etc/sysctl.d/tomcat.conf
# TCP 全連接隊列
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
# 端口複用
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# 文件句柄
fs.file-max = 1000000
limits.conf
tomcat soft nofile 100000
tomcat hard nofile 100000
maxThreads 再高,somaxconn 太小也會被 SYN 丟棄!
六、數據庫連接池搭配(別讓 DB 成為真瓶頸)
目標:Servlet 線程 ≤ 連接池最大連接
否則 線程等連接,白增 maxThreads
HikariCP 推薦公式:
maximumPoolSize = CPU 核心 * 2 + 1 (CPU 密集)
maximumPoolSize = maxThreads * 0.8 (IO 密集)
示例:
spring.datasource.hikari.maximum-pool-size=640
spring.datasource.hikari.minimum-idle=50
spring.datasource.hikari.connection-timeout=20000
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=900000
監控:HikariPoolMXBean ActiveConnections ≈ Threads Busy
七、可視化監控:JMX + VisualVM
1. 開 JMX
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9999 \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=false"
2. 連接 → MBeans → Catalina → ThreadPool → tomcatThreadPool
關鍵屬性:
currentThreadsBusy當前忙線程currentThreadCount總線程maxThreads/minSpareThreads
壓測時觀察:
currentThreadsBusy ≈ maxThreads → 繼續增線程或上異步
currentThreadsBusy < minSpare → 可降線程省內存
八、異步 Servlet 再加速(Bonus)
Servlet 3.0 非阻塞:
@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncApiServlet extends HttpServlet {
private final ThreadPool bizPool = Executors.newCachedThreadPool();
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
final AsyncContext ctx = req.startAsync();
bizPool.execute(() -> {
// DB IO
ctx.getResponse().getWriter().write("ok");
ctx.complete();
});
}
}
→ Worker 線程立即回池,業務用少量 Biz 線程
Tomcat 線程數 800 → 實際 200 也能跑 6000 RPS
九、常見異常 & 解決
|
日誌 |
原因 |
解決 |
|
|
線程數 > 系統限制 |
降 maxThreads / 升 ulimits |
|
|
accept 隊列滿 |
增 acceptCount / somaxconn |
|
|
長連接超時短 |
調大 keepAliveTimeout |
|
|
請求頭太大 |
增 Connector |
|
|
業務代碼同步 IO |
上異步 / 連接池 |
十、今日任務(30 分鐘)
- 用默認配置跑 wrk -c1000 -d30s → 記錄 RPS & P99 & 錯誤率
- 按本文調優 server.xml(800 線程 + 內核參數)→ 同樣壓測 → 填表對比
- JConsole 連接觀察 currentThreadsBusy 峯值截圖
- (可選)把 HelloServlet 改成 AsyncServlet → 再壓 → 看 RPS 能否再 +30%
明日預告
第 16 天《JVM 參數與 GC 調優》—— 從 CMS 到 G1/ZGC,現場演示 8G 容器如何壓到 5000 RPS 且 GC 停頓 < 100 ms,讓線程池不再被 GC 拖後腿!