Java

CPU 热线程抓到了也处理了,为什么 Load 还是下不来?

高 CPU 热线程已经找到甚至临时处理掉了,系统 Load 却还是居高不下时,问题常常不在“热点线程还在不在”,而在队列积压、等待链、上下文切换、I/O 或容量恢复滞后。先把 CPU、Load、线程状态和资源队列重新放回同一条证据链,再判断下一步该查什么。

  • Java
  • CPU
  • Load
  • 性能排查
  • 线上问题
17 分钟阅读

线上 CPU 高时,很多排查会先集中在一个非常具体的目标上:

  • 找高 CPU 线程
  • 抓线程栈
  • 识别热点方法
  • 临时降级、限流、回滚或修复那条热路径

这条顺序本身没问题。

但真实现场里,非常让人困惑的一幕是:

  • 热线程抓到了
  • 问题代码也处理了
  • 甚至 CPU 已经开始回落
  • 可系统 Load 还是高
  • 接口 RT 也没有立刻恢复

这时候团队很容易陷入两种相反的判断:

  • 一种说,说明刚才的热点线程根本不是根因
  • 另一种说,说明还有别的隐藏热线程没抓到

这两种都可能对,但都太快了。

因为 CPU 热线程处理掉之后,Load 不立刻恢复,并不自动说明前面的判断错了。 很多时候,它只是说明:

  • 热线程虽然处理了,但积压还没消化
  • 线程没在烧 CPU 了,却还在等资源
  • 系统层还有上下文切换、I/O、连接队列、下游等待在拖尾
  • 负载恢复本来就有滞后

所以这篇文章想解决的问题是:

CPU 热线程已经抓到并处理了,为什么 Load 还是高?这时候下一步到底该查哪里,而不是继续盲抓线程?

如果先记一句话,可以先记这个顺序:

先分清 CPU 是否真的下来了,再判断 Load 高是因为 runnable 还多、I/O 等待高、队列还在积压,还是等待链没有恢复;不要把“热线程已处理”直接等同于“系统已恢复”。

如果 CPU 热线程修了但 Load 还高

你现在更像哪种情况更适合先看哪篇为什么
CPU 热线程已经抓到并处理了,但 load、RT 或队列迟迟不恢复继续看本文这正是本文的恢复期复核角色
你还在前半场,仍然需要先抓 CPU 热线程和热点方法Java CPU 飙高怎么排查:一套线上定位顺序先把 CPU 主因坐实
想判断 async-profiler、Arthas、jstack 先用哪个async-profiler、Arthas、jstack 到底什么时候该先用哪个?那篇更偏工具选择
你更像是在看系统层工具和 iowait、连接状态、磁盘设备top、pidstat、ss、netstat、iostat 分别适合回答什么问题?那篇更偏系统层分工

先把这篇的边界说清

这篇文章更适合在你已经处理掉前半场热点,但系统还没完全恢复时来看。

它更适合处理后半场问题:前半场的 CPU 热点已经处理后,为什么系统指标和负载没有同步恢复。

一、为什么“CPU 下来了但 Load 还高”最容易让人误判

因为很多人会把 CPU 和 Load 直接理解成同一件事。

但在线上排障里,这两者虽然相关,却不是一回事。

CPU 更像什么

更像:

  • 某一段时间里,处理器正在忙什么
  • 哪些线程在消耗时间片
  • 是业务线程热、GC 线程热,还是系统态忙

Load 更像什么

更像:

  • 当前系统上有多少任务在等待获得推进机会
  • runnable 线程是不是还很多
  • 不可中断等待、I/O 等待、资源等待是不是还在压着系统

所以完全可能出现这种现场:

  • 热点线程已经不再明显烧 CPU
  • 但系统上仍然有很多线程处于 runnable 或资源等待状态
  • 队列和积压还没消掉
  • load 仍然高

这也是为什么这类问题不能只继续盯“还有没有热线程”,而要把系统推进能力重新看一遍。

二、第一步先确认:是 CPU 真的回落了,还是你只处理掉了最显眼的一根刺

这一刀非常关键。

先不要急着讨论 load,先回答:

  1. 实例级 CPU 是否真的明显回落了?
  2. 高 CPU 线程是否真的不再稳定出现?
  3. 是不是只是最热的线程消失了,但还有第二梯队线程在顶着?

这一步更建议怎么做

  • 再看一轮 top -Hp
  • pidstat -t 看线程趋势是否整体回落
  • 对比处理前后 CPU、RT、QPS、线程池 active

为什么这一步重要

因为很多现场并不是“热线程已经处理掉”,而是:

  • 最显眼的热点没了
  • 但还有一批次热点线程接上来
  • 总 CPU 其实并没有实质恢复

