ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux性能分析利器perf:从CPU飙高到火焰图,运维排查必会

Linux性能分析利器perf:从CPU飙高到火焰图,运维排查必会 干运维这行CPU 突然飙到 100%、接口延迟无故翻倍、线上进程卡住不动……这些场景迟早会遇到。大部分人会先用 top、vmstat、pidstat 一通乱敲看哪个进程在吃资源但顶多看到“某个线程 CPU 占用 80%”再往深问一句“它内部到底在哪一行烧掉的 CPU”传统命令就答不上来了。这时候就需要 perf。perf 是 Linux 内核自带的性能分析工具它在内核里有一套完整的计数和采样机制能同时在用户态和内核态抓取热点函数、调用链、锁竞争和调度情况。对运维工程师来说它应该列入排查线上性能问题的标配技能也是区分运维能力高低的一道分水岭。这篇文章不打算讲太虚的东西而是按一个运维平时真实的工作流来组织从“为什么需要 perf”入手然后是安装和权限设置、三大常用命令、一个完整的 CPU 飙高排查案例再到火焰图、进阶读数和常见问题。不管你是负责传统服务器运维还是做云平台、容器平台甚至只是偶尔处理业务机器只要服务跑在 Linux 上perf 都能帮你回答一个问题时间到底都花在哪儿了。1. 为什么运维工程师必须掌握 perf1.1 先搞清楚 perf 到底是个什么东西很多人第一次听说 perf以为是某个第三方监控 agent其实它是 Linux 内核自带的性能剖析工具集官方名字叫 Performance analysis tools for Linux源代码就在内核树里的tools/perf目录。它不是一个孤立的命令而是基于内核perf_event子系统的一整套工具。内核向用户态暴露了大量“性能事件”大概可以分成几类硬件事件CPU cycles、instructions、cache-misses、branches、软件事件context-switches、cpu-clock、page-faults、还有内核 tracepoint 事件调度、块设备、系统调用等。perf 的核心工作方式有两种一种是计数counting统计某段时间内事件总共发生了多少次另一种是采样sampling每隔固定时间“抽一帧”记录当前程序执行到的指令地址和调用栈。打个比方统计城市交通拥堵一种办法是每条路出口装计数器数总量另一种是在路口每隔一段时间拍一张照片抽样的照片里大部分车堵在哪一段哪里就是热点。perf 就是靠“拍照”的比例来推算函数消耗的 CPU 占比。因为这是内核原生的机制所以它能看到用户态、也能看到内核态比如网卡中断、磁盘驱动、协议栈的 CPU 开销这些用用户态工具基本看不见。1.2 它和 top、strace、gprof 这些老伙计的区别我经常在群里看到有人问top 都能看到 CPU 了还要 perf 干嘛这里面的认知差异很大。top 只能告诉你“这个进程占了多少 CPU”但你问不出“它内部哪个函数在烧 CPU”。strace 能跟踪系统调用但它只能看系统调用用户态里密集的字符串操作、内存拷贝、逻辑判断一概看不到而且 strace 的 ptrace 开销大线上长跑根本扛不住。gprof 需要重新编译并插桩相当于给每个函数入口放一个计数器对在线业务基本不可行。工具能给出什么主要限制top / pidstat进程或线程级的 CPU、内存占用停在进程层进不到函数内部vmstat / sar系统全局指标只能看趋势无法定位业务代码strace系统调用及耗时开销大看不到用户态 CPU 热点gprof函数级调用耗时需要插桩重新编译线上难落地perf函数级热点、调用链、指令级、内核态需要权限和符号表支持一句话总结top 管“是哪个进程”perf 管“是这个进程里的哪行代码”。运维排查故障时第一步用 top 定位进程没错但第二步入坑点往往就是停在这里开始猜。perf 的价值就是让你从“猜”变成“看”。1.3 它最擅长解决的四类场景第一类是 CPU 使用率突增。业务没变化CPU 却顶到 100%这类问题用 perf 采样一下就能看出热点是正则回溯、JSON 解析还是内存拷贝。第二类是版本升级后的性能回退明明代码逻辑看起来差不多接口就是慢一倍。用 perf 分别对老版本和新版本采样对比调用栈宽度就能找到差异。第三类是锁竞争和调度延迟处理高并发时线程都在等锁CPU 看起来不高但吞吐上不去perf 有专门的 lock 和 sched 子命令。第四类是缓存不友好的代码采样结果里 cache-misses 极高说明内存访问模式有问题。另外说个现实的理由运维面试里“线上 CPU 飙高你怎么排查”几乎是必考题。答 top、答 uptime 都只是基本功能把perf top和perf record -g说清楚再用火焰图讲出调用链这个答案的含金量会高一个级别。2. 装上 perf 并配好权限别在第一关就卡住2.1 不同发行版的安装方式perf 最大的优点之一是它跟着内核走一般不需要装内核模块只要把用户态工具装上就行常见的坑是发行版的包名不对。Debian/Ubuntu 上安装命令是这样的sudo apt-get update sudo apt-get install -y linux-tools-common linux-tools-generic linux-tools-$(uname -r)注意最后那个带内核版本号的包它才是真正匹配当前内核的工具。装完之后如果执行perf还是提示没找到可以去/usr/lib/linux-tools/目录下看有哪些版本然后直接调用带版本号的perf或者做一个软链接指向匹配的那一个。RHEL/CentOS 系更简单sudo yum install -y perf如果是最小化安装的基础环境可能还需要补一下elfutils-libelf-devel、binutils-devel这些依赖否则编译某些事件时会报缺少头文件。还有一个更硬核的方式就是从内核源码编译。去 kernel.org 拉一份和当前内核版本接近的源码进到tools/perf目录执行make再执行make install。好处是能用上最新特性坏处是要装一堆编译依赖而且内核升级后要重新编译。对于运维日常工作我建议直接用发行版打包好的别在这里浪费时间。2.2 kernel.perf_event_paranoid 权限问题到底是什么逻辑装完 perf第一次跑perf top大概率会看到一行报错You may not have permission to collect stats。这不是工具坏了而是内核参数kernel.perf_event_paranoid在限制普通用户访问性能计数器。这个参数的逻辑可以理解成一个安全开关数值越高限制越严格。以目前常见内核为例-1表示完全放开0允许对 CPU 级别的计数访问但不允许内核态采样相关的部分1和2逐步收紧用户态有些新发行版默认值是3甚至4导致普通用户一跑 perf 就报权限错误。排查环境或生产环境如果想用 perf一般这样处理sudo sysctl -w kernel.perf_event_paranoid1 echo kernel.perf_event_paranoid1 | sudo tee /etc/sysctl.d/99-perf.conf改完之后重新跑perf top通常就能正常采样了。生产环境改这个参数之前要和安全团队确认一下因为过于宽松的 perf 权限确实能让同机用户嗅探到一些指令级别信息。但实践里设置成1已经能满足大多数分析和故障排查需求不用一味追求-1。还有另一个参数叫kernel.kptr_restrict它控制内核符号地址能否被普通用户看到。如果这个值太高perf 采样到内核态时会显示一堆地址而不是函数名。调试时需要设成0但同样要考虑安全策略不是所有机器都适合敞开。改法是sudo sysctl -w kernel.kptr_restrict0同样可以持久化到 sysctl 配置文件里。2.3 符号表perf 能看懂你的程序才有分析价值权限搞定了还会遇到一个更尴尬的问题perf 采样了一大堆数据打开报告一看全是十六进制地址没有函数名。这通常不是 perf 的问题而是程序本身没有保留符号信息。Linux 的 ELF 文件里函数名等调试信息默认是包含的但很多发布流程里为了减少体积会执行strip把 symbol 表删掉。这对运行没有任何影响却把 perf 的眼睛遮住了。所以自己负责编译的服务建议在编译时加-g参数保留调试符号压测或者发布机上不要轻易 strip 核心服务二进制。生产上如果已经跑着 strip 过的二进制那就找同版本带 symbols 的二进制文件用perf report支持指定符号文件的方式去间接分析。内核的符号看到与否取决于内核有没有开CONFIG_KALLSYMS绝大多数发行版默认是开的所以内核态符号一般没问题。如果你的采样结果里大量显示[unknown]先别急着分析把符号问题解决了才有意义。3. 三条最常用的命令覆盖 80% 的日常排查3.1 perf top像 top 一样看热点先锁定“谁在烧 CPU”perf top是最直观的入口它相当于给 top 做了一个“透视”实时显示当前采样周期里CPU 都耗在哪些函数上。基本用法perf top -p 12345 -F 99-p指定进程号-F 99表示每秒采样 99 次这是 Brendan Gregg 在很多场合推荐的采样频率具体原因后面讲。不加-p就是看整个系统的热点第一次跑会看到一堆内核函数和驱动函数不要慌这很正常。界面字段不多Overhead是采样占比Command是进程名Shared Object是符号所在的库或二进制Symbol是函数名。如果某一行的占比明显偏高直接按t或者d切换排序方式就能细化到线程级。交互按键里常驻的几个是h查看帮助、P切换事件类型、t在线程和进程视图间切换、1展开多个 CPU 核的统计。实际用的时候建议先让它跑几秒再按z暂停免得界面刷新太快看不清。生产环境高负载机器上跑perf top会有一定性能开销但对定位问题来说这点开销能接受只要别长时间挂着看热闹就行。3.2 perf stat不猜了量化指标直接摆出来perf top适合实时看但要量化一个任务的各项指标就得用perf stat。它可以对某个命令、某个进程运行一段时间做事件统计输出简洁的汇总表。常见用法perf stat -p 12345 -- sleep 10输出大概是这个样子Performance counter stats for process id 12345: 1234.56 msec task-clock # 0.789 CPUs utilized 32 context-switches # 0.026 K/sec 0 cpu-migrations # 0.000 K/sec 123 page-faults # 0.100 K/sec 1,234,567 cycles # 1.000 GHz 2,345,678 instructions # 1.90 insn per cycle 12,345 cache-references # 10.000 M/sec 4,567 cache-misses # 37.000 % of all cache refs第一行task-clock是任务实际占用 CPU 的时间context-switches是上下文切换次数如果进程一直不主动 sleep切换却很多说明它可能在被频繁打断或者和别人抢 CPUcpu-migrations是线程在 CPU 核心之间迁移的次数迁移多了会破坏 cache 亲和性。后面那几行最关键cycles是 CPU 周期数instructions是执行指令数两者的比值insn per cycle通常叫 IPC。IPC 是一个非常实用的指标。IPC 小于 1一般意味着指令在等数据、等锁、等内存计算单元闲着IPC 大于 2 甚至 3说明这是计算密集型代码CPU 的运算是真正瓶颈。看cache-misses / cache-references的比例能判断缓存压力比例太高说明数据访问不连续或者多核共享数据导致缓存频繁失效。还可以自定义事件比如perf stat -e cache-misses,cache-references,L1-dcache-loads -p 12345 -- sleep 10把关注点精确到你想看的事件上。多花点时间熟悉perf list列出来的事件后面排查会很有底气。3.3 perf record perf report抓现场、看调用链perf top只能告诉你热点函数是谁但没法回答“这个函数是被谁调进来的”。要回答这个问题就得用perf record把采样数据落盘再用perf report离线分析。perf record -p 12345 -F 99 -g -- sleep 30命令执行完会在当前目录生成一个perf.data文件。-g表示记录调用栈这是最关键的一个参数没有它你就只能看到一个个孤立的函数样本看不到调用关系。-- sleep 30表示采样 30 秒后自动停止比自己手动 Ctrl-C 更标准化。分析数据perf report进入交互界面后最上面是热点函数列表每个函数有Self和Children两列。Self 是指这个函数自身直接消耗的 CPUChildren 是它的子函数调用算在一起后的占比。按下回车或者键可以展开调用链从顶层一路看到底层。需要明确指出一个常见误解只看 Self 高的函数没错但真正要优化的是整条调用链。比如malloc的 Self 很高但它是被业务代码调进来的优化方向可能是减少内存分配而不是去改 malloc。perf report --stdio可以把结果输出成文本适合贴到工单或者沉淀到文档里。4. 实战给一个 CPU 飙高的服务做完整的性能画像4.1 准备一个“问题进程”纸上谈兵没意思我写了一个简单的 C 程序来模拟一个日志解析服务它不停地拼接日志行、切分字符串、统计空格是很典型的 CPU 密集型代码。你可以把它当线上问题进程来练手。#include stdio.h #include string.h #include unistd.h static int count_spaces(const char *s) { int c 0; while (*s) { if (*s ) c; s; } return c; } static void parse_line(char *line) { char *saveptr NULL; char *token strtok_r(line, , saveptr); while (token) { (void)count_spaces(token); token strtok_r(NULL, , saveptr); } } int main(void) { char line[1024]; while (1) { for (int i 0; i 5000; i) { snprintf(line, sizeof(line), 2025-01-01 10:00:00 INFO request_id%d path/api/order status200, i); parse_line(line); } usleep(1000); } return 0; }编译的时候别忘记加符号gcc -g -O2 test_worker.c -o test_worker ./test_worker 跑起来后先用top看一下确认这个进程确实吃掉了大量 CPU。我没用更复杂的 Nginx 或 Java 服务做例子是因为符号解析一旦受运行时影响新手会额外踩很多坑。用这个纯 C 程序能干干净净地把排查方法论练透。4.2 用 pidstat 和 top 先定位到具体进程性能排查的第一条铁律是先定位进程。比如执行pidstat -p $(pidof test_worker) 1 5输出里能看到这个进程的 CPU 占用率基本稳定在高位。这时候你已经知道问题进程是谁了接下来就是 perf 上场。注意一个细节pidof返回的可能有多个 PID你可以用pgrep -f test_worker精确匹配或者直接看top里最大的那个 PID。4.3 用 perf top 实时观察热点函数拿到 PID 后执行perf top -p $(pidof test_worker) -F 99大概跑五秒你会看到热点函数集中在strtok、count_spaces、snprintf这些符号上。这个结果已经说明一个方向CPU 不是消耗在业务逻辑本身上而是消耗在字符串处理和大量分配上。但perf top到这里就差不多到头了它没法直接告诉我们“谁调用了 strtok”。这时候就该perf record出场。4.4 用 perf record -g 抓取调用栈并剖析对目标进程采样 20 秒perf record -p $(pidof test_worker) -F 99 -g -- sleep 20采样完成后看报告perf report在热点函数列表里我用展开strtok的调用链能看到它是由parse_line调进来的再展开parse_line父函数是main里的 for 循环。整个路径一目了然程序在疯狂地切分字符串而每次切分又调用了count_spaces去遍历 token属于典型的重复性字符串扫描。这就是我前面强调的单独看到strtok高没用看到parse_line - count_spaces这条链你才知道优化点是整个解析逻辑而不是盲改底层库函数。如果只想快速出文本结论可以执行perf report --stdio --sortcomm,dso,sym | head -40这会把占比最高的函数和调用栈用纯文本打出来便于贴到工单或者写复盘报告。4.5 输出火焰图把调用链变成可视化的“地图”文本报告虽然能看但调用链一深看起来确实吃力。所以我会把perf record的结果顺手转成火焰图。先克隆 Brendan Gregg 的开源脚本git clone https://github.com/brendangregg/FlameGraph.git然后分三步转换perf script out.perf ./FlameGraph/stackcollapse-perf.pl out.perf out.folded ./FlameGraph/flamegraph.pl out.folded flame.svg用浏览器打开这个 SVG 文件你会看到一个类似“火山”的图。怎么读它X 轴是采样占比不是时间线某个函数显示得越宽代表它的 CPU 占比越高Y 轴是调用栈深度越靠上是越底层的被调函数每个方块的颜色是随机的和性能无关别根据颜色判断热度。回到这个案例火焰图顶部一定会有一个很宽的大平顶对应的就是parse_line或者strtok。看到这种形状基本可以断定瓶颈就在字符串解析和逐字节遍历。优化方向也很明确减少strtok的重复调用、用memchr定位空格、合并掉没有必要的count_spaces遍历。这些优化做不做是开发的事但你已经把问题精确地交到开发手里而不是丢一句“CPU 高快看看”就完事。5. 进阶perf 里的数字到底该怎么读5.1 IPC 和 cache miss 背后的真实含义很多运维把perf stat跑一遍数据量了一堆却不知道下一步该看什么。这里我给一个最基本的判断框架。指令数instructions代表程序实际干了多少活周期数cycles代表用了多少时钟两者相除得到的 IPC 代表每个时钟周期执行了多少条指令。同样的工作量IPC 低说明等待多。最常见的等待原因是内存访问。CPU 每秒可以去内存读很多数据但内存延迟比 L1 Cache 高一个数量级一旦 cache miss流水线就得空转。这时候反映到指标上就是 IPC 跌到 1 以下同时cache-misses占比很高。另一种常见的等待是锁线程在锁上打转的时候 CPU 没闲着但指令大多在自旋IPC 也不会好看。要验证是不是 cache 的问题可以执行perf stat -e cache-misses,cache-references。如果 miss 比例常年超过 20%内存访问模式大概率有问题。但注意一点云主机和虚拟化环境下硬件 PMU 不一定完整透传有时候 cycles 事件读出来是 0那就只能退而求其次用软件事件和指令数来分析。5.2 perf annotate 怎么看单条指令有时候函数级热点还不够比如两个函数占比接近但一个函数体特别长一个是短函数被调用几百万次这时候需要看到指令级分布。perf annotate就是干这个的perf annotate -p $(pidof test_worker) --stdio它会反汇编热点函数并在每条汇编指令前标注采样占比。比如你会在某个mov指令前面看到很高的百分比说明内存加载或存储是热点在循环分支指令前看到很高的百分比说明循环体本身太“重”。对于绝大多数运维场景看指令级是为了确认“热点到底在循环的哪个操作上”不用真的去逐行理解汇编找到那个占比最高的指令再看对应 C 源码位置就够了。5.3 锁竞争和调度也能用 perf 看吗可以但属于进阶范围我给你留个路标。高并发服务里经常遇到“CPU 不高但应用卡死”的情况这通常是锁竞争、上下文切换过多或者 run queue 排队。这时候用perf lock record采集一段时间再用perf lock report看锁等待排名。想看线程调度延迟可以perf sched record之后再perf sched latency。需要注意这两个子命令对权限要求更高且生产环境采集会带来一定负载要先和业务方沟通好。先掌握好普通采样分析再慢慢延伸到这里顺序不会错。6. 常见问题与排查技巧实录6.1 权限不足报错处理跑perf top报You may not have permission to collect stats多半是kernel.perf_event_paranoid卡住了。临时放开的命令是sudo sysctl -w kernel.perf_event_paranoid1持久化就写进/etc/sysctl.d/99-perf.conf。如果这台机器还设置了kernel.kptr_restrict2内核符号会显示为全 0调试时可以临时改成 0但生产环境要先评估风险。6.2 符号显示成十六进制地址程序被 strip 过、或者没有安装 debuginfo 包的时候报告里全是裸地址。一种是让开发提供带 symbols 的版本另一种是找到同版本包安装 debuginfo。Ubuntu 下可以执行apt-get install -y 包名-dbgsymCentOS 下安装kernel-debuginfo。装完一般符号就能解析。Java 程序更特殊perf 默认看不到 JIT 出来的方法需要配合 JVM 的-XX:PreserveFramePointer和 perf-map-agent 这类方案下一篇再展开说。6.3 -g 抓下来的栈只有一帧怎么办-g的默认栈回溯方式在不同发行版上可能不一样。老版本默认用 frame pointer也就是基于帧指针的栈回溯但如果编译时优化级别较高没有保留帧指针perf 就抓不到完整调用链。解决办法是改用 dwarf 方式perf record -p PID -F 99 --call-graph dwarf -- sleep 20dwarf 方式更准代价是采样开销更大。还有一个配合手段就是让开发在编译时显式加上-fno-omit-frame-pointer这样用 frame pointer 也能抓到完整栈。6.4 采样频率为什么要选 99Hz这是 Brendan Gregg 反复强调过的细节。选 99 而不是 100是为了避免采样频率和任务的周期性行为产生共振。如果程序刚好每 10ms 做一次 sleep、100ms 触发一次清理采样频率选 100Hz每次采样都落在同一阶段结果会严重失真。99Hz、49Hz 这种带一点“错位”的频率能更均匀地覆盖程序的不同执行阶段。30 秒采样大概能拿到 2970 个样本对一个函数做统计性分析完全够了。6.5 常见问题速查表现象常见原因处理办法perf 报 permission deniedperf_event_paranoid 限制sysctl 设置为 1内核态符号全是 0kptr_restrict 过高安全允许时临时设为 0报告里全是[unknown]程序 strip 或缺少 debuginfo加 -g 编译/安装 debuginfo调用链只有一帧高优化级别失去帧指针用--call-graph dwarf或加编译选项容器里看不到进程PID namespace 隔离在宿主机采集或使用 nsenter 进入容器视角cycles 事件读出来是空虚拟化 PMU 未透传改用软件事件、tracepoint 分析6.6 我个人踩过的坑最后分享两个实际踩过的坑。第一个是采样时间太短。刚开始我用perf record -p PID -g -- sleep 3结果热点函数不稳定同一问题不同时段采出来排序都不同。现在线上采样我至少跑 30 秒如果问题有周期性的特征会跑满一个完整业务周期再停止。第二个是忘记确认内核态和用户态的比例。有一次perf report里全是我这边的内核函数研究半天才发现是网卡驱动的中断把 CPU 吃满了和业务代码一点关系没有。可以先花十几秒看perf top的整体分布分清是用户态还是内核态占主导再决定下一步往哪走。按照这套流程遇到 CPU 相关的问题基本不会再陷入“盲猜重启”的局面。我还是那句话perf 的瓶颈从来不在工具难不难而在你有没有形成“用数据定位、而不是凭感觉猜”的思维习惯。把权限和符号表这两个前置问题解决掉你会越用越觉得它顺手慢慢就会发现线上性能排查这件事居然可以做到这么通透。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进