1. 为什么需要性能分析工具?
1.1 一个典型场景
想象一下:你的程序在生产环境中突然变慢,P99 延迟从 10ms 飙升到 200ms。CPU 使用率没有明显变化,日志里也没有报错。你该怎么办?
传统做法是"加日志→重新部署→观察→再加日志",这个循环效率极低,而且:
- 内核级问题看不到:上下文切换、缓存未命中、锁竞争,这些日志记录不了
这时候就需要性能分析工具上场了。
1.2 什么是"可观测性"?
在计算机系统中,"可观测性"(Observability)指的是:
在不修改程序代码的前提下,通过外部工具了解系统内部运行状态的能力。
性能分析工具就是实现可观测性的核心手段。它们能回答这些问题:
| 问题 | 工具 |
|---|
| 程序在 CPU 上干什么? | perf、火焰图 |
| 程序不在 CPU 上的时候在等什么? | off-CPU 分析、eBPF |
| 哪个函数最耗时? | perf record + report |
| 缓存未命中有多严重? | perf stat |
| 锁竞争在哪里发生? | eBPF + kprobe |
| 系统调用都做了什么? | perf trace、bpftrace |
2. 性能分析的三层模型
在深入学习具体工具之前,先建立一个核心框架——性能分析的三层模型:
┌─────────────────────────────────────────────────────┐
│ 性能分析三层模型 │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 第一层:信号层 (Signals) │ │
│ │ 工具:Prometheus, Grafana, top, vmstat │ │
│ │ 回答:"有什么问题?" │ │
│ │ CPU 100%?内存 OOM?IO 延迟异常? │ │
│ └──────────────────────┬──────────────────────┘ │
│ │ │
│ ┌──────────────────────▼──────────────────────┐ │
│ │ 第二层:分析层 (Analysis) │ │
│ │ 工具:perf, eBPF, ftrace, bpftrace │ │
│ │ 回答:"哪个进程?哪个函数?为什么?" │ │
│ │ 热点函数定位、锁竞争分析、IO 延迟分解 │ │
│ └──────────────────────┬──────────────────────┘ │
│ │ │
│ ┌──────────────────────▼──────────────────────┐ │
│ │ 第三层:修复层 (Fix) │ │
│ │ 回答:"怎么解决?" │ │
│ │ 代码优化、配置调优、架构调整 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
关键认知:大多数工程师的排查路径是从第一层直接跳到"加日志",完全跳过了第二层——这是效率最低的做法。本文介绍的工具正是第二层的核心武器。
2.1 核心公式:Wall Clock Time
性能分析最核心的公式:
实际耗时 (Wall Clock Time) = On-CPU 时间 + Off-CPU 时间
- On-CPU 时间:程序真正在 CPU 上执行的时间(消耗 CPU 周期)
- Off-CPU 时间:程序被阻塞等待的时间(等 I/O、等锁、等网络、sleep)
重要事实:生产系统中,应用的 wall clock time 中60–80% 是 Off-CPU 时间!但大多数人只看 CPU 火焰图(On-CPU),完全忽略了 Off-CPU 的问题。
3. perf:Linux 性能分析基石
3.1 什么是 perf?
perf(Performance Counters for Linux)是 Linux 内核自带的性能分析工具,从2.6.31 内核版本(2009 年)开始引入。它是 Linux 上最基础、最通用的性能分析工具。
核心原理:采样(Sampling)
perf 不是"记录每一次函数调用",而是每隔一段时间中断一次 CPU,记录当前 CPU 正在执行的指令地址和调用栈。就像每隔 10 分钟拍一张照片,最后统计你在哪里出现的次数最多。
时间线: ──┬────┬────┬────┬────┬────┬────┬────▶
↓ ↓ ↓ ↓ ↓ ↓ ↓
采样点: A A B A C A B
统计结果: A=4次 (57%), B=2次 (29%), C=1次 (14%)
采样开销极低(通常 < 1% CPU),适合在生产环境中使用。
3.2 发展历程
| 时间 | 里程碑 | 说明 |
|---|
| 2009 | perf 首次引入 | Linux 2.6.31,替代 OProfile |
| 2010 | perf probe 加入 | 支持动态插桩(kprobe/uprobe) |
| 2012 | perf trace 加入 | 系统调用追踪 |
| 2014 | perf c2c 加入 | Cache-to-Cache 分析(检测伪共享) |
| 2016 | eBPF 集成 | perf 可以附加 eBPF 程序 |
| 至今 | 持续演进 | 成为 Linux 性能分析的事实标准 |
3.3 安装
# Debian/Ubuntu
sudo apt-get install linux-tools-common linux-tools-generic linux-tools-$(uname -r)
# CentOS/RHEL
sudo yum install perf
# 验证
perf --version
注意:perf 版本必须 ≥ 内核版本,否则某些功能可能不可用。
3.4 核心子命令详解
perf 有几十个子命令,但作为初学者,你只需要掌握以下6 个核心命令:
A.PERF LIST— 查看可用事件
# 列出所有可用事件
perf list
# 按类别过滤
perf list hardware # 硬件事件(由 CPU PMU 提供)
perf list software # 软件事件(由内核提供)
perf list tracepoint # 内核静态追踪点
perf list cache # 缓存相关事件
理解三类事件:
| 事件类型 | 数据来源 | 例子 | 说明 |
|---|
| 硬件事件 | CPU 的 PMU | cycles,instructions,cache-misses | 直接读取 CPU 硬件计数器 |
| 软件事件 | 内核计数器 | page-faults,context-switches,cpu-clock | 内核维护的软件计数器 |
| 追踪点 | 内核埋点 | syscalls:sys_enter_openat | 内核代码中预先埋设的静态追踪点 |
B.PERF STAT— 事件计数
回答:"这段程序在 CPU 上发生了什么?"这是 perf 的计数模式,给出精确的汇总数据。
# 统计 ls 命令的性能指标
perf stat ls /usr/bin > /dev/null
输出解读:
Performance counter stats for 'ls /usr/bin':
0.53 msec task-clock # 0.731 CPUs utilized ← CPU 利用率
968 context-switches # 1.816 M/sec ← 上下文切换
0 cpu-migrations # 0.000 /sec ← CPU 迁移
276 page-faults # 0.518 M/sec ← 缺页异常
2,512,678 cycles # 4.713 GHz ← CPU 周期
1,845,321 instructions # 0.73 insn per cycle ← IPC(每周期指令数)
372,456 branches # 0.699 G/sec ← 分支指令
12,345 branch-misses # 3.31% of all branches ← 分支预测失败率
关键指标速查:
| 指标 | 含义 | 健康值 | 异常值意味着 |
|---|
| IPC(insn per cycle) | 每个 CPU 周期执行多少指令 | > 1.0 | < 0.5:受内存延迟约束(Memory Bound) |
| branch-misses | 分支预测失败率 | < 2% | > 5%:代码分支复杂,考虑 branch-free 写法 |
| cache-misses | 缓存未命中率 | < 5% | > 10%:数据结构局部性差,考虑缓存行优化 |
| context-switches | 上下文切换次数 | 越低越好 | 突然升高:锁竞争或 I/O 密集 |
常用场景:
# 指定事件计数
perf stat -e cycles,instructions,cache-misses,branch-misses ./myapp
# 重复运行 5 次取平均
perf stat -r 5 ./myapp
# 附加到运行中的进程,统计 10 秒
perf stat -p 12345 sleep 10
# 按 CPU 分别统计
perf stat -a -A sleep 5
C.PERF TOP— 实时热点监控
回答:"现在系统中哪个函数最消耗 CPU?"类似top命令,但显示的是函数级别的 CPU 占用。
# 实时显示 CPU 热点(需要 root)
sudo perf top
# 显示调用栈
sudo perf top -g
# 只看用户态函数
sudo perf top -e cycles:u
输出解读:
Samples: 12K of event 'cycles', Event count (approx.): 1234567890
Overhead Shared Object Symbol
9.20% [kernel] [k] _raw_spin_lock_irqsave
5.83% myapp [.] compute_heavy_function
3.12% libc.so.6 [.] __memcpy_avx_unaligned
| 列 | 含义 |
|---|
Overhead | 该函数占采样总数的百分比 |
Shared Object | 函数所在的可执行文件或库 |
Symbol | 函数名,[k]= 内核空间,[.]= 用户空间 |
D.PERF RECORD+PERF REPORT— 采样与报告
这是 perf 最强大的功能组合。perf record采集数据生成perf.data文件,perf report分析并展示结果。
# === 第一步:采集数据 ===
# 采样程序运行全过程
perf record -g ./myapp
# 采样运行中进程,持续 30 秒,频率 99Hz
perf record -F 99 -g -p $(pidof myapp) -- sleep 30
# 采样所有 CPU(全系统),10 秒
sudo perf record -F 99 -g -a -- sleep 10
# 指定调用图模式(dwarf 不需要 -fno-omit-frame-pointer)
perf record -g --call-graph dwarf -p 12345 -- sleep 30
调用图模式选择:
| 模式 | 命令 | 优点 | 缺点 |
|---|
fp(帧指针) | --call-graph fp | 快速 | 需要编译时加-fno-omit-frame-pointer |
dwarf | --call-graph dwarf | 无需帧指针,信息完整 | 较慢,perf.data 文件更大 |
lbr(最后分支记录) | --call-graph lbr | 精确、高效 | 仅 Intel CPU 支持,栈深度有限 |
# === 第二步:分析报告 ===
# 交互式报告
perf report
# 文本输出
perf report --stdio
# 按函数排序
perf report --sort symbol
# 只看用户态
perf report --dsos=myapp
E.PERF ANNOTATE— 汇编级热点分析
回答:"热点函数中,具体是哪几条指令在消耗 CPU?"
# 对热点函数进行汇编级分析
perf annotate -s compute_heavy_function
输出示例:
Percent | Source code & Disassembly of myapp
--------+---------------------------------------------------
:
: int compute_heavy_function(int n) {
0.12 : push %rbp
0.05 : mov %rsp,%rbp
:
45.23 : mov 0x8(%rax),%rdx ← 这条指令消耗了 45% 的 CPU!
12.34 : add %rcx,%rdx ← 这条消耗了 12%
0.01 : cmp %rbx,%rax
用途:定位到具体的 CPU 指令,判断是内存加载延迟(movfrom memory)还是计算瓶颈(add/mul),从而指导优化方向。
F.PERF C2C— 伪共享检测
这是 perf 中专门用于检测**缓存行伪共享(False Sharing)**的子命令。
# 采集 cache-to-cache 数据
sudo perf c2c record -g ./myapp
# 分析报告
sudo perf c2c report
输出解读:perf c2c会列出竞争最严重的缓存行,显示哪些核心在争夺同一个缓存行,以及涉及的函数和变量。
=================================================
Shared Data Cache Line Table
=================================================
# Index Address Node PA cnt ...
0 0x7f1234567800 0 12345 ← 这个缓存行竞争最严重
1 0x7f1234567880 0 5678
=================================================
Shared Cache Line Distribution Pareto
=================================================
# -- HITM -- -- Store Refs -- Data address
45.23% 12345 0x7f1234567800
↑ ↑
核心A修改后 这个地址就是
核心B读取 "伪共享"的缓存行
典型结果:如果发现两个不相关的变量(如head和tail)在同一缓存行中被不同核心频繁访问,那就是伪共享——解决方案是用alignas(64)将它们分开。
3.5perf trace— 系统调用追踪
类似strace,但开销更低,适合在生产环境中使用。
# 追踪 ls 的系统调用
sudo perf trace ls /usr/bin
# 附加到运行中进程
sudo perf trace -p 12345
输出示例:
0.000 ( 0.012 ms): ls/12345 openat(dfd: CWD, filename: "/etc/ld.so.cache") = 3
0.025 ( 0.008 ms): ls/12345 mmap(len: 123456, prot: READ) = 0x7f...
0.050 ( 0.015 ms): ls/12345 close(fd: 3) = 0
0.070 ( 0.003 ms): ls/12345 write(fd: 1, buf: "file1 file2", count: 12) = 12
3.6perf probe— 动态插桩
无需修改代码,动态地在任意内核函数或用户态函数上添加探测点。
# 为内核函数 do_sys_open 添加探测点,捕获第二个参数(文件名)
sudo perf probe -a 'do_sys_open filename=+0(%si):string'
# 记录探测点数据
sudo perf record -e probe:do_sys_open -a sleep 5
# 查看结果
sudo perf script
# 清理
sudo perf probe -d do_sys_open
4. eBPF:可编程内核的革命
4.1 什么是 eBPF?
eBPF(extended Berkeley Packet Filter)是 Linux 内核中的一项革命性技术。它允许你在不修改内核源码、不加载内核模块的前提下,在 Linux 内核中运行沙箱化的程序。
用一个比喻来理解:
- perf像是给你一个望远镜——你可以看到 CPU 在干什么,但能看什么取决于望远镜的镜头
- eBPF像是给你一个显微镜工厂——你可以自己设计显微镜,去观察任何你想观察的东西
4.2 发展历程
1992 ── BPF 诞生 ── 伯克利包过滤器,仅用于网络数据包过滤
│
2014 ── eBPF 引入 ── Linux 3.18,扩展为通用内核虚拟机
│ 支持 kprobe、tracepoint、maps
│
2015 ── BCC 发布 ── 第一个让普通开发者能写 eBPF 程序的工具集
│
2016 ── eBPF 验证器完善 ── XDP(高速数据路径)加入
│
2018 ── bpftrace 发布 ── DTrace 风格的脚本语言,一行命令即可排查
│
2019 ── BTF(BPF Type Format)── 消除对内核头文件的依赖
│
2020 ── CO-RE(一次编译到处运行)── eBPF 程序可跨内核版本移植
│
2021 ── Parca/Pyroscope ── 持续性能分析平台
│
至今 ── eBPF 成为云原生基础设施核心(Cilium、Falco、Pixie...)
4.3 eBPF 工作原理(简化版)
┌─────────────────────────────────────────────────────────────┐
│ eBPF 工作流程 │
│ │
│ 1. 用户态编写 eBPF 程序(C 语言子集 / bpftrace 脚本) │
│ │ │
│ ▼ │
│ 2. 编译为 BPF 字节码(LLVM / bpftrace 自动编译) │
│ │ │
│ ▼ │
│ 3. 通过 bpf() 系统调用加载到内核 │
│ │ │
│ ▼ │
│ 4. 内核验证器 (Verifier) 检查安全性 │
│ • 不能有无限循环 │
│ • 不能访问未初始化的内存 │
│ • 栈深度有限(512 字节) │
│ • 指令数有限(100 万条) │
│ │ │
│ ▼ │
│ 5. JIT 编译器将字节码转为本地机器码 │
│ │ │
│ ▼ │
│ 6. 挂载到钩子点 (kprobe/tracepoint/uprobe/XDP...) │
│ │ │
│ ▼ │
│ 7. 事件触发时执行 eBPF 程序 → 结果写入 BPF Maps 或 perf buffer │
│ │ │
│ ▼ │
│ 8. 用户态程序从 Maps/buffer 读取结果并展示 │
└─────────────────────────────────────────────────────────────┘
4.4 eBPF 的钩子点类型
eBPF 程序可以挂载到以下位置:
| 钩子类型 | 说明 | 用途 |
|---|
| kprobe / kretprobe | 内核函数入口/返回 | 追踪任意内核函数 |
| uprobe / uretprobe | 用户态函数入口/返回 | 追踪应用层函数 |
| tracepoint | 内核静态追踪点 | 稳定的追踪接口 |
| USDT | 用户态静态追踪点 | 应用自定义追踪点 |
| XDP | 网络驱动层 | 高速包处理 |
| TC (Traffic Control) | 网络调度层 | 网络策略 |
| LSM (Linux Security Module) | 安全模块钩子 | 安全策略 |
4.5 eBPF 的"宪法":验证器(Verifier)
内核验证器是 eBPF 安全性的核心保障。它会逐条检查你的 eBPF 程序:
// ❌ 这个程序会被验证器拒绝
int bad_program() {
while (1) { // 无限循环 → 拒绝
do_something();
}
}
// ❌ 这个也会被拒绝
int another_bad() {
int *p = NULL;
*p = 42; // 空指针解引用 → 拒绝
return 0;
}
// ✅ 这个可以通过
int good_program() {
for (int i = 0; i < 100; i++) { // 有界循环 → 通过
bpf_trace_printk("hello %d", i);
}
return 0;
}
5. 火焰图:性能数据的可视化利器
5.1 什么是火焰图?
火焰图(Flame Graph)是由Brendan Gregg(Netflix 前首席性能工程师)发明的性能数据可视化方法。它把 perf 采样数据变成一张"看颜色就能找到问题"的图。
5.2 如何阅读火焰图
┌──────────────────────────────────────┐
│ main() ← 栈底 │
├──────────┬───────────────────────────┤
│ init() │ process_data() │
├──────────┼──────────┬────────────────┤
│ ... │ parse() │ compute() │
│ ├──────────┼────────┬───────┤
│ │ ... │ loop() │ ... │
│ │ ├────────┤ │
│ │ │ heavy()│ ← 栈顶 │
└──────────┴──────────┴────────┴───────┘
宽度 = 该函数在采样中出现的比例(即 CPU 占用比例)
颜色 = 无特殊含义(暖色=用户态,冷色=内核态)
从上到下 = 调用栈(栈顶在上,栈底在下)
读图三步法:
- 看"平原":平坦宽阔的区域(没有子函数)是"自己干活"的函数
- 看"山峰":金字塔形的区域是"调度者",底下是"干活者"
5.3 生成火焰图的完整流程
# 步骤 1:安装 FlameGraph 工具
git clone FlameGraph.git
export PATH=$PATH:$(pwd)/FlameGraph
# 步骤 2:用 perf 采集数据
sudo perf record -F 99 -g -p $(pidof myapp) -- sleep 30
# 步骤 3:转换为火焰图格式
sudo perf script > out.perf
# 步骤 4:折叠调用栈
stackcollapse-perf.pl out.perf > out.folded
# 步骤 5:生成 SVG 火焰图
flamegraph.pl out.folded > cpu_flamegraph.svg
# 步骤 6:在浏览器中打开 cpu_flamegraph.svg
# SVG 是交互式的:可以点击放大、搜索函数名
5.4 火焰图的变体
| 类型 | 生成命令 | 用途 |
|---|
| CPU 火焰图 | flamegraph.pl | On-CPU 热点分析 |
| Off-CPU 火焰图 | flamegraph.pl --color=io --title="Off-CPU" | 阻塞等待分析 |
| 内存火焰图 | flamegraph.pl --color=mem --title="Memory" | 内存分配热点 |
| 差异火焰图 | difffolded.pl | 两个版本之间的性能变化(红=变慢,蓝=变快) |
| 差分火焰图 | flamegraph.pl --negate | 对比两个时间段的性能差异 |
5.5 On-CPU 与 Off-CPU 必须一起看
这是性能分析中最容易犯的错误——只看 On-CPU 火焰图。
如果你只看 On-CPU 火焰图:
"程序花了 40% 的时间在 malloc() 上,我得优化内存分配!"
如果你同时看 Off-CPU 火焰图:
"程序花了 60% 的时间在等 mutex_lock(),这才是真正的瓶颈!"
正确的优化方向:先修锁竞争,再优化内存分配。
6. BCC 与 bpftrace:eBPF 的两大前端
直接写 eBPF C 程序门槛很高(需要理解内核数据结构、Verifier 规则、BPF 指令限制)。BCC和bpftrace就是解决这个问题的——它们是 eBPF 的两大前端,让普通开发者也能使用 eBPF。
6.1 BCC:可编程的工具箱
定位:Python 写逻辑,C 写 eBPF 内核代码。附带100+ 预置工具。
# 安装
sudo apt-get install bpfcc-tools linux-headers-$(uname -r)
# BCC 预置工具速查
| 子系统 | 工具 | 用途 | 一句话命令 |
|---|
| CPU | profile | CPU 栈采样 | profile-bpfcc -F 99 -p PID 10 |
| CPU | offcputime | Off-CPU 时间分析 | offcputime-bpfcc -df -p PID 10 |
| CPU | cpudist | CPU 使用时间分布 | cpudist-bpfcc -p PID |
| CPU | runqlat | 运行队列等待延迟 | runqlat-bpfcc -p PID |
| 内存 | memleak | 内存泄漏检测 | memleak-bpfcc -p PID |
| 磁盘 | biolatency | 块设备 I/O 延迟分布 | biolatency-bpfcc -D |
| 磁盘 | biosnoop | 每次 I/O 操作追踪 | biosnoop-bpfcc |
| 网络 | tcplife | TCP 连接生命周期 | tcplife-bpfcc |
| 网络 | tcpretrans | TCP 重传追踪 | tcpretrans-bpfcc |
| 文件 | ext4slower | 慢 ext4 操作 | ext4slower-bpfcc 10 |
| 文件 | filetop | 文件读写排行 | filetop-bpfcc |
| 锁 | deadlock | 死锁检测 | deadlock-bpfcc -p PID |
BCC 实战示例 — 追踪文件读延迟分布:
from bcc import BPF
prog = """
#include <linux/fs.h>
BPF_HASH(start, u32, u64);
BPF_HISTOGRAM(dist);
int trace_read_entry(struct pt_regs *ctx) {
u32 tid = bpf_get_current_pid_tgid();
u64 ts = bpf_ktime_get_ns();
start.update(&tid, &ts);
return 0;
}
int trace_read_return(struct pt_regs *ctx) {
u32 tid = bpf_get_current_pid_tgid();
u64 *tsp = start.lookup(&tid);
if (tsp != 0) {
u64 delta = bpf_ktime_get_ns() - *tsp;
dist.increment(bpf_log2l(delta / 1000)); // 微秒为单位
start.delete(&tid);
}
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event="vfs_read", fn_name="trace_read_entry")
b.attach_kretprobe(event="vfs_read", fn_name="trace_read_return")
print("Tracing vfs_read latency... Ctrl-C to stop.")
b.trace_print()
6.2 bpftrace:瑞士军刀
定位:即时的 one-liner 工具——不需要写文件、不需要编译、一行命令开始排查。
# 安装
sudo apt-get install bpftrace
bpftrace 12 课速成(来自官方教程):
第 1 课:列出可用探针
bpftrace -l 'tracepoint:syscalls:sys_enter_*'
# 列出所有系统调用入口的追踪点
第 2 课:HELLO WORLD
bpftrace -e 'BEGIN { printf("hello world\n"); }'
# BEGIN 是启动时触发的特殊探针(类似 awk 的 BEGIN)
第 3 课:追踪文件打开
bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
printf("%s %s\n", comm, str(args.filename));
}'
# 输出每个进程打开的文件名
第 4 课:按进程统计系统调用次数
bpftrace -e 'tracepoint:raw_syscalls:sys_enter {
@[comm] = count();
}'
# Ctrl-C 后自动打印统计表
第 5 课:READ() 字节数分布
bpftrace -e 'tracepoint:syscalls:sys_exit_read /pid == 12345/ {
@bytes = hist(args.ret);
}'
# 直方图显示每次 read() 返回的字节数分布
第 6 课:内核动态追踪
bpftrace -e 'kretprobe:vfs_read {
@bytes = lhist(retval, 0, 2000, 200);
}'
# 追踪内核函数 vfs_read 的返回值分布
第 7 课:计时 READ() 调用
bpftrace -e '
kprobe:vfs_read { @start[tid] = nsecs; }
kretprobe:vfs_read /@start[tid]/ {
@ns[comm] = hist(nsecs - @start[tid]);
delete(@start, tid);
}'
# 统计每个进程 vfs_read 的耗时分布
第 8 课:统计进程级事件
bpftrace -e 'tracepoint:sched:sched* {
@[probe] = count();
} interval:s:5 { exit(); }'
# 统计 5 秒内各类调度事件的发生次数
第 9 课:采样 ON-CPU 内核栈
bpftrace -e 'profile:hz:99 {
@[kstack] = count();
}'
# 以 99Hz 频率采样,统计内核栈出现频率
第 10 课:调度器追踪
bpftrace -e 'tracepoint:sched:sched_switch {
@[kstack] = count();
}'
# 追踪线程离开 CPU 的原因(阻塞事件)
第 11 课:块 I/O 追踪
bpftrace -e 'tracepoint:block:block_rq_issue {
@ = hist(args.bytes);
}'
# I/O 请求大小的分布
第 12 课:内核结构体追踪
# path.bt 脚本文件
kprobe:vfs_open {
printf("open path: %s\n", str(((struct path *)arg0)->dentry->d_name.name));
}
# 运行
bpftrace path.bt
6.3 BCC vs bpftrace 如何选择?
bpftrace = 瑞士军刀 → 临时排查,一行命令快速定位方向
BCC = 工具箱 → 深度调查,需要复杂逻辑的可复用工具
90% 的临时排查用 bpftrace one-liner 就够了
需要复杂聚合、对接监控系统时用 BCC
7. ftrace:内核自带的追踪器
7.1 什么是 ftrace?
ftrace 是 Linux 内核自带的函数追踪框架,从2.6.27 内核版本(2008 年)开始内置。它的设计哲学是"最小侵入性"——关闭时性能开销为零。
原理:内核编译时在每个函数入口插入mcount/fentry调用点。不追踪时,ftrace 将这些调用点替换为nop(空操作)。追踪时,替换为 ftrace 的处理函数。
7.2 核心用法
# 进入 ftrace 目录
cd /sys/kernel/debug/tracing
# === Function Tracer:追踪函数调用 ===
echo function > current_tracer
echo "tcp_sendmsg" > set_ftrace_filter
cat trace_pipe
# === Function Graph Tracer:显示函数调用图和时间 ===
echo function_graph > current_tracer
echo "ext4_file_write_iter" > set_graph_function
cat trace
# === Trace Events:内核静态追踪点 ===
# 追踪所有 TCP 重传事件
echo 1 > events/tcp/tcp_retransmit_skb/enable
cat trace_pipe
# === 追踪特定进程 ===
echo 12345 > set_ftrace_pid
7.3 ftrace 的定位
ftrace 是内核开发者调试的首选工具,但对应用开发者来说体验不够友好(需要通过/sys文件系统操作)。它最适合作为"确认怀疑方向"的初步工具,而非深度剖析的主要武器。
8. 完整排查实战案例
8.1 案例:P99 延迟突然飙升
场景:Go API 网关的 P99 延迟从 15ms 飙到 120ms,但 CPU 使用率只从 40% 涨到 55%。
排查过程:
第一步:perf top — 快速看 CPU 热点
────────────────────────────────────
sudo perf top -g -p $(pidof apigw)
发现:
40% runtime.mallocgc ← 内存分配和 GC
15% runtime.lock ← 锁操作!
10% encoding/json.Unmarshal
第二步:生成 On-CPU 火焰图
────────────────────────────────────
sudo perf record -F 99 -g -p $(pidof apigw) -- sleep 30
sudo perf script | stackcollapse-perf.pl | flamegraph.pl > cpu.svg
On-CPU 火焰图显示:
40% 时间在 malloc 相关路径上
第三步:生成 Off-CPU 火焰图 — 关键!
────────────────────────────────────
offcputime-bpfcc -df -p $(pidof apigw) 30 > offcpu.stacks
flamegraph.pl --color=io --title="Off-CPU" offcpu.stacks > offcpu.svg
Off-CPU 火焰图显示:
60% 时间在 mutex_lock 等待!
第四步:bpftrace 深入锁竞争分析
────────────────────────────────────
bpftrace -e '
kprobe:mutex_lock {
@start[tid] = nsecs;
@lock_addr[tid] = arg0;
}
kretprobe:mutex_lock /@start[tid]/ {
$delta = (nsecs - @start[tid]) / 1000;
if ($delta > 1000) {
printf("SLOW LOCK: tid=%d wait=%dus addr=%p\n",
tid, $delta, @lock_addr[tid]);
}
delete(@start[tid]);
delete(@lock_addr[tid]);
}'
发现:全局连接池的 mutex 导致严重竞争
根因与修复:
| 维度 | 占比 | 根因 | 修复方案 |
|---|
| On-CPU: malloc | 40% | 高频小对象分配 | 对象池化(sync.Pool) |
| Off-CPU: mutex 等待 | 60% | 连接池全局锁 | 分片设计,N 个连接池各带独立锁 |
效果:P99 从 120ms → 18ms(降 85%)
核心教训:如果只看 On-CPU 火焰图,你会花时间优化 malloc——这有帮助,但 P99 不会降太多。因为真正的瓶颈在锁等待上,而锁等待不消耗 CPU!
8.2 案例:伪共享导致性能下降
场景:多线程计数器程序,4 线程的吞吐量反而低于 2 线程。
# 用 perf c2c 检测伪共享
sudo perf c2c record -g ./counter_app
sudo perf c2c report
输出显示:两个atomic_int计数器在同一缓存行(0x7f...7800),被 Core 0 和 Core 2 频繁争抢。
修复:
// ❌ 修复前:两个原子变量在同一缓存行
struct Counters {
atomic_int send_count;
atomic_int recv_count;
};
// ✅ 修复后:各自独占缓存行
struct Counters {
alignas(64) atomic_int send_count;
alignas(64) atomic_int recv_count;
};
9. 工具全景图与选型指南
9.1 Brendan Gregg 的 Linux 性能工具全景图
性能分析领域最经典的参考图来自 Brendan Gregg。以下是简化版的核心工具矩阵:
Linux 性能可观测性工具
┌──────────────────────────────────────────────────────────┐
│ │
│ 应用层 strace ltrace gdb perf eBPF(uprobe)│
│ ──────── │
│ 系统调用 perf trace bpftrace BCC(syscount) │
│ ──────── │
│ 调度器 perf sched offcputime runqlat │
│ ──────── │
│ 文件系统 perf trace ext4slower biosnoop │
│ ──────── │
│ 网络 tcpdump tcplife tcpretrans XDP │
│ ──────── │
│ 内存 perf stat memleak Valgrind │
│ ──────── │
│ 内核 ftrace perf probe kprobe tracepoint │
│ ──────── │
│ 硬件 perf stat turbostat CPU PMU 计数器 │
│ │
└──────────────────────────────────────────────────────────┘
9.2 工具选型速查表
| 场景 | 推荐工具 | 具体操作 |
|---|
| CPU 热点定位 | perf record → 火焰图 | perf record -F 99 -g -p PID -- sleep 30 |
| 缓存命中率 | perf stat | perf stat -e cache-misses,instructions ./app |
| 分支预测效率 | perf stat | perf stat -e branch-misses,branches ./app |
| 伪共享检测 | perf c2c | perf c2c record -g ./app |
| Off-CPU 分析 | BCC offcputime | offcputime-bpfcc -df -p PID 10 |
| 系统调用追踪 | bpftrace / perf trace | bpftrace -e 'tracepoint:raw_syscalls:sys_enter {...}' |
| 锁竞争分析 | bpftrace + kprobe | 追踪 mutex_lock 耗时 |
| 内存泄漏 | BCC memleak / Valgrind | memleak-bpfcc -p PID |
| 磁盘 I/O 延迟 | BCC biolatency | biolatency-bpfcc -D |
| TCP 重传分析 | BCC tcpretrans | tcpretrans-bpfcc |
| 文件操作排行 | BCC filetop | filetop-bpfcc |
| 调度延迟分析 | BCC runqlat | runqlat-bpfcc -p PID |
| 函数级延迟分析 | ftrace function_graph | echo function_graph > current_tracer |
| 汇编级热点 | perf annotate | perf annotate -s hotspot_func |
9.3 工具演进全景
perf (2009) → 首次让用户态看到 CPU 在忙什么
│
ftrace (2008) → 函数入口/出口插桩,记录每次调用
│
BCC (2015) → 第一个让普通开发者能写 eBPF 程序的工具集
│
bpftrace (2018) → 一行命令即可排查,不用写 C、不用等编译
│
Parca/Pyroscope (2021+) → 从"响应式"变为"始终采样、事后分析"
核心认知跃迁:
Parca/Pyroscope让我们再也不用担心"问题发生时我不在"
10. 持续性能分析:Parca 与 Pyroscope
10.1 传统方式的痛点
传统性能分析是响应式的:出了问题 → 手动跑 perf → 分析。问题在于:
- 问题发生时有 30 秒"窗口期",错过就再也看不到
- 间歇性问题(每天凌晨 3 点发生一次)几乎无法捕捉
10.2 持续性能分析(Continuous Profiling)
持续性能分析把 profiling 变成像 metrics 一样的基础设施——始终运行、始终采集、需要时再查。
┌─────────────────────────────────────────────┐
│ 持续性能分析架构 │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Agent │ │ Agent │ │ Agent │ │
│ │ (eBPF │ │ (eBPF │ │ (eBPF │ │
│ │ 采样) │ │ 采样) │ │ 采样) │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ Server 端 │ │
│ │ (列式存储) │ │
│ └──────┬──────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ UI 查询 │ │
│ │ - 火焰图 │ │
│ │ - 时间段选择 │ │
│ │ - 差异对比 │ │
│ └─────────────┘ │
└─────────────────────────────────────────────┘
关键特性:
- 差异火焰图:对比两个时间段的性能差异(红=变慢,蓝=变快)
- 与 Prometheus 集成:CPU 使用率升高告警 → 一键跳转到对应时段火焰图
- 极低开销:采样频率 19Hz,CPU 开销 < 1%
| 参数 | 值 | 原因 |
|---|
| 采样频率 | 19Hz 或 49Hz | 素数避免与系统定时器对齐导致采样偏差 |
| CPU 开销 | < 1% | 实测通常 0.1–0.5% |
| 内存开销 | ~50 MB | Agent 常驻内存 |
11. 学习资源与推荐路径
11.1 推荐学习路径
阶段 1:基础感知 (1 周)
├── 理解 CPU 缓存体系(L1/L2/L3、缓存行)
├── 理解操作系统调度器基本原理
└── 理解 Wall Clock Time = On-CPU + Off-CPU
阶段 2:perf 上手 (2 周)
├── perf stat → 看懂 IPC、缓存命中率
├── perf top → 实时监控 CPU 热点
├── perf record + report → 采样分析
└── 火焰图生成 → 可视化热点
阶段 3:eBPF 入门 (2 周)
├── bpftrace 12 课 → 掌握 one-liner 排查
├── BCC 预置工具 → 了解工具生态
└── 理解 eBPF 工作原理(Verifier、Maps、钩子点)
阶段 4:深入实践 (持续)
├── perf c2c → 检测伪共享
├── Off-CPU 分析 → 阻塞问题排查
├── perf annotate → 汇编级热点
└── 真实项目性能优化实战
11.2 推荐资源
| 类型 | 资源 | 作者/来源 | 说明 |
|---|
| 书籍 | "BPF Performance Tools" | Brendan Gregg | eBPF 性能分析的圣经 |
| 书籍 | "Systems Performance" (2nd Ed.) | Brendan Gregg | 系统性能分析方法论 |
| 网站 | https://www.brendangregg.com/ | Brendan Gregg | 火焰图、perf、eBPF 权威资料 |
| 网站 | https://ebpf.io/ | eBPF 基金会 | eBPF 入门与社区资源 |
| 教程 | https://bpftrace.org/ | bpftrace 项目 | 12 课快速上手 |
| 工具 | https://github.com/brendangregg/FlameGraph | Brendan Gregg | 火焰图生成脚本 |
| 工具 | https://github.com/iovisor/bcc | IO Visor 项目 | eBPF 工具集 |
| 工具 | https://github.com/KDAB/hotspot | KDAB | perf 数据的 GUI 分析器 |
11.3 内核版本要求
| 功能 | 最低内核版本 | 推荐版本 |
|---|
| perf 基本功能 | 2.6.31 | 4.x+ |
| ftrace | 2.6.27 | 所有版本 |
| eBPF 基本功能 | 4.9 | 5.4+ |
| eBPF CO-RE(一次编译到处运行) | 5.4 | 5.15+ |
| bpftrace | 4.9 | 5.4+ |
| BCC | 4.9 | 5.4+ |