资讯动态

Linux性能分析利器perf:从采样原理到火焰图实战

发布时间:2026/10/22 13:47:46 来源:尧图企业网站定制
聊到 Linux 性能分析绕不开一个名字perf。我最早接触它是在排查一次 CPU 异常飙升的问题一群人在那儿猜是不是缓存失效、是不是锁竞争最后用perf record跑了一分钟调用链清清楚楚地摆在眼前真相一目了然。从那以后perf 就成了我线上排查和性能优化时的首选工具。这篇文章想聊的“perf 基本原理”不光罗列命令用法而是把工具背后怎么采集数据、怎么区分采样和计数、怎么把热点定位到具体指令这套机制讲清楚。因为只有理解了原理你才懂得什么时候该用perf stat、什么时候该用perf record、火焰图的纵轴到底在表达什么遇到“采样不准”“符号找不到”这类问题时也知道去哪排查。无论你是刚入门的后端开发者、负责系统运维的工程师还是已经在做性能优化但没系统看过 perf 的人这篇文章都能给你一条清晰完整的路线。1. perf 到底是什么能解决什么问题1.1 一个 profiling 工具背后是整个内核子系统表面上看perf 是一套命令行工具集包含perf stat、perf record、perf report、perf top、perf annotate等子命令但真正有意思的是它背后不是简单的“读 /proc 文件”而是一套完整的内核级性能分析框架叫perf_event子系统。这个子系统从内核 2.6.31 版本开始合入一直维护至今是 Linux 内核官方提供的性能事件基础设施。我印象很深的一点是perf 和 gprof 这类旧式剖析器有本质区别。gprof 靠编译期插入统计代码你得重新编译程序而且统计粒度受到函数级别的限制perf 则完全不同它默认不会修改被分析程序而是通过内核暴露的接口直接向 CPU 硬件申请事件计数或采样。也就是说你拿到一台正在运行的生产机器不用重新编译、不用改任何代码就可以直接分析任意进程的 CPU 行为。这个“零侵入”的特征是 perf 在线上问题排查中被广泛采用的核心原因。从架构上看perf_event 子系统承担了三类事情管理事件源、维护事件计数器、处理采样数据并将结果传给用户态。事件源来自两个方向一个是 CPU 自带的性能监控单元PMUPerformance Monitoring Unit另一个是内核自身的软件事件和 tracepoint。工具层只是把内核能力包装成易用的命令行接口数据流本质上是“硬件/内核 - 内核缓冲区 - 用户态分析程序”。1.2 什么时候该用 perf典型场景与判断标准我自己的经验是没必要遇到性能问题就上 perf它解决的核心问题是“CPU 时间花在哪里”。更具体地说适合用 perf 的场景包括CPU 占用率居高不下但不知道是哪个函数在消耗。应用的延迟明显劣化怀疑是缓存命中率下降、分支预测失败等硬件行为影响。需要对比优化前后的指令数、缓存 miss 数、上下文切换次数等指标。新代码上线后出现性能回退要快速定位热点函数。多线程程序出现锁竞争、spinlock 自旋通过采样可以看到自旋等待调用栈。如果你遇到的是明显的 I/O 等待、网络延迟、数据库慢查询这种偏外部的瓶颈perf 能提供部分侧面信息但不是最直接的定位工具。判断标准可以简化成一句话性能瓶颈在 CPU 执行路径上就用 perf瓶颈在等待外部资源优先看延迟分布和队列指标。2. 基本原理数据到底是从哪儿来的2.1 硬件 PMU 与内核 perf_event 子系统的配合现代 CPU 内部有一套专门用于性能监控的硬件单元也就是 PMU。它内部有若干组计数器每个计数器可以配置为监控特定事件比如“CPU 周期数”“指令退休数”“L1 缓存 miss 数”“分支预测失败数”等。这些计数器是物理寄存器计数过程完全由硬件完成零软件开销也不会干扰程序执行。pmu 计数器的数量是有限的通常每个核心只有几个到十几个通用计数器外加一些固定用途的计数器。这意味着你想同时监控大量事件是不可能做到的perf 采用的策略是“时间复用”把多个事件轮流放进有限的计数器里分时采样最后按比例换算成完整时间内的估值。这也就是为什么一次性监控几十个事件时某些事件的数据精度会下降。内核的 perf_event 子系统正好把这些硬件能力封装成一个个可打开的“事件文件”。用户态调用perf_event_open系统调用传入事件类型和过滤器参数内核返回一个文件描述符之后对这个文件描述符进行read、ioctl、mmap操作就能读取计数或采样数据。整个链路大致是用户态配置事件 - 内核绑定 PMU 计数器 - 硬件开始统计 - 中断或定时通知 - 数据写入内核缓冲区 - 用户态读取。这里有个关键点值得展开硬件采样是怎么触发中断的以“每隔 N 个周期采样一次”为例perf 会把 PMU 计数器配置成“倒计数模式”先预设一个负值 N然后每个 CPU 周期计数器自动加 1当计数到 0 时PMU 发出一个中断内核的中断处理程序就把当前的指令指针IP、进程标识、调用栈等信息记录下来。中断完成后perf 重新装载计数初值继续下一轮采样。这个机制保证了采样的统计学有效性每个周期点被捕获到的概率基本均等。2.2 软件事件、tracepoint 与内核探针除了硬件 PMU 事件perf 还支持软件事件和内核 tracepoint。软件事件不依赖专门的硬件计数器比如context-switches、cpu-clock、task-clock、page-faults都是内核在运行过程中通过代码主动累加的计数。它们适合分析调度行为、缺页异常这类系统层面的活动。tracepoint 是内核预先埋好的静态探针点分布在调度、块设备、内存管理、系统调用等关键路径上。使用perf list可以看到大量tracepoint类型的事件比如sched:sched_switch表示进程切换事件syscalls:sys_enter_read表示进入 read 系统调用事件。这类事件的价值在于它们能精确捕捉系统中每一次指定行为的发生配合过滤器可以用来分析特定场景比如“哪个进程频繁触发磁盘 I/O”。perf 还支持动态插桩也就是通过 kprobes/uprobes 机制动态地在内核函数或用户态函数入口处插入探针。这个功能在排查比较复杂的业务场景时非常有用比如你怀疑某个特定函数调用频率过高但又没有现成 tracepoint就可以用perf probe动态添加。我实际用下来perf probe最大的坑是符号地址解析和权限问题而且一旦内核版本升级原本可用的探针定义可能失效所以它更适合短期的深度排查不适合长期部署。2.3 采样模式与计数模式两种核心工作方式的差别理解 perf 的一把关键钥匙是区分两种工作模式。第一种是计数模式对应perf stat。它的逻辑很简单按你指定的事件去统计“总量”然后除以运行时间得到速率。比如运行一个程序统计它一共执行了多少条指令、发生了多少次缓存 miss、经历了多少次上下文切换。计数模式开销极小适合对整体瓶颈做快速判断但它不告诉你“这些事件发生在代码的哪些位置”。第二种是采样模式对应perf record。它的逻辑是周期性触发采样把当前正在执行的函数地址、调用栈、进程号等保存下来。由于采样点是离散的大量采样点累积之后统计上就能近似反映程序的时间分布某函数出现在采样点里的比例越高说明它消耗的 CPU 时间份额越大。采样模式的核心价值在于“定位热点”但它能采样到的函数集合、采样点数量、干扰程度都会影响最终分析的准确性。打个比方计数模式像看电表只知道你家一个月用了多少度电采样模式像每隔几分钟拍一张房间照片照片多到一定程度你就能看出大多数时候人待在哪个房间。perf record 产生的数据量由采样频率和运行时长共同决定所以实战中要合理选择采样周期不能盲目追求高频。3. 实战基础从 stat 到 record 的一整套用法3.1 perf stat先看整体数据再谈优化我遇到性能问题时的第一步永远是perf stat而不是急着 record。因为先看整体数据可以帮你建立全局判断是 CPU 跑满了还是根本没用满是用户态耗时多还是内核态耗时多有没有大量的上下文切换最基础的用法是perf stat ./your_program输出里含几个非常关键的指标task-clock表示程序实际占用的 CPU 时间context-switches表示进程切换次数cpu-migrations表示进程在不同 CPU 核心间迁移的次数page-faults表示缺页次数cycles和instructions则直接反映 CPU 执行情况。其中instructions per cycle也就是 IPC是我个人判断代码效率的重要指标。IPC 越高说明 CPU 流水线利用得越充分。如果 IPC 低而 cycles 高说明 CPU 在等待什么事情发生比如缓存未命中、分支预测失败、内存访问延迟等。这时候就可以用缓存事件进一步判断。perf stat还可以叠加多个事件用-e指定perf stat -e cycles,instructions,cache-misses,cache-references,context-switches ./your_program有个小经验要对比优化前后的效果最好固定同样的机器、同样的 CPU 频率策略、同样的输入数据并且多做几次取中位数避免噪音干扰。某次我在某模拟项目 X 上做优化验证第一轮压测结果看起来提升了 30%第二轮却只提升了 8%后来发现是 CPU 频率在 turbo 模式上下波动造成的。遇到这种情况可以先绑定 CPU 核心或者用taskset固定进程到某个核心减少调度带来的变量。3.2 perf record采样记录等待后续分析perf stat只能给你总量无法定位热点函数所以第二步通常是采样perf record -F 99 -g -p pid -- sleep 10这条命令的含义是以每秒 99 次的频率采样指定进程同时记录调用栈-g持续 10 秒。-F 99这个频率选择有两个讲究。第一频率不宜过高否则采样中断本身会显著影响被测进程尤其是高吞吐服务外部干扰过大会让结果失真。第二99 这个数字避开了一些常见的整数同步周期也兼容 100Hz 和 1000Hz 内核时钟中断的节拍对齐问题可以减少采样点与程序运行节奏产生同频共振的可能性。这是 kernel 社区和性能工程师之间长期实践形成的经验值直接照用就行。-record 默认把数据写入当前目录的perf.data文件。这个文件包含采样点、调用栈、进程映射、符号表等信息。如果程序是动态编译的或者运行时间很长perf.data可能很大。建议跑完立刻检查文件大小避免磁盘占用过高。还有一个常被忽略的点采样时尽量在业务请求量相对平稳的时间窗口进行。我见过有人在压测刚开始的阶段去 record结果采样到的几乎都是初始化逻辑完全不能代表稳态行为。正确做法是等服务运行平稳后再开始采样比如先预热几秒再执行 record。3.3 采样周期怎么定频率、时间与精度的权衡采样周期是 perf record 最重要的参数它直接决定数据量、精度和干扰程度。常见的选择有两种固定频率和固定周期。固定频率模式用-F指定比如每秒采样 99 次。这种模式的好处是采样点数量与时间成正比方便估算热点占总时间的比例。缺点是进程运行越快两次采样间隔内执行的指令数越多采样点“投中”的热点分布更偏向长耗时函数。固定周期模式用-c指定比如每发生 10000 次 cycles 就采样一次。这种模式保证每个采样点对应相同数量的硬件事件分析结果更能反映“事件”的分布而不仅仅是“时间”的分布。缺点是采样频率会随 CPU 频率变化分析时间比例时要小心。还要考虑采样时长。根据统计学规律热点函数在采样点中出现的次数越多比例越可信。一般来说达到几万个采样点之后热点比例的置信区间才比较窄。perf record结束时也会输出采样点数我通常要求自己分析的数据不低于 3 万个采样点低于这个量级时结论要谨慎。如果你发现写perf.data的速度成了瓶颈可以适当降低采样频率或者缩小采样范围用--tid只采某个线程而不是整个进程用-C只采某个 CPU 核心。这两种过滤方式在排查单线程抖动、NUMA 调度问题时特别有效。4. 采样数据怎么分析report、annotate 与热点定位4.1 perf report 读法从顶到下的热点挖掘采样结束后用perf report查看结果perf report --stdio交互界面和文本输出都展示了同一个核心信息每个函数或指令占采样点总数的百分比以及对应的调用栈。我自己的读法通常是三步第一步看整体列表里排在最前面的几个函数。它们就是所谓的“热点”。如果前三个函数的开销相加超过 60%优化焦点就非常明确了没必要再往下深挖。第二步选一个热点函数展开它的调用链。看它是由哪些上游函数调进来的哪些路径占大头。这里要注意perf 默认的--children模式会把子函数消耗的 CPU 时间累计到父函数上这个模式适合回答“程序启动后哪些顶层路径消耗最大”而--no-children模式只看函数自身直接消耗的 CPU 时间适合回答“哪个函数本身执行最耗时”。两种视角要配合使用不能只看其中一个。第三步是结合代码逻辑理解数据。比如一个函数自身开销很高但你打开源码发现它只是做了几次简单加法这时候要怀疑是不是采样点显示的函数名不对、内联函数没有展开、或者是编译器优化把代码挪到了奇怪的位置。perf 的数据反映的是二进制执行层面的行为和源码行号不一定严格对应。有个常见误区是把采样百分比直接等同于“函数执行时间占比”。严格来说采样百分比只是采样点落在该地址区间的比例近似等于“该地址在 CPU 上处于可执行状态的时间比”。对于计算密集任务这个近似很准但如果函数大部分时间在等待锁、睡眠、阻塞 I/O采样点可能根本不会落在它身上它在 report 中的占比反而很低。所以用 perf 分析锁竞争时要看的是采样到的自旋等待指令而不是被阻塞的代码。4.2 perf annotate把热点落到具体指令perf report告诉你哪个函数热perf annotate告诉你这个函数里哪条指令热perf annotate --stdio --symbol function_name输出会显示函数的汇编代码并标注每条指令占该函数内采样点的比例。这个能力对应到原理上是因为采样中断发生时会精确记录指令指针IPperf 通过符号表与调试信息把 IP 翻译成对应的函数和指令偏移。指令级定位的价值在于很多性能问题只有到指令这一层才现出原形。比如你看到一个函数很热但打开 C 源码发现无非就是几行循环。用perf annotate一看发现热点集中在一条mov指令上说明问题很可能是缓存未命中热点集中在pause或cmp自旋循环上说明是锁等待。这些信息在函数层面是完全看不出来的。注意一点perf annotate依赖二进制的符号表和调试信息。如果是编译时没加-g的程序你只能看到地址看不到函数名如果是 JIT 类程序比如某些解释器、运行时动态生成代码默认情况下的汇编解释很有限需要加载 JIT 的符号映射才能发挥作用。4.3 调用链分析的几个细节坑调用链是perf record -g最核心的产物但也是最容易出问题的部分。我踩过几次坑这里记录几个关键点。第一个坑是栈帧指针缺失。现代编译器默认开启优化后不再维护标准的帧指针寄存器perf 就无法通过帧指针回溯调用栈。解决方法是编译时加-fno-omit-frame-pointer或者依赖内核的 dwarf 栈回溯机制。前者对性能有一点影响后者对内核的栈解析能力要求更高。线上环境没有帧指针的时候我一般用perf record --call-graph dwarf但要注意它采集到的栈深度更深、数据量更大。第二个坑是采样点发生在进程切换时。如果采样中断到来时进程刚好在内核态运行记录到的 IP 是内核地址而用户态调用栈可能根本没被完整保存。这会让热点被错误归因到内核函数。第三个坑是符号偏移和重定位。共享库的动态加载会让函数地址随加载基址变化perf 在收集数据时会记录进程的内存映射报告阶段再根据映射关系解析符号。如果你把perf.data拿到另一台机器上分析但那台机器没有对应的符号文件解析结果就会变成一片地址。所以归档性能数据时要连同二进制、符号文件一起保存不然过几天你自己也看不懂那份数据。5. 火焰图实战把 perf 数据变成可视化视图5.1 火焰图的原理与读法perf report 的文本界面在函数数量不多时很好用一旦调用链错综复杂读起来就很累。这时候我习惯把数据转成火焰图。火焰图本质上只是把 perf 的调用链采样点画成一张矩形堆叠图但它让“热点在哪条路径上”变成了一眼就能看懂的信息。火焰图有几个读图要点。横轴不是时间轴而是采样点数量占比某个函数对应的横条越宽说明它在采样点中出现的次数越多也就是 CPU 时间开销越大。纵轴是调用栈深度下层函数调用上层函数最顶端才是真正执行代码的函数。颜色本身没有强弱含义只是为了区分不同函数。最常见的火焰图有两种on-CPU 火焰图展示 CPU 上执行的热点适合定位计算型瓶颈off-CPU 火焰图展示进程睡眠、阻塞、等待时的栈适合定位 I/O 和锁等待瓶颈。生成 off-CPU 火焰图需要采集调度事件命令路径略有不同但读图逻辑是一样的。最开始接触火焰图时我总把横轴当成时间轴导致看到一个函数横条很宽就认为它执行了很久。实际上它只是“出现在采样点中的概率”很高。一个函数频繁被调用但每次执行很短和一个函数不怎么被调用但每次执行很长在火焰图上可能都表现为同样的宽度。要想区分这两种情况需要结合函数自身开销、调用次数等信息综合判断。5.2 从 record 数据生成一张标准火焰图生成火焰图的工具有多种我自己最常用的是开源脚本集FlameGraph。流程分三步。第一步用 perf 采样得到带调用链的数据perf record -F 99 -g -p pid -- sleep 30第二步把perf.data折叠成火焰图脚本需要的文本格式。这一步实际上是把每一条调用链展开成一行并用空格分隔层级末尾加上采样次数perf script out.perf stackcollapse-perf.pl out.perf out.folded第三步生成 SVG 矢量图flamegraph.pl out.folded flamegraph.svg生成的 SVG 可以在浏览器里交互查看支持点击放大、搜索函数、查看调用关系。我团队内部排查问题时经常把 SVG 直接放到共享文档里大家看着同一张图讨论比各自看文本输出效率高很多。如果采样对象是 Java 或 Go 这类运行时语言火焰图的符号解析稍微复杂一些。Java 需要开启-XX:PreserveFramePointer或使用 JIT 符号映射Go 需要确保二进制包含足够的 pclntab 数据。我处理这类问题时的建议是先看语言运行时自带的官方 profiling 工具再用 perf 做交叉验证。6. 常见问题与排查技巧实录6.1 权限不足、符号缺失与采样噪音实际使用 perf 的过程中会碰到不少和环境、权限相关的坑我把最常见的整理成一张速查表现象常见原因解决方法perf_event_open权限不足容器或内核配置限制了 perf_event 的访问检查sysctl kernel.perf_event_paranoid多数环境需要小于等于 1容器内可能需要 privileged 模式采样结果全是地址没有函数名二进制没有符号表或符号文件被 strip编译时加-g部署时保留符号文件分析时指定--symfs指向符号目录内核态符号显示为[unknown]需要 root 权限加载 kallsyms用 root 运行或确认/proc/kallsyms可读调用栈只有一层编译器优化消除了帧指针重新编译加-fno-omit-frame-pointer或改用 dwarf 回溯火焰图里看到大量锁相关函数确实存在锁竞争结合 off-CPU 火焰图和锁事件进一步确认关于perf_event_paranoid多说一句。这个内核参数的值直接影响普通用户能否使用 perf值为 2 时用户态只能采样自身进程的部分事件且硬件采样受限制值为 1 时普通用户可以采样自身进程但不能采样内核地址值为 -1 时无限制。很多开发机默认值偏保守导致第一次跑perf record就报权限错误。这类环境问题通常不影响原理理解但会让你在最开始就卡壳所以我建议先确认一下这个参数再往下排查。采样噪音是个更隐蔽的问题。perf 本身会占用 CPU 来响应中断和处理数据采样频率越高、perf.data写入越频繁对被测程序的干扰越大。在数据量特别大的情况下磁盘 I/O 反而可能成为新的瓶颈误导你做出“程序在频繁写盘”的错误结论。我在压测环境里验证过-F 999的采样会把被测服务的吞吐拉低好几个百分点。所以线上分析采用 99Hz 或更低频率优先保证系统行为不被扭曲。6.2 三个实战排查案例用三个虚构但典型的案例说明这些技巧如何串联起来。第一个案例某 Web 服务 CPU 突然升高。我先跑perf stat -p pid发现context-switches异常高每秒超过两万次。紧接着采样 30 秒生成火焰图顶部出现大量futex_wake和pthread_cond_signal相关栈最终定位到业务代码里一处过度使用条件变量唤醒线程的逻辑。这个案例的启示是perf 不仅能看计算热点也能通过采样栈判断同步原语是否是瓶颈。第二个案例某图像处理 Demo 在特定数据集上延迟暴涨。perf record和perf annotate显示热点集中在一处memcpy上进一步用缓存事件统计发现cache-misses占比很高。原因是处理时按行拷贝一张未对齐的巨大图像每次访问都导致 cache line 频繁替换。改成按块拷贝并做对齐后延迟降了将近一半。这个案例说明指令级分析的力量光看memcpy函数甚至不知道问题出在哪只有到prefetcht0、movups这些具体指令语境里才看清真相。第三个案例某后台批处理任务比预期慢。perf record显示用户态函数占比很低大部分采样点落在内核的entry_SYSCALL_64上同时page-faults数量高到离谱。进一步发现程序每处理一行数据就调用一次mmap来分配小块内存频繁的系统调用导致陷入内核的开销远大于业务计算本身。这个案例的价值在于提醒你perf 的采样结果会明明白白告诉你时间消耗在哪一层别被业务代码的复杂度迷惑系统调用、缺页这类“外围开销”也占据大量 CPU。这几个案例里有一个共性思路在铺开分析之前先用perf stat判断瓶颈的宏观类型再决定用采样、调用链、指令级分析哪条路径深入。这套流程我用了很多年比直接上来就perf record要高效得多。最后分享一个小技巧分析完热点之后别忘了把采样数据和符号文件一块保存起来。性能问题往往有周期性这次优化完了下个版本可能又会冒出来。留一份历史数据后面对比回归也能省不少功夫。perf 是个功能很重的工具但它的核心原理并不复杂说到底就是“让硬件计数器替我们记录软件执行过程中的统计样本”。掌握了这个思想很多花哨的命令只是不同侧面的展示罢了。

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

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

免费获取报价 →
↑