Java

Tomcat busy threads 很高,和业务线程池 backlog 是一回事吗?

Tomcat busy threads 高和业务线程池 backlog 长,看起来都像“线程满了”,但指向的并不是同一层问题。更有用的做法是先看请求线程究竟卡在本地阻塞、等待内部任务,还是被连接池和下游一起拖住,再决定该盯入口线程、业务线程池还是更深的等待链。

  • Spring Boot
  • Tomcat
  • 线程池
  • 性能排查
  • 故障排查
18 分钟阅读

Spring Boot 服务一发慢,监控上最容易同时冒红的,往往就是 Tomcat busy threads 和业务线程池的 queue / backlog

现场也很容易马上收敛成一个过早的问题:

  • 是 Web 容器线程不够了
  • 还是业务线程池先堵了
  • 这两个是不是其实就是一回事

这类争论高发,是因为它们在监控面板上看起来都像“线程满了”。

但真实项目里,这两个指标就算一起变红,指向的也不是同一层等待:

  • Tomcat busy threads 代表请求入口层有多少工作线程正被占着
  • 业务线程池 backlog 代表应用内部执行层有多少任务正在排队

两者可能同时出现,也可能只有一边先出问题。

所以别急着把讨论收敛到“哪个线程池参数太小”。更值得先回答的是三件事:

请求线程现在是在自己做阻塞逻辑,还是在等内部任务结果?业务线程池里的任务是真在算,还是在等数据库、连接池或下游?如果两边一起变红,是不是同一段等待链把入口层和执行层同时拖慢了?

如果你现在只知道“接口慢、资源图不刺眼”,但还没确认 busy threads 或 backlog 到底有没有哪一边先恶化,更适合先回 接口很慢,但 CPU、GC、数据库都正常,隐藏等待点可能在哪?。这里讨论的是更靠后一步的现场:Tomcat busy 或业务线程池 backlog 至少已经有一边抬头,需要判断堵点先卡在入口层、执行层,还是同一条等待链把两边一起拖慢。

一、先确认是不是入口层和执行层都被卷进来了

不是所有并发等待都该落到 Tomcat busy 或 backlog 这组判断里。先用这张表收窄范围:你现在看到的,是否已经是入口线程和执行层需要一起分诊的场景。

你现在看到的现象更像什么问题下一步更适合看什么
Tomcat busy threads 高、业务线程池 backlog 也长,或者至少一边明显恶化典型入口线程 vs 执行线程分诊继续看本文
active 打满、queue 持续增长、reject 出现,但主要是业务 worker 层问题更像线程池打满现象层线程池打满以后,应该先查队列、拒绝策略还是慢任务?
queue 不长但任务仍慢更像伪正常 / 隐性等待线程池队列不长但任务还是慢,常见瓶颈在哪里?
backlog 主要出现在异步任务、补偿任务、MQ 消费更像异步 backlog 主要链路异步任务越堆越多,问题常常不在异步本身
CPU 不高、GC 正常、数据库不高,但接口已经慢更像先把慢请求卡住的等待段找出来接口很慢,但 CPU、GC、数据库都正常,隐藏等待点可能在哪?
获取连接变慢、pending 上升、事务型接口更慢更像数据库连接池等待链线程池和数据库连接池的容量,为什么要一起做预算?

如果你最像第一行,下面这套判断就对路;如果更像后面几行,别把所有线程问题都写成 Tomcat 和业务线程池的二选一。

二、先把两个指标语义拆开:它们都叫线程,但不是同一层线程

这一步特别关键。

1. Tomcat busy threads 是入口层

它回答的是:

  • 现在有多少 HTTP 请求线程正在忙
  • 请求线程是否长期不释放
  • 新请求进来时入口层是否已经接近饱和

它更接近“用户请求现在卡在入口多久”。

2. 业务线程池 backlog 是执行层

它回答的是:

  • 应用有没有把内部任务堆在执行队列里
  • 内部线程资源是否被主要链路、异步任务、定时任务、回填任务占满
  • 请求是否正在等待内部任务完成

它更接近“应用内部干活的那层是不是已经排起来了”。

这两个层级当然会相互影响,但绝对不能直接画等号。

三、最典型的三种现场:表面都像线程多,根因却完全不同

1. Tomcat busy 很高,业务线程池并不高

这类现场更像:

  • 请求线程自己就在做阻塞调用
  • Controller / Service 直接串行调用数据库、下游 HTTP、文件 IO
  • 应用并没有把活真正转移到业务线程池

典型表现:

  • Tomcat busy 很高
  • 业务线程池 active / queue 没有明显同步飙升
  • 线程栈里大量请求线程停在 JDBC、HTTP、锁等待或慢逻辑里

