Skip to content

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 频率和暂停、Metaspacejcmd、GC 日志
应用业务异常、慢 SQL、外部调用超时应用日志、APM
系统CPU、内存、磁盘 IO、网络topvmstatiostat

先用 Nginx access log 区分慢在入口层还是后端层:$request_time 高且 $upstream_response_time 也高 → 后端慢;只有 $request_time 高 → 客户端传输或 Nginx 侧问题。

一、JVM 内存参数

bash
CATALINA_OPTS="-Xms2g -Xmx2g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
参数做什么
-XmsJVM 初始堆大小
-XmxJVM 最大堆大小
-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 等待 + 排队)
acceptCountmaxConnections 满了之后,OS 层还能排多少个连接
connectionTimeoutTCP 连接建立后,等请求到来的超时
keepAliveTimeoutkeepalive 连接空闲多久后关闭
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 外置 sessionsession 存 Redis,多实例共享多一层依赖,Redis 故障影响登录
无状态 token(JWT 等)登录态放客户端 token,服务端不存 session应用要改造,token 吊销不如 session 直接

Nginx ip_hash 的局限:办公网出口 NAT 下,几百人看起来是同一个 IP——全部打到同一台后端,负载均衡形同虚设。核心系统多实例部署时,把 session 外置到 Redis 或改成无状态登录更稳。

六、Tomcat access log——看请求耗时

server.xmlHostContext 里配置:

xml
<Valve className="org.apache.catalina.valves.AccessLogValve"
       directory="logs"
       prefix="localhost_access_log"
       suffix=".txt"
       pattern="%h %l %u %t &quot;%r&quot; %s %b %D" />
字段含义
%h客户端 IP(前面有 Nginx 时通常是 Nginx IP)
%t请求时间
%r请求行(GET /api/users HTTP/1.1)
%sHTTP 状态码
%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 里请求耗时越来越长、线程栈里大量线程处于 WAITINGTIMED_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.out

Java 应用发布后不能只看首页能不能打开——首页能打开只说明最短路径正常。要盯一段时间的 5xx 错误率、接口平均耗时、GC 频率和数据库连接池状态,确认高峰流量下也稳。