Java

Spring Boot Actuator、线程池、连接池、慢请求这些运行态观测点,应该怎么串起来看?

线上一慢,最麻烦的不是没图,而是每个人盯着不同一张图说话。把慢请求、线程池、连接池和下游等待放回同一分钟里,才看得出究竟是哪一段先把整条链拖住了。

  • Spring Boot
  • Actuator
  • 线程池
  • 连接池
  • 性能排查
18 分钟阅读

11:07,支付确认接口的 p99 从 180ms 抬到 2.8s。群里很快分成三派:

  • 一派盯着 http.server.requests,说就是接口整体变慢了。
  • 一派盯着业务线程池,看到 active=64/64queue=312,说线程池顶满了。
  • 还有人看见 Hikari active=48/50pending=19,马上想扩连接池。

问题是,这三拨人都拿着现场里真实存在的信号,但谁也说不清第一块骨牌到底是什么时候倒下的。

那次真正把线索串起来,是把 11:06 到 11:11 的几个关键指标放回同一张时间表里看:

时间慢请求Tomcat / 业务线程池HikariCP下游 / 数据库当时更该怎么理解
11:06p99 180msTomcat busy 42,业务池 active=21active=17pending=0下游库存 RT 90ms基线正常
11:07p99 900msTomcat busy 118,业务池 active=39queue=0active=19pending=0库存 RT 升到 420ms第一波慢先出现在下游等待
11:08p99 1.7sTomcat busy 176,业务池 active=64queue=84active=33pending=0库存 RT 620ms请求线程开始把慢放大成排队
11:09p99 2.8sTomcat busy 198,业务池 queue=312active=48/50pending=4库存 RT 760ms连接池开始被后续排队拖紧
11:10错误率 3.2%Tomcat 拒绝出现,业务池 queue=510pending=19数据库 CPU 47%,锁等待上升连接池红了,但它已经不是第一现场
11:11限流后回落Tomcat busy 121,队列清空一半pending=0库存 RT 回到 210ms说明前面那段等待链被截断了

这就是运行态观测最常见的误区:图很多,但没有按先后顺序说话。看到线程池红,不代表线程池是起点;看到连接池满,也不代表数据库先坏。

先把图收成一句完整的话

如果现场已经有 Actuator、线程池、连接池、下游 RT 这些观测点,我通常先逼自己把图翻译成一句话,而不是继续翻更多图:

某个时间点开始,哪类请求先慢;随后是谁在等待;这种等待又把哪个池子拖紧;最后错误是从哪里冒出来的。

这句话里有四个位置,少一个都容易误判:

  1. 哪类请求先慢:是全站一起抬头,还是只有 /pay/confirm/order/submit 这类接口在抬。
  2. 谁在等待:Tomcat 线程在本地算,还是很快把活扔进业务池后开始等结果。
  3. 哪个池子被拖紧:是业务线程池先排队,还是数据库连接先借不出来。
  4. 最后错误从哪里冒出来:超时、拒绝、504、连接池 pending,这些都是后果,不一定是起点。

把这四件事钉住,现场就不会再停留在“很多指标都不对”。

这类图最值钱的,不是峰值,而是谁先动

只看某一分钟的峰值,几乎一定会把因果关系看反。真正值钱的是前后 3 到 5 分钟的先后顺序

1. 慢请求先抬,线程和连接后面才跟着红

这种长相最常见:

  • 某个下游 RT 先从 80ms 升到 400ms 以上。
  • 请求线程还没满,但同一批请求耗时已经明显变长。
  • 业务线程池随后 active 拉满、队列开始堆。
  • 再之后才看到连接池 active 高、pending 开始出现。

这时线程池和连接池都在报警,但它们更像是慢请求被放大后的受害者。

2. 业务线程池先堆,连接池还算平

这种情况常见在:

  • 请求线程很快把活交给异步池、批处理池、RPC 池。
  • 业务池 queue 已经拉长,但 Hikari pending 仍然接近 0。
  • CPU 也许不高,因为线程大多在等远程调用、等锁、等结果汇总。

这时如果只因为连接池没红就排除应用内部问题,通常会漏掉真正的等待点。很多“CPU 不高但就是慢”的现场,卡的正是这里。

3. 连接池先紧,但数据库并不一定“打满”

另一个常见错觉是:只要 pending 出现,就说明数据库先扛不住了。

不一定。

连接池会被拖紧,至少有三种完全不同的原因:

看到的现象更像什么还要补哪份证据
active 高,pending连接借出去很忙,但还能周转看 SQL RT、事务时长、连接持有时长
active 高,idle=0pending 也高真正借不出来了看慢 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:看回落顺序,而不是只看有没有回落

真正有价值的是回落顺序:

  1. 先是下游 RT 或锁等待下降。
  2. 再是业务线程池队列缩短。
  3. 然后连接池 pending 清零。
  4. 最后 p95 / p99 和错误率回落。

这个顺序如果能对上,因果链通常就比较稳了。

什么时候别再留在这篇里

如果你已经很明确地看出问题长在某一段,就别继续在“观测点串联”上绕:

运行态观测真正的难点,从来不是指标名太多,而是没人替这些指标补上动词和先后顺序。

把“谁先慢、谁在等、谁被拖紧、错误最后从哪里冒出来”说清,Actuator、线程池、连接池这些图才会从一堆红灯,变成一条能收敛问题的等待链。