perf 实战:怎么聚焦热点函数? perf 实战怎么聚焦热点函数实验环境Ubuntu 24.04 / 内核 6.8.0-106-generic / Cgroup v2 / 华为云 FlexusX 8C16G镜像加速已配置本文所有命令输出均来自真实实验机可直接复现。一、引子线上 CPU 飙到 100%但你看不见里面在干嘛一个 Go/Java 服务 CPU 打满你top看到进程占 800% CPU却说不清到底是哪个函数、哪段逻辑在烧 CPU。加日志重启上 pprof在 C/C 原生服务、或者不想改代码也不想重启的现场最顺手的武器是perf——Linux 内核自带的采样型性能分析器。perf 的核心思想一句话用极低的 overhead周期性地看当前 CPU 正在执行哪条指令把大量样本聚合成一张谁最忙的排行榜。本文带你把 perf 的stat/record/report跑通并重点解决一个容器场景难题宿主机上怎么 profiling 一个容器里的进程符号还能解析得出来。二、准备靶子一个 CPU 密集型程序为了有稳定热点我在宿主机编译了一个纯计算的busy程序递归fib(20)编译成静态二进制带-g调试符号// busy.clonglongfib(intn){returnn2?n:fib(n-1)fib(n-2);}longlongwork(){longlongs0;for(inti0;i300000;i)sfib(20);returns;}intmain(){printf(result%lld\n,work());return0;}rootecs-a8bb-0002:~# gcc -O0 -g -static busy.c -o /root/busy-O0是为了让fib保持为独立函数好观察-g是为了能解析符号名。三、perf stat先看清负载长什么样perf stat不采样而是用PMUPerformance Monitoring UnitCPU 硬件计数器精确统计一段时期的硬件事件。它对判断负载类型特别有用。3.1 CPU 密集型高 IPC极低上下文切换rootecs-a8bb-0002:~# perf stat -d /root/busyPerformance counter statsfor/root/busy:11,220.07 msec task-clock# 1.000 CPUs utilized40context-switches# 3.565 /sec0cpu-migrations29page-faults32,530,466,004 cycles# 2.899 GHz1,722,444,232 stalled-cycles-frontend95,851,405,222 instructions# 2.95 insn per cycle IPC19,171,030,134 branches66,937,450 branch-misses# 0.35% of all branches27,903,244,400 L1-dcache-loads3,099,970 L1-dcache-load-misses# 0.01% of all L1-dcache accesses11.221611813secondstimeelapsed解读几个关键列IPCinstructions per cycle 2.95每时钟周期执行近 3 条指令非常高说明是典型的计算密集型、流水线喂得满。IPC 1才说明卡在访存/停顿。context-switches 408 核机器上跑 11 秒只有 40 次上下文切换说明它几乎一直在同一个 CPU 上算没被调度打断——典型的 CPU-bound。branch-misses 0.35%分支预测很准递归里全是直接调用 predictability 高。3.2 I/O 密集型低 IPC海量上下文切换同样用perf stat看一个 I/O 压力负载做对照rootecs-a8bb-0002:~# perf stat -e context-switches,cpu-migrations,instructions,cycles,task-clock \stress-ng--io4--timeout6519,640context-switches# 41.925 K/sec 爆炸多896cpu-migrations10,252,313,861 instructions# 0.57 insn per cycle IPC 暴跌18,091,490,923 cycles12,394.42 msec task-clock一眼对比指标busyCPU 密集stress-ng --ioI/O 密集IPC2.950.57context-switches40519,640排查结论遇到CPU 高但 IPC 低、上下文切换多大概率不是算得多而是频繁等 I/O / 锁 / 唤醒——该去看 syscall、锁竞争、磁盘而不是去优化算法。四、perf record report定位热点函数perf stat只给宏观指标要精确到函数用perf record采样 perf report聚合。4.1 采样rootecs-a8bb-0002:~# perf record -g -F 99 -o /root/perf-busy.data /root/busyresult2029500000[perf record: Captured and wrote0.217MB /root/perf-busy.data(1068samples)]-F 99每秒采样 99 次避开 100 的倍数防止与某些周期性任务共振。-g同时记录调用栈call graph这样能看到谁调用了热点。采样 11 秒得到 1068 个样本开销极小。4.2 聚合报告rootecs-a8bb-0002:~# perf report --stdio -i /root/perf-busy.data# Samples: 1K of event cycles:P# Event count (approx.): 39053415166# Children Self Command Shared Object Symbol# ........ ........ ....... ............. .......100.00%0.00% busy busy[.]_start|---_start __libc_start_main_impl|--99.85%--__libc_start_call_main main work fib fib fib|--99.66%--fib|--98.81%--fib|--97.40%--fib|--93.07%--fib|--77.15%--fib|--59.35%--fib --38.05%--fib结论一目了然fib函数占据了 99.85% 的采样Children 列即包含它的调用链。调用链是main → work → fib → fib → ...递归把栈打得很深。Self列fib自身也接近 100%说明热点就在这个函数体内部——典型的计算量爆炸型热点。这就是 on-CPU 分析的标准闭环采样 → 按函数/栈聚合 → 顺着 Children 最高的链往下钻找到 Self 占比最高的叶子函数。4.3 perf top交互/批量perf top能实时看热点但它是交互式 TUI在 SSH/批处理里得用-bbatch模式。本实验机 PMU 不支持 branch stack直接perf top -b会报错Error: cycles:PH: PMU Hardware or event type doesnt support branch stack sampling.因此在无法交互的远端直接用perf recordperf report --stdio更稳。这也是生产排障的推荐姿势采一段样本落盘再离线分析不依赖 TTY。五、原理perf 为什么能看穿函数perf 的采样来源有两类PMU 硬件事件如cycles、cache-missesCPU 内部的性能计数器由硬件在每个时钟/事件触发时计数精度高、开销极低。软件/跟踪事件如context-switches、page-faults、syscalls:sys_enter_*内核埋点由内核在事件发生时记录。on-CPU分析用cycles事件perf 通过perf_event_open系统调用让内核按固定频率如 99Hz给当前正在跑的 CPU 发一个中断记录当时程序计数器 RIP / 指令指针和调用栈。把海量样本按符号用/proc/kallsyms、二进制里的符号表、DWARF 栈展开映射回函数名就得到热点排行榜。关键点采样 ≠ 精确计数它是统计推断。样本越多越准极短函数可能被低估采样偏差。overhead 低99Hz 意味着每核每秒仅 ~100 次中断对业务影响通常 1%。只反映 on-CPUperf 默认看不到在睡觉/等锁的时间那是 off-CPU需要别的方法如offcputime/eBPF。六、难点在宿主机上 profiling 容器进程 符号解析这是容器排障最常卡住的一步。容器里的进程在宿主机上就是普通的 host PID但默认看不到容器内的符号/二进制路径而且 cgroup v2 下 perf 的 cgroup 过滤有坑。6.1 按 PID 过滤最直接容器在宿主机上有真实 PID。先拿容器 init 进程的 host PIDrootecs-a8bb-0002:~# docker run -d -v /root/busy:/busy --name perf-ct alpine sh -c while true; do /busy; donerootecs-a8bb-0002:~# PID$(docker inspect perf-ct --format {{.State.Pid}})rootecs-a8bb-0002:~# echo 容器 init 主机 PID$PID容器 init 主机PID21350在宿主机上对它及其子进程采样rootecs-a8bb-0002:~# perf record -g -F 99 -p $PID -o /root/perf-ct.data sleep 6rootecs-a8bb-0002:~# perf report --stdio -i /root/perf-ct.data# Samples: 285 of event cycles:P# Children Self Command Shared Object Symbol# ........ ........ ....... ............... ..............99.44%0.00% busy busy[.]_start|---_start __libc_start_main_impl|--98.66%--__libc_start_call_main|main|work|fib|fib||--97.94%--fib||--96.86%--fib|--95.78%--fib关键发现在宿主机上按 PID 采样居然能直接看到容器内busy进程的fib热点99.44%这是因为容器的进程本质就是宿主机进程perf 在 host 视角采样再用宿主机的文件系统去解析符号——只要宿主机能读到那个二进制我们-v挂了/root/busy且二进制是静态的符号自包含解析就 OK。6.2 符号解析失败的坑与对策如果你的容器镜像是scratch/精简镜像、或宿主机没有对应二进制/调试符号perf report里会看到一堆[unknown]或只有地址。对策挂载容器根文件系统到宿主机docker cp c把/拷出来或docker run --privileged -v /:/host把整个宿主挂进去再用perf report --symfs /host指定符号根。--privileged或在宿主机直接跑 perf容器里通常没权限用 perf需要CAP_SYS_ADMIN/CAP_PERFMON所以排障实践是在宿主机侧 perf而不是进容器里 perf。保留调试符号出问题的二进制最好带-g且别 strip生产镜像若 strip 了可保留一份带符号的版本用于解析。-k /path/to/vmlinux要解析内核符号时需要对应内核的vmlinux本机/usr/lib/debug/boot/vmlinux-$(uname -r)或linux-image-*-dbgsym。6.3 按 cgroup 过滤v2 的坑理论上可以用perf record -G cgroup只采集某个容器的事件不必先找 PID。但本实验机的 perf6.8.12在 cgroup v2 下对-G的支持有限直接-G cgroup会报参数错误cgroup 监控需配合 system-wide 且与 cgroup v2 的接口对接要求较苛刻。因此实战建议容器场景优先用宿主机按 PID 过滤稳定且符号解析天然正确。cgroup 过滤更适合我想一次性看某个 cgroup 下所有进程、且不关心具体 PID的离线分析场景。七、排查思路总结perf 怎么用到排障里先看宏观perf stat跑几秒看 IPC 和 context-switches——判断是 CPU-bound 还是 I/O/锁-bound。再定位函数perf record -g -F 99 -p pid -- sleep 10采一段perf report --stdio顺着 Children 最高的链钻到 Self 最高的叶子函数。容器里在宿主机按容器 init 的 host PID 采样符号解析靠宿主能访问到带符号的二进制--symfs/--privileged挂载。内核态热点加-e cycles看[k]内核函数要更深就上 ftrace/eBPF见本系列后两篇。off-CPU 不在 perf 默认视野线程在睡、等锁、等网络时 perf 采不到——这类要 eBPF 的offcputime。八、小结与思考题小结perf 用 PMU/软件事件做低开销采样通过stat看 IPC 与上下文切换判断负载类型通过recordreport定位 on-CPU 热点函数。容器排障应在宿主机按 host PID采样并解决符号解析带符号二进制 --symfs/--privileged挂载。perf top -b在本机因 PMU 不支持 branch stack 失败生产更推荐 recordreport 离线分析。思考题为什么perf stat里IPC比CPU 利用率更能说明负载性质CPU 100% 但 IPC 0.3 可能是什么问题perf record -F 99的采样频率越高越准为什么不用 9999Hz过高会带来什么副作用一个线程 80% 时间在futex_waitperf record能抓到它等锁的调用栈吗为什么提示on-CPU vs off-CPU容器里perf报Permission denied除了--privileged还有哪些最小权限组合CAP_PERFMON/CAP_SYS_ADMIN下一篇《ftrace 实战怎么查看长延时内核函数》——当热点进入内核态perf 不够细时用 ftrace 看调用链与耗时。