CPU是系统的"心脏",但很多人只会看
top里的百分比数字。真正的高手,懂得用火焰图看见时间花在哪里,用perf数清每个周期的去向。
第3章 CPU性能分析:从top到火焰图的进阶之路
3.1 故事:那个让全公司加班的"慢查询"
2024年3月,某物流平台的轨迹查询接口突然变慢。平时200ms的响应,飙到了8秒。
DBA小赵第一时间查数据库:"SQL执行计划没变,索引也走了,数据库CPU只有20%。不是数据库问题。"
后端开发小钱看了应用日志:"代码也没改,上周还好好的。"
两人僵持不下,决定一起看火焰图。
perf采集了60秒数据,生成的火焰图让两人同时倒吸一口凉气——java.util.HashMap.get()占了35%的CPU时间。
"HashMap.get()? 这不可能啊,就是个简单的查询。"小钱不解。
继续放大,发现get()下面连着一长串:hash() → ThreadLocalRandom.next() → Unsafe.getLong()。
问题浮出水面:新版本的JDK在某个更新中改了ThreadLocalRandom的实现,在特定并发场景下产生了严重的伪共享(False Sharing)。HashMap在并发读取时,多个线程的随机数生成器在相邻的内存位置竞争缓存行。
根因:JDK版本升级引入的性能退化,表现为"HashMap变慢"。
修复:降级JDK版本 + 改用ConcurrentHashMap(内部实现不同,避开了这个问题)。响应时间恢复到180ms。
教训:没有火焰图,两个人可能吵到第二天也找不到原因。
3.2 CPU性能指标全景
graph TD
A[CPU性能指标] --> B[利用率]
A --> C[饱和度]
A --> D[错误]
B --> B1[user% 用户态]
B --> B2[sys% 内核态]
B --> B3[iowait% 等待I/O]
B --> B4[steal% 被虚拟机偷走]
B --> B5[idle% 空闲]
C --> C1[load average]
C --> C2[runnable队列长度]
D --> D1[Machine Check]
style B fill:#e3f2fd
style C fill:#fff3e0
style D fill:#ffebee关键指标解读
# top 输出解读
top - 14:32:01 up 30 days
Tasks: 234 total
%Cpu(s): 25.0 us, 5.0 sy, 0.0 ni, 65.0 id, 5.0 wa
# us(user):用户态CPU,应用程序消耗
# sy(system):内核态CPU,系统调用消耗
# id(idle):空闲CPU
# wa(iowait):等待磁盘I/O的CPU时间
# st(steal):被hypervisor分配的给其他VM的时间关键洞察:
- us高 → 应用代码热点,看火焰图
- sy高 → 系统调用频繁,看syscall
- wa高 → 磁盘I/O瓶颈,看iostat
- id低 + load高 → 可能是锁竞争或大量 Runnable 线程
3.3 负载平均值(Load Average)
什么是Load Average?
graph LR
A[Load Average] --> B[1分钟平均]
A --> C[5分钟平均]
A --> D[15分钟平均]
E[含义] --> F[正在运行的进程]
E --> G[等待运行的进程]
E --> H[不可中断睡眠的进程]
style B fill:#ffebee
style C fill:#fff3e0
style D fill:#e8f5e9Load = 正在运行 + 等待运行 + 不可中断睡眠的进程数
# 查看负载
uptime
# 14:32:01 up 30 days, load average: 2.50, 2.30, 2.10
# 1min 5min 15min
# 理想情况:load ≈ CPU核心数
# load > CPU核心数 → 有进程在排队(饱和度)
# load < CPU核心数但id很低 → 可能在等I/O或锁解读技巧:
| 场景 | 负载 | CPU使用率 | 含义 |
|---|---|---|---|
| 健康 | 低 | 低 | 系统空闲 |
| CPU瓶颈 | 高 | 高 | 计算密集型,需要扩容或优化 |
| I/O瓶颈 | 高 | 低 | 进程等磁盘,wa会高 |
| 锁竞争 | 高 | 中 | 进程等锁, Runnable 多但CPU没充分利用 |
3.4 perf:Linux性能分析神器
perf能做什么?
mindmap root((perf
能力)) 硬件事件 CPU周期 缓存命中/miss 分支预测 软件事件 上下文切换 页错误 系统调用 跟踪点 内核事件 调度事件 文件系统事件 动态探针 uprobes kprobes
常用perf命令
# ===== 基础采样 =====
# 采集CPU热点(生成火焰图数据)
perf record -F 99 -a -g -- sleep 60
# 查看报告(TUI界面)
perf report
# 实时热点Top
perf top
# ===== 计数模式 =====
# 统计程序运行期间的事件数
perf stat -e cycles,instructions,cache-references,cache-misses ./program
# 输出解读:
# cycles: CPU周期数
# instructions: 指令数
# IPC (instructions per cycle) = instructions / cycles
# IPC < 1 → CPU在等待(缓存miss、分支预测失败等)
# IPC > 1 → 指令级并行好
# ===== 跟踪系统调用 =====
perf trace -e 'syscalls:sys_enter_*' ./program
# ===== 查看调度延迟 =====
perf sched record -- sleep 10
perf sched latency
# ===== 查看缓存行争用 =====
perf c2c record -a sleep 10 # 需要特定内核配置
perf c2c report3.5 火焰图实战
生成火焰图的完整流程
graph TD
A[perf record
采集样本] --> B[perf script
输出文本]
B --> C[stackcollapse
折叠栈]
C --> D[flamegraph.pl
生成SVG]
D --> E[浏览器打开
交互分析]
style A fill:#e3f2fd
style D fill:#e8f5e9
style E fill:#fff3e0# 一键生成火焰图
git clone https://github.com/brendangregg/FlameGraph
cd FlameGraph
# 采集(-F 99:每秒99个样本,避免与100Hz定时器对齐)
sudo perf record -F 99 -a -g -- sleep 30
# 生成(管道方式,省磁盘)
sudo perf script | \
./stackcollapse-perf.pl | \
./flamegraph.pl > flamegraph.svg
# 差分火焰图(对比优化前后)
sudo perf record -F 99 -a -g -- sleep 30
sudo perf script | ./stackcollapse-perf.pl > out.folded1
# ... 修改代码 ...
sudo perf record -F 99 -a -g -- sleep 30
sudo perf script | ./stackcollapse-perf.pl > out.folded2
./difffolded.pl out.folded1 out.folded2 | ./flamegraph.pl > diff.svg
# 红色:第一版有、第二版减少的(优化掉的部分)
# 蓝色:第二版新增的火焰图分析技巧
# 火焰图上的关键信息
# 1. 平顶(宽且顶部平):实际消耗CPU的函数
# 2. 尖塔(窄但高):调用链深,但本身不占时
# 3. 颜色:随机分配,无特殊含义
# 4. 宽度 = 该路径下所有样本占总样本的比例示例分析:
████████████ java.util.HashMap.get 35.2%
████████ java.lang.ThreadLocalRandom.next 20.1%
████ sun.misc.Unsafe.getLong 12.3%→ HashMap.get()是热点,但其中超过一半的时间花在生成随机数上。这很奇怪,值得深挖。
3.6 Off-CPU分析
CPU分析只能看到"在CPU上干什么"。但如果进程在等锁、等I/O、等网络呢?
graph LR
A[进程状态] --> B[On-CPU
正在执行]
A --> C[Off-CPU
等待中]
B --> D[火焰图分析]
C --> E[Off-CPU火焰图]
E --> F[等锁]
E --> G[等I/O]
E --> H[等网络]
E --> I[等定时器]
style B fill:#e8f5e9
style C fill:#ffebee# 使用eBPF采集Off-CPU时间(需要bcc或bpftrace)
sudo offcputime-bpfcc -df 30 > out.offcpu
./flamegraph.pl --color=io < out.offcpu > offcpu.svg
# bpftrace方式
sudo bpftrace -e '
tracepoint:sched:sched_switch {
$prev = args->prev_comm;
$next = args->next_comm;
@start[$prev] = nsecs;
}
kprobe:finish_task_switch {
if (@start[comm]) {
@us[comm, kstack] = (nsecs - @start[comm]) / 1000;
delete(@start[comm]);
}
}
'3.7 调度延迟分析
进程从"可运行"到"真正运行"要等多久?
# 采集调度事件
sudo perf sched record -- sleep 10
# 查看延迟统计
sudo perf sched latency
# 输出示例:
# ------------------------------------------------------------------------------
# Task | Runtime ms | Switches | Avg delay ms | Max delay ms
n# ------------------------------------------------------------------------------
# java:1234 | 1234.56 | 5678 | 0.05 | 12.34
# ...
#
# Avg delay:平均等待时间
# Max delay:最坏情况(可能意味着优先级问题或CPU过载)调度延迟高的常见原因:
- CPU过载:Runnable队列太长
- 优先级问题:低优先级进程被饿死
- CFS参数:sched_min_granularity_ns太小,切换太频繁
- NUMA问题:进程在等远程内存
3.8 本章总结
graph TD
A[CPU性能问题] --> B{看top}
B -->|us高| C[火焰图找热点函数]
B -->|sy高| D[perf trace找syscall]
B -->|wa高| E[iostat找磁盘]
B -->|load高| F[看Runnable队列]
C --> G[优化算法/数据结构]
D --> H[批量处理减少syscall]
E --> I[优化I/O或减少访问]
F --> J[减少线程/优化锁]
style C fill:#e8f5e9
style D fill:#e8f5e9
style E fill:#e8f5e9
style F fill:#e8f5e9核心工具链:
| 场景 | 工具 | 命令 |
|---|---|---|
| 整体概览 | top/vmstat | top, vmstat 1 |
| 单核分布 | mpstat | mpstat -P ALL 1 |
| 进程CPU | pidstat | pidstat -u 1 |
| 热点函数 | perf+火焰图 | perf record -F 99 -a -g |
| 系统调用 | perf trace | perf trace -e syscalls:* |
| 调度延迟 | perf sched | perf sched record |
| Off-CPU | bcc/bpftrace | offcputime-bpfcc |
"CPU分析不是找谁用了最多的CPU,而是找CPU时间在做什么——以及不在CPU上的时间在等什么。"