值班电话响起来的时候,我正趴在工位上改脚本。业务那边报了一个接口超时告警,连续三台机器CPU都窜到400%以上。老规矩,先top一把抓进程,再strace看系统调用,可折腾了快二十分钟,除了看到线程名在变,愣是没找到CPU到底烧在哪个函数里。后来换perf上去,一条perf record -F 99 -g -p 12345,等三十秒钟,perf report一压栈,热点函数立刻现形,原来是一个字符串拼接函数在循环里反复分配内存。
从那以后,perf就成了我排障工具箱里的常驻成员。这工具对运维来说,真不是高不可攀的内核开发专用品,它就是一把性能手术刀,只要你愿意花半小时摸清它的脾气,日常定位CPU飙高、锁竞争、系统调用瓶颈、内存分配异常这些问题,效率能有质的提升。这篇东西就是我自己的perf入门实战笔记,从为什么学、怎么装、怎么用到真实场景复盘,一路写下来,尽量让没碰过perf的运维同学也能照着操作。
1. 为什么运维要掌握perf:先搞清楚它解决什么
1.1 从一次CPU飙高故障说起
那次告警的排查过程其实很有代表性。我用top看到了PID,确认了某个Java进程吃满了多核CPU,但Java层面看不到栈,jstack打出来的线程栈也只是业务代码的调用关系,GC线程和业务线程混在一起,完全分不清到底是哪段代码在疯狂消耗计算资源。接着用strace -p,结果输出全是futex和mmap,因为高并发进程本来就频繁做锁操作和内存映射,这些系统调用频率高并不代表它们就是罪魁祸首。
后来我用了perf top -g -p PID,界面一刷出来,占第一位的符号就是某个native方法的调用链,跟着栈往下走,最终定位到了我们自己写的一个正则表达式工具类。问题根因是正则回溯导致CPU忙等,这在纯业务视角下根本没法快速发现。perf的价值就在这里:它能直接告诉你CPU的每一个百分点花在哪个函数、哪条调用路径上,这是top、htop、strace这些工具给不了的视角。
遇到类似CPU告警,我现在的标准动作就三步:top确认进程,perf record采样,perf report看热点。整套流程不超过五分钟,比对着jstack瞎猜快了不知道多少倍。这也是我在团队里反复强调的:先让perf告诉你“CPU在哪烧”,再考虑“怎么修”,顺序反了必走弯路。
1.2 perf到底是什么定位
perf是Linux内核自带的性能剖析工具集,它不是一个单命令,而是一族命令:perf stat、perf top、perf record、perf report、perf trace、perf sched等等,都是围绕内核perf_event子系统展开的。简单来说,perf能做到两件事:一是读取CPU性能计数器的数据,比如执行了多少条指令、发生了多少次缓存缺失、多少次分支预测失败;二是对CPU进行周期性采样,每隔一段时间记录一次当前正在执行的是哪个进程、哪个函数,最后汇总成调用统计。
这套机制不是靠打断程序运行实现的,而是使用硬件性能计数器和内核采样点,对业务进程的开销很小。我在4核8G的云主机上跑过perf record -F 99 -g,业务接口RT的波动基本在可接受范围,平时排障完全够用。很多运维一听perf就发怵,觉得那是内核开发或者性能专家才干的事,其实真不是。你不需要读懂每一个性能事件,先掌握三五个高频场景就够了。
它的定位是“采样剖析工具”,天然适合找热点。与动态追踪工具bpftrace相比,perf更偏“看整体分布”,bpftrace更偏“看具体事件路径”。两者可以互补,但入门先从perf开始,因为内核自带、零额外依赖、生态最成熟。
1.3 和常用工具的分工配合
很多运维同学手头已经有一堆工具,想弄清楚perf到底该在哪个环节参与排查。我根据自己的使用习惯,整理了一个简单的分工视角,不一定权威,但很实用:
| 工具 | 能看到的 | 看不到的 | 适合场景 |
|---|---|---|---|
| top/htop | 进程CPU、内存、负载 | 进程内部哪个函数在消耗CPU | 第一眼粗筛,确认对象 |
| vmstat/iostat | 系统整体CPU分布、IO压力 | 具体调用栈 | 判断瓶颈在CPU还是IO |
| strace | 系统调用与信号 | 用户态函数内部、CPU热点 | 排查异常行为、文件路径 |
| jstack/pstack | 用户态线程栈 | 栈上每个函数的CPU占比 | 看业务线程在干嘛 |
| perf | 函数级CPU热点、调用链、硬件事件 | 业务逻辑内部不可见符号 | 定位CPU、锁、缓存类性能瓶颈 |
实际排查时,我的顺序一般是:top定位进程,vmstat确认是不是CPU型瓶颈,strace排除明显异常调用,最后用perf收口。perf放在最后一棒,是因为它给出的结论最接近根因,但前期也需要粗筛工具来缩小范围。工具之间不是替代关系,是接力关系。
2. 地基:环境准备与三类核心概念
2.1 安装与内核要求
perf在内核源码的tools/perf目录下,很多发行版已经单独打包。Debian/Ubuntu系列装linux-tools-common和linux-tools-generic,CentOS/RHEL系列装perf。安装命令很简单,但有几个小坑需要注意。
Ubuntu上只装linux-tools-common往往不够,因为perf版本要和当前内核版本严格对应。你装了generic包之后,实际调用的是/usr/lib/linux-tools/<内核版本>/perf这个路径下的二进制,它通过/usr/bin/perf软链出来。如果内核是HWE版本或者自己编的,可能找不到对应工具包,这时可以直接从内核源码编译:
# 以5.15内核为例 cd /usr/src/linux-source-5.15.0/tools/perf make -j4 make install编译依赖主要是flex、bison、elfutils-libelf-devel、libunwind-devel这些,缺了哪个装哪个。编译完的perf会安装到/usr/local/bin/perf,注意和系统自带版本区分开。
我第一次在CentOS 7上跑perf时报错“WARNING: perf not found for kernel 3.10”,后来才发现是kernel-devel包没装全。建议大家在干净的测试机上先把perf跑起来,确认perf version输出的内核版本和uname -r一致,再做后续学习,否则后面跑出来的数据可能全是乱的。
2.2 权限问题:perf_event_paranoid
不少同学第一次执行perf top会碰到这样的提示:You may not have permission to collect stats. Consider tweaking /proc/sys/kernel/perf_event_paranoid。这是内核的权限控制参数在起作用,它控制普通用户能否访问性能计数器,不同的发行版默认值不一样,从2到-1都有。我习惯把它理解成一把锁,越宽松的数值权限越大:
| 值 | 含义 |
|---|---|
| 2 | 只允许用户态自己的计数器,禁止内核态采样,默认最严格 |
| 1 | 允许CPU事件,但禁止内核级测量 |
| 0 | 允许几乎所有事件,包括内核态 |
| -1 | 不做任何限制,老内核的默认行为 |
临时调整一行命令就行,重启失效:
sudo sysctl -w kernel.perf_event_paranoid=0如果想永久生效,写入/etc/sysctl.conf或者/etc/sysctl.d/99-perf.conf,然后sysctl -p。生产环境我建议设置成0而不是-1,既能采样内核态热点排查问题,又保留基本审计能力。自己本地开发机直接调成-1也问题不大,但别在客户环境乱改,先看清对方安全基线允许不允许。
容器里跑perf有可能遇到额外的权限限制,需要--privileged运行容器,或者确保seccomp配置放行perf_event_open系统调用。这个放到后面问题排查章节详细展开。
2.3 事件、计数、采样
perf有三个最核心的概念:事件、计数、采样。事件是性能测量的最小单元,比如CPU周期、指令数、缓存缺失、页错误、上下文切换,甚至可以是某个具体的动态函数调用。事件分为硬件事件、软件事件和跟踪点事件。硬件事件来自CPU内部的性能计数器,软件事件来自内核的计数逻辑,跟踪点则是内核代码里预埋的钩子。
计数是“统计总量”,就是在一个时间段内,某个事件一共发生了多少次。perf stat用的就是计数模式,它不会告诉你哪个函数消耗了这些事件,只告诉你系统整体或者某个进程的整体指标。采样则像抽签,每隔固定周期记录一次当前运行位置,攒上一堆样本,就能算出CPU时间在各个函数上的分布比例。采样的核心参数是频率或周期,我常用-F 99表示每秒采样99次,这个数字不是随手乱写的,它避免了和常见频率成整数倍关系,减少采样相位偏差。
打个比方,计数像查账本只看总支出,采样像偷偷记录你每笔钱花在了哪里。两者结合使用效果最好:先用stat确认瓶颈指标,再用record做采样定位具体热点函数。
2.4 如何快速自检perf可用性
装完perf,我建议先跑一轮自检再正式用,避免排障到一半才发现工具本身有问题。第一步看版本:
perf version输出里会显示perf版本和构建信息。第二步看事件支持:
perf list 2>/dev/null | head -30perf list会列出当前内核支持的硬件事件、软件事件和跟踪点。如果你的CPU出现“no permission”字样,大概率是权限参数没调好;出现“not supported”字样,可能是CPU本身不支持该硬件事件。第三步跑个简单的采样验证,随便拿一个进程做五秒采样,确认能生成perf.data并正常读取。我在新机器上都是这三步走一遍,五秒钟就能确认环境没问题。
3. 五个高频子命令实战
3.1 perf stat:先量化再优化
perf stat是性能评估的入口,也是我日常用得最频繁的命令之一。它统计一个命令或一个运行中进程的性能事件总量。比如我想评估一个Java服务在运行时的IPC(每周期指令数)表现:
perf stat -e cycles,instructions,cache-references,cache-misses,branches,branch-misses -p 12345 sleep 10输出会分成几行,每行显示事件总量、单位以及每秒速率。最有价值的指标不是绝对数,而是比例关系。instructions / cycles就是IPC,一般大于1说明CPU的有效执行率不错,小于0.5就要警惕流水线停顿、缓存缺失或者分支预测失败。cache-misses / cache-references如果超过5%,说明数据局部性可能有问题,可以考虑调整数据结构或者访问方式。
用perf stat有个容易踩的坑:对比时要保证采样时长和进程负载一致。比如你想验证一个优化前后的效果,两次测量中间程序还在跑不同的业务逻辑,对比出来的数据就没意义。我一般用固定时间窗口,比如sleep 10,或者固定请求量,比如压测脚本跑同样的请求数,这样才有可比性。
perf stat -a -- sleep 30-a参数表示统计全系统,不指定进程。看整个宿主机当前的负载特征时非常有用,尤其是要判断是CPU繁忙、内存带宽瓶颈还是IO等待时,先看整体再往下拆。
3.2 perf top:实时热点追踪
perf top和top的交互方式类似,但它展示的不再是进程列表,而是热点函数列表,实时刷新。直接跑:
perf top -p 12345 -g-g开启调用链记录,能看到每个热点函数的父调用是谁。执行之后,界面默认按采样占比排序,第一列是采样比例,第二列是进程名,第三列是符号名。看到某个符号占比超过20%,基本可以确定它是主要CPU消耗者。
这个命令对排查“进程CPU高但不知道该从哪下手”特别好用。有一次MySQL实例CPU异常,perf top -p mysql_pid刷出来,占比最高的符号是ut_delay,顺着调用栈发现是InnoDB在等待锁的时候自旋空转,后来调了innodb_spin_wait_delay和innodb_sync_spin_loops参数,CPU立刻降了下来。这个场景如果用jstack,只能看到线程在等锁,但看不到锁等待本身的CPU开销。
perf top有个小问题:对未加载调试符号的动态库,函数名会显示成地址或者[unknown]。这时候需要在系统里安装对应包的debuginfo,或者确认二进制没有strip掉符号表。这个细节后面单独讲。
3.3 perf record与perf report:定位热点函数的核心组合
perf record是采样模式的灵魂,它把采样结果写入磁盘上的perf.data文件,之后用perf report或perf script分析。日常排障我的固定用法是这样:
# 采样,每秒99次,记录调用链,指定进程,持续30秒 perf record -F 99 -g -p 12345 -- sleep 30 # 以实时模式查看热点 perf report # 或者输出文本,方便在终端里直接搜 perf report --stdio-F 99表示每秒采样99次,这个频率对大多数场景都足够且开销可控。-g打开调用链记录,记录的是每个采样点的函数调用栈。sleep 30是采样窗口,意思是对目标进程采样30秒后自动结束。如果不想限制时间,可以不写,用Ctrl+C手动结束。
perf report打开后默认展示的是函数的热度列表,第一列是Self,表示该函数本身消耗的采样比例,第二列是Children,表示该函数连同它调用的子函数的整体消耗比例。我看热点时通常先看Children,因为它能反映一条调用链的总体开销,再逐个展开查看到底哪个子调用最耗时。按回车键可以下钻,展开调用栈的下一层。
这里有个重要的解读技巧:Self高说明这个函数自身计算量大,比如数学运算、内存拷贝、字符串处理;Children高但Self低,说明问题出在它调的某个子函数上,要用下钻方式找到底层的真正开销点。我在一次内存排序优化中,就看到某个自定义比较器的Children占比很高,Self却只有2%,下钻后发现问题其实是它内部调用的Objects.compare触发了大量反射,根因不在排序本身。
perf record生成的文件大小取决于采样频率、调用链深度和运行时长。一个高并发进程采样一分钟,perf.data可能轻松超过几百MB,分析时要注意磁盘空间。如果只想记录少量事件,可以用-e限定事件类型,或者用--call-graph dwarf减少栈记录开销。
3.4 perf annotate:逐指令定位热点
当函数级定位还不够精细,想知道热点到底是函数里的哪一段代码时,就要用到perf annotate。它能把热点函数的机器指令和源码关联起来,逐行标注每条指令的采样开销:
# 先采样 perf record -F 99 -g -p 12345 -- sleep 30 # 打开热点指令分布 perf annotate界面里会显示函数每行汇编代码,旁边标注采样百分比。占比最高的那条指令往往就是最热路径。这个工具对C/C++和Go这类编译型语言特别有效,对Java和Python这类解释执行的语言效果有限,因为Java是JIT编译,Python的解释器热点会落在ceval这类通用函数上。
实际操作中,我发现perf annotate最常用的场景是排查自旋锁和原子操作竞争。比如看到cmpxchg指令反复重试,说明多个线程在抢同一把锁,CPU都耗在重试上而不是业务逻辑里。这时候调整锁粒度或者改用无锁数据结构,效果立竿见影。汇编看不懂没关系,关键指令就那么几条,cmpxchg、lock前缀、pause,记住这几个标志性指令就够排查了。
3.5 perf trace与perf sched:系统级问题排查
除了CPU热点,运维日常还会遇到系统调用频繁、进程调度延迟这类问题。perf trace是对系统调用进行跟踪的入口,类似strace,但基于perf事件机制,效率和信息量更好用:
# 跟踪指定进程的系统调用 perf trace -p 12345 # 统计各类系统调用次数和耗时 perf trace stat -p 12345 sleep 10perf trace stat的输出会按系统调用汇总次数、平均耗时、错误次数。我曾经用这个命令抓到一个诡异问题:服务偶发线程卡顿,perf trace stat显示futex调用次数极高且大量超时,最终定位到锁竞争导致的线程切换风暴,而不是业务代码本身。
perf sched则专注调度器行为,常用的是这两个命令:
# 采集调度事件,默认采样10秒 perf sched record -- sleep 10 # 查看延迟统计 perf sched latency输出里会展示每个进程的调度延迟、运行时间、等待时间。当业务反馈“响应偶尔突刺”但又查不到CPU热点时,我通常怀疑调度器有问题,用perf sched latency看一眼是不是有优先级反转或CFS带宽限制。之前碰到过cgroup的cpu.max设置不当,导致容器内进程频繁被限流,perf sched一眼就看到了进程大量处于throttled状态。
4. 典型场景实战复盘
4.1 场景一:Java/Python进程CPU烧满怎么查
这是运维最常遇到的场景。Java进程CPU飙高,第一反应是jstack打线程栈,但jstack只能看到Java层方法,看不到JVM内部native方法的开销。如果热点在JIT编译出来的代码或者某个native库里,jstack基本无能为力。
我的做法是先用perf采样拿到内核视角的热点。对Java进程执行:
# 使用Java进程的PID采样 perf record -F 99 -g -p <java_pid> -- sleep 30 perf report --stdio > java_perf_report.txt输出的热点函数通常分两类。一类是JVM内部的运行时函数,比如G1ParScanThreadState::copy_to_survivor_space,说明GC线程在频繁做对象复制,问题在内存分配速率过快;另一类是Java方法编译后的代码,符号名经常是Interpreter或者[JIT]标签,配合jstack才能确认业务方法。举个例子,我遇到过sun.nio.ch.SocketDispatcher.read热点非常高,配合perf调用栈往下走,发现是业务代码在循环里不断发起小的网络读取,而不是批量读取,导致系统调用开销被放大。
Python进程更麻烦,因为Python解释器几乎所有的纯Python代码最终热点都会落在_PyEval_EvalFrameDefault这个C函数上。perf只能告诉你“解释器在忙”,没法告诉你是哪一行Python代码在忙。这种情况我的建议是用py-spy:
py-spy record -p <python_pid> -o /tmp/profile.svg --duration 30py-spy用process_vm_readv读取Python进程内存中的栈信息,能看到纯Python层的函数调用热点。perf和py-spy配合,一个看C层,一个看Python层,组合起来基本能覆盖Python性能排障的大部分场景。Java也有类似的工具配合,比如async-profiler,它基于perf事件但能输出Java方法栈,比直接用perf看JVM内部符号高效得多。场景复杂时别偷懒,该上专用工具就上专用工具。
4.2 场景二:内核态CPU占用偏高
有时候top显示CPU的sy占比很高,us很低,业务代码看起来根本没怎么跑,但系统CPU就是忙得不行。这种问题用perf查起来特别有效。
先看一眼全系统的热点分布:
perf top -g -a如果热点集中在native_queued_spin_lock_slowpath或者_raw_spin_lock这类符号上,说明存在内核锁竞争。常见原因是多个进程同时密集访问某些内核资源,比如网络收包、io_uring操作、ext4文件系统锁。之前我处理过一次Nginx频繁accept惊群导致的内核锁热点,后来开启SO_REUSEPORT并配合socket的reuseport分配策略,锁竞争明显下降。
另一种常见情况是软中断热点,符号会显示为do_softirq、net_rx_action这类。如果perf top里它们占比很高,大概率是网络包处理过于频繁,可能是小包太多触发大量中断,或者网卡队列被某个CPU独占。这时再配合perf stat -e irq_vectors:local_timer_entry这类追踪点看中断分布,基本能确认问题方向。
4.3 场景三:三分钟生成火焰图
火焰图几乎已经成为perf排障后的标准产出物,因为调用链数据用火焰图展示比perf report文本直观得多。生成火焰图的工具是Brendan Gregg的开源脚本,项目地址就是GitHub上的brendangregg/FlameGraph。整个流程我固定三分钟走完:
# 第一步:采样 perf record -F 99 -g -p <pid> -- sleep 30 # 第二步:将perf.data转换为脚本可读的文本 perf script > out.perf # 第三步:折叠调用栈 stackcollapse-perf.pl out.perf > out.folded # 第四步:生成火焰图 flamegraph.pl out.folded > flame.svg生成的SVG用浏览器打开,能看到从底部到顶部的调用链堆叠。每个色块的宽度代表样本占比,宽度越宽说明CPU时间消耗越多。排障时我重点关注“平顶山”,也就是顶部很宽且颜色统一的色块,那通常是热点函数。再看从底部到顶部的路径,就能还原出“到底是谁调用到这个热点”的完整链路。
有一次排查一个Node.js服务CPU异常,火焰图显示v8::internal::JsonParser宽度特别大,沿着栈往下看发现是JSON.stringify在循环里被高频调用。于是改成了批量序列化加缓存,CPU直接降了一半。火焰图的阅读门槛很低,业务同学也能参与讨论,这是它比纯文本报告容易推动优化落地的原因。
5. 常见问题与踩坑记录
做这个实战笔记之前,我在测试环境反复折腾perf,也踩了不少坑。这里把高频问题整理成一份速查,方便大家直接对着排查。
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 采样后符号全是[unknown] | 二进制被strip,缺少debuginfo | 安装对应的debuginfo包,或用perf archive打包符号 |
| 报告里只有地址没有函数名 | 内核符号表未加载 | 检查kptr_restrict参数,需要root或下调该参数 |
| 采样数据只有几百个样本,热点不明显 | 采样时长不足或频率太低 | 提高-F频率或拉长采样时间 |
| JIT代码显示为[JIT]标签 | 缺少JIT符号信息 | 配合perf-map-agent或async-profiler |
| 容器内报permission error | perf_event_open被seccomp或capability拦截 | 增加privileged权限或调整seccomp配置 |
| perf.data文件过大 | 采样频率高、调用链深、时长长 | 用-F 99+--call-graph lbr或控制时长 |
5.1 符号表缺失问题
perf的灵魂是符号表,符号表丢了,采样再准确也都是白搭。排查热点时看到[unknown],第一件事先确认是不是调试符号没装。Debian/Ubuntu用apt-get install linux-image-$(uname -r)-dbgsym,CentOS/RHEL用debuginfo-install kernel。第三方编译的软件,尽量在生产环境保留带符号的二进制,或者用perf archive把符号文件收集起来,之后perf report --symfs指向符号目录。
我把符号问题放在第一个讲,是因为它最容易影响新手信心。你辛苦采样一分钟,结果report界面一半是[unknown],谁都会崩溃。还有一种情况是容器场景,代码在镜像里,符号依赖在宿主机上,这时要确认perf报告读取的是宿主机还是容器内符号路径,必要时用--symfs指向容器rootfs。
5.2 采样频率与时长怎么权衡
-F 99是我最常用的采样频率,但这并不意味着所有场景都该用99。如果热点函数非常集中,比如占比超过30%,降低到-F 49也能拿到足够样本,还能减少对业务的影响。如果热点很分散,函数占比只有几个百分点,就要提高采样频率或者拉长时长,确保每个热点函数至少能攒到几百个样本,否则统计误差太大。
我自己的经验是,通用排障先采30秒,看看sampled counts够不够。perf report左上角会显示total samples,如果低于5000,说明样本偏少,可以用-F 199或者采到60秒。这里不要盲目追求高频,生产环境还要考虑开销。比如低负载服务用-F 999其实也能接受,但高吞吐服务在每核CPU都很忙的情况下,高频采样本身就会明显干扰进程运行。
5.3 容器与虚拟化环境里的perf
现在很多服务跑在Docker容器里,有的还套了一层Kubernetes,时间久了大家可能会觉得perf这种内核工具在容器里没法用。实际上普通的Docker容器可以共享宿主机的内核,只要容器有足够的perf权限,就能在容器里面直接采样本容器内的进程。启动容器时加上--privileged是最省事的方案,但安全要求高的环境可以只添加SYS_ADMIN能力或者调整seccomp白名单,放行perf_event_open。
虚拟机环境情况不同。如果是KVM/QEMU这类全虚拟化虚拟机,性能计数器通常能透传给客户机,perf能正常用;如果是云厂商的轻量容器或者serverless环境,底层可能是gVisor或Firecracker这类轻量虚拟化,perf可能无法采集到有效数据,这种时候就得靠云厂商自带的可观测工具或者eBPF方案替代。
5.4 内核版本差异与工具版本匹配
不同内核版本的perf符号解析能力和事件支持差异很大。老内核3.10上的perf缺少很多新事件,比如没有cgroup相关的追踪点,对新硬件的支持也差。新内核6.x则显著改善了JIT符号采集和--call-graph lbr的效果。生产环境如果内核版本不是特别旧,建议优先用系统自带perf包,不要随意升级,因为perf和内核要对应。
这里特别提醒一点:perf record的数据格式在不同版本之间并不是完全兼容的。在A机器上采集的perf.data,拿到B机器上跑perf report,如果两边perf版本差距太大,可能报错或者符号解析异常。所以我一般要求采样和分析在同一台机器完成,如果要拷贝数据,就把perf版本也一起带过去,或者用perf script把数据转换成文本再传输。
最后再分享一个我很推荐的习惯:每次排障结束,把perf report的结果连同火焰图存档到一个共享目录,标注日期、问题描述和结论。时间长了,这会变成团队自己的性能问题案例库,很多新問題根本不用从头查,翻翻历史记录就知道大概方向。这个习惯救过我很多次,现在也推荐给所有刚开始学perf的同路人。