CPU 高先分账再定位: us/sy/wa/st 各指向不同病灶 — run queue 才是饱和的真话筒, 平均 CPU 是最大的骗子
perf / pprof / py-spy 接力找函数CPU 分析像超市收银台: %Cpu(s) 是把收银员的 100% 时间拆成"扫码(us)、对账找主管(sy)、等顾客掏钱包(wa)、被隔壁店借走(st)"四本账; run queue 是门口排队的顾客数 — 收银员忙不忙看利用率, 队排多长才是顾客真正的痛。而 top 平均值像"全店平均客流": 一号窗口排爆了, 平均下来还是"健康" — 所以定位永远从"分账 + 按核看"开始, 再用 perf/pprof 把显微镜对准函数。
$ uptime # load average: 2.31, 1.94, 1.72 → 1m>5m>15m, 压力爬升中 # load average: 20, 10, 3 → 刚刚突然出事, 查 5 分钟内变更
# load=30, 32 核, 不能直接说健康: # vmstat r=2 b=28 wa=70% → 28 个进程在等 IO, 是磁盘的事
us sy ni id wa hi si st 各答一题: 业务多重 / 内核多重 / 等 IO 多久 / 被宿主机偷多少。 # us 业务 · sy 内核 · ni nice · id 空闲 # wa 等 IO · hi 硬中断 · si 软中断 · st 虚拟化偷走
$ ps -eo pid,stat,cmd | grep -E "D|Z" # 大量 D → 磁盘/NFS 卡; 大量 Z → 父进程回收缺失
$ mpstat -P ALL 1 # CPU0 %usr=98 CPU1 10 CPU2 8 ... → 单核热点实锤
$ vmstat 1 # r=40 (8核) → CPU 饱和排队; b=25 wa=70 → IO 瓶颈
$ pidstat -w 1 # cswch/s 巨高 → 大量阻塞等待; nvcswch/s 高 → CPU 抢得凶
-u CPU、-r 内存与缺页、-d 磁盘、-w 切换、-t 线程粒度。 $ pidstat -p 1234 -durwt 1 # 一个进程五个维度, 1s 刷新
$ pidstat -t -p 1234 1 # 按线程看, 揪出 100% 的那个线程/proc/PID/status 的 Threads 字段: 正常 100 突然 5000, 必然伴随 cs 暴涨与 P99 抖动 — 请求级建线程是元凶。 $ grep Threads /proc/1234/status # Threads: 5000 ← 正常 100, 突增 50 倍不是好消息
strace -c 统计 syscall: futex 多 = 等锁, epoll_wait 多 = 等 IO, connect 卡 = 建连慢。 $ strace -c -p 1234 # futex 3800 92% → 92% 时间在等锁, 继续 profile 锁
perf top -p PID 实时看函数热点; perf record -F 99 -g 采样后 perf report 看调用栈, 是火焰图的数据源。 $ perf record -F 99 -p 1234 -g -- sleep 30 $ perf report # json.Marshal 45% / memcpy 20% ...
pprof 进 runtime 找哪段代码。 # pidstat: API 进程 750% → 进 pprof: $ go tool pprof -top http://svc:6060/debug/pprof/profile?seconds=30
# 错: "平均 8%, CPU 没问题" → 服务照样卡 # 对: mpstat -P ALL 1 → CPU7=100% → 单核热点定位
新人看总 CPU 8% 直接排除 CPU 嫌疑。真凶是单线程组件 (event loop / GIL / 全局锁) 烧满一个核, 其他核全程围观。
# 第一步: 按核看, 不看平均 $ mpstat -P ALL 1 # CPU %usr %sys %idle # 0 98 1 1 ← CPU0 满血 # 1~31 2 0 97 ← 其余围观 # 第二步: 找到烧 CPU0 的线程 $ pidstat -t -p 1234 1 | sort -k8 -rn | head -3 # tid 4482 %usr 97.8 ← 单线程热点 # 第三步: 对该线程做 runtime 画像 (py-spy / perf -t)
结论指向单线程架构 (asyncio event loop / GIL / 单 reactor), 解法是拆分或多进程, 不是加机器。
用户态烧满说明在"算"。Linux 层先锁定进程, runtime 层锁定函数, 两棒接力。
$ pidstat -u 1 | sort -k8 -rn | head -3 # 12:34:56 UID PID %usr %system %CPU Command # 1000 1234 690 12 702 api-server ← 8核用7核 $ go tool pprof -top http://127.0.0.1:6060/debug/pprof/profile?seconds=30 # flat flat% sum% cum% # 12.3s 45.2% 45.2% encoding/json.Marshal # 3.1s 11.4% 56.6% encoding/json.encodeState.object
顺藤摸瓜发现接口返回 2MB 大 JSON → 加字段裁剪 + 分页, CPU 降 40%。
us=20 sy=60 的机器值得彻查: 内核在替你干大量活。syscalls 统计一锤定音。
$ strace -c -f -p 1234 -o /tmp/st.txt; sleep 10; head -10 /tmp/st.txt # % time calls syscall # 41.2 18342 connect ← 每秒 1800 次建连, 短连接风暴 # 30.8 15200 close # 12.1 6100 sendto # 根因: HTTP client 每次 new, 没有连接池 # 修复: 复用 http.Client + Transport MaxIdleConnsPerHost=100 # 效果: sy 60% → 8%, connect 调用归零
注意: 高频 syscall 进程别直接 strace -p 跟踪 (会拖慢), 用 -c 统计模式。
wa 只说"CPU 在等 IO", 不是磁盘利用率。转交 iostat/pidstat 拿到真凶 (详见 IO 篇)。
$ vmstat 1 # r=2 b=25 wa=65% ← b 高: 25 个进程卡在不可中断 IO $ iostat -xz 1 # await=500ms aqu-sz=80 ← 设备排队爆炸 $ pidstat -d 1 | sort -k5 -rn | head -3 # 09:10 PID kB_rd/s kB_wr/s Command # 5678 0 409600 log-agent ← 凶手是日志 agent
链路: wa/b (vmstat) → await/aqu-sz (iostat) → 凶手进程 (pidstat -d)。
虚拟机/CNI 环境里 st 高说明宿主机过载, 应用怎么优化都没用, 应当换宿主或升配。
$ mpstat -P ALL 1 | tail -1 # all %usr %sys %iowait %idle %steal # 30 5 2 43 20 ← 20% CPU 被偷 # 判断: st 持续 > 5% 开始留意, > 10% 找云厂商/换宿主机 # 排除项: 不是应用问题, pprof 里也看不到"偷走的时间" # 佐证: 同宿主机其他 VM 同时变慢 (云厂商口径)
把 st 加进基础设施告警, 迁移后 CPU 同样的业务延迟立刻恢复。
QPS 没变, cs 从 20k/s 涨到 500k/s — 线程数失控是第一嫌疑。
$ vmstat 1 # r=45 cs=500000 ← 8 核扛 50 万次切换/秒 $ grep Threads /proc/1234/status # Threads: 5200 ← 正常 120 # 代码审查发现: 每个请求 exec.Command + new thread pool # 修复: 全局 worker pool, 固定 64 worker + 有界队列 # 效果: cs 回落 2 万/s, P99 1.2s → 90ms
线程越多不一定越快: cache miss + 切换开销在核数之上惩罚你。
load 高 CPU 不高, 先查 b 列和 D 状态进程, 不要按"CPU 瓶颈"扩容。
$ uptime → load average: 30, 28, 26 $ vmstat 1 # r=2 b=28 wa=70% ← 全在等 IO $ ps -eo pid,stat,wchan,cmd | awk '$2 ~ /D/' # 7891 D rpc_wait tar -czf /mnt/nfs/backup.tgz ← NFS 挂了 # 处置: 换本地盘备份 + 给 NFS 挂 soft,timeo=100 重试参数 # load 随即回落 — 它统计的是 D 状态, 不是 CPU
kill -9 都杀不掉的 D 状态进程, 只能等 IO 返回或重启主机 — 预防靠监控挂载健康。
进程级只说"谁在烧", 线程级才知道"哪个线程在烧", 再用 runtime dump 对齐到代码。
$ pidstat -t -p 1234 1 | sort -k8 -rn | head -3 # tid 4482 %usr 97.8 # Python: 直接看进程在干嘛 $ py-spy dump --pid 1234 # Thread 4482 (idle): "worker-7" # regex.py:88 (re.compile) ← 每次循环重编译正则 # 修复: 预编译进模块常量 → CPU 97% → 12%
Linux 视角 (tid) 与 runtime 视角 (goroutine/thread name) 的映射是排障关键一步。
pprof 只看得到 runtime 内, perf 能看到内核态。火焰图宽块 = 热点候选。
$ perf record -F 99 -p 1234 -g -- sleep 30 $ perf script > out.stacks # 用 FlameGraph 工具生成: stackcollapse-perf.pl | flamegraph.pl > cpu.svg # 火焰图最宽的三块: # __memmove_avx_unaligned_erms 38% ← 大 payload 拷贝 # json.Marshal 27% # tcp_sendmsg 12% # 根因: 一次返回 8MB, 拷贝 3 次 (decode/encode/send) # 修复: 加分页上限 → 拷贝量降 95%, CPU 降一半
火焰图横轴是 CPU 样本占比, 不是时间轴 — 找最宽的, 不是最"深"的。
现场已经恢复, top 一无所获。sysstat 历史数据是唯一的目击证人。
$ sar -u -f /var/log/sa/sa26 | awk '$1 ~ /^03:/' # 03:00:01 %usr %sys %iowait %idle # 03:00:01 82 8 2 8 ← 尖峰开始 # 03:10:01 15 2 1 82 ← 自动恢复 $ sar -q -f /var/log/sa/sa26 | awk '$1 ~ /^03:/' # loadq 也在 03:00 跳升, 与备份 cron 时间对齐 # 修复: crontab 里 backup 加 nice -n 19 + ionice -c3, 限速 rsync
生产机常备: 打开 sysstat 采集 (ENABLED="true" in /etc/default/sysstat), 本地历史救命。
vmstat r/b 分开看。 # 错: load=30, 32核 → "快爆了" # 其实 b=28 在等磁盘 # 对: vmstat 1 → r=2 b=28 → 查 IO
mpstat -P ALL 1 / top 按 1。 # 错: top 顶部 "Cpu(s): 8.0% us" 就翻篇 # 对: top 按 1 → 逐核查看
pidstat -t / top -H。 # 错: pidstat -u 1 → 1234 700% → 没线索了 # 对: pidstat -t -p 1234 1 → tid 4482 97%
# 错: 继续调代码优化 P99 # 被偷的 20% 回不来 # 对: mpstat 看 %steal → 工单 + 换宿主
iostat await / %util / aqu-sz。 # 错: "wa 50% = 磁盘饱和一半" # 对: iostat -xz 1 → await/aqu-sz 判定磁盘状态
mutex profile / strace futex 佐证。 # 错: cs 50万/s → "锁竞争!" → 改锁 → 白改 # 对: strace -c 看 futex 占比 + mutex profile 交叉验证
vmstat 1 连看 + sar 历史。 # 错: top 一眼 100% → 误判饱和 # 对: vmstat 1 10 看连续 + sar 对齐历史
pidstat -u 不加 1 输出开机以来均值, 全是历史水分. 原因: 用法不对. 正解: 永远带 interval [count]。 # 错: pidstat -u # 开机均值, 掩盖当下 # 对: pidstat -u 1 5 # 逐秒 5 次
pidstat -w 两列都看。 # 错: 只看 cswch/s (等锁多) 就下结论 # 对: nvcswch/s 也高 → CPU 竞争激烈, 该扩容了
# 错: 8核→64核, 吞吐不变 (还是 1 核上限) # 对: 单核热点 → 水平分片或多进程
-c, 或改用 eBPF (syscount)。 # 错: strace -p 1234 (生产 hot path) # 服务卡死 # 对: strace -c -p 1234 (10s) 或 syscount-bpfcc
-F 99 足够统计意义。 # 错: perf record -F 9999 -p PID # 对: perf record -F 99 -p PID -g -- sleep 30
-g 记录调用链。 # 错: perf record -F 99 -p PID # 无栈, 难归因 # 对: perf record -F 99 -g -p PID
--symfs。 # 错: 78.2% 0x00007f8a2c1e4f10 [unknown] # 对: apt install libc6-dbg + 重跑 perf report
/sys/fs/cgroup/cpu.max 与 throttling。 # 错: 宿主 CPU 10% → "资源充足" # 对: cat cpu.stat → nr_throttled 增长 = 容器被限流
# 错: nice -n -20 ./api # 非root无效且不创造算力 # 对: 备份任务 nice -n 19 + ionice 给业务让路
irqbalance。 # 错: CPU0 %soft 90%, 其余空闲 → 误判"单核业务瓶颈" # 对: 确认 irqbalance 运行 + RSS 队列分散
allocs profile 找分配大户。 # 错: 优化 gcBgMarkWorker 函数本身 (不可优化) # 对: allocs profile → json.Decode 65% → 流式解析
pyroscope / pprof 带 label) + 报警触发时抓。 # 错: 每次 profile -- sleep 30, 一年遇不到尖峰 # 对: P99 告警 webhook 自动抓 profile 入对象存储
# 错: CPU 100% → 加 4 台 → 下月流量×2 再爆 # 对: pprof: json 45% ← response 20KB→8MB ← 分页参数失效