3.1 采样原理:为什么 perf 能“猜”得这么准?
perf 的核心机制是采样。它不是一条一条指令去数,而是每隔一段时间“咔嚓”拍一张快照。比如每 1 毫秒中断一次 CPU,看看当前在跑什么函数。跑得越多的函数,被拍到的概率就越大。
这就像你站在路口数车——你不需要每辆车都记下来,每隔 10 秒看一眼,哪个方向车多,一目了然。
关键点:采样频率决定了精度。频率太高,系统开销大;频率太低,可能漏掉热点。我个人建议,生产环境用 99Hz 或 199Hz,开发环境可以开到 1000Hz。
我曾经在一个数据库项目里遇到过,采样频率设成 49Hz,结果热点函数完全没抓到。后来改成 99Hz,问题立刻暴露——一个锁竞争函数占了 40% 的 CPU。你说这坑踩得值不值?
3.2 perf 工具实战:从安装到出结果
perf 是 Linux 内核自带的工具,大部分发行版需要单独安装:
# Ubuntu/Debian sudo apt install linux-tools-common linux-tools-$(uname -r) # CentOS/RHEL sudo yum install perf # 验证安装 perf --version安装完,咱们直接上手。假设你要分析一个正在运行的进程,PID 是 12345:
# 采样 10 秒,频率 99Hz sudo perf record -F 99 -p 12345 -g -- sleep 10 # 查看结果 sudo perf report -g这里-g是关键——它开启调用链采样。没有它,你只能看到函数名,看不到谁调了谁。我刚开始用 perf 时,经常忘了加-g,结果分析半天发现信息不够,还得重跑。嗯,这算是个小教训。
小技巧:如果不想 attach 到进程,可以直接跑命令:perf record -F 99 -g -- ./your_program。适合压测场景。
3.3 火焰图生成:把数据变成“看得见”的热点
perf report 的输出是文本,虽然能看,但不够直观。我强烈推荐用火焰图。它把调用栈堆叠起来,宽度代表 CPU 占用率,一眼就能看出哪个函数最“胖”。
生成火焰图需要 Brendan Gregg 的脚本:
# 下载脚本 git clone https://github.com/brendangregg/FlameGraph.git # 生成折叠栈 perf script > out.perf ./FlameGraph/stackcollapse-perf.pl out.perf > out.folded # 生成 SVG 火焰图 ./FlameGraph/flamegraph.pl out.folded > flame.svg打开 flame.svg,你会看到类似这样的结构:
你看这个图,compute_hash()占了 35%,明显是热点。再往下看,sha256()和memcpy()是它的子函数。这说明什么?要么是哈希算法太慢,要么是内存拷贝太多。
注意:火焰图不是万能的。它只能告诉你“CPU 花在哪”,不能告诉你“为什么花在这”。比如一个函数频繁被调用但每次执行很快,火焰图上也会显得很宽。这时候需要结合源码分析。
3.4 火焰图解读:三个常见模式
我总结了几种常见模式,你对照着看,基本能快速定位问题:
| 模式 | 火焰图特征 | 可能原因 | 优化方向 |
|---|---|---|---|
| 平顶山 | 顶部有一个很宽的矩形,底部窄 | 函数自身计算量大(如加密、压缩) | 算法优化、硬件加速、缓存结果 |
| 尖塔群 | 很多细长的矩形,高度相似 | 频繁调用小函数(如日志、格式化) | 减少调用次数、内联、批量处理 |
| 深井 | 调用链很深,每一层都很窄 | 多层抽象、虚函数、回调嵌套 | 减少抽象层次、使用直接调用 |
我记得有一次,看到一个“尖塔群”模式的火焰图,全是std::string的拷贝构造。后来发现是代码里大量传值而不是传引用。改完之后,性能提升了 30%。
3.5 避坑指南:我踩过的几个坑
perf 和火焰图虽然好用,但有几个坑你得注意:
- 采样周期太短:我曾经只采样 1 秒,结果热点函数没出现。建议至少采样 10 秒,或者跑完一个完整业务周期。
- 忘记加 -g:没有调用链信息,火焰图就是一堆孤立的函数名,根本看不出调用关系。
- 内核符号未加载:如果看到大量
[unknown],说明内核符号没加载。加-k参数或者安装kernel-debuginfo包。 - JIT 代码不显示:Java、Node.js 等 JIT 语言,需要额外配置才能看到热点。比如 Java 要加
-XX:+PreserveFramePointer。
我的习惯:每次优化前,先跑一次 perf 生成火焰图,保存为基线。优化后再跑一次,对比两张图。这样能直观看到优化效果。
3.6 实战案例:一个真实的热点定位过程
最后,我分享一个真实案例。某次线上服务 CPU 飙到 90%,我按以下步骤排查:
- perf record:采样 30 秒,频率 99Hz,带调用链。
- 生成火焰图:发现
json_parse()占了 45% 的 CPU。 - 放大查看:火焰图显示
json_parse()内部大量调用strlen()和malloc()。 - 源码分析:发现每次解析 JSON 字段时,都重新分配内存并拷贝字符串。
- 优化:改用零拷贝解析库,预分配缓冲区。
- 验证:再次 perf 采样,
json_parse()降到 12%,整体 CPU 降到 40%。
整个过程不到 2 小时。如果没有 perf 和火焰图,我可能还在猜是数据库慢还是网络慢。
好了,热点函数定位就讲到这里。记住一句话:不要猜,要测。perf 和火焰图就是你的“性能显微镜”。
核心总结:
- perf 基于采样,频率 99Hz 是黄金值
- 火焰图宽度代表 CPU 占用,重点关注“平顶”区域
- 三种常见模式:平顶山、尖塔群、深井
- 优化前后都要采样,对比验证效果