Linux perf 性能分析实战:用火焰图定位 CPU 瓶颈

当服务器 CPU 莫名其妙被打满、接口延迟忽高忽低,你第一时间可能想到 top、htop 看进程,但真正要回答「到底是哪一行代码、哪一个函数吃掉了 CPU」,就得靠更底层的工具。Linux perf 是内核自带的性能分析利器,基于硬件性能计数器(PMC)与软件事件采样,能直接告诉你热点函数、缓存命中率、分支预测失败率,甚至把调用栈画成火焰图。本文从环境准备讲到火焰图落地,带你用 perf 把 CPU 瓶颈一锤定音。

一、perf 是什么:内核级采样器

perf 全称 Performance Counters for Linux,它直接对接 CPU 的 perf_event 子系统。和纯用户态的 Profiler 不同,perf 能同时采集:

  • 硬件事件:CPU 周期(cycles)、指令数(instructions)、缓存未命中(cache-misses)、分支预测失败(branch-misses)。
  • 软件事件:上下文切换、页错误(page-faults)、CPU 迁移。
  • 追踪点(tracepoint):内核调度、系统调用、块设备等内核埋点。

它最大的优势是「零侵入」——不需要改代码、不需要重新编译,一条命令就能对正在运行的进程采样。对于数据库、网关这类不便重启的服务尤其友好(关于数据库侧的慢查询分析思路,可参考 PostgreSQL 慢查询实战)。

二、环境准备与权限

大多数发行版自带 perf,若没有可通过 linux-tools-common 安装。采样需要读取性能计数器,受内核参数 perf_event_paranoid 限制:

# 查看当前限制(数值越小权限越宽松,默认常为 2 或 3)
cat /proc/sys/kernel/perf_event_paranoid

# 临时放开,允许普通用户采样(生产环境按需,分析完改回)
sudo sysctl kernel.perf_event_paranoid=1
# 或仅本次会话
echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid

# 内核符号需要 root 才能读取,建议直接用 sudo 运行 perf
sudo perf --version

在容器里跑 perf 更麻烦:需要 --privileged 或挂载 /sys/kernel/debug、/proc,并赋予 CAP_SYS_ADMIN。这正好和容器运行时的权限模型相关,详见 Docker 实战 中关于特权容器与 capabilities 的部分。

三、perf top:实时看热点函数

perf top 类似 top,但展示的是「当前最耗 CPU 的函数/符号」,适合快速定位热点:

# 按 CPU 占用实时排序展示内核+用户态符号
sudo perf top

# 只看某个进程
sudo perf top -p 1234

# 按指定事件(如缓存未命中)排序
sudo perf top -e cache-misses

输出里 Symbol 列带 [k] 表示内核函数、[.] 表示用户态。如果看到某个业务函数常驻榜首,方向就找到了。

四、perf stat:用硬件计数器算 CPI

想知道「指令执行为什么慢」,perf stat 能给出宏观指标。其中 CPI(Cycles Per Instruction,每指令周期数)是关键:数值越高说明单条指令等得越久(多因缓存未命中或访存延迟)。

# 运行一次命令并统计默认事件
sudo perf stat ls

# 统计指定事件,重复 10 次取均值
sudo perf stat -e cycles,instructions,cache-misses,branch-misses -r 10 ./app

# 输出示例
#  1,234,567,890  cycles
#    980,123,456  instructions     #  0.79  insn per cycle  (CPI≈1.27)
#     12,345,678  cache-misses

CPI 偏高(如 >1.5)通常意味着大量时间花在等内存/缓存,而不是计算本身——这类问题靠「加 CPU」是没用的,要从数据局部性、数据结构下手。

五、perf record + report:离线采样

perf top 是实时快照,perf record 则把采样落盘,后续用 perf report 离线分析,还能保留调用栈:

# -g 采集调用栈;-F 99 表示每秒采样 99 次;-p 指定进程;sleep 30 采样 30 秒
sudo perf record -g -F 99 -p 1234 -o perf.data sleep 30

# 生成带符号的报告(TUI 交互)
sudo perf report -i perf.data

# 纯文本摘要,便于 CI/脚本里记录
sudo perf report -i perf.data --stdio | head -n 40

采样频率别设太高(如 99 比 999 更稳),否则采样开销本身会干扰业务;生产环境建议在低峰期、短时采样。

六、火焰图:把调用栈画出来

文本报告不够直观,火焰图(Flame Graph)把调用栈按「谁调用谁、各自占比多少」横向铺开:横向宽度 = 采样占比,纵向 = 调用深度。最宽的栈顶就是热点。

