聊Linux性能分析,绕不开perf。它是Linux内核自带的性能剖析工具,从CPU热点定位、缓存失效分析,到内核函数动态插桩、火焰图生成,几乎覆盖了日常性能排查的所有主流场景。简单说,perf就是一套“内核级探针+采样器+数据分析器”的组合,不需要额外安装agent,只要内核支持perf_events子系统,拿来即用。
这篇文章我把perf从安装、权限配置,到stat、record、report、annotate这些高频命令,再到火焰图、动态追踪这类进阶玩法,按我平时排查问题的实际路径完整过一遍。适合刚接触性能调优的运维和开发,也适合已经会用perf top、但还没把record/report链路跑通的进阶用户。实战中踩过的坑我会重点标出来,这些在官方文档和网上教程里基本看不到。
1. perf到底能解决什么问题
1.1 perf的定位:不只是“CPU占用率”工具
很多人把perf理解成“看CPU占用”的工具,这个理解太窄了。top、htop也能看CPU占用,perf真正的价值在于回答三个问题:程序的时间到底花在哪条指令上?程序为什么频繁发生上下文切换?用户态到内核态的调用路径上,哪个环节成了瓶颈?
perf基于内核的perf_events子系统实现,它既能做基于硬件的性能计数器采样,也能做基于软件事件和tracepoint的追踪。硬件计数器这块尤其强大,比如CPU缓存命中率、分支预测失败率、流水线停顿周期,这些指标用普通系统监控工具根本看不到,但perf可以直接从PMU里读出来。
我用一个生活化的比喻:top相当于看体检报告上的体重和体温,perf则像是给你做一个全身CT加心电图,能把问题定位到具体是哪一块肌肉在抽搐。比如你发现CPU使用率90%,top只能告诉你进程CPU高,perf能告诉你高在libc里的memcpy,还是高在内核的page fault处理路径上,差很远。
1.2 典型场景:什么时候应该掏perf
我自己的使用习惯,遇到下面几类问题会第一时间想到perf:
第一类是CPU飙高但找不到原因。业务进程多线程,线程名又起得乱七八糟,top看不出具体是哪个线程在空转,用perf top -p按进程/线程抓,热点函数直接浮出来。
第二类是性能数据“正常”但用户体验差。比如请求RT抖动,平均延迟不高,但P99很高。这种情况多半和调度延迟、锁竞争、CPU迁移有关,用perf sched timehist配合perf record -g追踪callchain,能比较清楚地还原当时的时序。
第三类是内核态开销异常。用户态CPU才占了30%,但整机CPU已经100%,剩下的哪去了?大概率在内核。普通工具看不透内核,perf可以采样内核调用栈,直接定位到某个驱动、某个协议栈函数,甚至某个锁。
第四类是缓存和访存密集型应用的调优。数据库、搜索引擎这类对内存带宽敏感的场景,用perf stat看cache-misses和stalled-cycles,基本能判断是访存模式出了问题还是数据布局出了问题。
2. 环境准备与第一次出手
2.1 安装perf:不同发行版的差异点
perf工具在大多数发行版里不是默认安装的,但装起来很省事。Debian/Ubuntu系列跑:
sudo apt install linux-tools-common linux-tools-generic linux-tools-$(uname -r)CentOS/RHEL/Rocky系列跑:
sudo yum install perf有个细节容易被忽略:Ubuntu如果只装了linux-tools-common,没装linux-tools-$(uname -r),执行perf会提示“WARNING: perf not found for kernel X”,然后建议你装某个具体版本包。这个提示其实完全可以照做,它指向的包名通常就是当前内核对应的tools版本。
国产Linux发行版近两年我接触得也多了,比如基于CentOS的、基于Debian的都有,安装方式和上游基本一致,要么yum install perf,要么apt install linux-tools。但国产发行版的内核版本可能定制过,偶尔会遇到perf工具版本和内核版本不匹配的情况,报错一般是“Kernel address maps”或“module ... not found”之类。遇到这类问题,别硬折腾工具本身,先确认内核的debugfs和perf_event是否被默认关闭。
编译安装也算一条路。内核源码里tools/perf目录拿下来,直接make就能编出perf,依赖libelf、libdw、slang这些。不过这属于少数派需求,绝大多数服务器用发行版自带的包就够了。
2.2 权限配置:perf_event_paranoid是第一个坑
权限配置几乎是perf新手的第一道坎。内核有一个参数叫kernel.perf_event_paranoid,默认值很多发行版设的是2,这个值下普通用户不能访问硬件性能计数器,也不能做内核态采样。
查当前值:
sysctl kernel.perf_event_paranoid临时改:
sudo sysctl -w kernel.perf_event_paranoid=1永久生效写到/etc/sysctl.conf里。值设多少合适,我建议是这样:
- 2:默认安全值,用户态的基本统计分析还能做,但硬件计数器用不了
- 1:允许使用CPU硬件计数器,但内核态采样受限
- 0:允许内核态采样,perf probe也可以用了
- -1:完全放开,没有任何限制
生产环境建议先设成1,排查内核态问题时再临时降到0或-1,排查完再改回来。你如果直接装完perf就report,很容易遇到“perf_event_open failed: Permission denied”之类的报错,第一反应就应该是查这个参数。
2.3 第一眼定位热点:perf top实战
perf top是我最常用的“快速扫描”命令,类似top的交互界面,但它显示的是热点函数而不是进程。
sudo perf top默认按CPU周期采样,刷新实时显示热点函数。界面里第一列是开销占比,第二列是符号名,第三列是调用信息。按t可以切换排序字段,按1展开或收起CPU核心视图。
几个参数值得记一下:
# 只看某个进程 sudo perf top -p <pid> # 显示调用链,能看到热点函数是被谁调上来的 sudo perf top -g # 指定采样事件,比如看cache miss热点 sudo perf top -e cache-misses # 指定采样频率,默认4000Hz,低一点对系统影响更小 sudo perf top -F 999这里有个经验:perf top -g开调用链后,输出信息量暴涨,但如果你对代码不熟,反而容易懵。我个人的习惯是先不开-g,直接看哪个函数占比最高,然后去查这个函数属于什么模块,再针对性地用record抓调用链。一步一步来,比一次性把所有信息糊脸上效果好得多。
2.4 perf list:先看看你的机器支持什么
perf支持的事件非常多,取决于CPU型号、内核版本、芯片厂商。查看全部事件:
perf list这个命令输出很长,可以按类别过滤,比如只看硬件事件、软件事件、tracepoint事件:
perf list hw perf list sw perf list tracepointtracepoint是内核预设的静态插桩点,数量巨大,按子系统分类,比如sched:*、block:*、kmem:*、syscalls:*。我在排查进程调度问题的时候,最常用的是这组:
perf list 'sched:*'看输出你会发现,sched_switch、sched_wakeup这些事件都能直接作为采样源。这比单纯采CPU周期更适合回答“线程为什么卡住”这类问题。每次拿到一台新机器,我会扫一眼perf list,心里大概有个数,知道哪些事件可用,到真正排障时就不用手忙脚乱。
3. 三条主命令:stat、record、report彻底吃透
3.1 perf stat:一条命令看穿硬件计数器
perf stat适合对“一次运行”做全量统计,用法简单:
perf stat <command> # 也可以针对运行中的进程做一段时间统计 sudo perf stat -p <pid> -- sleep 10输出长这样:
Performance counter stats for './myapp': 1,023.45 msec task-clock # 0.999 CPUs utilized 5 context-switches # 0.005 /sec 0 cpu-migrations # 0.000 /sec 124 page-faults # 0.121 K/sec 2,542,310,101 cycles # 2.484 GHz 1,982,113,452 instructions # 0.78 insn per cycle 302,123,411 branches # 295.203 M/sec 4,510,223 branch-misses # 1.49% of all branches每个字段都有讲究:
- task-clock:进程占用CPU的总时间
- context-switches:上下文切换次数,频繁切换往往意味着锁竞争或调度问题
- cpu-migrations:进程在不同CPU核心之间迁移的次数,大量迁移会搞坏CPU缓存
- page-faults:缺页次数,频繁缺页说明进程内存访问模式有问题
- cycles:CPU周期总数
- instructions:执行的指令数
- insn per cycle(IPC):每周期指令数,这是评估CPU利用率的核心指标,IPC接近1说明流水线利用得不错,低于0.5大概率有访存停顿或分支预测问题
- branch-misses:分支预测失效率,对分支密集的代码影响巨大
一条命令就能发现应用是“计算密集”还是“访存密集”还是“调度频繁”,这是perf stat最有价值的地方。实战中我建议多跑几组对照,比如并发量低的时候跑一次,并发量高的时候再跑一次,对比cache-misses和context-switches的变化趋势,问题基本就浮出来了。
如果要叠加更多定制事件:
perf stat -e cycles,instructions,cache-references,cache-misses,context-switches <command>cache-references和cache-misses的比例是判断缓存命中率的关键。命中率低于95%、甚至低于90%的时候,程序基本就是在等内存,加再多CPU核心也没用。
3.2 perf record + perf report:采样与离线分析的黄金组合
perf stat给的是宏观统计数据,如果要定位到具体函数,就必须用record做采样。
# 全系统采样,带调用链,99Hz采样频率,持续60秒 sudo perf record -a -g -F 99 -- sleep 60 # 针对某个进程采样 sudo perf record -p <pid> -g -- sleep 30 # 针对某次命令执行的采样 sudo perf record -g ./myapp --input data.bin # 也可以指定事件,比如采上下文切换事件 sudo perf record -e context-switches -a -g几个参数的含义:
-a:全系统模式-g:记录调用链,backtrace-F 99:采样频率99Hz,99这个数字是性能分析界的“标准值”,既能保证统计可靠性,又不至于开销太大-p:指定进程ID
采样结束后当前目录会生成perf.data文件,然后replay输出:
sudo perf reportreport界面是交互式的,常用操作键:
- 回车/展开:展开调用链
- 上下箭头:移动光标
t:切换绝对占比和相对占比a:进入annotate模式,查看热点代码的汇编和源码+/-:展开/折叠调用链h:帮助
第一次用report的人容易被海量信息淹没。我的建议是,先看顶部的“Children”和“Self”两列,Self占比高表示时间真正花在这个函数内部,Children占比高表示时间花在它的子函数里。优先处理Self占比高的函数,这些才是真正的CPU消耗点。Children占比高但Self很低的,通常是聚合节点,比如main函数,没必要在main上花时间优化。
3.3 perf annotate:热点代码的“显微镜”
report只能看到函数级别,annotate可以深入到指令级别。在report界面光标选中热点函数后按a,或者直接用命令行:
# 生成单个函数的汇编级注释报告 sudo perf annotate -s <function_name> --stdio输出会逐条显示指令,以及每条指令的采样占比。这里能直观看到耗时指令是哪条,比如一条movslq占了这个函数50%的采样,说明它极大概率在反复从内存加载数据。配合源码,基本能断定是循环、数组访问还是锁等待。
annotate能工作有个前提:二进制必须带符号表。很多线上程序strip过,只有地址没有符号名。解决办法有两个:一是优化编译时保留-g选项;二是针对线上程序,用perf内置的符号解析能力加载外部符号表文件。但最省事的手段,是让开发环境用同样的代码重新编译一个带符号的版本,采样数据拷过去做离线分析。整个过程不影响线上运行,只需要约定期限的采样文件和二进制副本。
3.4 record/report高频参数速查
我用过一段时间的perf之后,整理了一套高频组合,分享给大家:
# 用户态+内核态调用链,99Hz采样60秒,最适合CPU热点排查 sudo perf record -a -g -F 99 -- sleep 60 # 指定进程,带调用链,适合单服务分析 sudo perf record -p <pid> -g -- sleep 30 # 采调度事件,查延迟抖动 sudo perf record -e sched:sched_switch -a -g -- sleep 10 # 采缓存失效事件,查访存瓶颈 sudo perf record -e cache-misses -a -g -- sleep 30-F采样频率不是越大越好。频率太高,perf自身开销会明显影响采样结果,出现严重的观测者效应。99Hz已经能覆盖绝大多数场景,追求更细的时间分辨率可以用-F 999,但要评估对业务的干扰。
4. 进阶玩法:火焰图、动态追踪与延迟分析
4.1 火焰图:从采样数据到可视化
perf report的交互界面虽然强大,但在对外汇报、团队协作场景下,火焰图明显更直观。Brendan Gregg发明的火焰图已经成为性能分析的事实标准,perf的数据可以直接转换。
生成火焰图需要两个脚本:stackcollapse-perf.pl和flamegraph.pl,来自Brendan Gregg的FlameGraph项目:
git clone https://github.com/brendangregg/FlameGraph.git cd FlameGraph # 先把perf.data转成脚本数据 sudo perf script > out.perf # 折叠成调用栈计数格式 ./stackcollapse-perf.pl out.perf > out.folded # 生成SVG火焰图 ./flamegraph.pl out.folded > out.svg生成的SVG用浏览器打开,每个色块代表一个函数,宽度和采样占比成正比,鼠标悬停可以看到具体函数名和占比。火焰图底部是最底层的函数,顶部是最外层的调用入口。找性能瓶颈的核心方法很简单:找“平顶山”,也就是顶部很宽且颜色厚实的色块,这些函数占的CPU时间最多,值得深挖。
我在实践中发现一个坑:perf script输出的调用栈信息,如果程序是用内部符号表解析的,可能只显示地址不显示函数名。这种情况通常是符号表不完整,要么用带符号的二进制重新解析,要么在record前用--symfs指定符号目录。大多数时候,缺符号的问题根源就是strip,多说一句:线上程序最好保留带符号的副本,哪怕只用来看perf数据,也值这个磁盘空间。
4.2 perf probe:动态插桩的实用场景
perf还有一个很能打的功能是动态插桩,可以在运行中的内核函数或用户态程序的函数入口注入探针,采集参数和返回值。这在排查某些诡异问题的时候比采样好用得多。
先看内核函数插桩:
# 在系统调用入口插桩 sudo perf probe --add tcp_sendmsg # 查看插桩点 sudo perf probe -l # 按插桩事件采样 sudo perf record -e probe:tcp_sendmsg -a -- sleep 10用户态程序的动态插桩,用-x指定二进制路径:
sudo perf probe -x /usr/lib/x86_64-linux-gnu/libc.so.6 malloc sudo perf record -e probe_libc:malloc -a -- sleep 5更高级的用法是抓函数参数:
sudo perf probe 'tcp_sendmsg size' sudo perf record -e probe:tcp_sendmsg -a -- sleep 10 sudo perf script这样能看到每次tcp_sendmsg调用时size的具体值,对分析网络协议栈的报文大小分布很有用。
但要注意,perf probe在内核态插桩,强烈不建议在生产环境随意添加。插桩本身有风险,搞不好会引入内核不稳定因素。我在非生产环境试过在文件系统写路径上插桩,就遇到过因为探针事件过多导致的内核日志刷屏。生产环境要动它,务必先在预发环境完整验证,并且用perf probe --del及时清理不需要的探针。
4.3 perf sched:揪出延迟抖动和调度问题
对于“性能数据正常但体验卡顿”的问题,perf sched是一把好手。它专门分析内核调度器行为。
# 采样调度事件 sudo perf sched record -- sleep 10 # 查看调度延迟统计 sudo perf sched latencylatency输出会按任务展示平均调度延迟、最大延迟和延迟分布。我排查过一个网络服务P99延迟飙高的问题:业务线程的平均调度延迟只有0.2ms,但P99达到了50ms,perf sched timehist一看,原来是某个线程频繁让出CPU,然后被调度器放到另一个NUMA节点上,导致内存访问跨节点,延迟飙升。这就是典型的调度和NUMA共同作用的问题,单靠业务日志根本看不出来。
在虚拟化场景下,perf同样能帮上忙。比如你在一台KVM宿主机上跑了很多虚拟机,某个虚机网络时延异常。先在宿主机上执行:
sudo perf top -g大概率能看到vhost进程或者kvm模块的函数热点。如果是vhost作为网络数据平面的瓶颈,perf会告诉你热点是在vhost_work系列函数里,还是内核协议栈里。这个信息对判断“该调virtio队列数”还是“该换网卡多队列策略”非常关键。
4.4 perf kvm:虚拟化场景的性能视角
KVM环境下的性能分析,perf有一个专门的扩展模式:
# 采集host和guest的采样数据 sudo perf kvm --host --guest record -a -g -- sleep 30 # 生成report sudo perf kvm --host --guest report它会同时区分host和guest的调用栈,能看出是虚拟化层开销大还是guest内部自身开销大。排查虚机CPU steal高的场景,这类工具几乎是唯一能直接给你证据的手段。
不过perf kvm对内核配置有要求,某些发行版的内核没开KVM的perf支持,执行时会报错。遇到报错先去确认/boot/config-$(uname -r)里有没有CONFIG_KVM_INTEL或CONFIG_KVM_AMD,以及有没有CONFIG_KVM_TRACE。没有对应配置的话,perf kvm就用不了,只能退回到host侧做常规采样分析。
5. 常见问题与排查技巧实录
5.1 所有计数器都是0或Permission denied
这是perf最经典的问题。报错信息通常是perf_event_open failed: Permission denied,或者You may not have permission to collect stats。原因基本都指向kernel.perf_event_paranoid。解决方式前面说了:
sudo sysctl -w kernel.perf_event_paranoid=1如果设成1还不行,可能是Secure Boot或内核配置禁用了perf_event。这种情况需要检查内核启动参数,确认没有加perf_event_paranoid=2之类的hardcode,也没有在/sys/module/目录下禁用相关模块。
5.2 符号全是十六进制地址
perf report里看到大量地址而不是函数名,基本可以判断是符号表缺失。常见原因有三个:
- 程序编译时没加
-g参数,或者strip过 - 动态库版本更新过,采样时的库和当前解析时的库对不上
- 内核自身的kallsyms符号表被压缩或关闭了
对于用户态程序,解决方法是找一份带符号的二进制,用perf report --symfs /path/to/symbols指定符号路径,或者干脆在编译机上重新执行record和report。对内核,确认CONFIG_KALLSYMS已开启,并且/proc/kallsyms有读取权限。
5.3 采样频率太高导致系统卡顿
perf的观测者效应是真实存在的。采样频率过高,比如-F 9999,系统的整体性能会被明显拖慢,采出来的数据也会失真。我的经验是:
- 常规CPU热点分析:99Hz足够
- 短时间高精度追踪:999Hz,最多持续十几秒
- 事件频率本身很高的场景(cache-misses、tracepoint),优先用
-c指定采样周期而不是-F指定频率
另外,perf record时加上--all-user或--all-kernel可以只采样用户态或只采样内核态,能减少一半的采样开销。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查/解决办法 |
|---|---|---|
| perf stat全0或Permission denied | perf_event_paranoid限制 | sysctl设置paranoid为1或0 |
| report显示地址无函数名 | 符号表缺失/strip | 加-g编译,或--symfs指定符号 |
| 调用栈只有一层 | 编译器优化内联 | 编译加-fno-omit-frame-pointer,或采样用--call-graph dwarf |
| record后perf.data太大 | 采样频率过高/无事件过滤 | 降低-F,或按事件过滤 |
| 热点函数不是业务代码 | 锁竞争/CPU迁移/内核开销 | 看sched事件,用perf sched分析 |
| KVM场景guest数据空白 | 内核KVM perf支持未开启 | 检查CONFIG_KVM_TRACE等选项 |
5.5 一个完整的排查路径参考
假设你处理的问题是“Java服务CPU飙升到300%”,我的排查路径大致是:
# 找到线程PID top -Hp <pid> # 看CPU热点,确认是JIT编译后的代码还是JVM内部 sudo perf top -p <pid> -F 999 # 采样带调用链,抓到JIT符号 sudo perf record -p <pid> -g --call-graph dwarf -- sleep 30 # 生成report,看热点是GC线程还是业务线程 sudo perf reportJava场景有个额外的问题:JIT即时编译出来的代码会产生动态符号,perf如果不配合-XX:+PreserveFramePointer和perf map文件,抓到的大概率是[unknown]。这属于perf的一个特殊应用方向,不过原理和普通程序完全一致,只是要额外处理JVM的符号映射。说白了,perf的核心能力不挑语言,挑的是你能不能把符号表准备好。
6. 最后的实操心得
perf这套工具,要说难用,它确实有学习曲线,命令多、参数多、输出信息密;要说好用,一旦熟悉了,几乎所有性能问题都能快速定位到“该看哪里”。
我个人最常组合使用的一条命令是:
sudo perf record -a -g -F 99 --call-graph dwarf -- sleep 60然后根据report结果,决定下一步用stat看宏观指标,还是用annotate深入指令级,或者用sched查延迟。这套流程我用了很久,从简单Web服务到性能敏感的中间件都验证过,稳定可靠。
最后分享两个小技巧。第一,perf的采样数据perf.data可以离线分析,完全可以在测试环境跑压测,把perf.data和带符号的二进制一起拷到本地分析,不影响线上。第二,养成“先看宏观再深入”的习惯,别一上来就开-g,很多新手被调用链淹没后反而下不了手。先从perf stat全局看一遍,再perf top找热点函数,最后record抓调用链,从粗到细,问题很快就能水落石出。