4. CPU高负载排查
CPU高负载是生产环境最常见的性能表象之一,但高CPU不一定是坏事——只有在请求处理变慢时才算故障。排查的核心是快速区分“CPU忙于计算”(良性高负载)和“CPU忙于空转或锁争抢”(恶性高负载)。本章按“现象识别→数据采集→原因定位→修复→复测→预防”六步展开,覆盖top/vmstat/perf/线程堆栈/上下文切换五大武器。
4.1 现象识别:何时触发CPU高负载排查
进入排查流程前,需要确认高负载是否真实影响业务。以下三类场景立即启动排查:
- 用户响应变慢:接口P99延迟翻倍,客户端超时增多。
- 监控告警触发:CPU使用率连续5分钟超过80%(或自定义阈值)。
- 系统负荷异常:load average 超过CPU核数的1.5倍,且us+sy占用高。
| 观察指标 | 典型表现 | 说明 |
|---|---|---|
| CPU利用率(us+sy) | > 90% | 处理器几乎满载,抢不到时间片 |
| Load Average | > CPU核数×2 | 大量进程在等待CPU,排队严重 |
| 上下文切换(cs/s) | > 50万次/秒 | 线程过频切换,可能触发cache抖动 |
| 单核热点 | 某个核us接近100% | 绑定核的线程/中断集中,其他核空闲 |
判断规则:先看
top中%CPU列,若单个进程占用极高且us(用户态)占比大,优先查热点函数;若sy(系统态)占比大,优先排查系统调用与锁竞争;若wa(等待IO)高,实际瓶颈在磁盘而非CPU。
4.2 数据采集:从宏观到微观的命令链
采集分三层:系统层(判断资源竞争)→ 进程层(锁定肇事进程)→ 线程层(定位行号)。
4.2.1 第一层:系统整体视图 —— top / vmstat
top提供实时CPU拆解(us/sy/id/wa/st)和进程排序;vmstat 1 5采集1秒间隔的上下文切换(cs)、运行队列(r)和阻塞线程(b)。
# 系统全局采样
vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
8 0 0 123456 1024 65536 0 0 0 1 2000 35000 45 15 35 5 0
- r(运行队列):大于CPU核数表示CPU饱和。
- cs(上下文切换):过高通常伴随锁争抢或线程膨胀。
- us/sy比例:us高→计算密集;sy高→系统调用或内核态锁。
4.2.2 第二层:进程级排查 —— top -H 与 pidstat
使用top -H -p <PID>查看进程内线程的CPU消耗。或使用pidstat -t -p <PID> 1 3输出稳定的线程CPU统计。
# 查看进程内线程CPU排行
top -H -p 17665
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
17673 app 20 0 4.1g 1.2g 32m R 78.2 1.5 12:34.56 worker-3
17674 app 20 0 4.1g 1.2g 32m R 10.3 1.5 2:15.23 worker-7
...
4.2.3 第三层:热点函数定位 —— perf / 线程堆栈
perf top -p <PID> -g直接显示CPU消耗热点函数及调用链;perf record -g -p <PID> --sleep 30; perf report用于离线分析。
# 实时热点函数(压缩采样)
perf top -p 17665 -g
Samples: 8K of event 'cpu-clock:pppH', 4000 Hz, Event count (approx.): 80000
Overhead Shared Object Symbol
42.5% libcrypto.so.1.1 [.] AES_encrypt
22.3% myserver [.] Worker::processRequest
8.1% libpthread.so.0 [.] __lll_lock_wait
...
4.2.4 线程堆栈转储:精确到代码行
对Java应用使用jstack <PID>,对C/C++使用pstack <PID>或gdb attach。多次堆栈(间隔5秒)可以判断锁等待和死循环。
# 线程堆栈片段(Java示例)
"worker-3" #12 daemon prio=5 os_prio=0 tid=0x00007f...
java.lang.Thread.State: RUNNABLE
at com.example.AESHelper.encrypt(AESHelper.java:45)
at com.example.Worker.processRequest(Worker.java:120)
at com.example.Worker.run(Worker.java:80)
常见遗漏:单看
perf top忽略上下文切换高的场景。如果vmstat中cs > 20万/s且sy较高,先排查锁争抢——即使perf显示热点在业务函数,也可能是锁保护下的频繁切换导致。
4.3 原因定位:三大根因与判断树
将采集到的数据映射到根因,常用故障树分叉判断:
- 计算密集(us高) → 热点函数集中在业务代码或加密/压缩库 → 优化算法或增加缓存。
- 系统调用密集(sy高) → 过多上下文切换、锁待、IO系统调用 → 减少锁粒度、使用无锁结构或批量IO。
- 线程膨胀(r队列高,cs极高) → 线程数超过CPU可并行上限,大量线程在调度排队 → 限流或调小线程池。
4.3.1 上下文切换排查表
| vmstat cs列 | 锁定pidstat -wt | 可能根因 | 下一步动作 |
|---|---|---|---|
| 30万+ | 大部分线程voluntary切换 | 锁竞争(线程主动让出CPU) | 查看perf lock或jstack锁等待分布 |
| 50万+ | 非自愿切换高 | 时间片耗尽,线程数远高于核数 | 减少线程池大小,或拆分服务 |
| 5万–20万 | 切换集中在少数线程 | 特定线程频繁唤醒网络/IO | 检查epoll/select使用或增加批处理 |
热点函数定位判断规则:
1. perf显示在libc/libpthread → 系统调用/锁。
2. perf显示在业务代码且无锁调用 → 纯CPU计算。查看能否加缓存或降级。
3. perf显示不明符号(如[.] 0xdeadbeef) → JIT编译代码或动态库缺失符号表,需加载debuginfo或查看perf map。
1. perf显示在libc/libpthread → 系统调用/锁。
2. perf显示在业务代码且无锁调用 → 纯CPU计算。查看能否加缓存或降级。
3. perf显示不明符号(如[.] 0xdeadbeef) → JIT编译代码或动态库缺失符号表,需加载debuginfo或查看perf map。
4.4 修复动作:针对根因的手术刀
| 根因分类 | 典型修复动作 | 需要配合的命令验证 |
|---|---|---|
| 热点函数(us高) | 增加缓存(如LRU)、优化循环、降低加密强度、批量计算 | perf diff对比修复前后 |
| 锁争抢(sy+cs高) | 替换为读写锁/原子操作、拆锁、减少临界区 | perf lock report 或 jstack -l 间隔采样 |
| 线程过多 | 调整线程池core/max、限流、改用协程 | pstree -p 查看线程总数 |
| 绑定CPU不均 | 均匀分配中断亲和性、taskset绑定线程 | mpstat -P ALL 检查各核使用率 |
4.5 复测:验证修复是否有效
复测同样遵循“先系统后进程、先宏观再微观”的顺序:
- 使用
vmstat 1 5观察cs是否回落到正常水平(如从30万→5万)。 - 使用
top确认CPU利用率降到预期,us+sy不超过60%。 - 使用
perf stat -p <PID> --sleep 10查看instructions/cycle(IPC)是否提高(表示CPU利用率更高效)。 - 业务层:P99延迟降低到SLO以内,超时错误率归零。
# 复测上下文切换
vmstat 1 3
r b cs us sy id wa
2 0 4500 25 8 67 0 # 修复后 cs 4.5k 正常水平
4.6 预防:构建CPU高负载防线
- 监控基线化:对us/sy/cs三个指标设定日均/峰值基线,偏离时自动告警。
- 压测与混沌工程:上线前使用
stress-ng模拟CPU争抢,观察系统恢复能力。 - 线程池异常检测:在Metrics中暴露线程活跃数/等待数,接入实时看板。
- -g调用链归档:定期跑一次
perf record -a -g -- sleep 60并生成火焰图,归档到时序数据库。
最佳实践:在生产环境允许低开销采样时,部署一个常驻的
perf stat(每秒输出一次+轻量级采样),可以大幅缩短每次CPU故障的定位时间。