土法炼钢兴趣小组的算法知识备份

【eBPF 系列】eBPF 性能分析工具链:perf → BCC → bpftrace → Parca 的演化

文章导航

分类入口
linuxebpfprofiling
标签入口
#ebpf#profiling#perf#bpftrace#bcc#parca#flame-graph#off-cpu

目录

你打开一张 CPU 火焰图,看到 std::sort 占了 38% 的 CPU 时间。优化完排序算法,性能提升了——但是用户的 P99 延迟纹丝不动。

为什么?

因为那个请求的 200ms 里,CPU 只忙了 40ms。剩下的 160ms,进程在等锁、等磁盘、等 DNS 解析——这些时间,火焰图一个字都不会告诉你。

这就是 on-cpu profiling 的根本盲区。过去 15 年,Linux 性能分析工具链从 perf 的硬件计数器采样,一路演化到 Parca 的持续性能分析平台,每一步都在试图补上前一代的盲区。本文要讲的就是这条演化路线:每个工具解决了什么问题、留下了什么问题、以及你在 2027 年应该怎么选。

  1. 火焰图只看 on-cpu 时间,而真实延迟 = on-cpu + off-cpu
  2. Off-CPU 分析通过 sched_switch tracepoint 追踪进程休眠,补上了火焰图的盲区
  3. 持续性能分析(Parca/Pyroscope)让你在问题发生后也能”时间旅行”回去看火焰图

如果你还没看过 eBPF 的基础知识,建议先读 eBPF:Linux 内核的隐藏武器。


一、Linux 性能分析工具的演化

性能分析工具演化时间线

Linux 性能分析工具的演化,本质上是一部”谁有权在内核里插代码”的历史。

perf(2009):内核 PMC 采样器

perf 是 Linux 内核自带的性能分析器,直接访问 CPU 的 Performance Monitoring Counters(PMC)。它的工作方式很简单:每隔 N 个 CPU 周期中断一次,记录当前的调用栈。

# 对进程采样 30 秒,99Hz 频率
perf record -F 99 -p $PID -g -- sleep 30

# 生成火焰图(需要 Brendan Gregg 的 FlameGraph 工具)
perf script | stackcollapse-perf.pl | flamegraph.pl > cpu.svg

perf 稳定、可靠、开销低。但它的问题是太底层了——你想做点复杂的聚合逻辑,比如”只统计持锁超过 10μs 的栈”,就得自己写用户态后处理脚本,内核态的数据管不了。

ftrace(2008):函数追踪框架

ftrace 比 perf 还早一年进入内核,它不是采样器,而是追踪器——在函数入口和出口插桩,记录每次调用。

# 追踪 do_sys_open 函数
echo do_sys_open > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/trace_pipe

ftrace 的优势是零依赖(内核自带),劣势是用起来像在跟 /sys 文件系统搏斗。它更适合内核开发者调试,而不是应用开发者做性能分析。

BCC(2015):Python 前端 + eBPF 后端

BCC(BPF Compiler Collection)是第一个让普通开发者能写 eBPF 程序的工具集。Python 写逻辑,C 写 eBPF 内核代码,编译后注入内核。

# BCC 示例:追踪 TCP 连接延迟
from bcc import BPF

prog = """
#include <net/sock.h>

BPF_HASH(start, struct sock *);

int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) {
  u64 ts = bpf_ktime_get_ns();
  start.update(&sk, &ts);
  return 0;
}

int kretprobe__tcp_v4_connect(struct pt_regs *ctx) {
  // 计算连接建立耗时
  struct sock *sk = (struct sock *)PT_REGS_RC(ctx);
  u64 *tsp = start.lookup(&sk);
  if (tsp) {
  u64 delta = bpf_ktime_get_ns() - *tsp;
  bpf_trace_printk("connect latency: %d us\\n", delta / 1000);
  start.delete(&sk);
  }
  return 0;
}
"""

b = BPF(text=prog)
b.trace_print()

