Tomcat 请求变慢或线程打满时应如何排查?
简化版
Tomcat 变慢或线程打满,核心是定位瓶颈到底在哪一环:连接、线程、应用逻辑,还是下游资源。标准排查流程:① 看监控确认现象(活跃线程数、连接数、请求耗时、错误率、QPS);② 抓线程栈(jstack / thread dump) 看 Worker 线程卡在哪——是 CPU 计算(RUNNABLE)、锁竞争(BLOCKED)、还是等下游(WAITING/TIMED_WAITING);③ 结合访问日志、慢 SQL、GC 日志、下游指标还原链路。关键原则:maxThreads 打满只是「症状」,真正的病因在线程栈里、下游里、GC 里——盲目调大 maxThreads 反而可能放大下游故障。
详细版
排查步骤(从现象到根因):
① 监控确认现象 → ② 抓 thread dump 看线程状态 → ③ 还原链路定位根因 → ④ 对症修复
线程状态与含义(thread dump 里看):
| 线程状态 | 可能在做什么 | 排查方向 |
|---|---|---|
| RUNNABLE | CPU 计算 或 native I/O | CPU 热点、算法、native 调用 |
| BLOCKED | 等锁(synchronized) | 锁竞争、临界区过大 |
| WAITING / TIMED_WAITING | 等连接池、等远程调用、sleep | 下游慢、连接池耗尽、超时 |
常见卡点(Worker 线程被占满的真凶):
- 慢 SQL、数据库连接池耗尽
- 外部 HTTP 调用没设超时(下游不返回,线程无限等)
- Redis / MQ 卡顿
- 锁竞争(synchronized 临界区大)
- Full GC 频繁(所有请求停顿)
- 大文件上传下载、日志同步刷盘
核心原则:先定位「线程为什么不释放」,再谈是否调参。maxThreads 打满是症状不是病因。
完整版教学
一、第一步:先确认「慢在哪里」,别急着甩锅 Tomcat
请求慢,可能慢在很多环节,不要一上来就归因于 Tomcat:
- 连接建立慢(网络、TLS 握手)。
- 排队慢(连接/请求在队列里等)。
- 业务执行慢(Controller 里的逻辑、查库、调接口)。
- 响应写回慢(大响应体、慢客户端)。
- 甚至是客户端或代理层慢(Nginx、网关)。
先用访问日志、APM(如 SkyWalking)、网关指标把整条链路的耗时拆开——看时间花在哪一段。很多「Tomcat 慢」其实是下游数据库慢或客户端网络慢,Tomcat 只是「背锅」。分段定位是第一步。
二、第二步:看线程池状态
如果确认是 Tomcat 处理慢,先看 Worker 线程池状态(通过 JMX、监控、manager 应用):
- 当前活跃线程数 vs maxThreads:活跃线程接近 maxThreads,说明 Worker 线程正被占满。
- 请求队列等待、连接数、请求耗时分布、错误率。
如果活跃线程接近 maxThreads 且响应时间上升,说明 Worker 线程在被占满——但这只是现象,还要继续查「它们为什么不释放」(下一步)。仅仅看到「线程满了」就调大 maxThreads 是错误的。
| 指标组合 | 更可能的问题 | 下一步动作 |
|---|---|---|
活跃线程接近 maxThreads,P99 升高 | Worker 被慢请求占住 | 抓 thread dump,看栈顶停在哪 |
连接数接近 maxConnections,活跃线程不高 | Keep-Alive 多、慢客户端或连接占用 | 看连接来源、超时、代理层 |
| 错误率升高且队列增长 | 请求排队/拒绝 | 看流量峰值、下游耗时、限流策略 |
| 周期性全站卡顿 | GC 或系统资源抖动 | 看 GC 日志、CPU、磁盘 I/O |
记忆钩子:
maxThreads打满是体温计报警,不是病因诊断;真正答案通常在线程栈、下游指标和 GC 日志里。
三、第三步:抓线程栈(最关键)
Thread Dump(jstack <pid> 或 kill -3)是定位「线程卡在哪」的核心手段。 它直接告诉你每个 Worker 线程此刻在执行什么代码、处于什么状态:
- RUNNABLE:线程在运行——可能在做 CPU 计算,也可能在 native I/O(如 socket read)。看栈顶是业务算法还是 I/O。
- BLOCKED:线程在等锁(
synchronized拿不到)——说明有锁竞争,临界区可能过大或有慢操作在锁内。 - WAITING / TIMED_WAITING:线程在等待——等数据库连接池、等远程调用返回、等
Object.wait()、Thread.sleep()。这是最常见的:大量线程停在数据库驱动或 HTTP 客户端的栈上 = 下游慢拖住了线程。
技巧:多抓几次(间隔几秒抓 3~5 次) 比单次可靠——如果多次都停在同一个地方,那就是真正的卡点;偶尔出现的可能只是瞬时。看线程名和栈顶的包名通常能直接给出线索(如栈顶是 com.mysql、OkHttp、redis 就知道卡在哪个下游)。
jstack -l <pid> > dump-1.txt
sleep 5
jstack -l <pid> > dump-2.txt
sleep 5
jstack -l <pid> > dump-3.txt
如果 200 个 Worker 里有 160 个连续 3 次都停在 JDBC 查询栈上,例如 com.mysql.cj.jdbc,就不要先调 Tomcat;应该去看慢 SQL、数据库连接池、数据库 CPU/锁等待。如果大量线程是 BLOCKED 且锁对象相同,就看谁持有锁以及锁内是否做了慢 I/O。
四、常见卡点清单
Worker 线程被占满,绝大多数是下面这些原因之一:
- 慢 SQL:查询没走索引、大表扫描、复杂 JOIN——线程停在数据库驱动上等结果。
- 数据库连接池耗尽:连接不够,线程排队等连接(停在连接池的
getConnection)。 - 外部 HTTP 调用没设超时:下游服务不返回,线程无限期等待(这是最危险的,会持续累积占满线程)。
- Redis / MQ 卡顿:缓存或消息中间件慢,拖住线程。
- 锁竞争:
synchronized临界区过大或锁内有慢操作,大量线程 BLOCKED。 - Full GC 频繁:GC 导致 Stop-The-World,所有请求停顿(见下节)。
- 大文件上传/下载、日志同步刷盘:I/O 密集操作占住线程。
五、区分连接问题和处理问题(结合参数看)
结合 Tomcat 参数状态判断问题性质(见「Tomcat 线程模型」专题):
- maxThreads 打满、活跃线程多 → 处理慢(业务或下游),查线程栈。
- acceptCount 队列在增长、连接被拒 → 连接接不过来,可能是处理能力不足或流量过大。
- maxConnections 接近上限但线程不忙 → 可能是 Keep-Alive 长连接多 或 慢客户端占着连接却没在传数据(连接问题,不是处理问题)。
- 连接不多但线程忙 → 单个请求耗时太长,查慢在哪。
「连接很多但线程不忙」和「线程忙但连接不多」指向完全不同的问题,别混。
六、GC 和内存也要查
频繁 Full GC 会让所有请求停顿(Stop-The-World),表现为整体变慢、周期性卡顿。要看:
- GC 日志:Full GC 频率和耗时。
- 堆使用、直接内存(Netty/NIO 用)、对象分配热点。
常见内存压力来源:文件上传/大响应对象、日志大量字符串拼接、缓存无界增长(内存泄漏)。如果慢是 GC 引起的,调 Tomcat 线程参数完全无用——要治理内存。
七、对症修复,而非盲目调参
根据定位到的根因对症下药:
- 慢 SQL → 优化索引、改写查询、加缓存。
- 远程调用 → 设置超时 + 熔断降级(避免下游不返回时线程无限等——这是最重要的防护)。
- 锁竞争 → 缩小临界区、换更细粒度的锁或无锁结构。
- 连接池 → 按下游容量合理配置(不是越大越好,太大会打垮下游)。
- 静态资源 → 交给 CDN 或 Nginx,别占 Tomcat。
- GC → 优化内存分配、调 JVM 参数、修内存泄漏。
Tomcat 参数(maxThreads 等)只是「容量边界」,不是万能药——盲目调大 maxThreads 会让更多线程一起去挤已经很慢的下游,甚至打垮数据库、放大故障。
八、压测与保护要提前做
性能排查不应只在故障后抓栈——更好的是提前压测知道容量边界:
- 压测时记录拐点:线程池、连接池、GC、数据库、下游接口分别在什么 QPS 下开始劣化。
- 上线后用保护措施守住边界:限流、超时、熔断、降级。
没有保护的情况下把 Tomcat 参数调大,往往只是让更多请求一起冲垮下游。 参数是边界,保护是护栏,两者都要有。
九、常见误区与追问
- 误区:Tomcat 线程满了就把
maxThreads调大。 线程满只是症状,若根因是慢 SQL 或下游无超时,调大会让更多线程同时压向下游。 - 误区:一次 thread dump 就足够定位。 单次只能看到瞬间状态,多次间隔抓取才能判断线程是否稳定卡在同一位置。
- 追问:大量 Worker 处于
WAITING/TIMED_WAITING怎么看? 重点看栈顶是在数据库连接池、HTTP 客户端、Redis 还是业务等待点,通常说明在等下游或等资源。 - 追问:大量
BLOCKED说明什么? 多数是锁竞争,要看等待的 monitor 和持锁线程,检查临界区是否过大或锁内有慢操作。 - 误区:连接数高就等于线程忙。 NIO 下大量 Keep-Alive 空闲连接可能不占 Worker,连接容量和处理线程要分开看。
- 追问:Full GC 为什么会表现成 Tomcat 慢? Stop-The-World 会暂停应用线程,所有请求同时卡顿,调 Tomcat 线程参数无法解决。
十、加强记忆
Tomcat 变慢/线程打满排查三步:① 监控确认现象(活跃线程、连接数、耗时、错误率,先用 APM/日志分段定位别甩锅 Tomcat)→ ② 抓 thread dump(jstack)看线程状态(RUNNABLE=CPU/native I/O、BLOCKED=锁竞争、WAITING/TIMED_WAITING=等下游/连接池,多抓几次、看栈顶包名)→ ③ 结合慢 SQL/GC 日志/下游指标还原链路。常见真凶:慢 SQL、连接池耗尽、外部调用无超时(无限等最危险)、锁竞争、Full GC。maxThreads 打满是症状不是病因——盲目调大会让更多线程挤垮下游、放大故障。对症修复:慢 SQL 优化索引、远程调用设超时+熔断、缩小锁、连接池按下游容量配、静态资源给 CDN、GC 治内存。提前压测知边界 + 限流熔断降级守边界。