CPU 使用率很高但找不到高 CPU 应用——短时进程排查

本文是 Linux 系统排查基本功 系列的第一篇 叙事框架:现象 → 排查过程 → 根因 → 修复 → 预防

问题现象

告警触发

某日上午 10:30,运维监控系统发出告警:

SRE 值班群告警通知

API 网关节点 api-gw-03 CPU 使用率持续高于 85%,SRE 值班群立即响应。

上机排查遇阻

值班同学 SSH 登入 api-gw-03,执行 top 确认 CPU 确实很高——92.3% us。但浏览进程列表后困惑了:

排在最前面的 dockerd 也才 3.7%,所有可见进程加起来不到 10%。CPU 占用 92.3%,但没有任何进程看起来像罪魁祸首。

初步猜测

这可能不是正常的业务进程行为。CPU 被消耗了却找不到对应的进程——说明进程的存在时间极短,短到 top 的采样周期(3 秒)根本抓不到它们。需要更高频的工具来验证。

排查过程

第一步:确认 not top 的盲区

top 默认刷新间隔是 3 秒。如果一个进程在 3 秒内启动、执行、退出,top 根本来不及采样。这就是 短时进程(short-lived process)——生命周期远小于 top 刷新周期的进程。

要验证这个猜想,先看 /proc/stat 中的上下文切换计数:

grep ctxt /proc/stat

如果 ctxt 数字异常高,说明系统正在疯狂创建和销毁进程。但更直接的方法是换用更高频的工具。

第二步:perf top 看内核热点

perf top 不追踪进程维度,而是直接采样 CPU 正在执行的指令。即使进程已经退出了,只要 CPU 执行过它的代码,perf 就能捕捉到。

perf top

perf top 显示 syscall 热点

关键线索出现了: - do_syscall_64 占 23.42%——系统调用入口 - __x64_sys_execve 占 15.67%——执行 execve 系统调用 - load_elf_binary 占 11.23%——加载 ELF 二进制文件 - curl_easy_perform 也有涉及

每次创建子进程都需要 fork() + execve(),内核要复制页表、加载 ELF、分配内存。这些操作都是 CPU 密集的。perf top 实际上在告诉我们:系统在不停地创建新进程。

第三步:pidstat 高频采样抓进程

确认了 CPU 花在进程创建上,下一步就是用更高频的工具抓短时进程。

pidstat -p ALL 1 3

pidstat 捕捉到 sh 和 curl 短时进程

pidstat 每秒采样一次,成功捕捉到了许多 PID 为 shcurl 的进程。每个进程的 CPU 使用率在 8-15% 之间,但问题是它们的 PID 每次都在变——这说明进程运行几十毫秒就退出了,下一秒出现的已经是新的进程实例。

第四步:execsnoop 精确捕获

pidstat 还需要等它刷新。更直接的工具是 execsnoop(BCC 工具集的一部分),它跟踪 exec() 系统调用,在进程创建瞬间就能捕获到:

execsnoop

execsnoop 捕获短时进程创建链

execsnoop 的输出一目了然: - 系统每 200ms 就在创建 curlsh 进程 - 目标 URL 都是 http://127.0.0.1:8080/health - 还有大量 sh -c 执行的 CPU 密集计算

结合这些信息,可以断定是某个本地进程在频繁调用健康检查接口。

第五步:strace 追查父进程

execsnoop 的 PID 信息反查父进程:

# 用 execsnoop 中多次出现的 PPID(这里假设是 123456)
strace -f -p 123456 -e trace=clone,execve,nanosleep

strace 追踪到父进程不断 fork

strace 确认了父进程 123456 的执行模式: 1. nanosleep({tv_sec=0, tv_nsec=200000000})——休眠 200ms 2. clone(...)——fork 子进程 3. 子进程执行 execve("/usr/bin/curl", ...)——发起健康检查

PID 123456 是部署在机器上的一个自定义 节点健康检查脚本 health-check.sh。这个脚本每隔 200ms 就执行一次 curl 检查本地服务健康状态,每个 curl 进程跑大约 50-80ms 退出,刚好逃过 top 的采样窗口。

第六步:查看问题脚本

找到脚本 /opt/monitor/health-check.sh

有问题的健康检查脚本

脚本的设计缺陷很明显: - while true 无限循环,无间隔控制 - 每次循环都启动一个新的 curl 进程 - sleep 0.2 间隔太短,每秒产生约 15 个短时进程 - 完全没有连接复用和超时控制

根因分析

fork() 开销逐层拆解

每次 fork 子进程,内核需要复制父进程的页表、文件描述符表、信号处理表等。虽然有 COW(Copy-on-Write)优化,但页表复制本身就有成本。在 48 核机器上,页表复制涉及 TLB shootdown 的 IPI 中断,会进一步增加开销。

execve() 的 ELF 加载成本