BCC 带来了 100 多个现成工具(execsnoop、biolatency、tcplife……),一行命令就能用。但它的问题是每次运行都要编译 eBPF 代码,启动慢,而且对内核头文件有依赖。

bpftrace(2018):DTrace 风格的高级语言

bpftrace 是给”我只想快速查个东西”的人准备的。一行命令,不用写 C,不用等编译:

# 统计系统调用次数(按进程)
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

# 追踪块设备 I/O 延迟分布
bpftrace -e 'kprobe:blk_account_io_done { @us = hist(nsecs - @start[arg0]); }
  kprobe:blk_account_io_start { @start[arg0] = nsecs; }'

如果你想深入了解 bpftrace 的实战用法,可以看 eBPF 追踪实战:用 bpftrace 在生产环境找到那个慢请求。

持续性能分析:Parca / Pyroscope(2021+)

传统 profiling 的最大问题是:只在”出问题时”才做。但生产环境的性能问题往往是间歇性的,等你手动上去 perf record,问题已经过去了。

Parca 和 Pyroscope 的思路是:始终采样,事后分析。一个 eBPF agent 常驻在每台机器上,以极低开销(<1% CPU)持续采集调用栈,存进时序数据库。什么时候出了问题,回头去看那个时间段的火焰图就行。

工具定位对比表

工具 出现时间 类型 适用场景 上手难度 运行时开销
perf 2009 采样器 CPU 热点分析、PMC 统计 中 极低
ftrace 2008 追踪器 内核函数追踪、延迟分析 高 低
BCC 2015 eBPF 工具集 复杂自定义分析、工具开发 中 低
bpftrace 2018 eBPF 脚本 快速排查、临时调查 低 低
Parca 2021 持续分析平台 全时段性能监控、回溯分析 低 极低(<1%)
Pyroscope 2021 持续分析平台 多语言应用性能分析 低 极低(<1%)

二、火焰图的威力和局限

火焰图大概是过去十年最成功的性能可视化工具了。但很多人只知道用它,不知道它的原理——更不知道它的盲区。

火焰图原理:栈采样 → 折叠 → SVG 渲染

火焰图的生成过程很简单:

  1. 采样:以固定频率(通常 99Hz)中断 CPU,记录当前的调用栈
  2. 折叠:把相同的栈路径合并,统计每条路径出现的次数
  3. 渲染:横轴是采样次数(不是时间),每个矩形是一个函数帧
# 完整的火焰图生成流程
perf record -F 99 -p $PID -g -- sleep 30
perf script > out.perf

# 折叠栈
stackcollapse-perf.pl out.perf > out.folded

# 渲染 SVG
flamegraph.pl out.folded > flamegraph.svg

关键点:火焰图的横轴不是时间轴。矩形的宽度代表的是这个函数在采样中出现的比例,不是它执行的时间顺序。这是很多人的误解。

CPU 火焰图只看 on-cpu 时间

标准的 CPU 火焰图只统计进程在 CPU 上运行的时间。也就是说,只有进程处于 TASK_RUNNING 状态时才会被采样到。

一个请求的生命周期是这样的:

请求到达 → [解析] → [查数据库] → [等锁] → [计算] → [写日志] → 响应
  CPU  等待I/O  等待  CPU  等待I/O
  on-cpu  off-cpu  off-cpu  on-cpu  off-cpu

如果这个请求花了 200ms,其中只有 40ms 在 CPU 上跑,那火焰图只能看到那 40ms 的故事。剩下的 160ms——可能是性能瓶颈的真正所在——完全不可见。

看不到的时间

CPU 火焰图看不到这些:

在我见过的生产系统里,应用的 wall clock time 中 60-80% 是 off-cpu 时间。你优化了火焰图上最宽的那个函数,P99 可能纹丝不动——因为瓶颈根本不在 CPU 上。

采样偏差

除了 off-cpu 盲区,火焰图还有采样偏差问题:


三、Off-CPU 分析

