Linux性能剖析实战:perf工具从CPU采样到火焰图生成全流程(2026)

📝 500 字 · ☕ 2 分钟阅读

上周处理了一个线上问题:Nginx 反向代理的 PHP 服务 CPU 持续 90%+,top 一看是 php-fpm 在吃 CPU,但具体是哪个函数、哪段代码在吃——完全不知道。重启只能管 10 分钟。

这种时候,strace 太慢、gdb 太重,真正好用的工具是 perf——Linux 内核自带的性能分析器,零依赖、非侵入式,能直接告诉你 CPU 到底在忙什么。

这篇文章就是我在那之后总结的一站式 perf 实战指南。从安装到生成火焰图,每一步都有命令和解释。

perf火焰图分析:优化前后CPU热点对比
perf 分析前后 CPU 热点分布对比:json_decode 从 28% 降到 5%

📎 推荐阅读:Python性能剖析三件套:py-spy、Scalene、memray实战对比 —— 三个工具的横向对比,从CPU热点到内存分配全覆盖。

📖 扩展阅读:eBPF + bpftrace 生产环境调试实战 — 不用改代码不用重启,一行命令定位线上问题。

🔗 延伸阅读:perf 能告诉你 CPU 热点在哪里,但有些问题 perf 看不到——比如进程卡在 futex 等锁、DNS 解析超时。这种场景用 strace 生产环境调试完全指南 更直接。

perf 是什么?为什么不用 strace?

perf 是 Linux 内核的性能分析子系统(perf_events),它通过硬件性能计数器(PMU)和软件事件来采样 CPU 正在执行什么。本质上它是采样不是追踪——每隔一定时间(比如 1ms)看一眼 CPU 在跑什么,然后统计。

strace 可以追踪系统调用,但它有两个致命伤:一是——每个 syscall 都要上下文切换,生产环境跑 strace 等于给服务踩刹车;二是 strace 看不到用户态 CPU 热点——如果你的代码在 while 循环里做 json_decode,strace 啥也看不出来。

perf 的采样开销通常在 1-3%,生产环境直接跑没问题。

第一步:安装 perf

不同发行版的包名不一样,这里踩过的坑直接告诉你:

# Ubuntu/Debian
sudo apt install linux-tools-common linux-tools-$(uname -r) linux-cloud-tools-$(uname -r)

# CentOS/RHEL
sudo yum install perf

# Alpine(容器里常见)
apk add perf

# 验证安装
perf --version

如果 Ubuntu 报 Unable to locate package linux-tools-xxx,多半是内核版本太新包还没跟上。装个通用版本就行:sudo apt install linux-tools-generic

第二步:perf top — 实时看 CPU 热点

最快的上手方式是 perf top,它像 top 一样实时刷新,但显示的不是进程而是函数

sudo perf top

输出大概长这样:

Samples: 12K of event 'cpu-clock', 4000 Hz, Event count: 2876543210
Overhead  Shared Object       Symbol
  28.32%  php8.3              [.] zend_hash_find
  18.71%  php8.3              [.] php_json_decode_ex
  14.25%  libc.so.6           [.] __strstr_sse2
  10.48%  php8.3              [.] _zend_array_merge
   8.11%  [kernel]            [k] copy_user_enhanced_fast_string

一眼就能看到:zend_hash_find 占 28%,json_decode 占 18%。你的直觉可能会说”json_decode 慢”,但 perf 告诉你——哈希查找才是真凶。

实用技巧:e 键展开调用链,能看到是调用了这个热点函数。按 d 可以过滤只看某个 DSO(动态共享对象),比如 php8.3

perf top 只能看当前,不能回溯。排查间歇性 CPU 飙高的时候,你需要采样数据保存下来——这就是 perf record 的主场。

第三步:perf record — 采样保存 + 离线分析

采样 30 秒,保存到 perf.data:

