← 返回题目列表

Tomcat 请求变慢或线程打满时应如何排查?

高频 困难 第 15 / 23 题 更新于 2026/07/25
Tomcat性能排查线程池Thread Dump

简化版

Tomcat 变慢或线程打满,核心是定位瓶颈到底在哪一环:连接、线程、应用逻辑,还是下游资源。标准排查流程:① 看监控确认现象(活跃线程数、连接数、请求耗时、错误率、QPS);② 抓线程栈(jstack / thread dump) 看 Worker 线程卡在哪——是 CPU 计算(RUNNABLE)、锁竞争(BLOCKED)、还是等下游(WAITING/TIMED_WAITING);③ 结合访问日志、慢 SQL、GC 日志、下游指标还原链路。关键原则:maxThreads 打满只是「症状」,真正的病因在线程栈里、下游里、GC 里——盲目调大 maxThreads 反而可能放大下游故障。

详细版

排查步骤(从现象到根因):

① 监控确认现象 → ② 抓 thread dump 看线程状态 → ③ 还原链路定位根因 → ④ 对症修复

线程状态与含义(thread dump 里看):

线程状态可能在做什么排查方向
RUNNABLECPU 计算 或 native I/OCPU 热点、算法、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.mysqlOkHttpredis 就知道卡在哪个下游)。

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 GCmaxThreads 打满是症状不是病因——盲目调大会让更多线程挤垮下游、放大故障。对症修复:慢 SQL 优化索引、远程调用设超时+熔断、缩小锁、连接池按下游容量配、静态资源给 CDN、GC 治内存。提前压测知边界 + 限流熔断降级守边界