—— 4C8G 撐起 5000 併發,maxThreads、acceptCount、keepAlive 一次玩轉


一、Tomcat 性能瓶頸到底卡在哪?

  1. CPU ?—— 多數 Web 場景 IO 等待 > 計算
  2. 內存 ?—— 線程對象 + 連接對象 最佔內存
  3. 網絡 ?—— 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 ActiveConnectionsThreads 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


九、常見異常 & 解決

日誌

原因

解決

java.lang.OutOfMemoryError: unable to create new native thread

線程數 > 系統限制

降 maxThreads / 升 ulimits

Connection refused: no further information

accept 隊列滿

增 acceptCount / somaxconn

Keep-Alive timeout expired

長連接超時短

調大 keepAliveTimeout

HTTP/1.1 400 Bad Request + maxHttpHeaderSize

請求頭太大

增 Connector maxHttpHeaderSize="16384"

CPU 高 + 線程阻塞

業務代碼同步 IO

上異步 / 連接池


十、今日任務(30 分鐘)

  1. 用默認配置跑 wrk -c1000 -d30s → 記錄 RPS & P99 & 錯誤率
  2. 按本文調優 server.xml(800 線程 + 內核參數)→ 同樣壓測 → 填表對比
  3. JConsole 連接觀察 currentThreadsBusy 峯值截圖
  4. (可選)把 HelloServlet 改成 AsyncServlet → 再壓 → 看 RPS 能否再 +30%

明日預告

第 16 天《JVM 參數與 GC 調優》—— 從 CMS 到 G1/ZGC,現場演示 8G 容器如何壓到 5000 RPS 且 GC 停頓 < 100 ms,讓線程池不再被 GC 拖後腿!