如果 on-cpu 火焰图只能看到一半的故事,那另一半怎么看?答案是 Off-CPU 分析。

核心等式

Wall Clock Time = On-CPU Time + Off-CPU Time

这个等式看似简单,但它意味着你需要两种火焰图才能看到完整的性能画面:

用 sched_switch 追踪进程休眠

Linux 内核在每次进程切换时都会触发 sched_switch tracepoint。通过 hook 这个事件,我们可以记录:

  1. 进程被调度走的时刻(off-cpu 开始)
  2. 进程被调度回来的时刻(off-cpu 结束)
  3. 被调度走时的调用栈(off-cpu 的原因)
进程 A 运行中  → sched_switch → 进程 A 休眠  → sched_switch → 进程 A 恢复运行
  记录栈 + 时间  计算休眠时长

BCC 的 offcputime 工具

BCC 自带了 offcputime 工具,开箱即用:

# 追踪进程 off-cpu 时间,持续 10 秒
offcputime-bpfcc -df -p $PID 10 > offcpu.stacks

# 生成 off-cpu 火焰图
flamegraph.pl --color=io --title="Off-CPU Time" offcpu.stacks > offcpu.svg

offcputime 的核心逻辑是这样的:

// BCC offcputime 核心逻辑(简化)
BPF_HASH(start, u32, u64);
BPF_STACK_TRACE(stack_traces, 16384);

// 进程被调度走时触发
int oncpu(struct pt_regs *ctx, struct task_struct *prev) {
  u32 pid = prev->pid;
  u64 ts = bpf_ktime_get_ns();

  // 记录被调度走的时间和栈
  start.update(&pid, &ts);
  return 0;
}

// 进程被调度回来时触发
int wakeup(struct pt_regs *ctx, struct task_struct *p) {
  u32 pid = p->pid;
  u64 *tsp = start.lookup(&pid);
  if (tsp == 0) return 0;

  // 计算 off-cpu 时间
  u64 delta = bpf_ktime_get_ns() - *tsp;

  // 聚合:栈 + 休眠时间
  // ...
  start.delete(&pid);
  return 0;
}

bpftrace 实现 off-cpu 分析

用 bpftrace 写一个简化版的 off-cpu 分析器,只需要几行:

bpftrace -e '
tracepoint:sched:sched_switch
{
  // prev_state: 1=TASK_INTERRUPTIBLE(可中断睡眠), 2=TASK_UNINTERRUPTIBLE(不可中断睡眠)
  if (args->prev_state == 1 || args->prev_state == 2) {
  @start[args->prev_pid] = nsecs;
  @stack[args->prev_pid] = kstack;
  }
}

tracepoint:sched:sched_switch
/args->next_pid != 0 && @start[args->next_pid]/
{
  $delta = nsecs - @start[args->next_pid];
  @offcpu_us[@stack[args->next_pid]] = sum($delta / 1000);
  delete(@start[args->next_pid]);
  delete(@stack[args->next_pid]);
}

END
{
  clear(@start);
  clear(@stack);
}
'

这段脚本做了三件事: 1. 进程被调度走时记录时间戳和内核栈 2. 进程被调度回来时计算休眠时长 3. 按栈聚合 off-cpu 时间

常见发现

在实际使用 off-cpu 分析时,我经常遇到这些”意外”发现:

意外的锁竞争:一个”无锁”的 Go 程序,off-cpu 火焰图里全是 runtime.lock——原来是 Go runtime 内部的 channel 操作在争锁。这种问题用 on-cpu 火焰图完全看不到。类似问题在内存分配器的 arena 设计中也有讨论,分配器内部的锁可能成为意想不到的瓶颈。

DNS 解析阻塞:一个 HTTP 服务的 P99 毛刺,off-cpu 分析发现是 getaddrinfo 在做 DNS 查询——每次超时都要等 5 秒。解决方案:本地 DNS 缓存 + 异步解析。

日志写入:同步写日志到磁盘,高负载下 write 系统调用阻塞 50ms+。Off-cpu 火焰图里一眼就能看到 ext4_file_write_iter 的高塔。

