资讯动态

perf性能分析实战:从CPU热点定位到火焰图

发布时间:2026/10/13 14:26:21 来源:尧图企业网站定制
简介perf 是 Linux 平台广泛使用的系统级性能分析工具这份资源专为需要定位程序热点、分析 CPU 缓存与调用链的开发者或运维人员准备覆盖从入门到中级的性能调优场景。压缩包共 8 个文件、约 9.08MB其中包含可直接运行的 perf 可执行程序、自动化测试脚本 auto.sh以及 libbfd、libelf、libunwind 等若干动态库便于离线环境快速部署和重复性性能测试。目前已有 2531 人学习使用。借助该包读者可以执行 perf record、perf stat、perf report 等经典分析流程结合硬件事件采样、缓存统计和源码级注解系统排查 CPU 周期、分支预测错误、内存访问等层面的性能瓶颈配套脚本还能持续采集数据或对比不同负载帮助从事件数据中提炼优化方向并验证改进效果特别适合对系统性能分析有实操需求的工程师。1. perf内核自带的性能剖析工具一条命令定位 CPU 热点线上服务 CPU 打满 100%负载一路飙到 32top 里只能看到进程名热点落在哪个函数没人敢拍板。换 gdb 要停业务装 py-spy 要搞运行时压测窗口根本等不起。这种时候Linux 内核自带的 perf 是最趁手的武器不装包、不插桩、不改代码一条perf record就能拿到整个调用栈的采样分布。它是内核事件子系统之上构建的性能剖析工具既能读 CPU 硬件计数器也能挂 tracepointCPU 热点、锁竞争、Cache Miss、上下文切换都能量化。适合性能测试工程师、后端开发和 SRE 三类人拿到压测报告后先跑perf stat定量再跑record跟调用链把性能问题从“玄学”变成可复现的数据。2. perf 的事件来源与子命令分工先搞懂采样逻辑再选工具perf 不是那种靠注入代码做 APM 的探针方案它走的是内核事件通道。任何一次 perf 命令背后都是perf_event_open这个系统调用在注册事件再由内核在事件触发时做采样或计数。先把这层机制看懂后面调参数才不会瞎碰。2.1 三类事件决定你能看到哪一层perf 的事件大致分三类。PMU 硬件事件走的是 CPU 内部专用寄存器专门统计周期数、指令数、分支预测失败次数、Cache Miss 这类微架构指标。perf 通过读取寄存器拿到硬件层面数据开销极低、精度高适合回答“CPU 在等什么”这类问题——是等内存、等分支还是纯粹在算。软件事件没有硬件寄存器也能数比如上下文切换次数、CPU 迁移次数、缺页次数、task-clock。它们由内核软件计数器维护粒度粗一些但正好用来判断调度层面有没有异常。比如 context-switches 每秒钟飙到几万次基本可以断定进程在频繁切换锁或 IO 等待逃不掉。tracepoint 是内核代码里预埋的钩子像sys_enter、sched_switch、block_rq_complete这类。挂上 tracepoint 能看到系统调用、调度切换、块设备 IO 的完整时间线定位系统级瓶颈非常好用但采样开销比前两类大线上使用要控制范围。大致分工是硬件事件看 CPU 内部瓶颈软件事件看调度和内存tracepoint 看系统调用与 IO。2.2 子命令怎么选从实时监控到离线剖析perf 的命令不少常用的六个我列了个表工作中按场景挑着用子命令用途典型场景perf top实时刷新当前热点函数进程正在跑先看哪个函数在烧 CPUperf stat跑完一次性输出事件计数压测后拿 IPC、Cache Miss 等指标基线perf record按频率采样并落盘事后分析调用栈数据可重复看perf report读 record 产物做交互分析按 Overhead 排热点逐层展开调用链perf annotate聚焦单一函数看行级/汇编热点确认热点是否在某一行代码或某条指令perf sched / lock调度延迟与锁竞争专项分析高切换、高等待场景我的建议是先perf top看大概再用perf record抓详细栈。如果只要一个能写进测试报告的指标数字直接perf stat就够不需要额外渲染任何插件。压测机里少装一个东西就少一个干扰变量。2.3 开跑前检查事件支持与权限水位很多 “perf 不能用” 的报错不是命令不存在而是权限没放开。开跑前先做两个检查。# 检查当前权限水位数值越小权限越大 sysctl kernel.perf_event_paranoid # 看当前系统暴露了多少硬件/软件事件 perf list | grep -iE hardware|software第一行输出 2 是多数发行版默认值。paranoid 为 2 时非特权用户能做用户态采样但内核态采样和部分硬件事件受限如果是 3perf 基本只能 root 跑普通用户连打开事件都会被拒。第二行perf list会列出可用事件如果硬件事件显示not supported多半是 paranoid 太高或者虚机没透传 PMU这个坑后面专章讲。调整权限的常见做法是临时改 sysctl或者写配置文件持久化sudo sysctl -w kernel.perf_event_paranoid1 echo kernel.perf_event_paranoid1 | sudo tee /etc/sysctl.d/99-perf.confparanoid1允许用户态采样同时放开大部分硬件事件适合性能测试机如果是隔离的压测环境可以放到 0。生产环境不建议动先和运维确认口径再操作。调完权限跑一次perf top能刷出热点列表就说明环境通了。3. perf stat先把性能测试指标跑出来再判断优化方向perf stat是我用得最频繁的子命令。它不做调用栈只做计数跑完一个程序或一段压测输出一张指标表。别小看这张表IPC、Cache Miss、分支预测失败率都在里面这是最客观的第一层性能测试指标。3.1 一次 perf stat 的指标解读拿一个典型场景举例假设服务进程 PID 是 12345采样 10 秒perf stat -p 12345 -- sleep 10输出里核心几行逐个说清楚。task-clock表示任务在这段时间内占用的 CPU 时间context-switches是自愿加非自愿上下文切换的总和cpu-migrations是进程在 CPU 之间搬家的次数page-faults是缺页次数cycles是消耗的 CPU 周期instructions是实际执行的指令数后面分支指令和分支失败是一对Cache 引用和 Cache Miss 是一对。我用一张表把这几个核心性能测试指标的经验判断写出来指标含义经验参考task-clock占用 CPU 时间跟实际运行时长对比判断多核利用是否充分context-switches上下文切换次数明显偏高要考虑锁竞争和 IO 等待cycles / instructions周期与指令数二者共同算出 IPCIPC每周期指令数长期小于 0.5 说明 CPU 在等待cache-missesCache Miss 次数占比高说明数据布局或伪共享严重branch-misses分支预测失败占比高说明热路径上分支模式对 CPU 不友好IPC 是这堆指标里优先级最高的一个。IPC 低通常意味着指令流水线被卡住常见因素是内存延迟、锁等待、分支预测失败。Cache Miss 高配合 IPC 低方向基本锁定在数据访问模式branch-misses 高则更多是代码结构问题比如在热路径里写了难以预测的大分支。3.2 设计一次可信的 perf stat 实验直接跑一次拿到的数字不值得信。压测环境里 CPU 频率、ASLR、后台任务都会扰动结果。我一般会这样组织实验# 关闭 ASLR确保两次加载地址一致 setarch $(uname -m) -R ./gateway_bench --reqs 100000 # 重复 5 次记录平均值和标准差 perf stat -r 5 -e cycles,instructions,branches,cache-misses \ -- ./gateway_bench --reqs 100000 # 结果落盘方便后续对比版本差异 perf stat -o /tmp/perf_stat_v2.txt -e cycles,instructions \ -- ./gateway_bench --reqs 100000setarch -R是关掉地址空间随机化让两次实验的加载地址一致减少采样噪声。-r 5表示自动重复运行 5 次perf 会输出平均值和标准差抖动一眼可见。-e指定要计数的指标不写就输出全量默认集合。-o把结果写到文件里。对比版本时我习惯先看instructions是否稳定。如果两次运行指令数差 10% 以上说明输入或环境没控住得先解决这个再谈优化。基线不牢后面所有结论都是空中楼阁。3.3 用 IPC 和 Cache Miss 决定下一步用什么工具perf stat是“先定量再定性”的第一步拿到指标不能停在表上。如果 IPC 低但 Cache Miss 正常下一步用perf record -F 99 -g跟调用栈看代码是不是卡在锁或 IO 上如果 Cache Miss 占比很高优先怀疑数据结构布局和伪共享用perf annotate进具体函数看汇编如果 context-switches 异常转perf sched看调度延迟。别一上来就开火焰图至少先把指标锚住再做深度采样。4. perf record report把 CPU 热点定位到具体函数这是整个工具链里最硬核的一步。perf record以固定频率采样 CPU 当前执行的指令地址并连带记录调用栈跑完之后perf report把样本按函数聚合成热榜。它像是给压测中的进程拍了一组“现场照片”业务代码不需要做任何埋点。4.1 采样参数怎么选-F、-g、-p、-a 的语义# 以 99Hz 采样记录调用栈附着指定进程持续 30 秒 perf record -F 99 -g -p 23344 -o /tmp/perf_cpu.data -- sleep 30 # 全系统视角采样适合不知道热点在哪个进程的压测场景 perf record -F 99 -g -a -o /tmp/perf_all.data -- sleep 60 # dwarf 模式展开调用栈弥补缺失帧指针的问题 perf record -F 99 --call-graph dwarf -p 23344 -o /tmp/perf_dwarf.data-F 99表示每秒采样 99 次。避开整秒对齐能防止和系统定时器产生锁步采样偏差99Hz 对 CPU 负载几乎无感是我长期用的默认值。-g记录调用栈这是火焰图的数据来源。-p指定进程 PID只采样这一个进程-a是全系统采样压测时常配合-a便于事后追溯所有进程。--call-graph dwarf走栈回溯库展开调用栈在缺少帧指针的二进制上会比-g完整但录制开销明显更大生产环境慎用。时长控制上面用-- sleep 30完成实际压测场景里更常见的做法是后台启动perf record等压测跑完再 kill采样窗口正好覆盖压测高峰段。压测结束后记得留几秒余量让 perf 把缓冲区的数据完整落盘。4.2 perf report 交互界面怎么读跑完 record执行perf report -i /tmp/perf_cpu.data进交互界面。第一屏按 Overhead 降序排列函数行首百分比就是采样落在该函数的比例。按 Enter 进入函数后能看到它的调用路径按a进入 annotate 视图看每一行 C 源码或汇编的采样占比。看报告有个经验法则先看 Overhead 最高的前三个函数再按 Children 列排序看谁吸收的调用最多。Self 高代表时间烧在这个函数自身的指令里Children 高代表时间消耗在它底下的子函数里。定位瓶颈先找 Self 高的因为那才是 CPU 真正干活的位点。如果 Self 高的函数是pthread_mutex_lock或memcpy问题基本可以锁定在锁竞争和内存拷贝上不需要再往深层钻。4.3 用 FlameGraph 把采样栈变成火焰图perf report 看单函数还好看跨模块调用路径就很费劲。脚本化生成火焰图是社区标准做法我每次定位完热点都会生成一张存档。# 从 perf.data 导出每个采样的调用栈文本 perf script -i /tmp/perf_cpu.data /tmp/perf_cpu.stack # 折叠成一行一栈的计数格式 ./FlameGraph/stackcollapse-perf.pl /tmp/perf_cpu.stack /tmp/perf_cpu.folded # 生成 SVG 火焰图 ./FlameGraph/flamegraph.pl /tmp/perf_cpu.folded /tmp/perf_cpu.svgstackcollapse-perf.pl把 perf 输出的多行调用栈折叠成以分号连接的路径加采样数flamegraph.pl再按它绘制成 SVG。读图时记住每个矩形宽度代表采样占比顶端是当前执行到的函数堆叠的是祖先调用链。顶部有又宽又平的矩形说明热点就在那一层顶部全是又高又窄的尖说明消耗发生在深层函数里多数是字符串处理、循环内分配这类问题。4.4 一次锁竞争热点的完整复盘拿一次实际压测说。服务压测中 TPS 上不去perf stat显示 IPC 只有 0.41context-switches 每秒超过 5 万次。接着跑perf record -F 99 -g -a采样 60 秒perf report 里pthread_mutex_lock的 Self 高达 38%调用路径挂的是请求处理函数。这个组合基本可以确诊锁竞争。如果内核开了 lock 事件还可以补跑perf lock record看持锁时间和等待队列长度确认热点锁的缓存行分布。最后调整锁粒度解决。整个定位流程不到半小时没改业务代码一行。之后我把 record 原始数据存档每次优化迭代都跑同类压测用perf diff对比两个采样文件的分布变化。5. 避坑perf 在真实环境里的五个高频翻车点perf 看起来很轻实际用起来坑不少。这里记录的五个问题都是我在测试机和云环境里真实遇到过的每条按现象、原因、解决的顺序写。5.1 权限拦截普通用户报 Permission denied现象普通用户执行perf record报Permission denied或Operation not permitted但同机 root 能正常跑。原因kernel.perf_event_paranoid被设为 3非特权用户的perf_event_open直接被拒绝。很多发行版出于安全考虑默认收紧了这个参数。解决按第 2 章方法临时降到 1配合运维确认生产策略。注意调完权限后已经启动的 perf 命令需要重新执行才能生效。5.2 虚机和容器里硬件事件全是 not supported现象perf list里 hardware 事件全部不可用perf stat的 cycles 和 instructions 输出为 0 或完全不计数。原因大多数云虚机没有把 PMU 透传给 guest容器场景则受 seccomp 或 cgroup 限制性能事件访问被隔离。解决虚机内改用软件事件和 tracepoint 做采样或者把采样放到宿主侧。云环境先验证perf stat -e cycles -- sleep 1输出是否正常不正常就不要在虚机里纠结硬件计数直接换思路。5.3 调用栈全是 unknown帧指针被编译器优化掉了现象perf report 或火焰图里出现一大片[unknown]调用栈在某个动态库处断掉。原因gcc 默认 O2 优化下会省略帧指针perf 的-g模式没有帧指针可回溯栈自然断掉。动态库剥离符号后也会出现同类问题。解决编译器加-fno-omit-frame-pointer重新编译被测模块线上二进制不方便重编时改用--call-graph dwarf配合 libunwind 回溯符号缺失则安装对应 debuginfo 包。火焰图里[unknown]比例超过 20%先解决符号问题再谈分析结论不然方向容易被误导。5.4 NMI watchdog 抢占固定计数器部分硬件事件拿不到现象perf stat 报某个硬件事件 open 失败或者 perf top 固定计数器列恒为 0。原因内核 NMI watchdog 默认占用了固定性能计数器跟 perf 的某些固定事件冲突。这类问题在长时间运行的服务器上尤其隐蔽因为不是每次都会立刻报错。解决临时关闭 watchdog 再测试命令是echo 0 /proc/sys/kernel/nmi_watchdog。压测机可以关掉让出计数器生产机建议保持默认换其他可用事件绕过冲突。5.5 perf stat 只跟踪主进程线程结果对不上压测报告现象压测工具起了几十个线程perf stat 里却只有主线程的计数TPS 跟指标对不上。原因perf stat默认 attach 到命令行给出的 pid命令启动的子线程不在统计范围。多线程程序尤其容易踩这个坑。解决加--per-thread或者直接用-a做全系统采样。多线程程序建议用perf record -a记录事后在 report 里按 comm 过滤线程这样不会漏掉任何执行流。6. 进阶perf script 二次分析与压测时间轴对齐把采样结果变成回归资产6.1 用折叠栈做 top-K 调用路径分析perf script能把二进制的采样流导出成文本配合脚本做很多 report 交互界面做不了的事比如统计最高的 top-K 完整调用路径。我通常先把 record 数据走一遍 FlameGraph 的 stackcollapse然后写个简单 Python 脚本做路径聚合。from collections import Counter counter Counter() with open(/tmp/perf_cpu.folded, r) as fp: for line in fp: callchain, count line.rstrip().split( ) frames callchain.split(;) # 只看最深的三层聚合出高频路径 counter[;.join(frames[-3:])] int(count) for path, cnt in counter.most_common(10): print(cnt, path)这段脚本读取折叠后的调用栈把每个栈取最后三层拼成路径再按累计采样数排序。输出前 10 条高频路径能直接看出系统大部分 CPU 时间花在哪条通路上比人肉在 report 里一层层展开快得多。做版本对比时我更喜欢分别采样两个版本再用perf diff对比两个文件省去记忆时间切片语法的麻烦。6.2 压测记录与 perf 采样对齐jmeter 跑步骤perf 捕状态做性能回归时我习惯让压测端和采集端各司其职jmeter 负责给目标服务施加负载记录 TPS、响应时间这类业务指标perf 在压测同时做服务侧全系统采样。两个数据流对齐很简单jmeter 每个线程组的运行窗口是确定的perf record 的采样窗口就对齐压测运行窗口。# 压测开始后台启动全系统采样 nohup perf record -F 99 -g -a -o /tmp/perf_regression_v2.data \ -- sleep 120 # 压测脚本跑同样的窗口 jmeter -n -t http_load.jmx -l /tmp/jmeter_v2.jtl跑完后先看 jmeter 聚合报告里 TPS 掉落的时刻再回来用perf diff对比两个版本的采样分布。如果某个版本在同一个接口路径上新出现了锁相关热点性能回退的根因基本可以锁定到代码变更。现在也看到有人在用大模型对 perf script 文本做第一轮筛选让模型列出可疑符号再人工复核方向上确实能减少盲查时间。但对符号不完整的数据模型只能靠猜别把它的结论当最终结果还是得回到原始调用栈确认。从那以后我每次做性能回归验证都强制走一遍这套流程先perf stat定量锚指标再perf record -g采样最后 stackcollapse 折叠成火焰图并用perf diff对比版本差异。不先看指标就开火焰图很容易被某个宽栈误导方向。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