ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux perf性能分析实战:从事件子系统到火焰图排查CPU问题

Linux perf性能分析实战:从事件子系统到火焰图排查CPU问题 开头先说个真实的排查经历。去年冬天线上有个Java服务 CPU 飙到 95%top 里看进程 PID 是 27891但无论是看 jstack 还是看日志都找不到明显异常。后来我就是在生产服务器上敲了一条perf top -p 27891三秒钟之后问题就现形了——热点不是 Java 业务线程而是 JIT 编译线程在反复优化同一个热循环配合 GC 线程的锁竞争整个进程的调度被打得一团糟。这就是 Linux perf 子系统的价值它不只给你看 CPU 占用率而是直接告诉你 CPU 时间到底花在了哪里、程序在用户态和内核态各跑了多久、哪些内核函数把你拖慢了几百毫秒。如果你对这台服务器的理解只停留在top、ps、free这几个命令那遇到性能问题基本就是在赌命而 perf 是我个人认为 Linux 平台优先级最高的一个性能分析子系统——它不是单一工具而是从内核事件子系统、PMU 硬件计数器、采样框架到用户态命令工具的完整体系。这篇文章我不打算讲那些书上的概念而是从“我用它排查真实问题”的角度把 Linux perf 子系统的核心逻辑、常用命令、权限配置和实操中的坑一次讲清楚。不管你是搞后端、搞嵌入式、搞数据库运维还是写 C/C/Rust/Go这套东西早晚用得上。1. 先搞清楚perf 到底是个“子系统”1.1 它不只是一条命令而是一整套内核事件框架很多人把perf理解成“Linux 的一个性能排查工具”这个说法是对的但只说对了一半。准确说perf是内核里一个名叫performance events subsystem性能事件子系统的对外接口加上一组用户态工具的集合。内核这边通过perf_event_open()这个系统调用向用户态暴露事件采集能力用户态这边则有perf、perf stat、perf record等一系列工具来和内核交互。你看到的perf top、perf report只是这个子系统最外层的壳。它的核心抽象是“事件event”。你可以把事件理解为 CPU 和内核在运行过程中对外发出的“探针信号”所有性能数据都来自这些信号的统计与采样。事件大致分四类硬件事件Hardware Events由 CPU 内部的 PMUPerformance Monitoring Unit硬件计数器直接统计比如cpu-cycles、instructions、cache-misses、branch-misses。这类事件精度高、开销小是所有性能分析的地基。软件事件Software Events内核软件层面的计数比如context-switches、cpu-clock、task-clock、page-faults不需要硬件支持任何机器都能跑。硬件 TracepointHardware TracepointCPU 的硬件特性触发的事件比如 Intel 的 PEBSPrecise Event-Based Sampling能在事件触发点给出精确的指令地址。软件 TracepointSoftware Tracepoint内核里预先埋好的静态探针比如sched:sched_switch、kmem:kmalloc、syscalls:sys_enter_openat可以追踪到内核的某个具体路径。这四类事件对应着不同粒度的观测能力。硬件事件告诉你“CPU 干了多少活”软件事件告诉你“操作系统在忙什么”tracepoint 告诉你“某个内核函数是否被调用、调用了几次”。你日常排查时就是在这几类事件之间切换。1.2 动手前先看事件清单在你开始“调参”之前最好先看一眼这台机器上到底支持哪些事件。命令就是perf list会输出一长串事件列表按分类分组。我在 Ubuntu 22.04 的云服务器上跑看到的硬件事件包括cycles、instructions、cache-references、cache-misses、branches、branch-misses等软件事件包括cpu-clock、task-clock、context-switches、page-faults、cpu-migrationstracepoint 那部分就多了几百上千个。一个新手常见的误区拿到perf list就蒙了不知道该看哪个事件。我的建议是——先事件后命令。比如你现在怀疑程序里某个算法是热点那就用perf stat看instructions和cycles计算 IPC每周期指令数怀疑内存访问有问题就盯cache-misses和cache-references怀疑是 CPU 调度问题就把context-switches和cpu-migrations摆在一起看。事件选对了后面的分析才不白做。还有一个细节perf list的某个事件名在不同架构上会略有差异比如 Intel 上的cache-misses和 AMD 上的cache-misses底层 PMU 计数器映射不完全一样。跨架构做对比时不要直接拿数值硬比先确认事件语义。2. 日常用得最多的三个命令先吃透2.1 perf stat用一组计数器量化进程行为如果你只想快速评估一个进程的性能特征perf stat是最省事的入口。用法很简单perf stat ./my_program perf stat -p 1234 sleep 5 perf stat -a sleep 5第一个是运行程序并统计第二个是附加到已运行的 PID 上统计 5 秒第三个是整个系统统计 5 秒。输出大致是Performance counter stats for ./my_program: 3,542.88 msec task-clock # 1.000 CPUs utilized 512 context-switches # 0.145 K/sec 24 cpu-migrations # 0.007 K/sec 24,685 page-faults # 0.007 M/sec 9,123,456,789 cycles # 2.575 GHz 4,567,890,123 instructions # 0.50 insn per cycle 234,567,890 branches # 66.180 M/sec 12,345,678 branch-misses # 5.27% of all branches我会优先看三个数字IPCinsn per cycle、cache-misses 比例和context-switches 数量。IPC 是 CPU 流水线利用率的核心指标。IPC 接近 1 甚至更高说明指令流基本没有被停顿拖累如果 IPC 在 0.5 以下大概率是大量 cache miss、分支预测失败或数据依赖停顿在压着你。我记得有一次排查一个 OpenSSL 加密程序的性能perf stat显示的 IPC 只有 0.2后来发现是指令级并行度不足代码里大量串行依赖换成支持 AES-NI 的库后 IPC 立刻跳到 1.6。再举一个对比场景。同一个程序分别用-O0和-O2编译perf stat跑完能看到-O0版本 instructions 数量比-O2多 3 倍以上但 cycles 差距更大导致 IPC 反而更低。很多人以为高 IPC 就一定快其实 IPC 高说明“每个周期干了更多事”但如果代码本身做了大量无用功效率依然很差。这也解释了为什么说perf stat适合量化分析不适合定位热点——定位热点得靠下一节讲的采样命令。2.2 perf top跟 htop 一样直观的实时热点榜perf top像是一个“加了内核视角的 htop”每秒钟刷新一次按事件采样比例从高到低排列。用法perf top perf top -p 1234 perf top -e cache-misses perf top -K # 只看内核符号 perf top -U # 只看用户态符号进入交互界面后我用得最多的按键按键作用e展开/折叠当前函数下的调用链展开后能看到热点是从哪一层调上来的E展开所有符号对应的函数链d过滤掉当前符号看其余热点q退出perf top是我定位“CPU 正在忙什么”的第一反应工具。有一次排查一个 Redis 实例的 CPU 异常perf top -p pid直接显示sock_read_iter占了 61% 的样本另一个tcp_recvmsg占了 20%一瞬间就明白问题在网络收包路径而非 Redis 本身的键操作。需要注意perf top是采样模式它只告诉你“在采样时刻CPU 正在执行什么函数”不是精确到每条指令的真相。采样频率越高越接近真相但开销也越大。线上环境我一般用-F 99每秒 99 次而不是默认值兼顾精度和开销——具体为什么是 99 而不是 100后面我会专门解释。2.3 perf record / report从热点到完整调用链perf top能告诉你当前热点在哪但大多数时候你还得知道“这个热点是被谁一层层调用上来的”。这时候就用perf record采样再用perf report展示perf record -F 99 -g -p 1234 -- sleep 30 perf report --stdio-F 99是采样频率每秒 99 次-g是生成调用链call graph-p附加指定进程sleep 30是采集 30 秒后自动停止。如果不想等手动 Ctrl-C 也可以perf 会把已经采到的样本落盘。perf report --stdio的输出里有两列很容易混淆Overhead和Children。Overhead 是本符号自身被执行在栈顶的样本比例Children 是它及其子函数在调用链上的样本合计。比如你在main的 Children 里看到 99%说明所有热点都来自main的调用树这很正常但如果free的 Overhead 很高说明很多时间花在释放内存上那就要看free是谁调的。我在看这种报告时的习惯是第一眼先扫 Overhead 最高的前三名符号第二眼看 Children 最高的调用栈入口第三眼看有没有异常的“跨层热点”——比如某个业务函数自己只占 2%但它下面挂着的copy_page_to_iter占 20%说明瓶颈在数据复制路径上。文本模式的输出虽然一目了然但在函数链很多很深的情况下还是不够直观。这也是我后面要单独讲火焰图的原因——perf record采到的原始数据永远能复用到火焰图上。3. 事件子系统的几个关键细节理解了才算入门3.1 采样频率不是越高越好99Hz 是有讲究的很多人第一次设采样频率时都纠结到底用多少我常看到有人直接-F 1000觉得“采得越多越准”。这个思路在半分钟内是对的长时间跑就会出问题。采样频率过高意味着每秒钟有大量信号打断进程执行内核需要频繁记录 IP、调用栈、时间戳这本身就把被测程序拖慢了甚至导致采样丢失——perf record跑完你会看到Error: Perf record sample too frequently或者输出里有大量“overrun”计数。所以线上环境我个人强烈不建议超过 99Hz 太多如果是短时间精确分析200Hz 到 500Hz 也够用再高基本属于自虐。那为什么偏偏是 99Hz 而不是 100Hz这个细节很多人没讲透。如果你用 100Hz也就是每 10ms 采样一次而这个程序里恰好有一个每 10ms 周期性运行的定时器或循环那么采样点和程序状态会同相位共振——你采到的永远是同一个状态热点分布严重失真。99 和大多数程序的循环周期不是整数倍关系采到的样本更接近“随机均匀抽样”统计意义更靠谱。这个道理在信号处理里叫“避免采样与信号周期同步”在 perf 场景里同样适用。实测过很多次同样的程序用 99Hz 和 100Hz 采前者能看到更平滑的热点分布。还有一个容易踩的建perf record默认的采样率是不指定的它会采用内核一个默认值。如果你需要精确对比两次试验建议每次显式写-F 99否则不同版本、不同机器上的默认值可能不一样数据不可比。3.2 权限、安全与内核参数刚开始用 perf 的新手十有八九会遇到下面这个报错perf_event_open(..., PERF_TYPE_HARDWARE, 0) failed: 不允许的操作这多半是 Linux 内核的安全策略——kernel.perf_event_paranoid这个 sysctl 参数挡了你。不同发行版的默认值不一样Ubuntu 桌面版默认是 3只允许用户跟踪自己的进程很多服务器版本默认是 1 或 20 则几乎放开所有限制除了内核函数追踪。可以用下面命令查看sysctl kernel.perf_event_paranoid如果你是在自己的环境调优临时开权限最直接的办法sudo sysctl -w kernel.perf_event_paranoid-1这个 -1 表示完全无限制能访问所有事件、所有进程、内核符号。生产环境我不建议随便开到 -1更稳妥的做法是保持在 1 或 0然后在需要时用sudo perf来执行分析。还有两个参数值得留意kernel.perf_event_mlock_kbperf 事件缓冲区每进程可锁定的内存上限太小会导致采样数据大量丢失。kernel.perf_cpu_time_max_percentperf 采样最多可占用的 CPU 时间百分比默认是 25%对性能敏感的生产环境建议调低到 5 以下。还有一点很容易被忽略容器里跑 perf 会遇到双重限制。即使你的宿主机权限开了容器默认的 seccomp 或 AppArmor 策略也可能拒绝perf_event_open。我在 Docker 容器里做性能分析时通常选择在宿主机上用-p 容器进程在宿主机的 PID来附加而不是进容器里跑 perf。这个操作姿势比瞎调容器权限省心得多。3.3 硬件 PMU 和虚拟化的坑perf 的硬件事件之所以好用是因为芯片里有专门的 PMU 计数器。但一旦进入虚拟机事情就变了。绝大多数云服务器KVM、Xen 等的虚拟 CPU 并不向 Guest 暴露完整 PMUperf stat里的硬件事件可能直接返回 0或者采样结果全是乱的。我在某云厂商的轻量服务器上就测过perf stat输出中cycles和instructions都是 0当时一度以为 perf 装坏了。后来确认是该虚拟化平台没有透传 PMU唯一的解决办法是换裸金属实例或者退而求其次用软件事件来分析——比如用task-clock、context-switches这种不依赖硬件的事件曲线救国。如果你用的是 Intel CPU 且开启了超线程/频率调节还有两个细节PEBS 精确事件像branch-misses这类事件在支持 PEBS 的 CPU 上可以做到精确定位到具体指令地址perf record -e branch-misses -b能拿到分支出错的位置。如果是老 CPU 没有 PEBS采样到的“地址”可能只是事件发生时的粗略位置分析时别太较真。offcore 事件Intel 的 offcore 响应涉及 L3 之外的系统内存访问这类事件通常需要管理员权限在用户态默认采样不到。看到一堆 0 不要慌先sudo perf stat试试。这些都是“perf 不是万能的”的地方但知道了原理就不会被异常数据带偏。4. 从数据到结论把 perf 数据变成火焰图4.1 为什么要火焰图文本模式的perf report在调用链浅的时候挺好用一旦调用树超过 20 层、函数几百个你就像在翻一本没有目录的字典。火焰图的价值在于把采样数据可视化成一张带层级的“热力图”一眼就能找出热点。火焰图的阅读规则并不复杂x 轴不是时间而是“采到的样本在该函数上所占的比例”所以越宽的条越热y 轴是调用深度一个条叠在另一个条上表示调用关系。顶层的宽条说明某个函数自身消耗了 CPU下方的叠加条则是它的调用路径。我自己的经验是看火焰图不要从下往上看而是从最宽的顶层条往下追。比如顶层很宽的是do_syscall_64那说明热点在内核系统调用路径上如果顶层很宽的是一个业务函数那就沿着它的父调用往上找入口在哪。同时也要留意那种“顶端很窄、中段很宽”的形状——这说明调用方本身没干什么事但它调用了一个很耗资源的底层函数瓶颈其实在下层。4.2 生成火焰图的三条命令假设你已经用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这三个脚本来自 Brendan Gregg 的 FlameGraph 仓库GitHub 搜 FlameGraph 就能拿到。先perf script把perf.data展开成文本采样流每一条采样包含进程名、PID、IP、符号和调用栈stackcollapse-perf.pl把文本折叠成“每个调用栈出现多少次”的聚合行flamegraph.pl把聚合行渲染成 SVG 图用浏览器打开即可也可以直接用-o flame.svg导出。这块有一个很容易踩的坑perf script输出每一行都带进程名而stackcollapse-perf.pl在解析带进程名的格式时如果不先过滤掉进程名列就会报错或产出错误的折叠结果。我的办法是先grep看几行确认格式然后perf script -F pid,tid,time,event,ip,sym,dso | ./stackcollapse-perf.pl手动指定字段把进程名排除在外就能稳定通过折叠。还有一点分析采集数据最好就在采集机上进行或者把相关机器的符号表一并带上换个环境跑perf script如果没有对应的 debuginfo符号就会显示成地址。火焰图不是只能看 CPU。perf record -e page-faults -g可以生成“缺页火焰图”perf record -e block:block_rq_complete -g可以生成 I/O 事件火焰图方法都是同一套。思路打开这个工具能解决的不止是 CPU 问题。5. 常见问题与排查技巧实录5.1 符号解析不出来全是地址现象perf report或火焰图中出现大量[unknown]或者函数名显示成十六进制地址。原因基本有三个目标程序是动态编译或优化的符号表被 strip 掉了。解决办法是重新用-g编译或者安装对应的 debuginfo 包。内核符号解析被安全策略限制了。检查kernel.kptr_restrict如果它的值是 1 或者 2非特权用户就看不到内核符号地址需要sudo perf或把它设为 0这个不建议生产环境随意改动。perf 的符号缓存过期了。删除~/.debug下的缓存后重试。一个很实用的兜底手段是addr2line把地址翻译成源文件行列addr2line -e ./my_program -f 0x401234如果采样数据显示的是用户态地址而程序是之前编译的旧版本addr2line解析出来的位置会和当前代码对不上。所以生产环境我强烈建议分析之前先记录二进制文件的 md5分析完之后对照确认版本一致否则所有定位都是白费。5.2 采样数据显示调用链缺失现象perf record -g采出来的火焰图只有一层或两层没有完整的调用路径。最经典的原因是帧指针缺失。现代编译器的-O2优化会省略rbp/x86平台上的帧基址寄存器导致基于帧指针的回溯方式只能“爬”到一半就断了。对应解法有几个重新用-fno-omit-frame-pointer编译程序或者改用 DWARF 调试信息做回溯perf record --call-graph dwarf。这个方式精度更好但开销更大样本记录的数据量会成倍增长采样时间一长perf.data 可能几个 GB。我一般先用fp模式短时间采一下看效果如果断层严重再切dwarf。还有一个小细节在 Java、Go 这类运行时环境里调用链的“栈”不是简单的用户态函数栈中间还可能穿插 GC、JIT 编译器等运行时函数。如果你想看到业务代码的火焰图Java 推荐配合async-profilerGo 则直接go tool pprof会方便很多。perf 在动态语言上不是不能做而是成本太高别硬刚。5.3 perf stat 显示某些计数器一直为 0现象cache-misses、branch-misses这类事件统计出来是 0明明程序有明显的内存缺页和分支行为。这种情况大概率不是“真 0”而是虚拟化环境没有透传 PMU前面说过了。平台架构不支持该事件到特定计数器的固定映射。比如某些 ARM 平台cache-misses的语义和 x86 完全不同可能没有对应硬件实现perf 只能报 0。需要管理员权限的事件Intel offcore在普通用户模式下没有权限也会计成 0。解决办法先用sudo perf stat试试如果还是 0用perf list查一下该事件是否在“支持”列表里如果支持但依然为 0就改用 raw event 直接指定 PMU 计数器编号比如perf stat -e r0104这种原始编码。这个方法确实不友好但能绕过语义映射把硬件的底数抠出来。遇到异常数值我的建议是永远先怀疑“环境”而不是“程序”。perf 作为一个内核接口受权限、虚拟化、架构、编译器等因素影响很大数值表现异常时用另一台裸机或者虚拟化级别不同的机器做个对照往往比埋进数据里死磕更高效。结尾最后再分享一个排查小技巧几乎每次给线上程序做性能分析我都会在采集完之后顺手跑一条perf archive。它能把当前机器上的符号表打包成 tar 文件和perf.data放在一起。这样如果我需要把数据拿回本地分析就不用担心目标机器上符号缺失直接tar -x解包后perf report照样能看到可读的函数名。特别是那种不便把二进制文件直接拷回办公室的场景这个命令比重新编译一个带符号版本省事得多。另外有个习惯采样时间不要太短也不要太长。我通常先perf top -F 99原地观察 5 到 10 秒确认热点稳定后再perf record采集 30 秒到 1 分钟。如果程序有阶段性波动比如周期性任务建议至少跨越一个完整周期必要时分两段采样对比——一次在波峰一次在波谷差异往往就是线索本身。perf 这个子系统第一眼看上去门槛不低但只要你掌握了“选事件、看计数、采样本、画火焰图、查符号”这一套流程绝大多数 CPU 类性能问题都能在三十分钟内给出一个可行动的结论。这个功夫迟早是值得下的。
返回列表