5、Perf高级技巧:perf top实时监控,perf annotate源码级分析,perf script自定义脚本
说实话,很多工程师用 perf 还停留在「perf stat 看看命中率」的阶段。但真正遇到棘手的性能问题——比如某个函数突然飙高、或者一段内联代码拖垮了整个系统——这时候就需要更锋利的工具了。
我个人习惯把 perf 分成三个层次:看趋势、挖细节、写脚本。perf top 解决「现在谁在吃 CPU」,perf annotate 解决「到底是哪一行代码在作妖」,perf script 解决「我要定制化分析逻辑」。今天咱们就把这三个工具挨个讲透。
5.1 perf top:实时监控的「任务管理器」Pro 版
perf top 和 top 命令长得有点像,但本质完全不同。top 看的是进程级别的 CPU 占用,perf top 看的是函数级别的采样热点。你想想看,top 告诉你进程 A 吃了 80% CPU,然后呢?你还要用 gdb 去 attach、去猜。perf top 直接告诉你:是进程 A 里的 foo() 函数吃了 60%,bar() 吃了 20%。
我在项目中遇到过这样一个场景:线上服务 CPU 突然飙到 90%,用 top 看到是 nginx worker 进程在忙。但 nginx 那么大,到底是哪个模块的问题?我直接跑:
sudo perf top -p $(pgrep nginx) -g
几秒钟后,热点函数就浮出来了——居然是某个第三方模块里的字符串处理函数。后来发现是日志级别配置错误,导致每条请求都在做无意义的格式化。perf top 帮我省了至少半天排查时间。
几个常用参数,我列个表方便你查阅:
| 参数 | 作用 | 我的使用习惯 |
|---|---|---|
| -p PID | 指定进程 | 排查单进程问题时必加 |
| -g | 开启调用链 | 默认关闭,但建议一直开着 |
| -e event | 指定事件 | 缺省是 cycles,查内存用 cache-misses |
| -K | 不显示内核函数 | 用户态分析时加,减少干扰 |
5.2 perf annotate:源码级「慢动作回放」
perf top 告诉你是哪个函数有问题,perf annotate 则告诉你这个函数里哪一行汇编代码最慢。说白了,就是带着采样数据反汇编,然后标注每条指令的采样命中率。
我曾经接手过一个图像处理库,性能始终上不去。用 perf record 采集数据后,perf annotate 打开一看——好家伙,一个 memcpy 操作占了 40% 的采样。但仔细看汇编,发现编译器把一个大循环里的内存拷贝优化成了 rep movsb,而那块内存是非对齐的,导致 CPU 走了微码辅助路径,慢了好几倍。
操作步骤很简单:
# 第一步:采集数据
perf record -g ./my_program
# 第二步:生成源码级分析
perf annotate -s hot_function
输出界面里,左边是采样百分比,右边是对应的汇编代码。你会看到类似这样的布局:
Percent | Disassembly of hot_function
----------------------------------------
| push %rbp
0.02 | mov %rsp,%rbp
12.50 | mov (%rdi),%eax ← 这里最热
0.05 | add $0x1,%eax
8.30 | mov %eax,(%rdi) ← 这里也热
0.01 | pop %rbp
| retq
看到百分比高的行,就是 CPU 花时间最多的地方。我一般会重点关注超过 5% 的指令,如果某条指令超过 20%,那基本就是瓶颈所在。
5.3 perf script:写脚本,让 perf 按你的逻辑跑
perf top 和 perf annotate 都是 perf 内置的展示方式。但有时候,你需要自定义分析逻辑——比如只统计某个时间窗口内的数据,或者把采样结果导出成 JSON 喂给其他工具。这时候 perf script 就派上用场了。
perf script 的本质是:把 perf.data 里的原始事件记录,按你指定的格式输出。你可以用 Python 或 Perl 写脚本,对每一条采样记录做处理。
我举个实际例子。有一次我需要排查一个「间歇性卡顿」问题,现象是每过几分钟服务就会卡几百毫秒。用 perf record 采集了 10 分钟的数据,然后写了个 perf script 脚本,只提取调度事件和周期超过 100ms 的停顿:
# 用 Python 写 perf script
#!/usr/bin/env python
import sys
def process_event(event):
# event 是一个字典,包含采样信息
if event['evtype'] == 'sched:sched_switch':
prev_comm = event['common']['prev_comm']
next_comm = event['common']['next_comm']
if 'my_service' in prev_comm or 'my_service' in next_comm:
print(f"Switch: {prev_comm} -> {next_comm}")
# 注册处理函数
perf_script_context = {
'process_event': process_event
}
跑起来:
perf script -s my_script.py
结果很快就锁定了问题:某个后台线程每隔 3 分钟会持锁做一次全量缓存清理,导致主线程被阻塞。perf script 让我能精准过滤出我关心的调度事件,而不是在几万条记录里人工翻找。
perf script 的常用输出格式:
- -F:指定输出字段,比如 -F pid,comm,event 只显示进程ID、进程名和事件类型
- --time:只分析某个时间区间,比如 --time 10.0-20.0 只取第10秒到第20秒的数据
- -g:输出调用链信息,配合脚本可以做火焰图的数据预处理
perf script -F comm,event,ip | awk '/my_func/ {count++} END {print count}'。不需要每次都写 Python 脚本。
5.4 三件套的配合使用场景
这三个工具不是孤立的。我通常的工作流是这样的:
- 先用 perf top 扫一遍,看看当前系统里哪些函数最热。这一步花 30 秒就够了。
- 锁定热点函数后,用 perf record 采集数据,采样时间根据问题严重程度来——CPU 持续高就采 10 秒,间歇性问题就采几分钟。
- perf annotate 打开热点函数,逐行分析汇编代码,找到真正的瓶颈指令。
- 如果问题复杂,写 perf script 脚本,做定制化分析,比如只看某个时间窗口、或者过滤掉不关心的进程。
下面这张图概括了这三个工具的关系和典型使用路径:
5.5 避坑指南
这几个工具虽然强大,但用不好反而会误导你。我踩过几个坑,分享给你:
- 采样频率别太高:perf top 默认 4000Hz,如果系统负载已经很高了,建议降到 1000Hz(-F 1000),否则 perf 本身会抢 CPU。
- 注意内核 vs 用户态:默认 perf 只采样用户态。如果怀疑内核有问题,加 -a 或者指定 -e cycles:uk(用户态+内核态)。
- perf annotate 看不到源码?检查两点:一是编译时有没有加 -g,二是 perf 有没有找到对应的 debuginfo 包。我一般用
perf buildid-list确认。 - perf script 输出太大:如果采样时间很长,perf script 的输出可能几个 GB。建议先用
perf report --sort comm,dso缩小范围,再用 perf script 导出。
好了,perf 的高级技巧就讲到这里。这三个工具配合起来,基本能覆盖从「系统卡了」到「找到具体哪一行代码有问题」的全过程。下次遇到性能问题,别急着拍脑袋优化,先让 perf 告诉你答案。