# 1. 用 perf script 把采样转成可解析的文本
sudo perf script -i perf.data > out.perf

# 2. 折叠调用栈(需要 FlameGraph 工具集,git clone 官方仓库)
git clone https://github.com/brendangregg/FlameGraph
./FlameGraph/stackcollapse-perf.pl out.perf > out.folded

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

# 一条命令流水线版
sudo perf script -i perf.data | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > cpu-flame.svg

打开 cpu-flame.svg 后,点击任意方块可下钻、悬停看精确占比。看到「平顶山」(顶层很宽且很平)往往意味着采样频率不足或符号缺失;看到某个具体业务函数占满顶部,那就是优化切入点。

七、Off-CPU 分析:等待也算瓶颈

CPU 火焰图只统计「在 CPU 上跑」的时间。但很多延迟来自「等」——等锁、等 IO、等网络。这类时间 perf 默认不采,需要用 off-CPU 分析(抓取调度事件,统计线程被阻塞的时长):

# 抓取调度器唤醒/切换事件,记录 off-CPU 时间
sudo perf record -e sched:sched_switch -a -g -F 99 -o offcpu.data sleep 30
sudo perf script -i offcpu.data | ./FlameGraph/stackcollapse-perf.pl |     ./FlameGraph/flamegraph.pl --colors=io --title="Off-CPU" > offcpu-flame.svg

把 On-CPU 和 Off-CPU 两张图结合看,才能完整回答「时间到底花哪了」。一个典型的误导是:CPU 利用率不高,但延迟很高——八成是 Off-CPU 在等锁或等下游。

八、常见瓶颈对照表

现象优先 perf 命令可能根因
CPU 100% 但业务没跑多少perf top / perf record -g某函数死循环、正则回溯、序列化开销
CPU 不高但延迟高off-CPU(sched_switch)锁竞争、下游 IO、GC 停顿
加了核也没变快perf stat -e cache-misses缓存未命中、伪共享、内存带宽瓶颈
吞吐量上不去perf stat -e branch-misses分支预测失败、热路径分支抖动

九、与 top/iostat 的配合

perf 不是万能钥匙,它该用在「已经定位到资源瓶颈、需要进一步下钻到函数」的阶段。推荐的分析顺序:

  • 先用 top/htop 看是不是 CPU 瓶颈;
  • 再用 iostat/vmstat 区分是计算还是 IO;
  • 确认是 CPU 后,上 perf 下钻到具体函数。

如果你还在「CPU/内存/IO 到底哪个是瓶颈」的体检阶段,建议先读 Linux 性能调优实战:CPU、内存与 I/O 瓶颈定位,把 perf 当作那篇文章的进阶下钻工具。

十、调试符号缺失:让火焰图显示函数名

新手最常遇到的坑是火焰图里满屏 unknown 或只有十六进制地址,看不到函数名。原因通常是二进制被 strip 掉了符号表,或采样的是未安装调试信息的系统库。排查顺序很固定:

# 检查二进制是否带符号
file ./app
nm ./app | head

# Debian/Ubuntu 安装带符号的库(以 libc 为例)
sudo apt-get install libc6-dbg

# 部分发行版可用 perf 自带脚本下载内核调试符号
sudo perf buildid-cache --add /usr/lib/debug/vmlinux

# 用 --no-children 只看函数自身开销,避免被调用方占比掩盖
sudo perf top --no-children

对于已经上线的容器镜像,建议在构建阶段保留带符号的版本专门用于排查,或用 perf probe 动态打点——它能在不重新编译的前提下,往指定函数入口插入一个临时追踪点,非常适合确认「到底有没有走到这个函数」。符号问题解决了,火焰图才真正有意义。

十一、落地清单

  1. 临时放开 perf_event_paranoid,用 sudo 运行 perf。
  2. 低峰期短时采样(-F 99 -g,30 秒足够初判)。
  3. perf top 快速看热点,perf stat 看 CPI/缓存指标。
  4. 生成 On-CPU 火焰图定位计算热点,必要时补 Off-CPU 图。
  5. 容器环境确认特权与 perf_event 可用性,避免采不到符号。

perf 的门槛不在命令多,而在「先问对问题」:到底是算得慢、等得久、还是取不到数据。把这套采样→火焰图→下钻的流程跑顺,绝大多数 CPU 类性能问题都能在半小时内定位到函数级根因。

上一篇 ripgrep 实战:用 rg 极速搜索代码的完整指南
下一篇 内存 swap 导致性能骤降排查实战
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]