这时优先级应该先给请求线程自己,而不是业务线程池。

2. Tomcat busy 一般,但业务线程池 backlog 很长

这类现场更像:

  • 请求线程很快把任务提交给内部线程池
  • 真正慢的是异步执行层
  • 请求可能在等待 Future、回调结果、任务汇总结果

典型表现:

  • Tomcat busy 没有夸张到顶满
  • 业务线程池 queue 已经很长
  • 异步任务、批处理、定时任务、业务线程混在一个池子里

这时重点就不在 Web 容器,而在业务线程池执行内容和隔离策略。

3. 两边一起高

这是最常见、也最容易误判的一类。

表面上像两个线程池都不够,但真实链路通常更复杂:

  • 请求线程自己在做一部分阻塞逻辑
  • 同时又依赖业务线程池结果
  • 业务线程池还在等连接池、数据库或下游
  • 最后入口层和执行层一起被拖慢

这种场景最怕一上来只扩线程。

四、先看 Tomcat busy 高时,请求线程到底在忙什么

Tomcat busy 高这件事,本身并不说明根因就是 Web 容器线程太少。

它只说明:

  • 入口请求线程被占住了

关键是这些线程在干什么。

1. 在直接等数据库

常见表现:

  • 线程栈大量停在 JDBC、MyBatis、事务执行
  • 连接池 active / pending 也在升高
  • 写接口或热点查询更慢

这时 Tomcat busy 高只是数据库等待链上浮到入口层的结果。

2. 在直接等下游 HTTP / RPC

常见表现:

  • 下游 RT 抬升
  • 线程栈大量停在网络调用和 socket read
  • CPU 不高,但 busy threads 高

这时入口线程其实是在替下游排队。

3. 在做重业务逻辑或长循环

常见表现:

  • CPU 同步升高
  • 热线程集中在计算、序列化、规则匹配、对象转换
  • 业务线程池不一定是起点

这时 Tomcat busy 高更像请求线程自己被 CPU 密集逻辑占住。

所以 Tomcat busy 高时,第一句话不该是“线程不够”,而应该是:

请求线程是在做计算,还是在等待数据库、下游或内部任务?

五、再看业务线程池 backlog:任务是在排什么队

业务线程池 backlog 长,也不等于线程池配置天然有问题。

真正更值得先看的,是队列里排的是什么任务。

1. 主要链路请求依赖的任务在排队

例如:

  • 请求进来后把核心逻辑丢给业务线程池
  • Controller 还要等任务结果返回
  • 一旦 backlog 变长,Tomcat 线程也会一起被拖住

这时业务线程池 backlog 会反向放大 Tomcat busy。

2. 后台任务和主要链路任务混用池子

例如:

  • 定时任务
  • 异步回填
  • 缓存刷新
  • 补偿任务
  • 业务异步任务

都用同一个池子。

这时你看到的 backlog,可能并不是主业务请求自己造成的,而是后台任务先把执行层拖住了。

3. 业务线程池里的线程主要在等资源

常见是:

  • 等数据库连接
  • 等锁
  • 等下游 HTTP / RPC
  • 等缓存回源

也就是说,backlog 长并不说明线程在“忙计算”,它也可能只是在“排等待链”。

六、最容易混淆的一种情况:请求线程和业务线程池一起卡在同一条等待链上

真实线上特别常见的是这种链:

  1. 请求线程接到请求
  2. 一部分逻辑在请求线程里直接执行
  3. 一部分逻辑提交到业务线程池
  4. 业务线程池任务再去等数据库或下游
  5. 请求线程又在等业务线程池结果

最后你看到的是:

  • Tomcat busy 很高
  • 业务线程池 backlog 很长
  • 连接池 pending 也在涨
  • 接口 RT 全面抬升

这时候如果你只问“到底是 Tomcat 还是业务线程池”,其实已经问错层了。

更准确的问题应该是:

入口线程和执行线程,是不是同时被同一个下游等待链拖住了?

七、一个更实用的判断顺序:先区分“自己阻塞”还是“等待内部任务”

如果线上已经出现“Tomcat busy 高 + 业务线程池 backlog 长”,我更建议按下面顺序看。

第 1 步:先看请求线程有没有直接阻塞

通过:

  • 线程栈
  • 慢接口调用链
  • Tomcat 线程状态

确认请求线程是不是直接:

  • 查数据库
  • 调下游
  • 做重计算
  • 等锁

如果答案是是,那入口层优先级更高。

第 2 步:再看请求线程是不是在等业务线程池结果

