第6章 CPU:CPU 高,不等于 CPU 已经成了问题

CPU 章节拆的是做工、排队、降频和失真四类现象;只看利用率不够,还要补 IPC/CPI、队列延迟、频率与缓存/NUMA 证据。

本页目录

CPU 高,先别急着说 CPU 不够

第 6 章先拦住一个最常见的误判:看到 CPU 利用率高,就把问题直接压成“CPU 瓶颈”。

这章真正想让你分开的,不是一种现象,而是四种完全不同的处境:

  • CPU 确实在高效做工
  • 线程已经开始排队等 CPU
  • CPU 表面很忙,但大量周期其实耗在等待内存、缓存或地址转换
  • 容器配额、throttling、steal time、降频把真实可用算力改写了

只看利用率,根本分不清这几件事。

别只看忙不忙,还要看算得值不值

如果把这章只读成一份“CPU 排障 SOP”,会漏掉它最值钱的一层:CPU 章不是只教你排队、热点和火焰图,它还在教你怎么分辨 CPU 到底是在算,还是在等。

这正是很多现场会卡住的地方。图上看起来 CPU 已经 100%,但机器并不一定是在高效计算,它也可能只是被缓存未命中、内存停顿、迁核、NUMA 远端访问拖住了。

利用率高,不等于 CPU 在高效做工

要补的第一个判断,不是“再看一个 top”,而是看 CPU 每个周期到底干了多少活。

第 6 章里更关键的信号是:

  • IPC:每周期执行的指令数
  • CPI:每条指令花掉的周期数
  • PMC:硬件性能计数器,拿它才能看到上面这些底层信号

这层判断很重要,因为两台都显示 100% CPU 的机器,含义可能完全相反:

  • 一种是 IPC 较高,说明 CPU 大部分时间真的在算
  • 另一种是 IPC 很低、CPI 很高,说明 CPU 很多周期耗在等待数据回来

所以以后看到 CPU 高,别只问“高不高”,先问一句:这是计算密集,还是停顿密集。perf stat 这类基于计数器的观测,才是把“有效做工”和“无效等待”拆开的硬指标。

100% CPU 还要问:它当时跑在什么频率

第 6 章还提醒了另一个经常被忽略的陷阱:CPU 的可用算力不是恒定的。

同样是 100% CPU,背后的处理器可能处在完全不同的频率状态:

  • 睿频拉高,短时间算得很猛
  • 节能策略把频率压低
  • 温度或功耗约束触发降频

这会直接改写你对“CPU 已满”的理解。因为 100% 只是“把当前这档频率吃满了”,不等于“把这颗 CPU 本来能给出的性能吃满了”。

所以现场里别只盯利用率,还要补频率与功耗状态。像 turbostat 这类工具,不是锦上添花,而是在防止你把“被降频后的 100%”错认成“原始算力的上限”。

“等 CPU”要看队列延迟,不只看平均负载

第 6 章并不是反对看排队,恰恰相反,它是在提醒你:如果要判断线程是不是已经拿不到 CPU,就得真正看到队列延迟。

这里至少要分清三层:

  • 平均负载和利用率,只能说明系统忙不忙
  • 运行队列长度,开始逼近有没有人在等
  • 队列延迟分布,才真正告诉你线程等了多久

所以传统的 topvmstatload average 只能给你轮廓,不能替你回答“线程到底卡了多久”。第 6 章更接近的做法,是直接看运行队列延迟,比如用 runqlat 这类工具把等待时间打出来。

这一步还有一个统计学坑:平均值太容易把微爆发抹平。秒级采样看起来不严重,不代表 200 毫秒内没有发生一轮很凶的 CPU 饱和。CPU 章强调的,是要能看到亚秒级偏移和突刺,而不是被分钟均值安慰。

热点不是根因,缓存、迁核和 NUMA 才决定热点是什么意思

火焰图当然有用,但它只能回答“谁最热”,不能自动回答“为什么热”。

第 6 章比很多常见 CPU 指南更进一步的地方,在于它把热点和底层硬件语义重新接上了:

  • 缓存未命中高,热点可能是在等数据,不是在高效算
  • 线程频繁迁核,热点可能被缓存失效放大
  • 多路机器上跨 NUMA 节点访问内存,热点可能只是远端访问的结果
  • 绑核和 CPU 亲和性,有时不是优化技巧,而是避免缓存热度被迁移冲掉