On-CPU + Off-CPU 联合分析实战

理论讲完了,来看一个真实的排查过程。场景:一个 Go 写的 API 网关,P99 延迟从 15ms 飙到 120ms,但 CPU 使用率只从 40% 涨到 55%。

第一反应:看 on-cpu 火焰图

# 用 Parca 或者手动抓 30 秒的 on-cpu 火焰图
bpftrace -e 'profile:hz:49 /comm == "api-gateway"/ { @[ustack()] = count(); }' -d 30 > oncpu.bt

On-CPU 火焰图显示:

40% — runtime.mallocgc → malloc 相关
25% — net/http.(*conn).serve
20% — encoding/json.Marshal
15% — 其他

40% 的时间在 malloc? 这不正常。但这只是 CPU 忙碌时的视图——进程不在 CPU 上的时候在干嘛?

第二步:看 off-cpu 火焰图

# 用 BCC 的 offcputime 抓 off-cpu 数据
offcputime-bpfcc -p $(pgrep api-gateway) -d 30 -f > offcpu.stacks
flamegraph.pl --color=io < offcpu.stacks > offcpu.svg

Off-CPU 火焰图显示:

60% — runtime.lock2 → sync.(*Mutex).Lock
  → connPool.Get → 连接池获取
20% — runtime.netpollblock → net.(*netFD).Read
15% — runtime.gopark → time.Sleep
 5% — 其他

60% 的等待时间在 Mutex 上! 连接池用了一把全局锁,高并发下严重争抢。

拼在一起看:

维度 占比 根因
On-CPU: malloc 40% 高频小对象分配,Go 默认分配器在多核下性能退化
Off-CPU: mutex 等待 60% 连接池全局锁,P99 延迟的主要来源

解决方案:

  1. malloc 问题:Go 1.21+ 的分配器已有改善。对热路径做对象池化(sync.Pool),减少 GC 压力。参考内存分配器竞技场的 arena 思路。
  2. Mutex 问题:把全局连接池改成分片设计——按目标 host 哈希到 N 个子池,每个子池一把锁。锁竞争从 O(并发数) 降到 O(并发数/N)。
  3. 效果:P99 从 120ms 降到 18ms。On-CPU 的 malloc 占比从 40% 降到 15%,Off-CPU 的 mutex 等待从 60% 降到 8%。

关键教训:如果你只看 on-cpu 火焰图,你会去优化 malloc——换 jemalloc、做对象池。这些都有帮助,但 P99 不会降太多,因为真正的瓶颈在锁上,而锁等待时间不消耗 CPU。 必须两张图一起看。


四、BCC vs bpftrace:深度对比

BCC 和 bpftrace 都是基于 eBPF 的工具,但它们的定位完全不同。

BCC:Python 可编程,适合复杂工具

BCC 的强项是可编程性。你可以用 Python 写复杂的数据处理逻辑,用 C 写 eBPF 内核代码,两者通过 BPF Map 通信。

