Spring Boot Actuator、线程池、连接池、慢请求这些运行态观测点,应该怎么串起来看?
线上一慢,最麻烦的不是没图,而是每个人盯着不同一张图说话。把慢请求、线程池、连接池和下游等待放回同一分钟里,才看得出究竟是哪一段先把整条链拖住了。
11:07,支付确认接口的 p99 从 180ms 抬到 2.8s。群里很快分成三派:
- 一派盯着
http.server.requests,说就是接口整体变慢了。 - 一派盯着业务线程池,看到
active=64/64、queue=312,说线程池顶满了。 - 还有人看见 Hikari
active=48/50、pending=19,马上想扩连接池。
问题是,这三拨人都拿着现场里真实存在的信号,但谁也说不清第一块骨牌到底是什么时候倒下的。
那次真正把线索串起来,是把 11:06 到 11:11 的几个关键指标放回同一张时间表里看:
| 时间 | 慢请求 | Tomcat / 业务线程池 | HikariCP | 下游 / 数据库 | 当时更该怎么理解 |
|---|---|---|---|---|---|
| 11:06 | p99 180ms | Tomcat busy 42,业务池 active=21 | active=17,pending=0 | 下游库存 RT 90ms | 基线正常 |
| 11:07 | p99 900ms | Tomcat busy 118,业务池 active=39,queue=0 | active=19,pending=0 | 库存 RT 升到 420ms | 第一波慢先出现在下游等待 |
| 11:08 | p99 1.7s | Tomcat busy 176,业务池 active=64,queue=84 | active=33,pending=0 | 库存 RT 620ms | 请求线程开始把慢放大成排队 |
| 11:09 | p99 2.8s | Tomcat busy 198,业务池 queue=312 | active=48/50,pending=4 | 库存 RT 760ms | 连接池开始被后续排队拖紧 |
| 11:10 | 错误率 3.2% | Tomcat 拒绝出现,业务池 queue=510 | pending=19 | 数据库 CPU 47%,锁等待上升 | 连接池红了,但它已经不是第一现场 |
| 11:11 | 限流后回落 | Tomcat busy 121,队列清空一半 | pending=0 | 库存 RT 回到 210ms | 说明前面那段等待链被截断了 |
这就是运行态观测最常见的误区:图很多,但没有按先后顺序说话。看到线程池红,不代表线程池是起点;看到连接池满,也不代表数据库先坏。
先把图收成一句完整的话
如果现场已经有 Actuator、线程池、连接池、下游 RT 这些观测点,我通常先逼自己把图翻译成一句话,而不是继续翻更多图:
某个时间点开始,哪类请求先慢;随后是谁在等待;这种等待又把哪个池子拖紧;最后错误是从哪里冒出来的。
这句话里有四个位置,少一个都容易误判:
- 哪类请求先慢:是全站一起抬头,还是只有
/pay/confirm、/order/submit这类接口在抬。 - 谁在等待:Tomcat 线程在本地算,还是很快把活扔进业务池后开始等结果。
- 哪个池子被拖紧:是业务线程池先排队,还是数据库连接先借不出来。
- 最后错误从哪里冒出来:超时、拒绝、504、连接池
pending,这些都是后果,不一定是起点。
把这四件事钉住,现场就不会再停留在“很多指标都不对”。
这类图最值钱的,不是峰值,而是谁先动
只看某一分钟的峰值,几乎一定会把因果关系看反。真正值钱的是前后 3 到 5 分钟的先后顺序。
1. 慢请求先抬,线程和连接后面才跟着红
这种长相最常见:
- 某个下游 RT 先从 80ms 升到 400ms 以上。
- 请求线程还没满,但同一批请求耗时已经明显变长。
- 业务线程池随后
active拉满、队列开始堆。 - 再之后才看到连接池
active高、pending开始出现。
这时线程池和连接池都在报警,但它们更像是慢请求被放大后的受害者。
2. 业务线程池先堆,连接池还算平
这种情况常见在:
- 请求线程很快把活交给异步池、批处理池、RPC 池。
- 业务池
queue已经拉长,但 Hikaripending仍然接近 0。 - CPU 也许不高,因为线程大多在等远程调用、等锁、等结果汇总。
这时如果只因为连接池没红就排除应用内部问题,通常会漏掉真正的等待点。很多“CPU 不高但就是慢”的现场,卡的正是这里。
3. 连接池先紧,但数据库并不一定“打满”
另一个常见错觉是:只要 pending 出现,就说明数据库先扛不住了。
不一定。
连接池会被拖紧,至少有三种完全不同的原因:
| 看到的现象 | 更像什么 | 还要补哪份证据 |
|---|---|---|
active 高,pending 低 | 连接借出去很忙,但还能周转 | 看 SQL RT、事务时长、连接持有时长 |
active 高,idle=0,pending 也高 | 真正借不出来了 | 看慢 SQL、锁等待、长事务 |
pending 高,但数据库 CPU/QPS 不夸张 | 连接可能被应用长时间拿在手里 | 看事务里是否夹了远程调用、文件 IO、批处理 |
也就是说,连接池红只能证明“有人在等连接”,还不能直接证明“数据库就是根因”。
4. 全都一起抬头,而且跟 GC / STW 时间重合
如果慢请求、线程池、连接池几乎在同一分钟一起跳,而且 JVM 停顿时间也重合,那就别只盯业务层等待链了。
这时更像是:
- Full GC 或长时间 STW 先把应用整体按住。
- 请求开始堆积,线程池被动顶高。
- 连接迟迟归还,连接池跟着变紧。
- 网关、客户端、上游再把这段停顿表现成各种 timeout。
这种现场如果一上来就扩线程池、扩连接池,效果通常很差,因为根部根本不在池子大小。
我更信“样本分布”,不太信单个平均值
平均 RT 很会骗人,尤其是问题只集中在一类请求上的时候。比起一条平均线,我更想先看到下面这张分布:
| 请求样本 | 占比 | 耗时主段 | 对线程 / 连接的影响 |
|---|---|---|---|
| 正常请求 | 82% | 60ms - 180ms | 基本不排队 |
| 轻度变慢请求 | 13% | 400ms - 900ms,下游等待变长 | 开始占满业务线程 |
| 重度慢请求 | 5% | 2s 以上,往往伴随重试或长事务 | 最容易把队列和连接池一起拖高 |
很多时候,真正拖垮现场的不是“全部请求都慢”,而是那 5% 到 10% 的重度慢请求先把等待链撕开,然后把剩下的大盘也拖进去。
所以我通常不会只问“线程池是不是满了”,而会继续问:
- 是哪些 URI 在把线程占住?
- 是不是某一类请求尾延迟特别长?
- 这些慢请求里,线程在等数据库、等下游、等锁,还是已经在本地算?
真正收敛时,我会沿着这条等待链往回找
不是从池子往外猜,而是从慢请求往回走。
先看入口:到底是哪类请求把时间吃掉了
先把 URI、状态码、分位耗时切出来。如果只有一两个接口抬头,优先追那一条;如果全站一起慢,再去想是不是共享资源、GC、网络或下游全局抖动。
再看线程:线程是在做事,还是已经开始排队等待
线程池最值钱的不是 active,而是线程栈和队列。
- 线程大多卡在 JDBC、HTTP client、Redis client:更像外部等待。
- 线程栈集中在本地锁、对象池、future.get:更像应用内部同步点。
- Tomcat 线程多,业务池空:请求还没扔出去,卡在入口层。
- Tomcat 线程少,业务池爆:入口不一定堵,内部执行层已经在排队。
最后再看连接池:它是在提醒你哪一段持有太久
连接池更像一面镜子,照出“连接归还变慢”这件事。你真正要找的是:
- SQL 本身慢。
- 事务里塞了不该塞的等待。
- 结果集太大,连接迟迟不释放。
- 某个批量任务和在线请求抢同一池连接。
如果你已经确定主要矛盾是数据库长事务、锁等待或慢 SQL,可以直接切到数据库连接池打满时,根因通常不是连接数太小。这篇更适合你还在分辨等待链起点的时候看。
别急着扩池,先做两个验证动作
很多现场之所以越改越乱,就是在没分清起点前先动了线程池和连接池。比起直接扩容,我更愿意先做下面两种验证:
验证动作 1:切断怀疑最重的那段等待
比如:
- 临时摘掉一个可疑下游的非核心调用。
- 暂停一个高峰期批任务。
- 关闭一段刚上线的额外回查逻辑。
如果线程池、连接池和慢请求一起明显回落,说明你切中的就是等待链前段,而不是只碰到了表象。
验证动作 2:看回落顺序,而不是只看有没有回落
真正有价值的是回落顺序:
- 先是下游 RT 或锁等待下降。
- 再是业务线程池队列缩短。
- 然后连接池
pending清零。 - 最后 p95 / p99 和错误率回落。
这个顺序如果能对上,因果链通常就比较稳了。
什么时候别再留在这篇里
如果你已经很明确地看出问题长在某一段,就别继续在“观测点串联”上绕:
- 只在高峰时才开始漂,而且同一实例高低峰差异特别大,去看Spring Boot 服务只在高峰期漂移,该从线程池、连接池还是缓存切入?。
- 主要矛盾已经变成 health、readiness 和下线时序的错位,去看Spring Boot 健康检查、就绪探针和优雅下线为什么经常让服务状态看起来不对?。
- 网关 504、客户端 timeout、应用 timeout 混在一起,去看网关 504、应用超时、客户端超时,到底谁先超时?。
运行态观测真正的难点,从来不是指标名太多,而是没人替这些指标补上动词和先后顺序。
把“谁先慢、谁在等、谁被拖紧、错误最后从哪里冒出来”说清,Actuator、线程池、连接池这些图才会从一堆红灯,变成一条能收敛问题的等待链。