如果这一步没先坐实,后面的 load 分析容易建立在错前提上。

三、如果 CPU 真下来了,Load 还是高,最常见先看哪 4 个方向

1. Runnable 线程和上下文切换仍然很多

这类现场很常见。

即使最热的单线程处理掉了,系统也可能还处在:

  • 线程数过多
  • runnable 线程很多
  • 调度仍然很忙
  • 吞吐恢复速度赶不上排队压力

典型信号

  • CPU 不再特别刺眼
  • vmstat 的运行队列和 cs 仍然高
  • pidstat -w 看到上下文切换仍然活跃
  • 线程池 active 很高,但单线程并不特别热

这时问题常常已经从“单点热点”转成“整体调度和并发模型空耗”。

2. I/O 等待还在压着系统

Load 高不一定都是 CPU 方向。

如果你看到:

  • CPU user 降了
  • 但 iowait 仍高
  • 磁盘、日志、落盘、临时文件、本地缓存或数据库客户端所在机器层仍有压力

那 load 高很可能是 I/O 等待在顶着。

这时更该补看的工具

  • top
  • iostat -x 1
  • pidstat -d -p <pid> 1

这一步很重要,因为不少团队在“热线程已处理”之后,会继续只盯 Java 栈,反而漏掉系统层拖尾。

3. 等待链没恢复,线程还在排队等资源

这是线上非常高频的一类。

例如:

  • 数据库连接池 pending 还高
  • 慢 SQL 还没恢复
  • 下游 RPC 超时还在持续
  • Redis、消息队列、对象存储等依赖仍然慢

这时会出现一种很典型的假象:

  • 热线程没了
  • 但接口还是慢
  • load 还是高

因为线程虽然不再烧 CPU,却仍然在系统里等推进。

常见特征

  • 大量线程停在 JDBC、HTTP、socket read、Future、连接池获取连接
  • queue 不一定很长,但 active 线程不低
  • RT 恢复明显滞后于 CPU 恢复

4. 积压还没消化完,系统恢复本来就有滞后

这类经常被误判成“根因还在”。

真实现场里,热点线程处理掉之后,系统不一定立刻回到正常状态,因为还有:

  • 线程池 backlog
  • MQ backlog
  • 请求超时重试遗留波峰
  • 缓存回源后的数据库拖尾
  • 失败补偿任务还在跑

也就是说,前面那根刺拔掉了,不代表前面刺出来的伤口已经瞬间愈合。

如果你忽略恢复滞后,就很容易误以为:

  • 先前判断错了
  • 或者还有更深层“神秘根因”

其实很多时候只是系统还在还债。

四、怎么判断现在更像“还在等”,还是“还在忙”

这是下一步最关键的一刀。

1. 更像“还在忙”的信号

  • CPU 总体仍然不低
  • runnable 线程仍多
  • 上下文切换高
  • 热点线程虽然换了一批,但仍持续存在

这时你要继续看:

  • 是否还有第二层热点路径
  • 线程池是否过度并发
  • 是否从单线程热点转成整体调度空耗

2. 更像“还在等”的信号

  • CPU 已明显回落
  • 但 RT、load、active 线程数还高
  • 大量线程停在连接池、JDBC、HTTP、Future、网络读写上
  • 下游依赖指标恢复明显滞后

这时你更该继续看:

  • 数据库
  • 下游服务
  • 连接池
  • 队列积压
  • 补偿 / 重试链路

五、一个更稳的排查顺序

如果你已经处理了热线程,但 load 还是高,我更建议按下面顺序继续走。

第 1 步:确认 CPU 是否真的恢复

  • 处理前后 CPU 曲线是否明显下移
  • 热线程是否不再稳定出现
  • 还有没有新的热点线程接力

第 2 步:区分 load 是谁在撑

  • runnable 线程多不多
  • iowait 高不高
  • 上下文切换是否仍很高

这一步可以用:

  • top
  • vmstat
  • pidstat -w
  • iostat

第 3 步:回到线程状态

  • 线程是在跑,还是在等
  • 是否大量卡在 JDBC、HTTP、连接池、Future、socket read
  • 有没有明显 backlog 未消化

第 4 步:看资源池和依赖链

  • 数据库连接池 active / pending
  • 下游 RT、timeout、错误率
  • MQ backlog
  • 缓存回源压力
  • 线程池 queue 和 reject

第 5 步:最后才下结论

  • 如果仍在忙,就继续找第二层热点或调度空耗
  • 如果主要在等,就转向等待链、资源池、积压恢复
  • 如果是恢复滞后,就关注积压消化速度和回归曲线,而不是立刻重新开新战场

六、一个典型例子:为什么热线程处理后,load 还是高

