Linux 基础体系 · 第 79/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。
Linux perf 与火焰图:采样、调用栈、符号、CPU 和 Off-CPU
perf 是 Linux 内核性能事件接口的用户态工具。它可以从硬件 PMU、内核 tracepoint、软件计数器和动态探针中采集事件,再通过采样记录“某个时刻正在发生什么”。火焰图则是一种把大量调用栈样本按层级聚合、按宽度可视化的图形。
二者经常一起使用,但需要先区分三个问题:
- CPU 时间花在哪里:线程被 CPU 执行时,采样它的调用栈。
- 线程为什么没有运行:线程处于阻塞、睡眠、锁等待或调度延迟时,记录它从运行态离开的原因和等待时长。
- 采样中的地址如何变成函数名:依赖可执行文件、共享库、内核和调试符号。
CPU 火焰图主要回答第一个问题;Off-CPU 分析主要回答第二个问题。它们使用不同的事件和解释方式,不能把一张 CPU 火焰图当成完整的线程耗时图。
一、从一次采样开始:perf 实际记录了什么
1.1 采样对象不是“函数执行时间”
假设一个线程在时间区间 内运行,perf 以频率 进行采样。理想情况下,采样点近似均匀分布在该线程实际占用 CPU 的时间上。
对函数 ,设它的包含调用栈区域为:只要采样时调用栈中出现 ,就算一次命中。若总样本数为 ,命中 的样本数为 ,则 CPU 时间占比可估计为:
如果线程在这段时间中实际运行了 秒,则:
这里的 是函数 的包含时间,也叫 inclusive time:包括 自己执行的时间以及它调用的子函数执行时间。
而函数自身的排他时间(exclusive time)近似为:
火焰图通常使用“调用栈折叠后的样本数量”表示宽度。因此:
- 一层函数的宽度表示包含它的样本数;
- 最顶部的函数通常代表采样发生时真正运行的叶子位置;
- 父函数比子函数宽,是因为父函数包含子调用的时间。
采样不是逐条指令计时,也不是由 perf 记录每次函数进入和退出。它通过统计估计热点,因此结果会有随机误差和系统性偏差。
1.2 采样频率、采样周期和误差
命令:
perf record -F 99 -p "$PID" -g -- sleep 30
其中:
-F 99请求大约每秒 99 次采样;-p "$PID"监控指定进程的线程;-g记录调用栈;sleep 30是采集持续时间,不是被监控进程;- 结果通常写入当前目录的
perf.data。
如果样本近似独立,某个比例估计的标准误差近似为:
例如真实热点占比 ,采集到 个样本,则:
约为 0.46 个百分点。样本越多,随机误差通常越小。
但实际系统并不满足完全均匀、独立采样:
- 周期性定时器可能和程序周期发生相位锁定;
- 某些短函数在采样间隔内执行完毕,可能完全不被观察到;
- 采样发生在中断、抢占或内核路径中时,调用栈上下文会不同;
- 高负载时 perf 缓冲区可能丢样本;
- 频率模式会受到内核调节,实际采样率不一定严格等于请求值。
因此,99 Hz 不是“精确测量每个函数 1% 时间”的保证,而是一个开销和统计精度之间的取舍。
1.3 频率模式与周期模式
perf record 可以按频率或事件周期采样:
perf record -F 99 -p "$PID" -- sleep 30
表示请求每秒约 99 次,内核会根据调度和事件计数调整周期。
也可以指定硬件事件和周期:
perf record -e cycles -c 1000000 -p "$PID" -- sleep 30
这里每累计约 1,000,000 个 CPU cycle 触发一次采样。cycles 常对应硬件 PMU 的 CPU 周期事件,但具体可用名称和实现取决于处理器与内核。可查看:
perf list
周期模式更直接地按某个事件加权。例如按 instructions 采样,结果更接近“指令退休位置”的分布;按 cache-misses 采样,则是在问“哪些调用栈更容易产生缓存未命中”。
不同事件不是同一个问题:
perf stat -e cycles,instructions,branches,branch-misses,cache-misses \
-- ./app
典型解释:
cycles:消耗的 CPU 周期;instructions:退休的机器指令数;branches:分支指令数;branch-misses:分支预测失败;cache-misses:由 PMU 定义的缓存未命中事件。
硬件事件的精确定义、可计数数量和是否支持精确采样(PEBS、IBS 等)依赖 CPU 型号。不能把不同架构上的同名事件当成完全等价的指标。
二、调用栈:一个样本如何变成一条路径
2.1 调用栈的含义
如果采样时程序处于如下路径:
main
└── serve
└── parse_request
└── malloc
一次样本记录的是这条调用关系,而不是只有 malloc 一个函数。把许多样本按调用路径聚合后,才能知道:
malloc是由哪个业务路径调用的;- 同一个库函数是否被多个上层路径使用;
- 热点属于通用函数本身,还是某个调用者传入了特殊数据;
- 内核时间是由系统调用、网络协议栈还是文件系统触发的。
调用栈通常有两部分:
用户态栈:应用函数 -> 共享库 -> libc
内核态栈:系统调用入口 -> 内核子系统 -> 驱动
一次样本可能只包含用户态栈,也可能同时包含用户态和内核态栈,取决于事件、权限、采集选项和调用栈展开方式。
2.2 三种常见调用栈展开方式
Frame pointer:fp
perf record --call-graph fp -p "$PID" -- sleep 30
这种方式依赖编译器在函数栈帧中保留 frame pointer。典型编译参数是:
-fno-omit-frame-pointer
优点是开销较低、展开较快。缺点是如果程序使用 -fomit-frame-pointer,或者部分函数通过优化破坏了传统栈帧,调用栈可能截断或错误。
现代编译器常在优化构建中省略 frame pointer,因此不能假设所有发行版二进制都可用 fp。
DWARF:dwarf
perf record --call-graph dwarf,16384 -p "$PID" -- sleep 30
DWARF 展开通常会在采样时保存一段用户栈,再使用调试展开信息恢复调用关系。16384 是每次采样保存的用户栈字节数示例,不同程序需要不同大小。
优点:
- 不要求 frame pointer;
- 对复杂编译优化、内联和非传统栈帧通常更可靠。
代价:
- 采样记录更大;
- 采集和解析开销通常更高;
- 栈过深或保存空间太小会出现截断。
Last Branch Record:lbr
部分 x86 处理器支持 LBR:
perf record --call-graph lbr -p "$PID" -- sleep 30
LBR 记录最近的分支跳转历史,适合特定硬件和内核条件下的用户态调用链或控制流分析。它不是所有 CPU 都支持,也不能把它当作跨平台的通用栈展开方法。使用前应检查:
perf list | grep -i branch
一般选择顺序是:
- 已知程序保留 frame pointer,使用
fp; - 不能确认时,使用
dwarf; - 有明确硬件和分析需求时,再考虑
lbr。
2.3 调用栈失败时的表现
调用栈问题常见表现包括:
unknown
[unknown]
或者火焰图中大量栈只剩下:
main
libc.so.6
这可能由以下原因造成:
- 没有调试展开信息;
- frame pointer 被省略;
- 采样时用户栈或内核栈无法访问;
- 进程在采样期间退出;
- 栈深超过保存空间;
- JIT 代码没有向 perf 提供映射;
- 容器内进程的文件系统视图与采集端不一致。
验证调用栈不能只看火焰图,应先检查原始报告:
perf report --stdio
再查看采样记录的元信息:
perf script --header
如果报告中已经是 [unknown],后续再运行火焰图脚本不会自动修复符号问题。
三、符号:从指令地址恢复函数和源码位置
3.1 符号解析的链路
CPU 采样首先得到地址,例如:
0x00007f2a1c123456
要显示为函数名,工具需要知道:
- 这个地址属于哪个进程映射;
- 映射对应哪个 ELF 文件或内核模块;
- 地址相对于该文件的偏移;
- ELF 符号表或 DWARF 信息如何将偏移映射到函数;
- 如果需要源码行,还要有行号调试信息。
因此,“能展开调用栈”和“能显示函数名”是相关但不同的问题:
- 栈展开解决“调用者是谁”;
- 符号解析解决“这个地址对应什么函数”。
一个被 strip 的 ELF 文件可能仍然保留足够的动态符号用于显示部分函数,但通常会缺少完整函数名、内联信息和源码行。
3.2 用户态二进制和共享库
推荐为待分析程序保留独立调试信息:
gcc -O2 -g -fno-omit-frame-pointer demo.c -o demo
其中:
-g生成 DWARF 调试信息;-fno-omit-frame-pointer让fp调用栈更可靠;-O2保留接近生产环境的优化行为。
生产环境通常不会把完整调试信息放进主二进制,而是将其拆分到 debug 包。不同发行版的包名不同,但思路相同:目标机器或分析机器必须能够找到与运行中 ELF 完全匹配的调试文件。
“同名文件”不等于“同一个二进制”。构建 ID、校验和、发行版本和编译参数都可能不同。二进制更新后,如果只保留旧的 perf.data 而删除旧 ELF,历史地址可能无法正确解析。
可以使用:
readelf -n ./demo | grep -A1 'Build ID'
file ./demo
查看构建 ID 和文件基本信息。perf 通常使用 build-id 缓存和 ELF 信息关联采样地址,因此保留采集时对应的构建产物非常重要。
3.3 内核符号、模块和内核地址隐藏
分析内核路径通常需要:
/proc/kallsyms中的内核符号;- 与运行内核匹配的
vmlinux; - 内核模块的符号;
- 必要时的内核调试信息。
发行版可能将 vmlinux 和 debug 信息放在单独的 debug 包中。若启用了内核地址隐藏,普通用户可能只能看到受限信息。
检查:
uname -r
cat /proc/sys/kernel/kptr_restrict
ls -l /proc/kallsyms
内核符号缺失时,报告可能显示:
[unknown]
或者只有十六进制地址。不要通过随意修改生产内核安全参数来“修复”报告。更稳妥的方式是使用经过授权的 debug 包、受控分析节点和最小权限。
3.4 容器和命名空间中的符号
容器中的进程可能使用宿主机采集,但其可执行文件和共享库路径只存在于容器文件系统。此时需要:
- 在采集时保留容器中的 ELF 文件;
- 在分析机上建立相同路径或使用对应的符号查找方式;
- 记录镜像 digest,而不是只记录镜像 tag;
- 对 JIT 程序保留 JIT dump 或运行时生成的符号映射。
容器隔离不会改变 CPU 上执行的指令,但会改变用户态文件路径、PID 视图和权限边界。
四、一个可运行的 CPU 火焰图流程
下面先构造一个包含递归计算和字符串处理的示例程序。
// demo.c
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
__attribute__((noinline))
static uint64_t fib(unsigned n)
{
if (n < 2)
return n;
return fib(n - 1) + fib(n - 2);
}
__attribute__((noinline))
static uint64_t work(void)
{
uint64_t total = 0;
for (unsigned i = 0; i < 1000; i++)
total += fib(24);
return total;
}
int main(void)
{
for (;;) {
volatile uint64_t result = work();
(void)result;
usleep(1000);
}
}
编译:
gcc -O2 -g -fno-omit-frame-pointer demo.c -o demo
./demo &
PID=$!
这里使用 noinline 是为了减少编译器把 fib 和 work 内联掉的可能性;volatile 防止结果被过度优化。不同编译器版本和优化设置仍可能影响最终代码,因此应以 perf report 中的实际符号为准。
采集 CPU 样本:
perf record -F 99 --call-graph fp -p "$PID" -- sleep 20
如果程序或共享库没有可靠 frame pointer,可以改用:
perf record -F 99 --call-graph dwarf,16384 -p "$PID" -- sleep 20
先查看文本报告:
perf report --stdio
可能看到类似结构:
70.00% demo demo [.] fib
28.00% demo demo [.] work
2.00% libc libc [.] __libc_usleep
这里的百分比是样本比例,不是某个函数被精确计时后的结果。fib 宽,说明采样时多数调用栈都处于 fib 或其递归子调用中;work 的包含比例会覆盖 fib 的样本,因此父子比例不能简单相加。
4.1 生成折叠栈和 SVG
常用火焰图工具链由三步构成:
perf.data
│
└── perf script
│
└── stackcollapse-perf.pl
│
└── flamegraph.pl
│
└── flame.svg
取得 FlameGraph 脚本后:
git clone https://github.com/brendangregg/FlameGraph.git
生成折叠栈:
perf script | ./FlameGraph/stackcollapse-perf.pl > cpu.folded
生成 SVG:
./FlameGraph/flamegraph.pl \
--title "CPU profile" \
--countname samples \
cpu.folded > cpu.svg
折叠栈中的一行通常类似:
main;work;fib;fib;fib 1532
分号左侧是从根到叶子的调用路径,右侧是该路径的样本数。火焰图脚本会把公共前缀和后缀聚合成矩形。
查看 SVG:
python3 -m http.server 8000 --directory .
然后在浏览器打开 http://127.0.0.1:8000/cpu.svg。如果在远程服务器上分析,应通过受控的端口转发或下载文件,不要直接暴露调试服务到公网。
停止示例程序:
kill "$PID"
wait "$PID" 2>/dev/null || true
4.2 火焰图的坐标和宽度
传统 CPU 火焰图中:
- 横轴通常不是时间轴;
- 左右位置主要是聚合布局,不表示调用先后;
- 纵轴表示调用深度;
- 矩形宽度表示样本数;
- 鼠标悬停通常可以查看函数名和样本比例。
因此,不能说“图中最右边的函数最后执行”,也不能用横轴距离推断两个函数之间的时间间隔。它是聚合统计图,不是时序跟踪图。
五、CPU 火焰图到底回答什么
5.1 On-CPU 的定义
On-CPU 指线程正在某个逻辑 CPU 上执行指令的时间。对一个用户线程而言,这段时间可能执行:
- 用户态应用代码;
- libc 或其他共享库;
- 系统调用进入的内核代码;
- 内核中的文件系统、网络协议栈或驱动代码;
- 中断或异常相关路径。
如果采样事件是 cycles,火焰图近似表示“CPU 周期在哪里消耗”;如果是 instructions,则更接近“退休指令在哪里出现”。
perf record -p PID 通常按目标进程的线程采样。若目标进程在等待锁、睡眠或等待 I/O,线程不在 CPU 上执行,因此这段等待时间不会自然出现在 CPU 火焰图中。
这解释了一个常见现象:
CPU 火焰图看起来很窄,但请求延迟很高。
程序可能不是 CPU 计算慢,而是在等待数据库、磁盘、网络、锁或调度。
5.2 进程、线程和 CPU 范围
常用目标范围:
# 监控一个进程及其线程
perf record -p "$PID" -g -- sleep 20
# 监控指定线程
perf record -t "$TID" -g -- sleep 20
# 监控指定 CPU
perf record -C 2 -g -- sleep 20
# 系统范围
sudo perf record -a -g -- sleep 20
区别很重要:
-p关注进程的线程;-t关注单个线程;-C关注某些逻辑 CPU 上发生的事件,不限进程;-a关注系统范围,可能包含其他租户、内核线程和中断。
多线程程序中,如果只监控主线程,可能错过真正消耗 CPU 的工作线程。相反,系统范围采集得到的“热点”可能来自其他进程,分析时应检查 perf report 的进程名、线程名和 CPU 信息。
5.3 每个 CPU 的火焰图与聚合火焰图
在多核系统中,同一进程可能被调度到不同逻辑 CPU。系统范围报告通常可以按:
- 进程;
- 线程;
- CPU;
- 用户态/内核态;
- 事件类型;
进行筛选。高性能服务出现单核热点时,聚合所有 CPU 可能掩盖问题,例如:
- 一个线程被绑定到 CPU 2;
- CPU 2 上有软中断或 IRQ 干扰;
- NUMA 远端内存访问只影响部分 CPU;
- cgroup 配额使某些 CPU 上的调度行为不同。
可以先使用:
perf stat -a -e cycles,instructions,context-switches,cpu-migrations \
-- sleep 10
观察全局行为,再用 perf report 或拆分数据分析具体线程和 CPU。
六、硬件事件、PMU 与“热点”的不同含义
6.1 cycles 热点不等于业务复杂度
一个函数在 cycles 火焰图中很宽,可能因为:
- 执行了大量指令;
- 频繁发生缓存未命中;
- 分支预测失败;
- 等待执行端口或内存;
- CPU 降频;
- 被硬件资源争用。
因此,CPU 周期热点通常需要和计数器一起解释:
perf stat -e \
cycles,instructions,branches,branch-misses,cache-references,cache-misses \
-p "$PID" -- sleep 10
常用派生指标:
IPC 低可能表示内存停顿、分支问题、执行端口受限或其他微架构瓶颈,但仅凭 IPC 不能确定具体原因。
例如:
cycles高、instructions高、IPC中等:可能确实执行了很多计算;cycles高、instructions不高、缓存未命中高:可能是内存停顿;- 分支未命中比例高:可能是分支不可预测;
- 事件值异常低:可能是 PMU 不支持该事件、被虚拟化、被 multiplex,或权限受限。
6.2 事件复用和计数缩放
现代 CPU 可同时计数的硬件事件数量有限。指定太多事件时,内核会 multiplex,在不同事件之间轮换。perf stat 通常会显示类似:
<not counted>
或带有缩放百分比。采样事件也可能受复用影响。
比较两个版本时,必须确认:
- 使用了相同 CPU 和内核;
- 事件实际运行时间接近 100%;
- 没有因频繁复用导致样本不足;
- 进程负载和输入数据一致。
硬件事件名称来自架构实现,不应把跨机器的原始数值直接比较为绝对性能结论。
七、Off-CPU:线程不运行时发生了什么
7.1 Off-CPU 的定义
Off-CPU 是线程没有占用 CPU 执行的时间。它可能处于:
sleep或定时等待;- 等待互斥锁、读写锁、futex;
- 等待文件 I/O;
- 等待 socket 数据或发送缓冲区;
- 等待条件变量;
- 等待其他线程;
- 被调度器延迟运行;
- 因 cgroup CPU 配额、优先级或 CPU 竞争而未及时运行。
Off-CPU 时间不是单一原因,也不是“CPU 空闲时间”。当线程等待锁时,CPU 可能正在执行另一个线程;当系统整体过载时,线程可能在运行队列中等待调度。
7.2 为什么 CPU 火焰图看不到等待
设一个请求生命周期如下:
t0 t1 t2 t3
│--------│------------------│--------│
CPU 等待锁/IO CPU
CPU 火焰图只在 和 期间采样线程调用栈,而 期间线程没有在 CPU 上运行。因此它可能显示:
handler -> parse -> compute
但不会自动显示中间等待了数据库或锁。
更完整的请求延迟分解是:
其中:
- :线程实际执行;
- :睡眠、锁等待、I/O 等阻塞;
- :已可运行但尚未获得 CPU;
- :等待远程服务;
- :用户态计时和观测边界之外的部分。
CPU 火焰图主要估计第一项。Off-CPU 分析主要观察第二项,有些调度分析还能观察第三项。远程服务耗时则需要网络或分布式链路证据,不能仅凭本机 Off-CPU 栈判断。
八、从调度事件理解 Off-CPU 采集
线程状态可以抽象为:
stateDiagram-v2
[*] --> Running
Running --> Sleeping: 阻塞系统调用/等待锁
Running --> Runnable: 被抢占或时间片结束
Runnable --> Running: 调度器选择该线程
Sleeping --> Runnable: I/O完成/锁释放/超时
Sleeping --> [*]: 线程退出
Running --> [*]: 线程退出
关键路径是:
- 线程正在运行;
- 内核记录它离开 CPU 的原因;
- 线程处于 Sleeping 或 Runnable;
- 某个事件使它唤醒或获得调度;
- 记录等待开始和结束;
- 用等待期间关联的调用栈聚合。
perf 可以直接观察调度事件。例如:
sudo perf sched record -p "$PID" -- sleep 20
sudo perf sched timehist
perf sched 适合查看调度延迟、运行队列等待和线程切换。不同内核和 perf 版本的输出列略有差异,但通常包含线程、运行时间、等待时间、调度延迟等信息。
也可以检查 tracepoint 是否存在:
perf list | grep -E 'sched:(sched_switch|sched_wakeup)'
原始调度事件:
sudo perf record -e sched:sched_switch,sched:sched_wakeup \
-p "$PID" -- sleep 20
perf script
但仅有 sched_switch 事件并不能自动产生漂亮的 Off-CPU 火焰图。还需要:
- 找到目标线程离开 CPU 的时间;
- 找到它重新变为可运行或重新运行的时间;
- 将两者配对;
- 保存阻塞前的调用栈;
- 处理线程退出、重复唤醒、迁移和丢事件;
- 计算持续时间并生成折叠栈。
这也是 Off-CPU 工具通常使用 eBPF、内核 tracepoint 或专门的跟踪逻辑的原因。
九、使用 eBPF 工具生成 Off-CPU 数据
现代发行版常提供 BCC 或 bpftrace,但工具名和包名存在差异。例如 Debian/Ubuntu 系列可能使用 bcc-tools、bpfcc-tools,RPM 系列可能使用不同的子包。应先确认:
command -v offcputime
command -v offcputime-bpfcc
offcputime --help 2>/dev/null || offcputime-bpfcc --help
某些 BCC 版本可以这样输出目标进程的折叠栈:
sudo offcputime -p "$PID" -f 30 > offcpu.folded
也有发行版命令名不同:
sudo offcputime-bpfcc -p "$PID" -f 30 > offcpu.folded
这里的 -p 指定进程,-f 在常见 BCC 实现中表示 folded 输出并带持续时间窗口;具体参数必须以本机 --help 为准,因为 BCC 工具选项随版本和打包方式可能不同。
若输出是可被 FlameGraph 处理的折叠格式,可以生成图:
./FlameGraph/flamegraph.pl \
--title "Off-CPU profile" \
--countname "milliseconds" \
--color=io \
offcpu.folded > offcpu.svg
Off-CPU 折叠栈的值通常是毫秒或微秒,而 CPU 火焰图的值通常是样本数。不要把两个 SVG 的宽度直接比较为同一物理量。
9.1 Off-CPU 图中的栈应该如何解释
假设 Off-CPU 图出现:
main;server_loop;pthread_mutex_lock 12500
它表示:从这条栈离开 CPU 后,关联到的等待时间累计较多。它不一定意味着 pthread_mutex_lock 函数本身执行很久,而是该调用路径导致线程进入等待。
如果出现:
main;handle_request;read 18000
可能表示线程在 read 相关路径等待数据,但还需要结合:
- 文件描述符类型;
- 读的是磁盘、管道还是 socket;
- I/O 完成时间;
- 设备延迟;
- 对端是否发送数据。
调用名只能提供线索,不能单独证明根因。
9.2 Off-CPU 的配对和误差
Off-CPU 工具必须处理以下边界:
- 线程离开 CPU 后退出,可能没有对应唤醒事件;
- 线程在等待期间被多次唤醒和重新阻塞;
- 线程从 Sleeping 变为 Runnable 后又等待一段调度时间;
- 同一个 TID 被回收后重新使用;
- 线程在 CPU 之间迁移;
- 采集缓冲区丢事件;
- 只过滤 PID 时,需要确认工具按进程、线程还是 cgroup 过滤;
- 用户栈展开失败时,等待原因仍可能被计数,但栈显示不完整。
因此,Off-CPU 工具的输出应和 perf sched timehist、应用延迟指标、锁指标或 I/O 指标交叉验证。
十、CPU 火焰图和 Off-CPU 火焰图的对照
假设一个请求耗时 100 ms:
| 阶段 | 时间 |
|---|---|
| 解析请求 | 2 ms |
| 计算 | 8 ms |
| 等待锁 | 40 ms |
| 等待数据库响应 | 45 ms |
| 线程重新运行和发送响应 | 5 ms |
CPU 火焰图可能主要显示:
handle_request;compute 8%
handle_request;send_response 5%
handle_request;parse 2%
Off-CPU 图可能显示:
handle_request;mutex_lock 40 ms
handle_request;recv 45 ms
如果只看 CPU 火焰图,可能错误地优化 parse 的 2 ms;如果只看 Off-CPU 图,又可能把远程数据库延迟误认为本地 recv 函数慢。
合理的证据链是:
- 应用指标确认延迟升高;
- CPU 火焰图确认是否存在本地计算热点;
- Off-CPU 分析确认本地等待类型;
- 锁、磁盘、网络或数据库指标验证等待对象;
- 修改后分别重新测量 On-CPU 和 Off-CPU。
十一、采样调用栈的生产风险
11.1 权限限制
perf 受内核权限策略控制。常见参数:
cat /proc/sys/kernel/perf_event_paranoid
不同发行版默认值不同。限制较严格时,普通用户可能无法:
- 访问系统范围事件;
- 访问其他用户进程;
- 使用内核态采样;
- 使用某些硬件 PMU 能力。
现代内核还可能使用 CAP_PERFMON 等能力进行细粒度授权。不要默认使用:
sudo sysctl -w kernel.perf_event_paranoid=-1
因为这会扩大整个系统的性能观测权限。生产环境更适合:
- 只授权给指定运维组;
- 只采集指定 PID、TID 或 cgroup;
- 只在短时间窗口运行;
- 采集完成后恢复临时配置;
- 保存审计记录。
11.2 采集开销和数据量
采样开销来自:
- 触发事件;
- 保存寄存器;
- 保存调用栈;
- 写入 perf ring buffer;
- 用户态读取和后处理;
- DWARF 栈展开。
过高频率和过深调用栈会增加开销。生产采集应观察:
perf record -F 99 --call-graph dwarf,16384 -p "$PID" -- sleep 20
完成后检查:
perf report --stdio --percent-limit 0.5
如果担心丢样本,可检查 perf script 或报告中的丢失提示,并减少频率、缩短窗口或增大缓冲区。不能因为“采样工具通常开销很小”就忽略低延迟服务、实时线程和高频系统调用场景。
11.3 数据泄露
调用栈和参数周边信息可能暴露:
- 服务内部函数名;
- 文件路径;
- 用户态库版本;
- 内核模块;
- JIT 代码映射;
- 线程名和租户信息。
perf.data 本身也可能包含进程、线程、映射和命令行元数据。生产数据上传到分析环境前,应限制访问并评估是否需要脱敏。
十二、常见误区与反例
12.1 “火焰图最宽的函数就是最慢的函数”
反例:
request
├── parse 1 ms
└── database 99 ms,其中本地线程只运行了 2 ms
如果数据库调用期间线程阻塞,CPU 火焰图可能几乎看不到 99 ms。火焰图最宽的本地函数可能只是 parse,但优化它只能减少 1 ms。
正确做法是分别测量:
- CPU 执行时间;
- 本地阻塞时间;
- 远程服务时间;
- 调度等待时间。
12.2 “父函数和子函数宽度相加就是总时间”
假设 100 个样本中:
A栈中出现 100 次;B是A调用的子函数,出现 70 次。
图中 A 宽度为 100,B 宽度为 70。若把两者相加,会得到 170 个样本,但实际只有 100 次采样。父子区间存在包含关系,不能相加。
12.3 “采样频率设为 1000 Hz 就一定更准确”
更高频率通常增加样本数,但也增加开销和数据量,并不消除:
- 周期相位偏差;
- 调用栈缺失;
- JIT 符号问题;
- 事件复用;
- 负载变化;
- 统计口径错误。
先保证调用栈和符号正确,再根据目标热点大小和观测窗口调整频率。
12.4 “看到 malloc 宽,就说明 malloc 实现效率差”
malloc 可能只是所有对象分配路径的汇合点。需要继续查看它的上层调用者,并结合:
perf record -e cycles --call-graph dwarf -p "$PID" -- sleep 20
perf report --stdio
如果要分析分配次数和大小,CPU 采样通常不够,还需要应用分配器统计、内存分析器或 eBPF/uprobes 等其他证据。
12.5 “Off-CPU 栈显示 futex,所以锁一定有问题”
futex 是用户态同步原语进入内核等待的通用路径。它可能对应:
- 互斥锁竞争;
- 条件变量;
- 线程池任务等待;
- 其他用户态同步实现。
需要结合锁持有者、锁等待时间、线程状态和应用代码定位真正的共享资源。futex 只是内核等待机制,不是具体业务锁名。
12.6 “没有符号时把地址强行映射成函数名即可”
如果二进制版本不匹配,地址映射可能生成看似合理但实际错误的函数名。尤其在:
- 二进制重新链接;
- 共享库升级;
- ASLR;
- JIT 代码变化;
- 容器镜像变化;
之后,地址只有在对应映射和 build-id 匹配时才有意义。
十三、一个系统化的诊断流程
步骤一:先确认问题属于哪类时间
使用应用指标和基础统计确认:
pidstat -p "$PID" -u -w -d 1
关注:
%CPU:是否确实消耗 CPU;- 上下文切换;
- I/O 等待线索;
- 线程级别差异。
如果 %CPU 高,先做 CPU 采样;如果 %CPU 低但延迟高,优先做 Off-CPU、I/O、网络和远程依赖分析。
步骤二:先用短窗口确认权限和栈
perf record -F 49 --call-graph fp -p "$PID" -- sleep 5
perf report --stdio
先不用高频率和很深的 DWARF 栈。确认:
- 命令有权限;
perf.data成功生成;- 函数名可解析;
- 调用栈没有大面积截断;
- 采集对象确实是目标线程。
步骤三:根据栈质量选择展开方式
如果 fp 栈明显截断:
perf record -F 99 --call-graph dwarf,16384 -p "$PID" -- sleep 20
如果 DWARF 数据量过大,可以降低频率、缩短窗口或减少栈大小。不要为了生成完整 SVG 而无限增大采集参数。
步骤四:关联硬件或软件事件
CPU 计算热点:
perf record -e cycles --call-graph dwarf -p "$PID" -- sleep 20
系统调用和调度线索:
sudo perf sched record -p "$PID" -- sleep 20
sudo perf sched timehist
计数器概览:
perf stat -p "$PID" \
-e cycles,instructions,context-switches,cpu-migrations \
-- sleep 10
每个命令都在回答不同问题,不应把 perf stat 的聚合计数直接当成调用栈热点。
步骤五:生成两类图并验证
CPU 图:
perf script |
./FlameGraph/stackcollapse-perf.pl |
./FlameGraph/flamegraph.pl --title "On-CPU" > oncpu.svg
Off-CPU 图则使用支持调度状态关联的工具,并确认其输出单位、过滤范围和栈来源。生成图后,至少抽查:
- 进程和线程是否正确;
- 时间单位是否正确;
- 是否出现大量
[unknown]; - 是否有缓冲区丢失;
- 采集窗口内负载是否稳定。
十四、把 perf 与 eBPF 证据链连接起来
perf 擅长基于 PMU 和内核事件进行低侵入采样;eBPF 可以在 tracepoint、kprobe、uprobe、USDT 等位置记录更具体的上下文。二者可以互补:
应用延迟
│
├── perf record:On-CPU 调用栈
├── perf sched:调度和运行队列
├── eBPF:锁、I/O、网络、系统调用参数
└── perf stat:cycles、instructions、cache、branch
例如 CPU 火焰图显示线程在 read 路径上消耗 CPU,但这并不说明磁盘慢。可以再观察块设备延迟、文件系统事件、socket 类型和进程状态。
反过来,Off-CPU 图显示线程在 futex 等待,也不能直接判断是锁竞争还是条件变量等待。应结合 eBPF 记录锁竞争、线程唤醒、持锁者或应用级锁名称。
观测工具之间的边界需要明确:
perf的硬件事件受 PMU 和权限影响;- eBPF 程序受 verifier、内核版本和 BTF 能力影响;
- kprobe 依赖内核符号和实现细节;
- tracepoint 接口通常比 kprobe 稳定,但字段仍可能有版本差异;
- 生产环境中都需要考虑采集开销、权限和数据敏感性。
十五、生产分析中的验证和恢复
一次采样结论至少应满足三个条件:
- 时间窗口与问题重合:不能在故障已经结束后采集正常流量。
- 符号版本匹配:采集时的 ELF、共享库、内核和模块可被恢复。
- 独立指标支持:热点应能和 CPU 利用率、延迟、I/O、锁或网络指标相互解释。
采集结束后,可以记录:
uname -a
perf version
perf report --header-only
cat /proc/sys/kernel/perf_event_paranoid
如果临时修改过权限或其他内核配置,应恢复原值:
OLD=$(cat /proc/sys/kernel/perf_event_paranoid)
# 在受控窗口内进行修改和采集
# 采集结束后恢复:
sudo sysctl -w kernel.perf_event_paranoid="$OLD"
更可靠的做法是把原值显式记录在变更系统中,而不是依赖 shell 变量,因为命令可能跨会话执行。
十六、结论:先区分时间,再选择证据
perf 采样的核心是:在事件发生时记录线程上下文,用大量样本估计 CPU 或某类事件在调用栈中的分布。调用栈展开决定“路径是否完整”,符号解析决定“地址是否可读”,硬件事件决定“热点的物理含义”。
火焰图的宽度表示聚合值,不表示横轴时间,也不能把父子函数宽度相加。CPU 火焰图描述 On-CPU 执行位置;Off-CPU 分析描述线程离开 CPU 后的等待或调度路径。两者必须分开采集、分开解释,再和应用、调度、锁、I/O、网络及远程依赖指标关联。
遇到性能问题时,最先要问的不是“哪张图最宽”,而是:
这段延迟发生在 CPU 执行、内核阻塞、运行队列等待,
还是远程依赖之外?
只有先回答这个时间归属问题,采样频率、调用栈方式、符号文件、PMU 事件和 Off-CPU 工具的选择才有明确依据。
系列导航与关联阅读
- 系列入口:Linux 完整学习路线:从内核与文件系统到网络、性能和生产运维
- 上一篇:Linux Core Dump 与崩溃诊断:coredumpctl、gdb、符号和隐私
- 下一篇:Linux eBPF 与 bpftrace:Hook、Map、Verifier、观测脚本和安全
- 延伸:Linux 性能分析方法:CPU、内存、磁盘、网络与 eBPF 证据链
官方资料
本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。

评论
0 条讨论