# 全系统采样(推荐,能看到所有进程)
sudo perf record -a -g -F 99 -- sleep 30

# 只采样 php-fpm 进程
sudo perf record -p $(pgrep -d',' php-fpm) -g -F 99 -- sleep 30

# 参数说明:
# -a: 全系统
# -g: 记录调用链(call graph),火焰图必需
# -F 99: 采样频率 99Hz(推荐值,避免与系统时钟同频导致偏差)
# sleep 30: 采样 30 秒

采样完后看报告:

sudo perf report --stdio

输出是文本版的调用树,能看到每个函数的 CPU 占比和父子调用关系。但说实话,文本版的可读性很差——当调用链深到 10 层以上时,你很难直观看出瓶颈在哪。

这时候就该上火焰图了。

第四步:生成火焰图(FlameGraph)

火焰图是 Brendan Gregg 发明的可视化方法,把 perf 的调用栈数据变成一张 SVG 图——宽度 = CPU 时间,颜色随机(或按函数类型着色),从下到上是调用栈

生成火焰图需要 Brendan 的脚本:

# 克隆 FlameGraph 仓库(你需要 git)
git clone https://github.com/brendangregg/FlameGraph.git
cd FlameGraph

# 1. perf script 把采样数据转成可读文本
sudo perf script -i /path/to/perf.data > out.perf

# 2. 折叠调用栈
./stackcollapse-perf.pl out.perf > out.folded

# 3. 生成 SVG 火焰图
./flamegraph.pl out.folded > flamegraph.svg

三条命令出图。打开 flamegraph.svg,你会看到一张像山峰一样层叠的图——最高的”山峰”就是 CPU 消耗最大的调用路径。点击任意函数块可以放大,鼠标悬停看百分比。

这里有个容易踩的坑:如果你的 perf.data 是在 Docker 容器里采样的,宿主机的符号表可能不匹配。解决方案是用 perf archive 打包符号文件:

perf archive /path/to/perf.data
# 生成 perf.data.tar.bz2,带到开发机解压后再 perf script

第五步:读懂火焰图 — 一个真实案例

回到开头那个 CPU 90% 的问题。火焰图出来后,我一眼看到两个大平顶:

  1. json_decode — 占了 28% 的 CPU 时间,看调用链是一个 API 接口在循环里反复 decode 同一个 JSON 字符串
  2. zend_hash_find — 18%,追踪下去发现是每次请求都去读一个 200KB 的配置文件做 Hash 查找

两个问题都是典型的重复计算。修复非常简单:

// 修复 1:缓存 json_decode 结果
// Before: 每个请求 decode 3 次同一个 JSON
$data = json_decode($raw_json, true);  // 第1次
// ... 中间代码 ...
$data2 = json_decode($raw_json, true); // 第2次(重复!)
// ... 
$data3 = json_decode($raw_json, true); // 第3次(重复!)

// After: 一次 decode,到处使用
$data = json_decode($raw_json, true);
// 后续全部引用 $data

修复 2 是把配置文件用 APCu 缓存起来,避免每次请求做 Hash 查找。

改完重启 php-fpm,CPU 从 90% 掉到 12%。perf 再采样一次生成火焰图,原来的”高原”变成了”丘陵”。

进阶:perf 采样事件不止 CPU

perf record 默认采样 cpu-clock 事件,但它能采样的远不止这个:

# 查看可用事件
perf list

# 采样缓存未命中(找内存瓶颈)
sudo perf record -e cache-misses -a -g -- sleep 10

# 采样上下文切换(找调度问题)
sudo perf record -e context-switches -a -g -- sleep 10

# 采样缺页错误(找内存分配热点)
sudo perf record -e page-faults -a -g -- sleep 10

# 采样分支预测失败(找 CPU 流水线停顿)
sudo perf record -e branch-misses -a -g -- sleep 10

记住:CPU 高不一定全是计算密集。如果你采样 cache-misses 发现火焰图最高的函数是 memcpy——那是内存带宽瓶颈,不是代码算法问题。