执行 execve() 时,内核要: 1. 校验二进制文件格式和权限 2. 解析 ELF 头部和段表 3. 为 .text/.data/.bss 段建立内存映射 4. 加载动态链接器(ld-linux.so) 5. 解析共享库依赖(libcurl.so、libc.so 等) 6. 重定位符号

curl 约 200KB+,依赖 libcurl.so(~400KB)和 libc.so(~2MB)。每次 execve 都要加载这些共享库——虽然 Page Cache 可能缓存了文件内容,但解析符号和重定位的花费无法避免。

调度器上下文切换

每个短时进程经历:创建 → 就绪 → 运行 → 退出 四个状态。调度器每次切换都要保存/恢复寄存器状态、更新调度队列。短时进程越多,上下文切换越频繁。进程退出时,内核还要回收资源、发送 SIGCHLD、让父进程 wait。

CPU cache 污染效应

短时进程的内存操作会污染 L1/L2 cache。curl 进程虽然很快就退出,但它访问过的内存地址占用了 cache line,把业务进程的热数据挤出 cache。下一个 curl 进程进来时又重复加载——这种 cache thrashing 会进一步降低整体吞吐。

累计效应

在这个案例中,每个 curl 子进程的生命周期约 50-80ms,CPU 使用率约 10-15%。每秒创建 15 个进程,叠加后就是约 3-4 个核心的 CPU 占用(48 核机器上约 8-10% 的总 CPU)。再加上 sh 子进程执行计算脚本的开销,最终合计占满 92% 的 CPU。

修复方案

第一步:评估现有问题

原始脚本的核心问题是 频率过高进程创建模式不合理。200ms 的健康检查间隔在绝大多数生产场景下毫无必要——健康状态不可能在 200ms 内发生有意义的变化。

第二步:确定优化方向

优化目标有三个: 1. 大幅降低检查频率 2. 减少进程创建次数 3. 增加超时保护

第三步:重写脚本

优化后的健康检查脚本

关键改进: - 检查间隔从 200ms 延长到 5s:频率降低 25 倍,进程创建量大幅下降 - 函数封装 + 并行检查:多个健康端点一次并行检查,而非逐个串行 - 合理的超时控制curl --connect-timeout 3 --max-time 5 防止 hang - batch 处理:用 wait 等待所有并行子进程完成

第四步:上线部署

更新脚本后重启 systemd timer 服务,监控 CPU 和健康检查成功率。

验证结果

即时指标

修复后,CPU 使用率从 92.3% 降至 12.3%:

修复后 CPU 恢复正常

负载从 8.74 降到了 1.12,上下文切换恢复正常。

持续观察

观察 30 分钟,CPU 使用率稳定在 10-15% 之间,健康检查成功率 100%。并行 curl 的总耗时反而比原来串行更短——因为网络 IO 可以重叠。

团队复盘

修复后团队复盘讨论

团队复盘确认了根因:一个看似无害的健康检查脚本,因为设计不当导致 fork 风暴,拖垮了整台机器的 CPU。

避坑建议

  1. 不要用 while true 做监控脚本:任何生产环境的循环都应该有合理的间隔和 backoff 策略。固定间隔 + 指数退避的组合更好。

  2. 进程创建有成本:fork + exec 看起来简单,但内核开销不小。在频繁调用的路径上,尽量复用连接和进程池。

  3. top 有盲区:top 的刷新周期是它的固有缺陷。当 CPU 高但找不到进程时,优先怀疑短时进程,用 perf top / execsnoop 进一步诊断。

  4. 区分 CPU 计算和 CPU 开销:业务计算和系统调用开销都消耗 CPU,但优化方向完全不同。perf top 可以帮助区分。

  5. health check 也需要注意效率:健康检查虽然是辅助功能,但设计不当会成为性能瓶颈。实测 200ms 间隔的健康检查比 5s 间隔多消耗约 7-8 倍的 CPU。

  6. execsnoop 是短时进程排查的首选工具:比 pidstat 更精确、更实时。如果没有 BCC 工具集,也可以用 ps -eo pid,ppid,%cpu,comm --sort=-%cpu | head -20 在循环中跑,但效率低很多。

  7. 设置进程创建速率告警/proc/sys/kernel/threads-maxvm.max_map_count 等指标可以帮助提前发现进程创建异常。

附:完整命令清单

# 1. 查看整体 CPU 和使用率
top
cat /proc/stat | grep ctxt

# 2. perf top 查看内核热点
perf top

# 3. pidstat 高频采样捕捉短时进程
pidstat -p ALL 1 3

# 4. execsnoop 精确捕获进程创建(需要 BCC 工具集)
execsnoop

# 5. strace 追查父进程
strace -f -p <PPID> -e trace=clone,execve,nanosleep

# 6. ps 高频采样(备选方案,无 BCC 时使用)
for i in $(seq 1 20); do
  ps -eo pid,ppid,%cpu,comm --sort=-%cpu | head -20
  sleep 0.3
done