# 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, delta;
  tsp = start.lookup(&tid);
  if (tsp != 0) {
  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()... Hit Ctrl-C to end.")
b.trace_print()

BCC 适合的场景: - 需要复杂的用户态后处理(聚合、过滤、报警) - 要把数据对接到 Prometheus、Grafana - 开发可复用的运维工具

bpftrace:one-liner 友好,适合临时调查

bpftrace 的强项是即时性。你不需要写文件、不需要编译、不需要引入依赖——一行命令搞定:

# 谁在读文件?读了多大?
bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret > 0/ {
  @bytes[comm] = sum(args->ret);
}'

# 哪些进程在做 DNS 查询?
bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc.so.6:getaddrinfo {
  printf("%s is resolving DNS\n", comm);
}'

# TCP 重传统计
bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb {
  @retrans[comm, args->sport, args->dport] = count();
}'

工具生态:BCC 的 100+ 预置工具

BCC 附带了超过 100 个开箱即用的工具,覆盖了几乎所有子系统:

子系统 工具 用途
CPU profile CPU 栈采样
CPU offcputime Off-CPU 时间分析
CPU cpudist CPU 使用时间分布
内存 memleak 内存泄漏检测
磁盘 biolatency 块设备 I/O 延迟分布
磁盘 biosnoop 每次 I/O 操作追踪
网络 tcplife TCP 连接生命周期
网络 tcpretrans TCP 重传追踪
文件系统 ext4slower 慢 ext4 操作
调度器 runqlat 运行队列延迟

性能开销对比

维度 BCC bpftrace
启动时间 慢(需要编译 eBPF C 代码) 较快(LLVM JIT)
运行时开销 低(内核态执行) 低(内核态执行)
内存占用 较高(Python 运行时) 低
编译依赖 需要内核头文件(或 BTF) 需要内核头文件(或 BTF)

内核版本要求

特性 最低内核版本 BCC 支持 bpftrace 支持
kprobe 4.1 是 是
tracepoint 4.7 是 是
uprobe 4.17 是 是
BTF(免头文件) 5.2 是 是
BPF ring buffer 5.8 是 是
bpf_d_path 5.10 是 是

一句话总结:bpftrace 是你的瑞士军刀,BCC 是你的工具箱。临时排查用 bpftrace,长期工具用 BCC。


五、持续性能分析:Parca 和 Pyroscope

到目前为止,所有工具都有一个共同的问题:你得在问题发生时手动去跑。但生产环境的性能问题,最讨厌的特点就是——你永远不知道它什么时候会出现。

传统 profiling 的问题

想象一下这个场景:

  1. 凌晨 3 点,报警响了:P99 延迟飙升到 2 秒
  2. 你爬起来 SSH 到机器上
  3. 跑 perf record 采样 30 秒
  4. 生成火焰图——一切正常
  5. 因为问题已经过去了

这就是传统 profiling 的致命缺陷:它是响应式的。你不可能 24/7 盯着 perf 的输出。

Continuous Profiling:始终采样,事后分析

Parca 和 Pyroscope 的解决方案是把 profiling 变成和 metrics 一样的基础设施——始终运行,始终采集,需要时再查。

核心思路:

每台机器运行 Agent(eBPF 采样)
  ↓
  低频采样(19Hz / 49Hz)
  ↓
  栈数据压缩上报
  ↓
  Server 端存储(列式存储 / 时序数据库)
  ↓
  UI 查询:选择时间段 → 看火焰图

Parca 架构详解

Parca 是 Polar Signals 开源的持续性能分析平台,架构分三层:

Agent 层(parca-agent):

# 安装 parca-agent(基于 eBPF,无需修改应用)
parca-agent \
  --remote-store-address=parca-server:7070 \
  --remote-store-insecure \
  --node=node-01 \
  --sampling-ratio=49

Agent 使用 eBPF perf_event 做栈采样,不需要修改应用代码,不需要重启进程,不需要特殊的编译选项。它自动发现机器上的所有进程,持续采集调用栈。

Server 层(parca):

# 启动 parca server
parca \
  --config-path=parca.yaml \
  --storage-active-memory=4096

Server 接收 Agent 上报的 profile 数据,存储在自研的列式存储引擎中(基于 FrostDB),支持高效的时间范围查询和聚合。

查询层(UI + API):

Parca 提供 Web UI 和 gRPC API,核心功能包括: - 时间段选择:选择任意时间范围查看火焰图 - 差异火焰图:对比两个时间段的性能差异 - 标签过滤:按节点、容器、命名空间过滤 - Icicle chart:倒置火焰图,从根到叶查看调用链

采样频率和开销控制

持续 profiling 最大的顾虑是开销。Parca 的做法:

配置 值 原因
采样频率 19Hz 或 49Hz 低于 perf 的 99Hz,减少中断开销
栈深度限制 127 帧 避免深栈采样的内存开销
上报间隔 10 秒 批量上报减少网络开销
CPU 开销 <1% 实测在大多数工作负载下
内存开销 ~50MB Agent 常驻内存

为什么用 49Hz 而不是 50Hz?因为 避免与系统定时器频率对齐(Linux 默认 HZ=250 或 1000),对齐会导致采样偏差——你总是在同一个位置采到样本。

Parca 的低开销机制:为什么生产环境敢一直开着

“持续采样会不会拖慢服务?” 这是每个 SRE 的第一反应。Parca 的答案是:开销控制在 1% 以内,代价几乎不可感知。它是怎么做到的:

1. 采样频率:19Hz,不是 100Hz

Parca Agent 默认用 perf_event 以 19Hz 采样(每秒 19 次中断)。为什么是 19 而不是 100?

2. 内核态栈去重:BPF Stack Map

关键优化:栈采样和聚合都在内核里完成,不是每次中断都把完整栈拷贝到用户态。

perf_event 中断触发
  → BPF 程序调用 bpf_get_stackid()
  → 内核在 BPF_MAP_TYPE_STACK_TRACE 中查找这个栈
  → 如果已存在:返回 stack_id,计数器 +1(零拷贝)
  → 如果不存在:存储栈帧,返回新 stack_id
  → 用户态只需要定期读取 (stack_id, count) 对

这意味着:如果同一个热点函数被采到 1000 次,内核只存储一份栈数据。用户态每次读取的数据量和不同栈的数量成正比,而不是和采样次数成正比。

3. 用户态:pprof 兼容格式 + 高效压缩

Agent 把从内核读到的 (stack_id → 栈帧, count) 数据转换成 pprof 格式(Protocol Buffers),然后:

4. 最终开销拆解:

开销来源 量级 说明
perf_event 中断处理 ~2μs × 19/s/核 每核每秒约 38μs,占 CPU 0.004%
BPF 程序执行 ~1μs × 19/s/核 栈查找 + 计数更新
用户态聚合 + 上报 ~10ms/10s 每 10 秒一次批量处理
总计 <1% CPU 实测通常在 0.1-0.5%

差异火焰图

差异火焰图是持续 profiling 最杀手级的功能。它对比两个时间段的 profile 数据,用颜色标记变化:

使用场景:
  发版前 vs 发版后 → 定位性能退化
  高峰期 vs 低谷期 → 理解负载变化
  正常节点 vs 异常节点 → 定位单点问题

这在多语言性能对比场景中特别有用——你可以直接看到不同实现在生产环境中的真实 CPU 消耗差异,而不是只依赖基准测试。

与 Prometheus / Grafana 的集成

Parca 支持把 profile 数据关联到 Prometheus 指标:

# parca.yaml 配置示例
object_storage:
  bucket:
  type: FILESYSTEM
  config:
  directory: /var/lib/parca

# Grafana 数据源配置
# Parca 提供 gRPC API,可通过 Grafana 插件查询
# 在 Grafana 面板中关联 metrics 和 profiles:
# "CPU 使用率升高" → 点击 → 查看该时段火焰图

这意味着你可以在 Grafana 里看到 CPU 使用率飙升的告警,然后一键跳转到那个时间段的火焰图——不需要 SSH,不需要手动 profiling,数据已经在那了。


六、工具选型指南

说了这么多工具,实际工作中怎么选?

按场景选工具

"服务的 P99 突然变高了"
  └→ 先看 on-cpu 火焰图(bpftrace / perf)
  └→ CPU 没问题?看 off-cpu 火焰图(offcputime)
  └→ 要持续监控?上 Parca

"内存泄漏了"
  └→ BCC memleak / bpftrace 追踪 malloc/free

"磁盘 I/O 变慢了"
  └→ BCC biolatency / biosnoop

"想搞清楚系统调用模式"
  └→ bpftrace one-liner

临时排查:bpftrace one-liner

90% 的性能问题,一行 bpftrace 就能定位方向:

# 谁在消耗 CPU?
bpftrace -e 'profile:hz:99 { @[comm] = count(); }'

# 系统调用延迟分布
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @start[tid] = nsecs; }
  tracepoint:raw_syscalls:sys_exit /@start[tid]/ {
  @ns[comm, args->id] = hist(nsecs - @start[tid]);
  delete(@start[tid]);
  }'