所以热点要和这些问题一起解释:

  • 这段代码是在正常计算,还是在忙等 / 重试 / 锁竞争
  • 这段热点伴随的,是高 IPC 还是高停顿
  • 线程有没有频繁迁核
  • 当前慢请求是不是落在远端内存或缓存未命中的路径上

如果这几层没接起来,火焰图再好看,也还只是方向感,不是解释链。

容器和云里,CPU 还会被“偷走”和“改写”

这部分你原来的稿子已经抓得比较对,但现在可以看得更完整一点。

在容器和虚拟化环境里,CPU 高还可能被下面这些因素扭曲:

  • cgroup quota
  • throttling
  • steal time
  • 每核分布失衡
  • 频率状态与宿主机调度策略

这意味着云上和容器里的 CPU 结论,不能只靠进程视角做。你要同时知道:

  • 应用是不是在等 CPU
  • 宿主机是不是只给了你一部分 CPU 时间
  • 这部分 CPU 时间是不是又被频率或调度状态打了折

到现场先跑这套 CPU 起手顺序

第 6 章最后能带走的,不是“CPU 高不一定是 CPU 问题”这句口号,而是一套从粗到细的起手顺序。

第一步:先固定对象、时间窗和症状

先固定三件事:

  • 具体是哪台主机、哪个容器、哪个进程
  • 问题发生在哪个时间窗
  • 对应的是哪类请求或哪批作业

对象不固定,后面的 CPU 证据就很容易和业务症状错位。

第二步:先看利用率、每核分布和 user/sys/steal

这一步先回答“它现在看起来忙不忙”,至少补:

  • 总体 CPU 利用率
  • 每核分布是否失衡
  • user / sys / steal 的构成
  • 当前频率是否异常偏低

这里只做第一层分流,不急着下结论。

第三步:再看有没有人在等 CPU

继续补:

  • 运行队列长度
  • 调度等待
  • 上下文切换
  • 队列延迟分布,例如 runqlat

如果只有忙,没有明显排队,CPU 更像工作显影;如果队列等待已经明显拉长,CPU 才更像进入关键路径。

第四步:用硬件计数器判断 CPU 在算还是在等

这一步是原稿里最缺的,也是第 6 章最不该漏掉的:

  • IPC / CPI
  • cache miss
  • 与内存停顿相关的硬件计数

这一步的目标只有一个:把“100% CPU”拆成“高效计算”还是“高比例停顿”。

第五步:容器和虚机里先排掉配额与频率失真

只要不是裸机,立刻补:

  • cgroup quota
  • throttling 次数和时长
  • steal time
  • 频率与功耗状态
  • CPU 亲和性和迁核情况

如果这一步没做,后面看到的热点和利用率都可能只是失真后的结果。

第六步:最后再看热点,并把它接回缓存、NUMA 和症状

这时再看 perf、火焰图或调用栈,问题才会更像是“解释”而不只是“找最热函数”。

看热点时要连着问:

  • 这段代码是在正常做工,还是在忙等、锁竞争、重试
  • 它对应的是高 IPC,还是高停顿
  • 它是否伴随缓存未命中、迁核或 NUMA 远端访问
  • 热点下降以后,时延或吞吐会不会一起改善

第七步:做一次缓解验证

CPU 真成了根因方向时,通常要能看到某种缓解效果,例如:

  • 降低负载后,请求时延跟着回落
  • 放宽配额后,等待明显下降
  • 修掉热点路径后,症状一起缓解
  • 调整亲和性或避免远端内存访问后,停顿明显减少

没有这类缓解验证,CPU 很多时候仍然只是显影位,不是终点。

CPU 结论至少要把这四件事分开

每次做完 CPU 排查,手里最好都留一句分类结论,强迫自己不要把不同现象混成“CPU 高”:

  • 正常做工变多,CPU 确实在高效算
  • 线程开始等 CPU,队列延迟已经拉长
  • CPU 看着很忙,但大量时间耗在停顿、缓存或内存等待
  • 配额、throttling、steal、降频等环境因素改写了可用算力

如果这句分类还写不出来,就说明这一章还没有真正落地。

CPU 分清以后,再看内存是不是也在骗人

第 6 章解决的是“CPU 高到底代表什么”。第 7 章接着解决另一个同样会骗人的表象:内存看起来快满了,到底算不算真的内存压力。