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

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

目录

你打开一张 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):函数追踪框架

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

# 追踪 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 多个现成工具(execsnoopbiolatencytcplife……),一行命令就能用。但它的问题是每次运行都要编译 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_ADMINCAP_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

延伸阅读


By .