# 进程创建追踪
bpftrace -e 'tracepoint:syscalls:sys_enter_execve {
  printf("%s -> %s\n", comm, str(args->filename));
}'

深度调查:BCC 工具 / 自定义脚本

当 one-liner 不够用,需要更复杂的分析逻辑时,上 BCC:

# 全方位性能画像(BCC 工具套装)
# 1. CPU 热点
profile-bpfcc -F 99 -p $PID 30 | flamegraph.pl > cpu.svg

# 2. Off-CPU 时间
offcputime-bpfcc -df -p $PID 30 > offcpu.stacks
flamegraph.pl --color=io offcpu.stacks > offcpu.svg

# 3. 内存分配热点
memleak-bpfcc -p $PID -a 10

# 4. I/O 延迟
biolatency-bpfcc -D 10

# 5. TCP 连接追踪
tcplife-bpfcc -p $PID

持续监控:Parca / Pyroscope

对于生产环境,最终你需要的是持续性能分析:

# Kubernetes 部署 parca-agent(DaemonSet)
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: parca-agent
  namespace: monitoring
spec:
  selector:
  matchLabels:
  app: parca-agent
  template:
  metadata:
  labels:
  app: parca-agent
  spec:
  hostPID: true
  containers:
  - name: parca-agent
  image: ghcr.io/parca-dev/parca-agent:latest
  args:
  - "--remote-store-address=parca.monitoring:7070"
  - "--remote-store-insecure"
  securityContext:
  privileged: true
  volumeMounts:
  - name: modules
  mountPath: /lib/modules
  - name: debugfs
  mountPath: /sys/kernel/debug
  volumes:
  - name: modules
  hostPath:
  path: /lib/modules
  - name: debugfs
  hostPath:
  path: /sys/kernel/debug