假设你处理前看到:

  • 某个 JSON 序列化热点线程把 CPU 顶到 95%
  • 修复后 CPU 降到 45%
  • 但 load 仍然维持在高位
  • 接口 RT 只恢复了一部分

继续往下看发现:

  1. 高峰期间积压了大量请求
  2. 数据库连接池 pending 仍高
  3. 一批重试请求在持续回放
  4. 部分线程还在 Future.get() 和 JDBC 获取连接上等待
  5. vmstat 显示 runnable 和 cs 仍偏高,但最热单线程已经不明显

这时真正的链路就很清楚:

  • 最初是 CPU 热点先把系统拖坏
  • 热点处理后,CPU 主因消失了
  • 但积压、重试和连接等待开始接管后半场
  • load 因为大量等待与调度压力还没恢复

这时候如果继续只找“新的热线程”,就会走偏。

七、关键误判:这类问题最容易在哪些地方走偏

误判 1:Load 没降,就说明热线程不是根因

不一定。

很多时候热线程是第一张多米诺骨牌,后面的 backlog 和等待链需要时间恢复。

误判 2:CPU 下来了,就说明系统应该立刻恢复

错。

CPU 恢复只代表最显眼的计算热点缓解了,不代表连接池、队列、下游和补偿链立刻恢复。

误判 3:load 高就继续只抓线程栈

不够。

你还要看:

  • iowait
  • runnable 队列
  • 上下文切换
  • 资源池 pending
  • backlog

误判 4:看到等待链,就完全否定之前的 CPU 热点结论

也不对。

很多线上事故就是分阶段演化的:

  • 前半段是 CPU 热点
  • 后半段是等待链和积压接管

它们可能都是真的,只是处在不同阶段。

八、FAQ:CPU 热线程处理后,load 还是高时最常被问到的几个问题

1. CPU 下来了但 load 不降,是不是系统层问题?

有可能,但不能直接下结论。

还要先分清:

  • 是 runnable 线程多
  • 是 iowait 高
  • 还是等待链和 backlog 在拖尾

2. 什么时候更该先看 vmstat / iostat,而不是继续抓 jstack

当你已经确认最热线程不明显了,但 load 仍高,这时系统层工具优先级会明显上升。

3. 热线程处理后 RT 没立刻恢复,正常吗?

正常,而且线上很常见。

如果前面已经积压了请求、重试、补偿任务或连接等待,恢复本来就会滞后。

4. CPU、load、RT 三者恢复顺序一定一致吗?

不一定。

很多时候先恢复的是 CPU,后恢复的是 RT 和 load,最后才是队列和错误率。

5. 这类问题下一步最容易漏掉什么?

最容易漏掉:

  • backlog
  • 连接池 pending
  • 下游 RT
  • iowait
  • 上下文切换

九、CPU 下来了,但现场还没收住时

如果 CPU 热点已经处理掉了,团队却还在追着 load、等待链和积压跑,下面这些文章适合按现场继续往下拆。

这篇放在哪条排查线上

  • JVM 与 Java 性能排查

如果你还在补 CPU 这一段

如果现场已经转成等待链和系统层拖尾

读到这里,下一步怎么接

  1. 先把这篇里的主线过一遍,确认 CPU 回落之后,load 还由谁撑着
  2. 如果前面的 CPU 热点其实还没坐实,继续看 Java CPU 飙高怎么排查:一套线上定位顺序async-profiler、Arthas、jstack 到底什么时候该先用哪个?
  3. 如果 CPU 已经不是主因,线程更多是在等资源,再去看 线程池队列不长但任务还是慢,常见瓶颈在哪里?数据库连接池打满时,根因通常不是连接数太小
  4. 如果你怀疑现在已经进入系统层拖尾,再补 top、pidstat、ss、netstat、iostat 分别适合回答什么问题?

十、最后总结:热线程处理掉,只是前半场结束,不代表系统已经恢复

CPU 热点处理后 load 还是高,这类问题真正容易带偏人的地方,在于它看起来像“前面判断错了”。

但很多真实线上事故并不是一个单点现象,而是一条链:

  • 先是热点线程把系统打偏
  • 再是积压、等待链和资源池压力接管后半场
  • 最后才慢慢恢复

所以更稳的主线应该是:

  • 先确认 CPU 是否真的恢复
  • 再分清 load 是谁在撑
  • 再看线程是在忙、在切,还是在等
  • 最后回到 backlog、连接池、下游和系统层拖尾

只要这条顺序不乱,CPU 热线程已经处理但 load 还高 就不会再只是一个让人沮丧的现象,而会慢慢收敛成一条可以解释、可以验证、也可以治理的恢复期故障链。