Tomcat busy threads 很高,和业务线程池 backlog 是一回事吗?
Tomcat busy threads 高和业务线程池 backlog 长,看起来都像“线程满了”,但指向的并不是同一层问题。更有用的做法是先看请求线程究竟卡在本地阻塞、等待内部任务,还是被连接池和下游一起拖住,再决定该盯入口线程、业务线程池还是更深的等待链。
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 长并不说明线程在“忙计算”,它也可能只是在“排等待链”。
六、最容易混淆的一种情况:请求线程和业务线程池一起卡在同一条等待链上
真实线上特别常见的是这种链:
- 请求线程接到请求
- 一部分逻辑在请求线程里直接执行
- 一部分逻辑提交到业务线程池
- 业务线程池任务再去等数据库或下游
- 请求线程又在等业务线程池结果
最后你看到的是:
- 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、超时、摘流量问题一起出现
这时应该把它们放回同一条慢链路里,不要强行二选一。
九、准备换方向时,我会看这几组信号
排到这里如果还想继续收窄,我会直接拿下面这组信号决定还要不要盯着这两个线程层。
- 本文只处理“入口请求线程”和“业务执行线程池”之间谁先堵、谁在等谁;如果你已经明确只是普通业务线程池拥塞,优先回到 线程池打满以后,应该先查队列、拒绝策略还是慢任务?。
- 如果 backlog 并不明显,重点其实是 worker 线程执行阶段为什么慢,更适合去 线程池队列不长但任务还是慢,常见瓶颈在哪里?。
- 如果你现在只知道接口慢,但还没分清入口线程、业务线程、连接池还是下游等待,就先回到 接口很慢,但 CPU、GC、数据库都正常,隐藏等待点可能在哪?。
- 如果 backlog 主要来自异步任务、补偿任务、MQ 消费,真正该查的是这些后台工作为什么把执行层拖住了;主线应切到 异步任务越堆越多,问题常常不在异步本身。
- 如果你已经确认真实瓶颈落在连接池 pending、长事务、锁等待,主线就该切到连接池和数据库等待;继续看 线程池和数据库连接池的容量,为什么要一起做预算? 更合适。
- 如果超时主要沿 RPC / Netty I/O 线程扩散,而不是 Tomcat 请求线程或普通 worker 线程,应该切到 Netty EventLoop 被阻塞后,为什么 RPC 超时会扩散?。
十、最容易出现的几个误判
误判 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 这时只是前面暴露出来的一层现象。
十二、证据够了以后,下一步往哪条线接
如果你已经确认不是单纯“线程数不够”,我通常不会继续在面板上打转,而是顺着手里最硬的证据往下接:
- 普通业务 worker 层已经明显拥塞:看 线程池打满以后,应该先查队列、拒绝策略还是慢任务?
- queue 不长,但 worker 拿到线程后还是慢:看 线程池队列不长但任务还是慢,常见瓶颈在哪里?
- backlog 主要落在异步任务、补偿任务、MQ 消费:看 异步任务越堆越多,问题常常不在异步本身
- 你还没确认慢到底落在哪层等待链,只看到接口慢而 CPU / GC / 数据库都不高:看 接口很慢,但 CPU、GC、数据库都正常,隐藏等待点可能在哪?
- 连接池 pending、长事务、锁等待和慢请求一起恶化:看 线程池和数据库连接池的容量,为什么要一起做预算?
- 你已经怀疑是 RPC / Netty I/O 推进不动,而不是 Tomcat 请求线程或普通业务线程:看 Netty EventLoop 被阻塞后,为什么 RPC 超时会扩散?