如果主要链路会:

  • submit 异步任务
  • wait future
  • join 结果
  • 聚合多个内部任务

那业务线程池 backlog 就会直接回传成 Tomcat busy。

第 3 步:看业务线程池里的线程到底在做什么

重点区分:

  • 真在计算
  • 等数据库
  • 等连接池
  • 等下游
  • 被后台任务占住

第 4 步:把连接池和下游指标拉进来

如果业务线程池大量线程在等待资源,就继续对齐:

  • Hikari active/pending/idle
  • 慢 SQL、锁等待、事务时长
  • 下游 RT、超时、错误率

第 5 步:最后再考虑是否是线程模型和隔离策略问题

例如:

  • 请求线程不该做的阻塞做太多了
  • 主要链路和后台任务没有隔离
  • 异步设计表面异步,实际上主要链路还在等结果

这个顺序的关键是:

先找阻塞位置,再谈线程池大小。

八、什么时候该优先怀疑 Tomcat,什么时候该优先怀疑业务线程池

更该优先怀疑 Tomcat 入口层的时候

常见特征:

  • Tomcat busy 高得更早
  • 业务线程池不一定同步恶化
  • 请求线程栈主要停在数据库、下游或本地重逻辑
  • 单接口或少数入口路径就能把入口线程拖满

更该优先怀疑业务线程池的时候

常见特征:

  • 请求线程很快把活交出去
  • 业务线程池 queue 更早、更明显增长
  • 主要链路依赖内部异步任务返回
  • 后台任务与主要链路共享池子

两边都不能单独看待的时候

常见特征:

  • 请求线程和业务线程池都在等数据库或下游
  • 连接池 pending 和慢请求同步抬升
  • RT、超时、摘流量问题一起出现

这时应该把它们放回同一条慢链路里,不要强行二选一。

九、准备换方向时,我会看这几组信号

排到这里如果还想继续收窄,我会直接拿下面这组信号决定还要不要盯着这两个线程层。

十、最容易出现的几个误判

误判 1:Tomcat busy 高,就说明 Web 容器线程数太小

不一定。

很多时候是请求线程在替数据库、下游或内部任务排队。

误判 2:业务线程池 backlog 长,就说明只要扩业务线程池就行

如果线程在等连接、等锁、等下游,扩线程池通常只会把更多任务同时推向同一个瓶颈。

误判 3:Tomcat busy 和业务线程池 backlog 本质一样

不是。

它们分别反映入口层和执行层,虽然经常相互传导,但语义不同。

误判 4:只看单个线程池面板就能定位问题

不够。

这类问题一定要把慢请求、线程栈、连接池和下游 RT 一起看。

十一、现场最容易问偏的几个问题

1. Tomcat busy threads 很高,先看什么最不容易走偏?

先看请求线程栈。重点不是数值本身,而是这些线程卡在 JDBC、下游 HTTP、锁等待,还是本地重逻辑里。

2. 业务线程池 backlog 很长,是不是就说明异步设计有问题?

不一定。更常见的是主要链路确实依赖这些任务结果,或者后台任务和主链路混用了同一个池子。先把任务来源分开,再谈设计问题。

3. Tomcat busy 和业务线程池 queue 一起高,先查哪边?

先对齐时间窗,看谁先恶化;再看请求线程有没有直接阻塞。很多现场最后都会落到同一段数据库或下游等待,而不是两个线程池谁“打赢了谁”。

4. CPU 不高,但两边线程都高,是不是该先扩线程数?

通常不要。CPU 不高时,两边一起红更常见的解释是线程都在等数据库、连接池、下游或锁,先扩线程只会把等待放大。

5. 这篇和“隐藏等待点”那篇到底怎么分工?

隐藏等待那篇解决的是“接口慢但资源图不高时,该先怀疑哪类等待”;本文解决的是“已经看到入口线程或业务线程明显变差后,怎么判断堵点落在入口层、执行层,还是两边被同一段等待拖住”。

6. 什么信号出来后,就该把重心切到连接池?

当获取连接耗时、pending、事务时长和慢请求一起抬升时,就别再只盯 Tomcat busy 或 backlog 了。那时它们更像结果,真正起点往往在连接池和长事务。

7. 如果入口线程等的是 RPC 结果,后面还要不要继续盯 Tomcat?

如果已经怀疑问题落在 Netty / RPC I/O 推进不动、请求发不出去或响应读不回来,就该沿 RPC 链继续看。Tomcat busy 这时只是前面暴露出来的一层现象。

十二、证据够了以后,下一步往哪条线接

如果你已经确认不是单纯“线程数不够”,我通常不会继续在面板上打转,而是顺着手里最硬的证据往下接: