perf 是 Linux 内核 perf_events 子系统提供的性能分析工具。它的核心能力是对 CPU 进行低开销的采样和计数,从而回答两个基础问题:时间花在哪了、以及为什么花在这。perf 的工作方式是内核态的,不依赖特定语言运行时。Java、Python、Go、C++ 的进程都可以分析,不需要修改代码或插入探针。这使得 perf 成为排查 CPU 相关性能问题的首选工具。
perf 包含数十个子命令,但日常排查中高频使用的只有四个:stat(计数,定方向)、top(实时采样,找热点)、record(记录采样,存证据)、report(分析采样,看调用链)。本文围绕这四个命令展开。
一、perf 能做什么、不能做什么
1.1 能力地图
perf 的底层是 CPU 硬件性能监控单元(PMU),以及内核提供的软件事件。基于这些事件源,perf 提供四类能力:
| | | |
|---|
| 计数 | | perf stat | 判断性能瓶颈类型(CPU 效率、缓存 miss、分支预测失败等) |
| 采样 | | perf top | |
| 追踪 | | perf record -g | |
| 探测 | | perf probe | |
计数和采样的区别需要理解清楚:计数回答"发生了多少次",采样回答"发生时的上下文是什么"。计数开销极低(通常 < 1%),适合快速判断方向;采样会产生较多数据,用于深度分析。
采样原理(理解火焰图的前提):perf 以固定频率(如 4000 Hz)触发硬件中断,每次中断记录"此刻正在执行哪个函数 + 调用栈"。样本数越多,统计越接近真实分布。采样频率由 -F 参数控制,生产环境建议显式指定(通常 99 Hz,见 3.4 的参数表)。
1.2 适用场景
perf 为以下场景而生:
| | |
|---|
| | perf top |
| | perf record |
| | perf stat |
| | perf top |
| | perf record |
1.3 不适用场景
perf 的设计目标是 CPU 性能分析,以下场景不是它的强项:
| | |
|---|
| | biosnoop-bpfcc |
| | tcpdump |
| | memleak-bpfcc |
| | |
| | trace-cmd |
| perf lock | lockstat |
理解这些边界很重要。一个常见错误是:遇到数据库查询慢,直接跑 perf record,看了半天火焰图发现时间都在等 I/O——这时候应该先用 iostat 或 biosnoop 确认 I/O 瓶颈,而不是在 CPU 采样里找答案。
二、使用方法论:三步排查法
perf 的命令很多,但排查 CPU 性能问题的思路是固定的。按照下面的顺序使用,可以避免两个最常见的错误:(1)跳过计数直接采样,导致方向错误;(2)采样时间过长,产生海量数据却抓不到关键信息。
2.1 方法论总览
┌─────────────────────────────────────────────────────────────────┐
│ Step 1: perf stat 定方向 │
│ ─────────────────────── │
│ 看 IPC、缓存 miss、分支预测失败 → 判断瓶颈类型 │
│ IPC < 0.5 → 内存瓶颈(优化数据局部性) │
│ IPC > 1.0 → CPU 在高效计算(优化算法或扩容) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 2: perf top 找热点 │
│ ─────────────────────── │
│ 实时看哪个函数占 CPU 最高 │
│ 异常高的陌生函数 → 重点调查 │
│ Java 注意 [unknown] 符号问题 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 3: perf record + 火焰图 定位根因 │
│ ──────────────────────────────── │
│ 记录调用栈 → 火焰图可视化 → 看调用链 │
│ 平顶 = 问题在这个函数内部 │
│ 山峰 = 问题在调用链深处 │
└─────────────────────────────────────────────────────────────────┘
这三步的核心逻辑是从宏观到微观、从方向到细节。第一步确定"是不是 CPU 的问题、是什么类型的 CPU 问题",第二步确定"哪个函数在吃 CPU",第三步确定"这个函数是怎么被调到的、调用链上哪里出了错"。
多数问题走到第三步就结束了。如果还需要知道热点函数里具体是哪一条指令在拖累,可以在第三步之后加第四步:perf annotate 指令级下钻(见 3.5)。
2.2 为什么第一步必须是 perf stat
很多新手看到 CPU 高就直接跑 perf record -g,采集 30 秒数据生成火焰图。这个做法的问题在于:如果 CPU 高的原因是缓存 miss 严重(IPC 只有 0.3),那么火焰图上的热点函数只是"受害者"——它因为等内存而占用了大量 CPU 时间,但真正的根因是数据结构的设计导致缓存不友好。这时候看火焰图会误判为"这个函数计算量太大",进而去优化算法复杂度,而实际上应该改数据布局。
perf stat 的作用就是在投入深度分析之前,先确认瓶颈类型。它的开销极低(通常 < 1%),几秒钟就能给出判断依据。
三、核心命令详解
3.1 perf stat:计数,定方向
perf stat 在指定时间段内统计硬件事件和软件事件的发生次数。默认输出包括 CPU 周期数、指令数、上下文切换次数等。
基础用法:
# 系统级统计(5秒)
perf stat -a sleep 5
# 针对单个进程统计(10秒)
perf stat -p <pid> sleep 10
# 统计特定事件:IPC 和缓存 miss
perf stat -e cycles,instructions,cache-misses -p <pid> sleep 10
输出解读:
典型的 perf stat 输出如下:
Performance counter stats for process id '12345':
10,234,567 cycles # 2.456 GHz
15,678,901 instructions # 1.53 insn per cycle
234,567 cache-misses # 2.292 % of all cache refs
123,456 context-switches
10.002345678 seconds time elapsed
需要关注三个核心指标:
IPC(Instructions Per Cycle):每 CPU 周期执行的指令数。它直接反映 CPU 的利用效率。
现代 CPU 有多条流水线,IPC 可以大于 1(理想情况下 Intel 可达 3-4,ARM 可达 4-6)。IPC 低于 0.5 是一个明确的信号:CPU 不是算不过来,而是等数据等太久。
一个常见的误读:IPC 低,不代表 CPU 闲——它代表 CPU 在"空转等待",可能是等待内存、锁或 IO。这时优化算法没有用,该查的是数据局部性和锁竞争。
cache-misses:缓存未命中次数。需要结合 cache-references 计算 miss 率。如果 miss 率超过 10%,说明数据访问模式对缓存不友好。常见的优化手段包括:结构体字段重排(把热字段放一起)、循环遍历方向与内存布局一致(行优先 vs 列优先)、减少指针追逐(链表 → 数组)。
context-switches:上下文切换次数。如果每秒超过 10,000 次,说明线程调度过于频繁。可能的原因包括:线程数远超 CPU 核数、锁竞争激烈导致线程频繁阻塞/唤醒、定时器精度设置过高。
一个典型的工作流程:
# 1. 先看总体 IPC 和缓存 miss
perf stat -e cycles,instructions,cache-misses,cache-references -p <pid> sleep 5
# 2. 如果 IPC 低,进一步看是哪级缓存 miss
perf stat -e L1-dcache-load-misses,L1-dcache-loads,LLC-load-misses,LLC-loads -p <pid> sleep 5
# 3. 如果怀疑分支预测问题
perf stat -e branches,branch-misses -p <pid> sleep 5
边界说明:perf stat 给出的是聚合统计,它告诉你"整体上有多少缓存 miss",但不告诉你"哪个函数导致了这些 miss"。要定位到具体函数,需要用 perf record 采样。
3.2 perf top:实时采样,找热点
perf top 以类似 top 的界面实时显示 CPU 占用最高的函数。它是交互式的,适合在问题发生时快速看一眼"谁最忙"。
基础用法:
# 实时查看全局热点
perf top
# 只看某个进程
perf top -p <pid>
# 显示调用关系(需要帧指针或 LBR 支持)
perf top -p <pid> -g
# 只看用户态(过滤内核函数)
perf top -p <pid> --sort comm,dso,symbol
界面解读:
25.34% libjvm.so [.] Interpreter
18.56% [kernel] [k] _raw_spin_unlock_irqrestore
12.34% libcrypto.so [.] aesni_ctr32_encrypt
8.90% [unknown] [.] 0x00007f8b2c3a1234
5.23% libmysqlclient.so [.] mysql_parse
第一列是 CPU 占比,第二列是共享库/模块名,第三列是函数名。
需要注意三种典型情况:
- 热点是业务函数(如
processOrder、calculatePrice):占比高是预期行为,除非这个函数比历史基线明显增高。 - 热点是基础库函数(如
memcpy、strcmp、json_decode):如果这类函数的占比异常高,通常意味着上层业务在大量调用它们。比如 json_decode 占 15%,可能不是 JSON 库的问题,而是业务在循环里反复解析同一个 JSON。 - 热点是
[unknown]:表示 perf 无法解析符号。这在 Java 应用中特别常见,因为 JVM 默认不保留符号表。下面的章节会详细说明解决方法。
perf top 的局限: 它只显示"哪个函数在消耗 CPU",不显示"这个函数是怎么被调到的"。要分析调用链,需要 perf record -g。
3.3 Java 符号问题:为什么看到 [unknown]
Java 应用用 perf 分析时,默认会看到很多 [unknown] 和 Interpreter。这是因为:
- JVM 在运行时会进行 JIT 编译,把字节码编译成机器码。这些机器码存储在内存中,perf 能看到这些地址,但不知道对应的 Java 方法名。
- JVM 默认不保存帧指针(Frame Pointer),导致 perf 无法正确展开调用栈。
- Java 标准库的方法通常已经有符号(因为是通过
-ljava 加载的),但用户代码和 JIT 编译后的代码没有。
解决方法:
# 方法1:开启帧指针(JDK 8u60+)
java -XX:+PreserveFramePointer -jar app.jar
# 方法2:生成符号映射文件(适用于无法重启的进程)
# 下载 perf-map-agent: https://github.com/jvm-profiling-tools/perf-map-agent
cd /tmp/perf-map-agent/out
java -cp attach-main.jar:$JAVA_HOME/lib/tools.jar net.virtualvoid.perf.AttOnce <pid>
# 生成 /tmp/perf-<pid>.map,perf 会自动读取
# 方法3:使用 --call-graph=lbr(需要 Intel CPU 支持 Last Branch Record)
perf record -g --call-graph=lbr -p <pid> -- sleep 10
方法1是最好的方案(重启时加上参数即可),方法2适用于无法重启的生产环境。方法3依赖 CPU 特性,不是所有机器都支持。
3.4 perf record + 火焰图:记录调用链,定位根因
perf record 按固定频率采样,记录每次采样时程序的调用栈。配合 Brendan Gregg 的 FlameGraph 工具,可以生成直观的火焰图。
采样和生成火焰图:
# 1. 记录采样(-F 99 表示每秒采样99次,-g 记录调用栈)
perf record -F 99 -g -p <pid> -- sleep 30
# 2. 生成报告(文本形式)
perf report --stdio
# 3. 生成火焰图(需要 FlameGraph 工具)
git clone https://github.com/brendangregg/FlameGraph.git /opt/FlameGraph
perf script > out.perf
/opt/FlameGraph/stackcollapse-perf.pl out.perf > out.folded
/opt/FlameGraph/flamegraph.pl out.folded > flamegraph.svg
火焰图的阅读方法:
火焰图是 SVG 格式的交互式图表。y 轴是调用栈深度(从上到下是从主函数到被调函数),x 轴是采样占比(宽度代表该函数在总采样中的比例)。
判断性能问题类型,看两个特征:
读图的一个关键判断:优化优先级看顶层最宽的块,而不是调用链最深的部分。很多团队花大力气优化的函数,其实只占 3%——第一眼就该看最宽的那块。另外注意累计占比:同一父路径下多个子块的宽度之和,才是这条路径的真实成本,只盯单个符号会低估一条调用链。
差分火焰图:对比优化效果
火焰图最有价值的用法之一是对比。优化前采一张,优化后采一张,生成差分火焰图:
# 优化前
perf record -F 99 -g -p <pid> -- sleep 30
perf script | /opt/FlameGraph/stackcollapse-perf.pl > baseline.folded
# 优化后
perf record -F 99 -g -p <pid> -- sleep 30
perf script | /opt/FlameGraph/stackcollapse-perf.pl > optimized.folded
# 差分火焰图(红色=优化后增加,蓝色=优化后减少)
/opt/FlameGraph/difffolded.pl optimized.folded baseline.folded | \
/opt/FlameGraph/flamegraph.pl > diff.svg
差分火焰图的好处是直观:一眼就能看出优化是否命中了目标函数,以及是否有副作用(其他地方反而变多了)。
采样参数的选择:
| | |
|---|
| | 避免与 100Hz/1000Hz 等常见周期对齐,减少采样偏差 |
| | |
| fp | fp |
3.5 perf annotate:指令级下钻
perf record 完成后,根因通常已经定位到函数。但有时候还需要回答最后一个问题:函数里具体是哪一条指令在拖累?perf annotate 负责这"最后一公里"。
# 查看热点函数的指令级热力分布(交互式)
perf annotate -i perf.data
# 只看某个函数,文本输出便于 grep
perf annotate -i perf.data --stdio | grep -A 20 <function_name>
它会列出热点函数内每条指令的采样占比,把优化点从"函数级"精确到"指令级"。比如看到热点集中在某个循环体内的比较指令上,就知道该优化的是循环里的分支逻辑,而不是整个函数。
使用时机:函数级已经锁定、但不知道"函数里哪一行在拖累"时使用。日常大多数问题到函数级就够了,这一步按需选用——它和 perf probe 一样,是排障链路的"进阶档位"。
3.6 perf probe:动态探针
perf probe 允许在运行中的程序上动态插入探测点,不需要修改代码或重启进程。
典型用法:
# 在 libc 的 malloc 函数入口加探针
perf probe -x /usr/lib/x86_64-linux-gnu/libc.so.6 --add 'malloc'
# 查看已定义的探针
perf probe -l
# 记录探针触发时的调用栈
perf record -e probe:malloc -p <pid> -- sleep 10
这个功能在以下场景中有用:
- 怀疑某个第三方库函数被频繁调用,但不想为了验证这个假设而发版加日志
- 需要确认某个函数在实际运行中的参数值(配合
--vars 选项) - 追踪 USDT(User Statically Defined Tracing)探针,如 Java 内置的
sdt_java:method__entry
需要注意的是,perf probe 的开销比采样高,不建议在生产环境长时间开启。
四、完整排查示例
以下是一个基于真实工具输出的排查流程,展示如何从症状到根因:
症状:某 Java 服务的 CPU 使用率从 30% 升到 80%。
Step 1:perf stat 定方向
perf stat -e cycles,instructions,cache-misses,cache-references -p 12345 sleep 5
输出:
2,456,789,012 cycles
1,234,567,890 instructions # 0.50 insn per cycle
123,456,789 cache-misses # 18.5% of all cache refs
IPC 为 0.50,缓存 miss 率 18.5%。判断:CPU 效率偏低,存在内存访问瓶颈。
Step 2:perf top 找热点
perf top -p 12345 -g
输出显示 processOrder 占 35%,jsonDecode 占 22%,hashMapGet 占 15%。jsonDecode 的占比异常高——历史基线中这个函数通常只占 5% 左右。
Step 3:perf record + 火焰图 定位根因
perf record -F 99 -g -p 12345 -- sleep 10
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg
火焰图显示:processOrder → validatePayload → jsonDecode 是一个宽山峰。进一步看 jsonDecode 内部,大部分时间花在 String.parse 上。如果还想确认到底是哪条指令在拖累,可以按 3.5 的方法跑 perf annotate 下钻到指令级。
根因:validatePayload 在处理每个订单时都会解析一次 JSON payload,而这个 payload 在订单生命周期内是不变的。应该是升级后某个改动把缓存逻辑去掉了。
修复:在 validatePayload 中加入缓存,避免重复解析相同 payload。
验证(量化前后对比)
修复后重跑同一条 perf stat 命令:
2,456,000,000 cycles
2,650,123,456 instructions # 1.08 insn per cycle
12,345,678 cache-misses # 3.2% of all cache refs
IPC 从 0.50 升到 1.08,缓存 miss 率从 18.5% 降到 3.2%,CPU 使用率从 80% 回落到 35% 左右。如果还想直观对比优化前后的调用路径变化,可以按 3.4 的方法生成差分火焰图做二次确认。
五、常见问题速查
| | | |
|---|
| perf top/report 看不到 Java 方法名 | | 加 -XX:+PreserveFramePointer,或用 perf-map-agent |
| | | --call-graph=lbr(需 Intel CPU)或重编译加 -fno-omit-frame-pointer |
| perf.data | | |
perf record | | | 用 root,或 sudo setcap cap_perfmonep /usr/bin/perf |
| sys_call | | 用 perf stat -e syscalls:sys_enter_* 统计具体 syscall |
| | | |
| | | 用 perf annotate -i perf.data 下钻到指令级 |
六、命令速查
# ===== 快速诊断 =====
perf stat -a sleep 5 # 系统级计数(5秒)
perf stat -p <pid> sleep 10 # 进程级计数(10秒)
perf stat -e cycles,instructions -p <pid> sleep 5 # 只看 IPC
# ===== 实时热点 =====
perf top # 全局热点
perf top -p <pid> # 单进程热点
perf top -p <pid> -g # 带调用栈
# ===== 采样分析 =====
perf record -F 99 -g -p <pid> -- sleep 10 # 记录采样
perf report --stdio # 文本报告
perf script > out.perf # 导出原始数据
# ===== 指令级下钻 =====
perf annotate -i perf.data # 下钻到指令行
perf annotate -i perf.data --stdio | grep -A 20 <func> # 只看某函数
# ===== 火焰图 =====
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg
# ===== 差分火焰图 =====
difffolded.pl optimized.folded baseline.folded | flamegraph.pl > diff.svg
# ===== 动态探针 =====
perf probe -x <lib> --add '<function>' # 加探针
perf record -e probe:<name> -p <pid> # 记录探针触发