perf stat — 一行命令看整体

如果你只是想快速了解”这个程序到底哪里有问题”,perf statperf record 更快:

sudo perf stat -d php your_script.php

输出:

Performance counter stats for 'php your_script.php':
       1234.56 msec task-clock
          4567      context-switches     #  3.699 K/sec
           123      cpu-migrations       # 99.630 /sec
         98765      page-faults          # 80.012 K/sec
    3456789012      cycles               #  2.799 GHz
    1234567890      instructions         #  0.36  insn per cycle
     234567890      branches             # 190.012 M/sec
      12345678      branch-misses        #  5.26% of all branches
     345678901      L1-dcache-loads      # 280.012 M/sec
      12345678      L1-dcache-load-misses #  3.57% of all L1-dcache accesses

关键指标解读:

  • instructions per cycle (IPC) < 1.0:CPU 在等内存/等分支预测,代码有优化空间
  • branch-misses > 5%:分支预测失败率高,检查是否存在大量不可预测的条件分支
  • L1-dcache-load-misses > 5%:数据局部性差,考虑调整数据结构(AoS → SoA)

FAQ

Q: perf 采样需要 root 权限吗?

看内核参数 /proc/sys/kernel/perf_event_paranoid。值 ≤ 0 时普通用户也能采样(Docker 容器通常设为 -1),值 ≥ 2 时只有 root 能用。生产环境 Docker 里一般直接 sudo 就行。

Q: perf top 看不到函数名,只显示地址怎么办?

通常是缺少调试符号。安装对应的 dbgsym 包:sudo apt install php8.3-dbgsym。如果找不到 dbgsym 包,检查 /etc/apt/sources.list.d/ddebs.list 是否配置了 debug 源。

Q: perf record 生成的 perf.data 文件很大怎么办?

降低采样频率:-F 49 或更低。减少采样时间:sleep 10 而不是 60。或者在 Docker 容器里采样(容器内进程少,数据量小很多)。采样 30 秒 99Hz 的 perf.data 通常在 5-20MB,不算大。

Q: 火焰图中看到了 __memcpy_avx_unaligned 占大量 CPU,怎么优化?

这通常说明你的代码在做大量内存拷贝。排查方向:是否在函数间传递了大对象而不是引用?是否在循环中拼接大字符串?PHP 里 $a = $b 是写时复制但 $a .= $b 会触发拷贝。如果是字符串操作热路径,考虑用引用传递或分块处理。

Q: perf 和 eBPF(bpftrace)有什么区别?什么时候用哪个?

perf 是”看”——采样统计 CPU 热点、缓存未命中、分支预测等。bpftrace 是”插”——在函数入口/出口插桩,记录参数、返回值、延迟。简单说:先用 perf 找到热点函数,再用 bpftrace 深入分析那个函数的输入输出。两者配合使用效果最好。

总结

perf 是我用过性价比最高的性能分析工具——内核对它的支持是原生的,零依赖,采样开销低到可以忽略。配合火焰图,原本”CPU 高”这种模糊的问题能在几分钟内精确到函数级别。

记住这个排查流程:

  1. perf top — 30 秒看全局,找到可疑的 DSO/函数
  2. perf record -g -F 99 — 采样 30-60 秒,保存现场
  3. 生成火焰图 — 一眼看到最大的”平顶”
  4. perf stat — 补充 IPC、cache-miss 等宏观指标
  5. bpftrace(可选)— 在热点函数插桩看参数

别再用 echo 大法和 var_dump 排查性能问题了。CPU 从来不骗人,你只需要学会听懂它说的话。

💡 相关文章: 如果你还没看过,推荐搭配阅读本站的 Linux生产环境CPU 100%问题排查实战MySQL慢查询优化实战,这两篇和本文组成了完整的性能排查三部曲。

📤 分享这篇文章