告警短信半夜响,登上去 top 一看,load average 三个数字全在 30 以上。这时候最忌讳的就是上去先 kill -9。杀错了业务直接断,而且下次照样复发。下面这五步是我平时用的顺序,从「哪个进程」一路挖到「哪一行代码」。
第一步:先看 CPU 烧在哪个维度
敲 top,按 1 展开每个核心,重点看第三行那几个百分比。不同的高点意味着完全不同的方向。
| 指标 | 含义 | 高了说明什么 |
|---|---|---|
| us | 用户态耗时 | 业务代码在算,死循环或正则回溯 |
| sy | 内核态耗时 | 系统调用太频繁,上下文切换过多 |
| wa | 等 IO 的时间 | 磁盘或网络是瓶颈,CPU 其实在闲等 |
| si | 软中断 | 网络包量大,可能正被打 |
| st | 被宿主机偷走 | 虚拟机超卖,你自己改不了 |
wa 高但 us 不高的时候,别去优化代码,去查磁盘。iostat -x 1 看 %util 和 await,接近 100% 就是磁盘顶不住了。
st 持续超过 10,基本可以确定是共享宿主机上的邻居在抢资源。这种情况优化自己没用,找服务商或者换机型。
第二步:锁定进程
ps aux --sort=-%cpu | head -10
或者用 pidstat,能看到采样区间内的平均值,比 top 的瞬时值更可信:
pidstat -u 1 5
这一步要留意一个坑:如果 CPU 是被几十个短命进程轮番占满的,ps 抓不到。这时用 execsnoop(bcc-tools)或者土办法循环采样:
for i in $(seq 1 20); do ps -eo pid,comm,%cpu --sort=-%cpu | sed -n '2p'; sleep 0.5; done
输出里反复出现的进程名就是元凶,常见的是失控的 cron 脚本或者备份任务。
第三步:钻到线程级
进程定位了,但一个 Java 或 Node 进程里几百个线程,还得再往下一层。
top -H -p <PID> 找出最耗 CPU 的线程 TIDprintf "%x\n" <TID> 把十进制转成十六进制jstack <PID> | grep -A 30 nid=0x<hex> 拿到调用栈Java 服务这套组合拳基本一击必中,栈顶那几行就是问题代码。Node 换成 node --prof 或者线上直接 kill -USR1 <PID> 开 profiler。Python 用 py-spy dump --pid <PID>,不用改代码不用重启,特别适合线上救火。
第四步:分清在算还是在等
看不出来栈的时候,统计一下系统调用分布:
strace -c -f -p <PID> -o /tmp/strace.txt
# 跑 10 秒后 Ctrl+C,然后看 /tmp/strace.txt
如果 futex 或 epoll_wait 占了绝大多数调用次数,那 CPU 时间花在锁竞争和等待上,不是在算。strace 本身会明显拖慢目标进程,线上核心服务慎用,用完立刻退出。
更轻量的选择是 perf,开销小得多:
perf top -p <PID>
# 或者采样 30 秒生成报告
perf record -F 99 -p <PID> -g -- sleep 30
perf report
第五步:常见元凶速查
按出现频率排,我遇到最多的是这几类:
MySQL 全表扫描。某个没走索引的查询被高频调用,mysqld 单进程吃满几个核。SHOW FULL PROCESSLIST 看有没有长时间 Sending data 的语句,再 EXPLAIN 一遍。
正则回溯爆炸。用户输入触发了灾难性回溯,一条请求能跑几十秒。PHP、Node、Java 都中过招。栈里会明显停在正则相关的帧上。
日志写爆。某个循环里疯狂打 debug 日志,磁盘 IO 拉满,wa 跟着上去。lsof -p <PID> | grep log 看它在写哪个文件,文件大小涨得多快。
被爬虫或 CC 打。si 偏高,配合 ss -s 看连接数暴涨。这时候先在 Nginx 层限流,别急着扩容。
容器 CPU limit 太低。Kubernetes 里 cpu.limit 卡得死,进程被节流。宿主机看 CPU 不高,容器里却卡到不行。cat /sys/fs/cgroup/cpu.stat 里 throttled_time 在涨就是这个原因。
常见问题
问:load average 多少算高?
拿 load 除以核心数。nproc 输出 4,load 在 4 以内算正常,超过 8 就该看了。持续高于核数两倍,队列已经在积压。单看数字没意义,一定要对着核数算。
问:CPU 100% 但找不到高占用进程,怎么办?
两种可能。一是短命进程轮番上,用上面第二步的循环采样法抓。二是内核线程在忙,ps -eo pid,comm | grep '\[' 看有没有 kswapd0(内存吃紧在换页)或 ksoftirqd(软中断压力)在跑。
问:能不能直接 kill 掉最占 CPU 的进程?
应急可以,但先用 kill -3(Java)或 kill -QUIT 让它输出栈信息再杀,不然重启后你还是不知道原因。真要抢救优先用 renice +19 -p <PID> 降优先级,比直接杀温和得多。
你在排查 CPU 打满时踩过什么反直觉的坑,欢迎到 fenij.com 留言分享,这类经验往往比工具本身更值钱。