Linux性能排查利器perf实战:从CPU飙高到火焰图

发布时间:2026/10/7 10:45:09
Linux性能排查利器perf实战:从CPU飙高到火焰图 值班电话响起来的时候我正趴在工位上改脚本。业务那边报了一个接口超时告警连续三台机器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还是IOstrace系统调用与信号用户态函数内部、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-genericCentOS/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_paranoid0如果想永久生效写入/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秒后自动结束。如果不想限制时间可以不写用CtrlC手动结束。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-spypy-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 errorperf_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)-dbgsymCentOS/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的同路人。