某次给一个后端服务做压测,CPU一路飙到90%以上,QPS却卡在瓶颈上不去。top扫了一眼,只看到主进程在忙,究竟是哪段代码在烧CPU,完全抓瞎。当时的CPU占用分布就像一个黑盒,后来打开火焰图,才发现一个不起眼的字符串处理函数占了近40%的开销。帮我打开这个黑盒的,就是Linux自带的Perf性能分析工具。
Perf是Linux内核原生的性能剖析工具,依托内核的perf_event子系统,从CPU指令级到内核函数级都能做全方位分析。定位CPU热点、观察缓存命中率、分析锁竞争、追踪系统调用耗时,它都派得上用场。这篇文章不是念man手册,而是把我这几年实际排查性能问题的经验整理出来。不管你是刚准备上手性能优化的开发者,还是在生产环境救过火的运维、SRE同学,都能从这里找到可以直接用的套路。
1. 先搞清楚Perf到底是什么
很多人第一次接触Perf,是先跑一条perf top命令,结果看到满屏不认识的函数名,懵了,然后就没有然后了。实际上Perf的底层逻辑并不复杂,它本质上在做两件事:计数和采样。
1.1 性能事件:硬件计数器加软件事件
Perf所有功能都建立在“事件”这个概念上。事件分成两大类:
硬件事件由CPU内部自带,比如CPU周期数、已执行的指令数、缓存命中与缺失、分支预测失败次数等。这些指标通过CPU的性能监测单元(PMU)提供,精度非常高,而且几乎不产生额外开销。你可以把它想象成汽车仪表盘上的传感器,不用额外装设备就能实时读到油耗。
软件事件来自内核,比如上下文切换次数、进程迁移、缺页异常、CPU时钟等。这部分相当于外加的行车记录仪,需要内核帮忙配合记录,开销比硬件计数器略高。
有个关键事实需要注意:硬件性能计数器靠PMU寄存器实现,而寄存器数量是有限的。同一时刻能同时监控的事件数量通常只有几个到十几个,具体要看CPU型号。所以Perf不是一次性把所有指标都给你,而是让你选重点。这也是为什么理解事件类型如此重要——选对了事件,才能精准定位问题。
用perf list命令可以查看当前系统支持的全部事件名称。不同CPU对事件的命名可能不完全一样,比如有的平台叫cache-misses,有的叫LLC-load-misses,以实际输出为准。
1.2 子命令全家桶
Perf不是单条命令,而是一整套工具族。我把常用命令整理成一张表,看一眼就明白各自的分工:
| 子命令 | 作用 | 典型场景 |
|---|---|---|
perf stat | 运行程序或附着进程,统计指定事件的总数 | 快速体检,对比两个版本的性能差异 |
perf top | 类似top命令,实时刷新函数CPU占用热点 | 生产环境初步定位热点 |
perf record | 按固定频率采样,记录调用栈和指令地址到perf.data | 深度分析前的数据采集 |
perf report | 解析perf.data,按要求排序展示热点函数和调用链 | 离线分析采样结果 |
perf annotate | 反汇编热点函数,显示每条指令的采样占比 | 精确定位到某条指令 |
perf probe | 动态设置追踪点,支持传入参数和返回值 | 排查特定函数的调用关系 |
perf sched | 分析调度器行为,统计调度延迟 | 排查CPU调度导致的延迟抖动 |
perf mem | 分析内存访问模式,定位访存热点 | 缓存和内存带宽瓶颈排查 |
perf lock | 分析锁竞争和等待时间 | 多线程锁竞争问题 |
前五个是我最常用的,perf probe在定位特定内核路径时会用,后几个属于进阶场景。这篇文章重点讲前五个的实战组合。
1.3 采样原理:统计近似,而不是精确测量
perf record采用的是周期性采样:内核在每个固定周期产生一次采样中断,记录当前正在执行的指令地址和完整调用栈。固定周期可以按时间,也可以按指令数,最常见的用法是perf record -F 99,表示每秒采集99个样本。
99这个数字不是随便定的,它后面有讲究。首先,99Hz在大多数场景下已经能得到分布合理的样本;其次,对一个进程每秒只中断99次,CPU开销几乎可以忽略。如果你用1000Hz甚至9999Hz,样本量大了,但其实很多场景并不需要这么高的精度,反而白白增加系统开销。
采样本质上是统计近似。打个比方,一个函数如果真占了30%的CPU时间,那在足够数量的采样样本里,大约也会有30%的样本落在它身上。样本量越大,统计结果越接近真实值。但样本量大了,采样本身的开销和生成的perf.data文件大小也会同步膨胀。所以实际使用中,需要根据场景平衡精度和开销。
理解了这个统计本质,就能明白一个道理:perf输出的百分比不是精确值,而是近似估计。排查热点时,只要关注占比显著的那些函数就够了,不用揪着小数点后两位不放。
2. 第一步实操:用perf stat给系统做“体检”
定位性能问题前,我习惯先用perf stat快速看一下全局指标。这一步类似于去医院先量个血压、做个血常规,把大方向摸清楚再决定要不要做CT。
2.1 基本用法和输出解读
最质朴的用法是直接运行程序并附带统计:
perf stat ./my_program程序跑完之后,终端会打印一行行事件计数。不过实际工作中,这个用法用得反而少,因为服务程序往往是常驻进程,不可能为了统计停掉。更常用的是这两种方式:
# 附着到已经在运行的进程,统计到Ctrl-C结束 perf stat -p PID # 对全系统进行统计,观察整体状况 perf stat -a sleep 10perf stat -a sleep 10的意思是:对整个系统的性能事件计数10秒。sleep 10本身只是为了让统计有个时间窗口。
有个容易忽略但很实用的参数是-r N,表示重复运行程序N次并计算平均值。对于批处理程序,单次运行受噪音影响很大,用-r 5甚至-r 10能显著降低偶发因素带来的干扰。
还有个参数-I 1000,表示每隔1000毫秒打印一次统计快照,适合观察指标随时间的变化趋势。比如看垃圾回收前后缓存命中率的波动,用这个方式很直观。
2.2 看懂那几个关键指标
perf stat的输出类似这样:
5,361,395,732 cycles # 2.827 GHz 2,783,872,995 instructions # 0.52 insn per cycle 560,123,487 cache-references # 5.680 M/sec 12,345,678 cache-misses # 2.203% of all cache refs这些数字里,我最先盯的是两个比值:
IPC(instructions per cycle,每周期指令数)。这个值衡量CPU流水线的利用率。现代CPU每个周期通常能完成2到4条指令的发射,但实际能跑多少,取决于指令之间的数据依赖、访存等待、分支预测情况等。经验参考值:
| IPC范围 | 含义 |
|---|---|
| 2.0以上 | CPU流水线喂得很满,计算密集且访存局部性好 |
| 1.0左右 | 正常水平,但可能有优化空间 |
| 0.5以下 | 明显偏低,大概率被访存延迟、锁等待或分支预测失败拖累 |
缓存未命中率(cache-misses占cache-references的比例)。这个指标对性能的杀伤力极大。现代CPU访问一次L1缓存大约3到5个周期,L2大约10到15个周期,L3大约30到60个周期,而访问一次主内存可能需要100到300个周期。如果程序频繁访问不在缓存中的数据,CPU就得干等内存返回,IPC自然往下掉。
命中率低于95%,基本可以判断程序访存局部性有问题。这个时候重点排查是否频繁遍历大数组、数据结构指针跳跃过多、多线程共享数据导致缓存抖动等。
分支预测失败率也值得关注。现代CPU的流水线非常依赖分支预测器,一旦预测失败,流水线里预取和预执行的指令全部作废,需要重新填充流水线,代价是十几个甚至几十个周期。对于分支密集且规律性差的代码,这个指标可能成为隐形瓶颈。
需要注意的是:perf stat默认只会统计少量事件,如果这里看不到你关心的指标,可以使用-e参数显式指定:
perf stat -e cycles,instructions,cache-misses,cache-references,branch-misses -p PID2.3 结合业务特点选择事件
同样是体检,不同业务侧重点完全不一样。举几个典型场景:
- 计算密集型的数值计算程序:重点看cycles、instructions和IPC,确认CPU算力是否吃满。
- 数据库类工作负载:重点关注cache-misses、LLC-load-misses、dTLB-load-misses,很多数据库的性能瓶颈在访存而非计算。
- 高并发网络服务:关注context-switches、cpu-migrations、page-faults,这些软件事件能反映调度和内存分配方面的开销。
我遇到过不少同事只看默认输出,发现perf stat显示的指标“一切正常”就束手无策,其实是没选对事件。perf list这个命令在此时就是救命的,把当前平台支持的事件全部列出来,再针对场景挑选。
3. 深度定位:perf record配合火焰图锁定热点函数
perf stat给了全局画像,但要找到具体是哪段代码在烧CPU,必须用perf record采样,再配合火焰图把调用链可视化。
3.1 采集调用栈数据的关键参数
最常用的一条命令是:
perf record -F 99 -g -p PID -- sleep 30这里-F 99表示每秒采样99次,-g表示记录调用栈,-p PID指定进程,-- sleep 30让perf持续采样30秒后自动结束。这里的sleep 30不是让目标进程睡觉,而是给perf一个时间窗口。
采样时长的把握,我一般遵循一个简单原则:30到60秒。太短了热点不稳定,尤其是服务进程的繁忙程度有波动时;太长了生成的perf.data文件会非常庞大,后续处理也慢。
调用栈的记录方式有个容易踩的坑。-g默认使用frame pointer(帧指针)方式回溯调用栈,但很多现代编译器默认开了-fomit-frame-pointer优化,帧指针寄存器被挪作他用,导致采到的用户态调用栈残缺不全。这种情况下,要改用dwarf方式:
perf record -F 99 --call-graph dwarf -p PID -- sleep 30dwarf方式利用调试信息进行栈回溯,兼容性更好,但代价是生成的perf.data体积会膨胀好几倍甚至十几倍。所以编译时保留frame pointer的C/C++程序,用默认-g就好;用了-fomit-frame-pointer或者编译时不带调试信息的,老老实实加--call-graph dwarf。
3.2 火焰图的完整生成流程
火焰图是分析采样数据最直观的方式。生成步骤很固定:
# 第一步:采样,生成perf.data perf record -F 99 --call-graph dwarf -p PID -- sleep 30 # 第二步:把perf.data转成脚本文本 perf script > out.perf # 第三步:折叠调用栈 stackcollapse-perf.pl out.perf > out.folded # 第四步:生成SVG火焰图 flamegraph.pl out.folded > flamegraph.svg其中stackcollapse-perf.pl和flamegraph.pl是开源项目FlameGraph提供的脚本,下载后放到PATH里即可。
火焰图的读法非常简单:X轴是样本数占比,Y轴是调用栈深度。一张火焰图里,哪个函数占据的横向宽度越宽,它的CPU消耗占比就越高。顶层的“平顶”函数,往往意味着函数自身逻辑在消耗CPU,而子函数调用占比较少。这是优化的重点。
还有一个高阶技巧:如果你对两个版本的性能差异感兴趣,可以用FlameGraph里的diff-flamegraph.pl对两份采样结果做差值火焰图。红色表示新增的热点,蓝色表示减少的热点。这个功能在做优化前后的对比时特别有说服力。
3.3 终端里的perf report和perf annotate
不是所有环境都有图形界面,这时候perf report能满足大部分需求:
perf report --stdio --sort comm,dso,symbol这条命令会在终端里输出按符号排序的热点列表。如果想要按调用链的维度看,加-g参数,能展开每个热点的完整调用路径。
perf report只能定位到函数级别,想进一步缩小到某条指令,需要用perf annotate:
perf annotate --stdio --symbol=your_hot_function这条命令会反汇编热点函数,并显示每条指令的采样占比。比如看到mov (%rsp), %rax这条指令占了较大的采样比例,就说明大量时间花在了访存等待上,而不是计算本身。结合指令行号,配合代码和汇编的对应关系,就能把热点锁定到源码的某一行甚至某个数据结构访问。
这里要提醒一句:指令级占比高不代表这条指令就是根因。比如某个数据加载指令耗时高,可能是上游数据没命中缓存,也可能是内存屏障导致的。分析指令级热点时,要结合缓存命中率、是否存在锁等待等因素一起看,才能避免被表象误导。
4. 生产环境实战:实时追踪和动态探针
线上问题的排查往往等不起完整采样,需要先快速判断热点方向,这时候perf top和perf probe就派上了用场。
4.1 perf top:不用离线分析也能看到热点
perf top的用法和top类似,直接跑:
perf top -p PID界面上会实时刷新当前进程哪些函数CPU占用高。相比perf record,它的优势是即时反馈,适合在复现问题的时候边观察边定位。
这里有个需要注意的坑:perf top要看到内核态函数名,通常需要root权限。没有root权限时,内核那部分热点会显示成[unknown]或地址,根本看不出是什么函数。如果你确实需要看内核热点,又拿不到root,可以通过-k参数指定内核符号表路径,前提是符号表文件存在且未被清理。
perf top的采样频率默认不低,在生产环境注意控制,最好通过-F调低到能接受的范围。我一般在压测环境用默认参数,生产环境则用-F 99保持低调。
4.2 perf probe:不用重新编译的动态插桩
perf probe是Perf家族容易被低估的一个功能。它允许在运行中的程序上动态设置追踪点,不用重新编译、不用改代码、不用重启服务。
简单示例,分析内核的某一个系统调用路径:
# 添加一个探针,记录函数的入口和返回值 perf probe --add 'do_sys_open filename:string' # 对探针事件采样 perf record -e probe:do_sys_open -a sleep 10 # 查看采样结果 perf script # 用完清理探针 perf probe --del 'do_sys_open'用户态动态插桩依赖调试符号和运行时信息,在某些场景下支持不太稳定。我遇到用户态函数追踪需求时,会更倾向于用gdb或者bpftrace这类更聚焦动态追踪的工具。但在排查内核路径、定位系统调用参数、观察特定内核函数调用频率时,perf probe依然是我最先尝试的方案。
4.3 内核态热点的特殊处理
如果火焰图显示热点大量落在内核态,比如copy_page_range、do_sys_open、kmalloc这类函数,就不能只盯着用户态业务代码分析了。这些名字背后往往对应着更底层的问题:
copy_page_range频繁,说明fork或内存映射操作过多,可以考虑复用进程、减少大内存复制。do_sys_open热点高,说明打开文件的系统调用太多,可以用缓存或减少日志文件频繁切换来缓解。kmalloc相关热点高,说明内核内存分配频繁,可能涉及内存碎片或分配器锁竞争。
内核态热点往往需要配合其他工具一起分析。比如用vmstat看上下文切换是不是异常频繁,用perf sched看调度延迟,用iostat看IO瓶颈。单一工具的结论容易片面,多工具交叉验证会更可靠。
5. 高频问题与避坑指南
Perf用多了,总能碰到一些反复出现的问题。这里把我踩过的坑集中梳理一遍,做成速查表,以后遇到直接对照。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
Operation not permitted | 内核参数perf_event_paranoid限制权限 | 调整参数,或使用sudo/root权限 |
No such event | 事件名在当前CPU型号上不存在 | 用perf list查看可用事件名 |
大量[unknown]符号 | 目标程序被strip,或缺少debug符号 | 保留符号表,指定符号文件路径 |
perf record后报告文件太大 | 采样时间过长或dwarf模式膨胀 | 缩短采样时间,改用frame pointer模式 |
perf top内核函数全是地址 | 权限不足,无法读取内核符号 | 以root运行,或配置内核符号表 |
| 容器内无法使用Perf | 容器权限受限 | 以privileged模式运行容器,或主机上操作 |
5.1 权限问题
Perf默认只允许root访问内核性能事件。在部分发行版上,普通用户连统计自己进程的用户态事件都会受限,这取决于kernel.perf_event_paranoid参数:
2: 只允许用户态事件(默认值偏高) 1: 允许内核事件,但不允许CPU事件 0: 允许几乎所有事件在生产环境调整这个参数需要走变更流程,我一般是先在测试环境评估。如果只是排查自己的用户态程序,保持默认值问题不大;如果需要看内核态热点,就需要提高权限。
5.2 采样过热导致系统卡顿
高频采样不是免费的。-F 999甚至更高的频率在小规格机器上,会导致采样中断过于密集,本身就把性能拖垮了。我见过一个案例,有人用perf record -F 9999采集,结果采集过程中系统几乎无响应,采样结果全是Perf自身的中断开销,数值完全失真。
严格遵循“从低到高”的思路:先99Hz,样本不足再提到199、499,按需递进。99Hz已经能处理绝大多数定位问题。
5.3 符号文件缺失的排查
perf report里出现大量[unknown],最常见的三个原因:
第一,目标程序编译后做了strip操作,符号表被删掉。解决方法是保留未strip的版本,或者单独保留symbol文件。
第二,动态库在启动时临时加载,Perf没来得及记录库的加载路径。可以用perf report -D查看构建ID,再用perf inject --buildid-all合并符号信息。
第三,JIT编译的代码,比如Java、Python脚本等。这类代码的符号在运行时动态产生,Perf默认抓不到。需要额外处理:Java可以使用开源项目提供的JIT符号脚本,Python则可以通过perf-map-agent之类的工具生成符号映射文件。处理完这些,热点才能正确显示为业务函数名,而不是一堆地址。
5.4 容器和虚拟机的特殊限制
在容器内运行Perf,往往因为容器缺权限而失败。解决办法要么以privileged模式运行容器,要么在宿主机上对容器进程执行Perf操作,后者更安全可控。
虚拟化场景还要注意:如果虚拟机没有PMU直通,硬件性能事件大概率采集不到或显示为0。这种情况只能使用软件事件和perf probe做分析。在云服务器上排查性能问题时,先确认CPU型号和虚拟化类型,别在硬件事件上白耗时间。
5.5 一个容易被忽略的坑:CPU频率缩放的影响
现代CPU的动态调频会让频率在800MHz到3.5GHz之间剧烈波动。这意味着你用perf stat统计cycles时,同样的工作量在不同时间跑,cycles数量可能差一大截。高频和低频的对照实验,结论可能完全相反。
控制变量的做法是固定CPU频率,Linux下可以用cpupower frequency-set -g performance切换到性能模式,或者用taskset绑定CPU核心,减小调度迁移带来的噪音。做性能对比实验时,把这些因素控制住,结果才有可比性。
6. 一点实践心得
Perf用久了,最大的感受是:性能分析工具的价值不在于工具本身,而在于把它和CPU体系结构的知识结合起来。同一个perf记录,有人看到的是“某个函数占了30%”,有人看到的是“这个函数访存局部性差、IPC低、缓存命中率不好,需要改数据结构布局”——两者看到的层级完全不同。
在实际排查中,我通常遵循这个顺序:perf stat看全局方向,perf top快速瞄准,perf record加火焰图精确定位,perf annotate缩小到指令级,最后结合代码评审和实验验证。中间每一步的结论都要互相印证,不靠单一证据下判断。
还有一个实用的技巧:采样前先确认程序处于稳定状态。如果目标进程还在启动阶段、缓存还没跑热、线程池还在扩容,这时的采样结果几乎没有参考价值。让程序先跑几分钟,等各项指标平稳后再开始采样,数据才有意义。
Perf是一把锋利的刀,但用它的前提是你知道自己要切哪里。希望这组方法能帮你在下次面对“CPU高、找不出原因”的时候,把黑盒打开,看到问题真正的模样。