Tomcat 性能调优实战
概述
Tomcat 的性能瓶颈通常不在 Tomcat 本身,而在于 JVM 配置、连接器线程模型与业务代码。调优的目标是让资源(CPU、内存、连接、线程)在合理水位上被充分利用,既不空转,也不被打爆。本文从 JVM 参数、连接器参数、并发优化三个层面展开,最后给出压测验证的方法与一份可直接落地的调优清单。
一、调优的基本方法论
调优不是"抄参数",而是闭环过程:
建立基线(压测) → 定位瓶颈 → 调整参数 → 再次压测验证
↑ │
└────────── 收敛(稳定达标) ←────────┘关键原则:
- 先量化再调整:任何调优动作前后都要有压测数据(TPS、P99、错误率、资源水位)
- 一次只改一个变量:同时改多个参数无法归因
- 从下往上排查:先操作系统(文件句柄、TCP backlog),再 JVM,再连接器,最后业务代码
二、JVM 参数调优
2.1 内存模型分配
通过 JAVA_OPTS 设置(conf/ 或 bin/catalina.sh 顶部):
bash
JAVA_OPTS="-Xms4g -Xmx4g -Xmn1g \
-XX:MaxMetaspaceSize=512m \
-XX:SurvivorRatio=8 \
-XX:MaxDirectMemorySize=512m \
-XX:+UseG1GC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/opt/tomcat/logs/ \
-Djava.security.egd=file:/dev/./urandom"| 参数 | 作用 | 建议 |
|---|---|---|
-Xms / -Xmx | 堆初始/最大值 | 生产建议相等,避免扩容抖动 |
-Xmn | 新生代大小 | 约堆的 1/3~1/4 |
-XX:MaxMetaspaceSize | 元空间上限 | 防止无限加载类导致 OOM |
-XX:MaxDirectMemorySize | 直接内存 | NIO 读写大量数据时配置 |
-XX:SurvivorRatio | Eden 与 Survivor 比例 | 默认 8 即可 |
-XX:+HeapDumpOnOutOfMemoryError | OOM 时自动 dump | 必须开启,便于事后分析 |
堆大小设置逻辑:观察基线压测下老年代增长曲线,堆应能容纳一次完整 GC 周期的对象峰值,通常给到峰值的 1.5~2 倍。
2.2 GC 调优:G1 优先
JDK 8u191+ 与 JDK 11+ 推荐 G1,JDK 17 默认即 G1:
bash
JAVA_OPTS="-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:ConcGCThreads=4"| 参数 | 含义 | 说明 |
|---|---|---|
-XX:MaxGCPauseMillis | 目标停顿时间 | G1 会围绕该目标调整新生代大小 |
-XX:InitiatingHeapOccupancyPercent | 触发并发标记的堆占用率 | 过低频繁并发 GC,过高提前 Full GC |
-XX:G1NewSizePercent | 新生代下限占比 | 避免停顿目标下新生代过小 |
判断依据:观察 GC 日志(-Xlog:gc 或 -verbose:gc)中 Young GC 频率、Mixed GC 时长、是否出现 Full GC。Full GC 频繁通常是堆过小或对象分配速率异常,优先治本(优化代码),而不是无限加堆。
2.3 元空间与类加载
Tomcat 动态加载 WAR/热重载场景下元空间可能增长:
bash
JAVA_OPTS="-XX:MaxMetaspaceSize=512m"同时注意热重载时旧类加载器泄漏导致元空间不回收(详见类加载机制篇的排查方法)。
三、连接器参数调优
3.1 核心参数组合
xml
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="5000"
acceptCount="500"
maxThreads="400"
minSpareThreads="50"
maxKeepAliveRequests="100"
compression="on"
compressionMinSize="2048"
enableLookups="false" />| 参数 | 常见误配 | 正确姿势 |
|---|---|---|
maxThreads | 盲目调到几千 | 按 CPU/IO 密集度与压测定,IO 密集可数百到上千 |
acceptCount | 不设置 | 配合 OS backlog,acceptCount 大于突发连接峰值 |
connectionTimeout | 过大 | 普通业务 2~5 秒足够 |
maxKeepAliveRequests | 默认无限 | 设限(如 100),避免长连接耗尽 |
compression | 不开 | 大 JSON/HTML 响应开 gzip,吞吐显著提升 |
3.2 线程池与连接数的关系
并发连接进入
│
▼
maxConnections(连接上限)→ acceptCount(积压队列)→ maxThreads(处理能力)三者逐级兜底。调优原则:
- 连接数 > 线程数:NIO 下连接不占线程,允许大量 keep-alive 连接存在
- 线程数匹配处理能力:线程是"处理中的请求数",超过 CPU 承受能力会加剧上下文切换
- 观察队列:
maxThreads打满且任务堆积 → 优先扩容或优化业务,而不是继续加线程
3.3 显式 Executor 的使用
xml
<Executor name="webExecutor" namePrefix="web-exec-"
maxThreads="400" minSpareThreads="50" maxQueueSize="200" />
<Connector port="8080" protocol="HTTP/1.1" executor="webExecutor" />maxQueueSize 设置队列上限,防止任务无限堆积导致响应无限变慢——队列满后新任务被拒绝(503),比无限排队更可观测。
四、并发优化策略
4.1 操作系统层面
| 项 | 配置 | 说明 |
|---|---|---|
| 文件句柄 | ulimit -n 65535 | 高并发连接需要大量 socket 句柄 |
| TCP backlog | net.core.somaxconn=4096 | 与 acceptCount 配合 |
| 端口范围 | net.ipv4.ip_local_port_range | 出站连接充足 |
| TIME_WAIT 复用 | net.ipv4.tcp_tw_reuse=1 | 减少连接重建开销 |
4.2 应用层面
| 优化点 | 说明 |
|---|---|
| 减少同步阻塞 | 业务线程里不要做长时间 IO(读文件、远程调用),异步化或池化 |
| 连接池复用 | JDBC/Redis 连接池大小与 maxThreads 匹配,避免线程等待连接 |
| 响应体压缩 | gzip + 合理阈值 |
| 静态资源分离 | 图片/静态文件交给 Nginx/CDN,Tomcat 只处理动态请求 |
| 缓存 | 热点数据上本地缓存(Caffeine)或 Redis,降低重复计算 |
4.3 慢请求治理
- 识别慢接口:访问日志里按耗时排序,
%D(响应毫秒)字段 - 超时控制:
connectionTimeout之外,业务侧设置读超时/执行超时 - 线程池隔离:关键接口与批量任务分开线程池,防止相互拖垮
五、压测验证方法
5.1 工具选择
| 工具 | 特点 |
|---|---|
| JMeter | 图形化、脚本丰富,适合常规压测 |
| wrk | 轻量、高并发、脚本简单 |
| Apache Bench (ab) | 最简单,适合快速验证 |
5.2 压测步骤
bash
# 快速验证:200 并发、10 万请求
ab -n 100000 -c 200 -k http://localhost:8080/api/order/list
# 观察关键指标
# Requests per second 吞吐
# Time per request 平均响应
# Failed requests 错误压测中同步观察:
- Tomcat 侧:线程数、队列长度、活跃会话(JMX 或 jstack)
- JVM 侧:GC 频率与停顿、堆使用曲线(
jstat -gcutil) - 系统侧:CPU、内存、网络(
top、vmstat)
5.3 指标解读
| 指标 | 健康水位 | 异常含义 |
|---|---|---|
| 线程使用率 | 60%~80% | 打满 → 队列堆积,处理能力不足 |
| CPU | 70%~85% | 持续 90%+ → 计算瓶颈 |
| GC 停顿 | P99 的 1% 以内 | Young GC 频繁 → 对象分配过快 |
| 错误率 | 0 | 出现 503 → 队列/线程打满 |
六、完整调优案例
场景:单机 Tomcat 支撑商品查询接口,压测目标 TPS 5000、P99 50ms。
| 阶段 | 动作 | 结果 |
|---|---|---|
| 基线 | 默认参数压测 | TPS 1800,P99 240ms,线程打满 |
| JVM | 堆 4G + G1 + 目标停顿 200ms | P99 降至 150ms |
| 连接器 | maxThreads 200→400,acceptCount 100→500 | TPS 升至 3200 |
| 压缩 | 开启 gzip | TPS 升至 3900(响应体变小) |
| 应用 | 热点商品加本地缓存 | TPS 5200,P99 42ms,达标 |
结论:连接器参数解决"能接住多少",JVM 参数解决"内存与 GC 是否拖后腿",应用优化决定"处理得有多快"。三者配合,缺一不可。
七、调优陷阱清单
| 陷阱 | 说明 |
|---|---|
| 无限加大 maxThreads | 线程过多 → 上下文切换开销反噬,P99 变差 |
| Xmx 与 Xms 不等 | 启动后堆反复扩容,Full GC 风险 |
| 忽略元空间 | 热重载/动态代理多时元空间暴涨 OOM |
| 只调 JVM 不看代码 | 业务里的慢 SQL/阻塞调用是根因,调参只是缓解 |
| 压测环境与生产不一致 | 数据量、并发模型不同,结果不可信 |
| 调优不回归 | 每个参数改动都要重新压测并记录 |
参考链接: