Linux perf 性能分析实战:从 CPU 飙高排查到火焰图生成

发布时间:2026/10/6 10:04:10
Linux perf 性能分析实战:从 CPU 飙高排查到火焰图生成 简介perf 性能测试工具资源包面向 Linux 系统开发者、运维工程师及性能调优学习者用于在操作系统层面定位程序性能瓶颈。包内共 8 个文件以 so 动态库、perf 可执行文件、sh 自动化脚本及若干辅助文件为主压缩包约 9.08MB其中库文件为工具运行提供依赖支持脚本则便于重复执行采样与数据收集。资源涵盖 perf 的采样、事件统计、CPU 缓存分析、指令计数、调用链分析及内核与用户空间分析等核心能力可帮助读者理解硬件性能计数器的工作方式并结合实际负载排查热点代码与内存访问效率问题。已有 2539 人学习下载适合需要借助 perf 深入分析程序执行细节、优化代码效率的读者参考使用。1. 从一次 CPU 飙高排查说起perf 到底能帮你看到什么线上服务 CPU 突然从 30% 飙到 90%日志里没有任何异常业务指标却开始抖动。你登录机器top看到某个进程占满一个核但进程内部有几十个线程、上百个函数到底是哪一行代码在烧 CPU这时候strace只能告诉你系统调用gdb断点又会让服务卡住真正能回答“CPU 时间花在哪个函数上”的工具就是 perf。perf 是 Linux 内核自带的一套性能分析工具集全称 Performance Events基于内核的perf_events子系统实现。它不像很多商业 APM 需要埋点、改代码、重启服务而是直接利用 CPU 的 PMUPerformance Monitoring Unit硬件计数器和内核软件计数器对运行中的进程做采样。你可以把它理解成一个“CPU 的黑匣子”不打断业务按固定频率抓取当前指令指针最后聚合成火焰图或调用链报告。它适合后端工程师、SRE、内核开发者也适合任何需要定位性能瓶颈却不想改一行代码的人。下面我从安装、采样、报告解读到避坑把这条链路完整走一遍。2. perf 的安装与最小可用采样从perf stat到perf record2.1 为什么先跑perf stat而不是直接record很多人第一次用 perf 就上perf record结果采出来的数据一团糟因为不知道目标进程到底在干什么。perf stat是成本最低的入口它只做计数不采样输出的是事件总量和比率。常见做法是先用它确认 CPU 周期、指令数、缓存命中率、上下文切换这些宏观指标判断瓶颈方向是计算密集、内存密集还是调度频繁。# 统计目标进程 5 秒内的硬件事件 perf stat -p 12345 sleep 5 # 统计整个系统的 CPU 周期和指令数每秒刷新一次 perf stat -a -I 1000 sleep 5第一行-p 12345指定进程 PIDsleep 5表示统计持续 5 秒。第二行-a表示全系统-I 1000表示每 1000 毫秒输出一次增量。输出里重点看cycles、instructions、IPCinstructions per cycle。IPC 低于 1 通常意味着内存或分支预测拖累IPC 高但 cycles 也高才是纯计算密集。这一步不需要 root但部分硬件事件需要perf_event_paranoid权限后面避坑章会讲。2.2perf record的采样频率与调用栈选择确认方向后用perf record做采样。核心参数只有三个采样频率、调用栈深度、目标范围。频率用-F指定默认 4000 Hz 左右但线上服务建议降到 99 或 999避免采样本身成为负担。调用栈用-g开启深度由--call-graph控制常见是fp帧指针或dwarf调试信息。帧指针需要编译时加-fno-omit-frame-pointer否则栈会断dwarf 更准但开销大。# 对 PID 12345 采样 30 秒频率 999Hz记录调用栈 perf record -F 999 -g -p 12345 -- sleep 30 # 采样整个系统排除空闲 CPU持续 10 秒 perf record -F 999 -g -a --exclude-idle -- sleep 10第一行-F 999把采样频率压到 999Hz-g记录调用栈-- sleep 30表示采样 30 秒后自动结束。第二行-a全系统--exclude-idle跳过 idle 进程避免报告里全是swapper。采样结束后当前目录会生成perf.data这是二进制文件不能直接看必须用perf report或perf script解析。2.3 生成报告perf report的交互与perf script的导出perf report是交互式 TUI适合在终端里逐层展开调用链。按展开按Enter进入函数按a看汇编按d看源码行需要调试符号。如果要把数据导给火焰图工具用perf script输出文本再交给 FlameGraph 脚本。# 交互式查看报告按 CPU 周期排序 perf report --sortcycles --stdio # 导出调用栈文本供火焰图使用 perf script perf.out--stdio表示不用 TUI直接打印到终端适合远程 SSH 或日志留存。--sortcycles按周期数排序也可以换成comm、dso、symbol。perf script输出的每一行是一个采样点包含进程名、PID、CPU、时间戳和调用栈。火焰图工具会把这些行折叠成栈帧;栈帧;函数 计数的格式再渲染成 SVG。这一步是 perf 从“数字”变成“图形”的关键后面进阶章会给出完整命令。3. 从perf.data到火焰图采样、折叠、渲染的完整链路3.1 火焰图为什么比perf report更适合汇报perf report是树形结构适合自己排查但给团队或上级看时火焰图更直观横轴是采样占比纵轴是调用深度宽条就是热点。火焰图不是 perf 自带功能而是 Brendan Gregg 的 FlameGraph 工具链核心脚本是stackcollapse-perf.pl和flamegraph.pl。整个链路是perf record采样 →perf script导出 →stackcollapse-perf.pl折叠 →flamegraph.pl渲染 SVG。# 1. 采样 30 秒记录调用栈 perf record -F 999 -g -p 12345 -- sleep 30 # 2. 导出文本 perf script perf.out # 3. 折叠调用栈 ./stackcollapse-perf.pl perf.out perf.folded # 4. 渲染火焰图 ./flamegraph.pl perf.folded perf.svg第一步和前面一样关键是第二步perf script的输出必须完整如果调用栈里有[unknown]说明缺少符号表。第三步stackcollapse-perf.pl会把每个采样点的调用栈合并成一行格式是func_a;func_b;func_c 123数字是采样次数。第四步flamegraph.pl生成 SVG用浏览器打开即可。注意如果目标进程是 Java、Go 或 Python用户态栈需要额外处理Go 要开GODEBUGJava 要用perf-map-agent否则只能看到 JIT 后的地址。3.2 采样频率与开销的平衡99Hz、999Hz 还是 4000Hz采样频率直接决定开销和精度。4000Hz 是 perf 默认值适合短时间、低负载进程线上高负载服务建议 99Hz 或 999Hz。99Hz 每秒采样 99 次对 CPU 的额外占用通常低于 1%但可能漏掉短函数999Hz 精度更好开销约 1%3%。我一般先用 99Hz 跑 30 秒看整体分布如果热点不明确再升到 999Hz 跑 10 秒。不要用 4000Hz 跑几分钟采样中断本身会改变程序行为这就是观察者效应。# 低开销长采样99Hz持续 60 秒 perf record -F 99 -g -p 12345 -- sleep 60 # 高精度短采样999Hz持续 10 秒 perf record -F 999 -g -p 12345 -- sleep 10第一行适合生产环境长时间观察第二行适合定位具体函数。注意-- sleep 60里的sleep是独立命令不是目标进程的一部分它只是控制采样时长。如果目标进程在这段时间内退出perf 会自动结束并保存已采数据。3.3 符号解析为什么你的火焰图全是[unknown]火焰图里出现大量[unknown]是最常见的翻车现场。原因有三类一是二进制 stripped没有符号表二是 JIT 语言Java、Node.js动态生成代码perf 不认识三是容器环境里/proc/kallsyms权限受限内核符号读不到。解决办法对于 C/C编译时加-g保留调试信息对于 Java用perf-map-agent生成/tmp/perf-pid.map对于容器挂载宿主机的/sys/kernel/debug或调整perf_event_paranoid。# 检查二进制是否有符号 file /path/to/binary nm -C /path/to/binary | head # 查看当前 perf 权限 cat /proc/sys/kernel/perf_event_paranoidfile输出里如果有not stripped说明符号还在nm -C能列出 C 符号。perf_event_paranoid的值决定普通用户能做什么-1几乎无限制0允许访问 CPU 事件1允许内核 profiling2只允许用户态3几乎禁止。生产环境通常设为1或2需要 root 或CAP_PERFMON才能改。如果符号缺失且无法重新编译可以用perf buildid-cache添加调试包或者用addr2line手动映射地址。4. 避坑与排查perf 在生产环境里的五个血泪经验4.1 现象perf record报Permission denied原因perf_event_paranoid限制现象是普通用户执行perf record直接失败提示perf_event_open: Permission denied。原因是内核参数perf_event_paranoid默认值较高常见 2 或 3禁止非 root 用户采样内核或全系统事件。解决方法是临时调整sudo sysctl kernel.perf_event_paranoid1或者给用户加CAP_PERFMON能力。注意生产环境不要长期设为-1会带来安全风险。如果无法改内核参数只能对自有进程做用户态采样加--user或-e cpu-clock绕过部分限制。4.2 现象火焰图里目标函数被[unknown]淹没原因符号表缺失或 JIT 未注册现象是火焰图 80% 都是[unknown]根本看不出业务函数。原因是二进制 stripped、容器镜像精简掉了调试符号或者 Java/Node.js 的 JIT 代码没有注册到 perf。解决方法是C/C 重新编译加-gJava 启动时加-XX:PreserveFramePointer并运行perf-map-agentGo 程序用go build -gcflags-N -l关闭优化并保留帧指针。如果无法重新部署可以用perf script导出地址再用addr2line -e binary -f -C 0x地址手动解析。4.3 现象采样期间服务延迟抖动原因采样频率过高或调用栈深度过大现象是perf record一跑服务 P99 延迟立刻上升。原因是采样频率 4000Hz 加上dwarf调用栈每次中断都要遍历栈开销可能超过 5%。解决方法是降频到 99Hz调用栈改用fp并限制采样时长。如果还是抖动用perf stat先确认瓶颈或者改用perf top做实时观察不落盘、开销更低。线上采样尽量避开业务高峰或者用--exclude-idle和-G过滤特定 cgroup。4.4 现象容器里 perf 看不到宿主机内核符号原因/proc/kallsyms被隐藏现象是在容器内跑perf record -a内核函数全是地址没有名字。原因是容器默认挂载的/proc/kallsyms里地址被替换成 0或者kptr_restrict限制。解决方法是宿主机执行sysctl kernel.kptr_restrict0并在容器启动时挂载-v /sys/kernel/debug:/sys/kernel/debug和-v /proc/kallsyms:/proc/kallsyms。如果权限不允许退而求其次只采样用户态perf record -e cpu-clock --user。4.5 现象perf.data文件巨大原因采样时间过长或事件过多现象是跑了几分钟perf.data几个 GB传输和解析都卡。原因是默认记录所有事件且调用栈深度大。解决方法是限制事件类型perf record -e cycles -F 99 -g只记录 CPU 周期用--call-graph fp,32限制栈深度或者用perf record -o /tmp/perf.data指定小分区。如果已经生成大文件用perf report --header-only看元信息再用perf script按时间窗口截取不要直接cat。5. 进阶技巧用perf probe做动态追踪与条件采样5.1perf probe为什么能替代部分bpf场景perf probe允许你在内核或用户态函数的任意偏移处插入动态探针不需要重新编译也不需要写 BPF 程序。它适合追踪“某个函数被调用时参数是多少”“某个条件满足时抓一次栈”。常见做法是先用perf probe -x /path/to/binary -L function列出可插入行再perf probe -x /path/to/binary functionoffset插入探针最后perf record -e probe_xxx采样。# 列出用户态函数可插入的行 perf probe -x /usr/local/bin/myapp -L main # 在 main 函数第 10 行插入探针 perf probe -x /usr/local/bin/myapp main:10 # 记录探针事件抓 5 秒 perf record -e probe_myapp:main -a -- sleep 5第一行-L main列出main函数所有可插入位置。第二行main:10表示第 10 行也可以写main0x1a按偏移。第三行-e probe_myapp:main是探针事件名-a全系统sleep 5控制时长。探针事件默认只记录命中次数如果要抓参数加--add和-v查看变量名。注意用户态探针需要二进制有调试信息内核探针需要CONFIG_KPROBE_EVENTS。5.2 条件采样只抓“慢请求”的调用栈条件采样是 perf 的隐藏技能。比如只想抓耗时超过 100ms 的请求可以用perf probe在函数入口和出口插探针再用perf record的--filter过滤。更简单的做法是用perf record -e cycles -c 100000每 10 万个周期采样一次天然过滤掉短函数。如果要做精确条件用perf probe --add func arg100语法但需要内核支持。# 每 10 万个 CPU 周期采样一次只抓热点 perf record -e cycles -c 100000 -g -p 12345 -- sleep 30 # 用过滤器只抓特定进程名的采样 perf record -e cycles -g -a --filter comm myapp -- sleep 10第一行-c 100000是周期计数不是频率适合抓长函数。第二行--filter comm myapp只保留进程名为myapp的采样减少噪音。注意--filter对某些事件不支持如果报错就改用-G按 cgroup 过滤。5.3 验证采样是否可信交叉检查perf stat与perf report采样结果是否可信要用perf stat交叉验证。比如perf report显示某函数占 60% CPU但perf stat显示总周期数很低说明采样频率或事件选错了。我一般会跑两遍第一遍perf stat -e cycles,instructions,cache-misses看宏观第二遍perf record -e cycles看微观。如果两者趋势一致结论才可靠。另外火焰图里的百分比是采样占比不是真实 CPU 时间只能做相对比较不能当绝对值用。# 宏观验证统计 10 秒内的关键事件 perf stat -e cycles,instructions,cache-misses,branch-misses -p 12345 sleep 10 # 微观验证只采样 cycles 事件生成火焰图 perf record -e cycles -F 999 -g -p 12345 -- sleep 10 perf script | ./stackcollapse-perf.pl | ./flamegraph.pl cycles.svg第一行输出cycles、instructions、cache-misses、branch-misses的总量和比率。第二行只采样cycles避免多种事件混在一起导致报告失真。如果cache-misses比率高火焰图里应该能看到内存分配或哈希查找的热点如果branch-misses高重点看条件分支密集的函数。从那以后我每次做 perf 分析都强制先跑perf stat再跑perf record不然很容易被单次采样带偏。希望帮到你。本文还有配套的精品资源点击获取