生产安全考量

考量 建议
CPU 开销 持续 profiling 控制在 <1%;临时 profiling 可以到 5%
内核版本 推荐 5.8+(ring buffer 支持);最低 4.9(基础 eBPF)
权限 需要 CAP_SYS_ADMIN 或 CAP_BPF(5.8+)
安全审计 eBPF 程序受 verifier 约束,不会导致内核崩溃
数据量 持续 profiling 每节点约 10-50MB/天(压缩后)
敏感信息 调用栈可能包含函数名、文件路径——注意数据访问权限

回顾一下这条演化路线:

perf 让我们看到了 CPU 在忙什么。ftrace 让我们追踪到了内核函数调用。BCC 让我们能在内核里写自定义分析逻辑。bpftrace 让我们一行命令就能开始排查。Parca/Pyroscope 让我们再也不用担心”问题发生时我不在”。

但最重要的认知变化是这一个:你需要同时看 on-cpu 和 off-cpu 才能看到完整的性能画面。火焰图很好,但它只是故事的一半。

下次 P99 延迟飙升的时候,别只看 CPU 火焰图了。先问一句:进程不在 CPU 上的时候,它在等什么?


参考资料

  1. Brendan Gregg, BPF Performance Tools, Addison-Wesley, 2019
  2. Brendan Gregg, Off-CPU Analysis
  3. Parca 官方文档
  4. Pyroscope 官方文档
  5. BCC 工具列表
  6. bpftrace 参考指南
  7. Brendan Gregg, Flame Graphs

延伸阅读:

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-25 · linux / networking

【eBPF 系列】XDP:在网卡驱动层就把包丢掉

当 DDoS 洪水来袭,iptables 在协议栈里挣扎,而 XDP 在网卡驱动层就把垃圾包丢了。不进协议栈、不分配 skb、不走 netfilter——这才是丢包该有的样子。


By .