Appearance
Tomcat 调优与排错
Tomcat 故障很少只属于 Tomcat 本身。一个接口慢,可能是 Nginx 排队、Tomcat 线程池耗尽、JVM GC 暂停、数据库慢查询、Redis 超时或外部接口不响应。
完整链路:
text
client → nginx → tomcat connector → worker thread → application → database/cache/external排查时的分层视角:
| 层 | 关注点 | 常用入口 |
|---|---|---|
| Nginx | 状态码、upstream 耗时、代理超时 | access/error log |
| Tomcat Connector | 线程数、队列、连接数 | JMX、线程栈、Tomcat access log |
| JVM | 堆内存、GC 频率和暂停、Metaspace | jcmd、GC 日志 |
| 应用 | 业务异常、慢 SQL、外部调用超时 | 应用日志、APM |
| 系统 | CPU、内存、磁盘 IO、网络 | top、vmstat、iostat |
先用 Nginx access log 区分慢在入口层还是后端层:$request_time 高且 $upstream_response_time 也高 → 后端慢;只有 $request_time 高 → 客户端传输或 Nginx 侧问题。
一、JVM 内存参数
bash
CATALINA_OPTS="-Xms2g -Xmx2g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"| 参数 | 做什么 |
|---|---|
-Xms | JVM 初始堆大小 |
-Xmx | JVM 最大堆大小 |
-XX:MetaspaceSize | 元空间初始大小 |
-XX:MaxMetaspaceSize | 元空间上限,防止无限制增长 |
生产环境通常把 -Xms 和 -Xmx 设成一样大——避免运行时堆扩缩带来的停顿。但机器上还有操作系统、Nginx、监控 Agent、日志采集等进程,不能把物理内存全部给 JVM 堆。
查看当前 JVM 启动参数和实际生效的配置:
bash
ps -ef | grep '[j]ava'
jcmd | grep tomcat # 列出 Java 进程的 PID
jcmd <pid> VM.command_line # 启动时传入的参数
jcmd <pid> VM.flags # 所有 JVM 参数(含默认值和传入的)二、GC 日志
JDK 11/17 的 GC 日志参数:
bash
CATALINA_OPTS="$CATALINA_OPTS -Xlog:gc*:file=/opt/tomcat/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=100m"| 配置 | 含义 |
|---|---|
file=... | GC 日志输出路径 |
time,uptime,level,tags | 每条记录带时间戳、运行时长、级别和标签 |
filecount=5 | 保留 5 个滚动文件 |
filesize=100m | 单文件 100MB 后滚动 |
看 GC 日志重点关注:
| 现象 | 可能含义 |
|---|---|
| Full GC 频繁(几分钟一次甚至更短) | 堆压力大、老年代对象存活多、内存泄漏 |
| 单次 GC 暂停时间很长(几秒以上) | 堆大、回收器不适合当前场景、CPU 被抢占 |
| 老年代使用率持续上涨不回落 | 长生命周期对象持续累积——可能是泄漏 |
| Metaspace OOM | 类加载过多、应用热部署导致类被反复加载 |
接口开始超时的时候,如果 Nginx access log 显示一段时间内所有接口都慢,同时 GC 日志上出现长时间的 Full GC 暂停,因果关系就比较清楚——不是 Tomcat 线程池不够,是 JVM 在 Stop-The-World,所有线程都被暂停了。
三、线程池和连接器的参数关系
xml
<Connector port="8080"
protocol="HTTP/1.1"
maxThreads="300"
minSpareThreads="30"
maxConnections="10000"
acceptCount="200"
connectionTimeout="20000"
keepAliveTimeout="15000"
maxKeepAliveRequests="100" />| 字段 | 做什么 |
|---|---|
maxThreads | 最大工作线程数——同时可以处理多少个请求 |
minSpareThreads | 即使没有请求也保持的最小空闲线程数 |
maxConnections | 能同时保持的最大连接数(处理中 + keepalive 等待 + 排队) |
acceptCount | maxConnections 满了之后,OS 层还能排多少个连接 |
connectionTimeout | TCP 连接建立后,等请求到来的超时 |
keepAliveTimeout | keepalive 连接空闲多久后关闭 |
maxKeepAliveRequests | 单连接最多复用处理多少个请求 |
这三个数字的关系:maxConnections 是"接待大厅能站多少人",maxThreads 是"有几个窗口在办业务",acceptCount 是"大厅满了门外还能排多少人"。到达上限时新连接直接被拒绝(TCP RST)。
maxThreads 不是越大越好。如果数据库连接池只有 50,把 Tomcat 开到 1000 个线程,大部分线程只是堵在数据库前排队——线程切换开销反而让吞吐下降。线程数和下游资源(数据库连接池、Redis 连接池、外部接口的并发限制)要匹配。
四、线程栈——卡住时看线程在等什么
接口卡住时,线程栈比经验猜测可靠得多:
bash
# 找到 PID
jcmd | grep tomcat
# 导出线程栈
jstack -l <pid> > /tmp/tomcat-thread-$(date +%F-%H%M%S).txt
# 连续抓几份做对比,看哪些线程持续卡在同一个位置
for i in 1 2 3; do
jstack -l <pid> > "/tmp/tomcat-thread-$i.txt"
sleep 5
done线程栈里的常见状态:
| 状态 | 线程在做什么 |
|---|---|
RUNNABLE | 正在执行或等待 OS 系统调用返回 |
BLOCKED | 等待获取对象的 monitor 锁——有人持锁没放 |
WAITING | 等待条件满足(wait()、join() 等) |
TIMED_WAITING | 带超时的等待——sleep()、socket read timeout、数据库查询等待 |
如果几十个甚至上百个业务线程都卡在同一个数据库调用或同一个 HTTP 请求上,Tomcat 线程池被慢慢耗尽是结果,根因在下游。重启 Tomcat 能暂时恢复(线程释放),但下游不处理,流量上来还会复发。
五、session 管理
Tomcat 默认 session 放内存里。单机没问题,多实例负载均衡时有两个后果:同一用户的请求落到不同后端可能丢失登录态,某个实例重启后该实例上的 session 全丢。
常见方案对比:
| 方案 | 做法 | 代价 |
|---|---|---|
| 单机内存 | session 放当前 Tomcat 堆里 | 简单,但不适合多实例,重启就丢 |
Nginx ip_hash | 同 IP 固定到同一个后端 | 办公网 NAT 下失效,后端故障时 session 仍丢 |
| Redis 外置 session | session 存 Redis,多实例共享 | 多一层依赖,Redis 故障影响登录 |
| 无状态 token(JWT 等) | 登录态放客户端 token,服务端不存 session | 应用要改造,token 吊销不如 session 直接 |
Nginx ip_hash 的局限:办公网出口 NAT 下,几百人看起来是同一个 IP——全部打到同一台后端,负载均衡形同虚设。核心系统多实例部署时,把 session 外置到 Redis 或改成无状态登录更稳。
六、Tomcat access log——看请求耗时
在 server.xml 的 Host 或 Context 里配置:
xml
<Valve className="org.apache.catalina.valves.AccessLogValve"
directory="logs"
prefix="localhost_access_log"
suffix=".txt"
pattern="%h %l %u %t "%r" %s %b %D" />| 字段 | 含义 |
|---|---|
%h | 客户端 IP(前面有 Nginx 时通常是 Nginx IP) |
%t | 请求时间 |
%r | 请求行(GET /api/users HTTP/1.1) |
%s | HTTP 状态码 |
%b | 响应字节数 |
%D | 请求耗时,单位微秒 |
%D 是 Tomcat 端的处理耗时,可以跟 Nginx 的 $upstream_response_time 互相印证。筛选超过 1 秒的请求:
bash
awk '$NF > 1000000 {print}' /opt/tomcat/logs/localhost_access_log.*.txt | tail七、常见故障
502——Nginx 连不上 Tomcat
Nginx 返回 502 通常表示连后端失败或后端提前断开连接。
排查步骤:
bash
ss -lntp | grep ':8080' # Tomcat 端口是否在监听
curl -I http://127.0.0.1:8080/ # 本机直接连 Tomcat 是否正常
tail -n 100 /var/log/nginx/error.log
tail -n 100 /opt/tomcat/logs/catalina.out本机 curl Tomcat 失败 → Tomcat 没启动或挂了。本机正常但 Nginx 502 → 看代理地址(是不是写错了 IP 或端口)、防火墙、SELinux。
504——后端处理超时
Nginx 504 表示代理等待后端响应的超时。
bash
grep ' 504 ' /var/log/nginx/access.log | tail
tail -n 100 /opt/tomcat/logs/localhost_access_log.*.txt关键判断:Tomcat access log 里有没有这条请求?如果有且耗时很长 → 应用处理慢(查数据库、外部接口、GC)。如果没有 → 请求可能还没到达 Tomcat(Connector 满了、accept 队列满了)。
内存溢出
text
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: Metaspace查看:
bash
grep -i 'OutOfMemoryError' /opt/tomcat/logs/catalina.out
jcmd <pid> GC.heap_info配置 OOM 时自动生成堆转储文件(用于事后分析):
bash
CATALINA_OPTS="$CATALINA_OPTS -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/heapdump"堆转储文件可能很大(和堆大小同一量级),/data/heapdump 目录的磁盘空间要提前留够。OOM 后磁盘又满,会让排查更困难。
线程耗尽
表现:Nginx 504 增加、Tomcat access log 里请求耗时越来越长、线程栈里大量线程处于 WAITING 或 TIMED_WAITING、CPU 不一定高(多数线程在等待而非计算)。
现场:
bash
jstack -l <pid> > /tmp/tomcat-thread-full.txt
grep -c 'http-nio' /tmp/tomcat-thread-full.txt # 粗略看线程数
grep 'WAITING\|BLOCKED\|TIMED_WAITING' /tmp/tomcat-thread-full.txt | sort | uniq -c | sort -nr临时扩 maxThreads 可能缓解,但下游慢调用不处理,流量上来还会复发。线程耗尽的根因通常是下游某个环节变慢——数据库、Redis、外部 API——把 Tomcat 线程都拖住了。
八、发布和回滚
发布前保留信息:
| 项目 | 内容 |
|---|---|
| Java 版本 | java -version |
| Tomcat 版本 | /opt/tomcat/bin/version.sh |
| WAR 包 | 构建号、文件校验值 |
| 配置变更 | 数据库地址、Redis、外部接口 |
| JVM 参数 | CATALINA_OPTS 完整内容 |
| 回滚包 | 上一个可用版本的路径 |
回滚通过软链接切换:
bash
ln -sfn /data/apps/app/releases/20260521-001/app.war /data/apps/app/current.war
systemctl restart tomcat重启后验证:
bash
curl -I http://127.0.0.1:8080/app/health
tail -n 100 /opt/tomcat/logs/catalina.outJava 应用发布后不能只看首页能不能打开——首页能打开只说明最短路径正常。要盯一段时间的 5xx 错误率、接口平均耗时、GC 频率和数据库连接池状态,确认高峰流量下也稳。