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 原因定位:三大根因与判断树

将采集到的数据映射到根因,常用故障树分叉判断:

  1. 计算密集(us高) → 热点函数集中在业务代码或加密/压缩库 → 优化算法或增加缓存。
  2. 系统调用密集(sy高) → 过多上下文切换、锁待、IO系统调用 → 减少锁粒度、使用无锁结构或批量IO。
  3. 线程膨胀(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。

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故障的定位时间。
CPU高负载故障树:从现象到根因 现象:CPU高负载 & 响应变慢 层1:系统层 top/vmstat 层2:进程层 top -H / pidstat 层3:热点函数 perf/jstack us高 → 计算密集 sy高 → 系统调用/锁 cs高 → 上下文切换 根因A:热点算法 AES/序列化/循环 根因B:锁竞争 互斥锁/自旋锁 根因C:线程膨胀 线程数 >> 核数 修复动作:加缓存/减锁粒度/限流调线程池 复测:vmstat / perf stat / P99延迟 预防:基线告警 + 压测 + 火焰图归档