第一次用火焰图的时候我犯过一个错:抓了 30 秒,图看起来很“瘦”,几乎没有热点,于是断定程序没问题。实际上那个程序大部分时间在等锁,根本不在 CPU 上跑。
火焰图只画 on-CPU 的部分。线程睡着的时候不会被采到,所以“图很干净”和“程序很快”是两件毫不相干的事。
先做一次基本采样
1 | perf record -F 99 -g -p <pid> -- sleep 30 |
-F 99 是频率,-g 记调用栈,-p 指定进程。频率越高细节越多,开销也越大,先挑一个能接受的值。
输出里如果大量出现地址而不是函数名,说明符号缺失。常见原因是二进制被 strip 过、用了优化构建但没带调试信息,或者采的是 JIT 代码。先把符号补上,不然后面所有结论都不靠谱。
生成火焰图
1 | perf script > out.perf |
读图有两个坑。宽度是整个栈出现的比例,不是单个函数的独占时间,得沿着调用链看有多少是被上层带进来的。另外它只覆盖被采到的时间,线程大多在睡眠时图会很瘦,但这不代表程序不慢。
什么时候该换 off-CPU
一个实用信号:实际耗时远大于火焰图覆盖的时间,而且线程在可中断睡眠里。
1 | pidstat -p <pid> 1 5 |
线程大量时间处于 S 或 D,那热点函数多半不是原因,等待才是。这时候该去看阻塞点,比如用内核追踪点统计调度延迟和唤醒来源,而不是继续加采样频率。
off-CPU 的数据来源和 on-CPU 不一样,通常要内核追踪设施配合,采的是“等待事件”而不是“执行时刻”。把它和 on-CPU 火焰图画在同一张图里比较,得到的结论基本是误导的。
采样只是证据的一半
采样给的是统计分布,不是因果。两个函数在图上并排出现,只说明它们被采到的次数多,不说明后者是被前者拖慢的。
要把结论变成可验证的假设,通常还得做实验:固定可疑参数,只改一个变量,重新采样对比。两次采样之间记录负载、线程数、输入和构建版本,不然差异可能来自环境变化。
参考资料
证据边界:本文不含任何具体程序的性能数字。火焰图的形状和热点取决于输入负载、构建选项和运行环境;没有符号或者没有对照实验时,采样结果撑不起“某函数是瓶颈”这种结论。