项目介绍

介绍

这个项目是一个用 C++ 实现的 Linux 采样性能分析器,底层基于 eBPF 和 perf events。它的目标是像 perf 一样周期性采集程序运行时的内核态和用户态调用栈,然后在用户态做符号化,最后输出可读的调用栈信息,或者输出 FlameGraph 兼容的 folded 格式,用来生成火焰图。

我主要做的事情可以分成几块:

第一块是内核侧采集。我写了一个 eBPF perf_event 程序,在采样事件触发时通过 bpf_get_stack 同时抓取 kernel stack 和 user stack,再把 PID、CPU、时间戳、进程名和栈地址通过 ring buffer 传回用户态。

第二块是用户态控制程序。C++ 侧负责解析命令行参数、加载 BPF skeleton、初始化 perf event、给每个 CPU 绑定采样事件,并持续从 ring buffer 读取采样数据。这里支持设置采样频率、指定 PID、切换硬件事件和软件事件,也支持关闭符号化用于纯采集 benchmark。

第三块是符号化和输出。我接入了 blazesym,把原始地址解析成函数名、源码位置和内联函数信息。输出上做了两种格式:一种是标准的可读调用栈,方便直接看;另一种是 folded 格式,能直接喂给 FlameGraph 生成火焰图。

项目亮点

这个项目的亮点我会强调三个:

  1. 它不是简单地调用 perf,而是自己打通了 perf_event -> eBPF -> ring buffer -> C++ 用户态处理 -> blazesym 符号化 -> FlameGraph 输出 这一整条链路。
  2. 它同时支持用户态和内核态栈,并且能按 PID 过滤,既可以看整个系统,也可以聚焦某个进程。
  3. 我专门做了和 perf 的 benchmark 对比,而且没有简单宣传“比 perf 快”。目前结果是:仅看 collect-only 采集阶段,当前实现开销比 perf record 高;但如果看 end-to-end,也就是 perf record + perf script/report 的完整流程,这个项目的总开销更低。这个结论更真实,也更能说明我理解性能指标的口径差异。

麻烦点解决

实现过程中比较麻烦的点主要有几个:

一个是 eBPF 和 perf event 的配合。采样不是由 BPF 自己定时触发,而是要通过 perf_event_open 在每个 CPU 上创建事件,再把 BPF 程序 attach 到这些 perf events 上。这里涉及 CPU 数量、硬件事件和软件事件兼容性、权限、文件描述符生命周期管理等问题。

第二个是用户栈符号化。eBPF 采到的是地址,真正想变成函数名,需要结合目标进程的地址空间、ELF、debug info、内核符号等信息。这里我用了 blazesym 来处理,但还要区分 pid 为 0 的内核符号化和普通进程的用户态符号化。

第三个是性能对比的口径。最开始很容易只看一个数字,比如“相比 perf 降低多少开销”,但后来发现 collect-only 和 end-to-end 得出的结论不同。所以我补了 benchmark 脚本,用多轮运行统计 mean、std、p95,并把结论改成更准确的表达:不同口径下有不同优势。

FlameGraph 兼容格式是什么样的

FlameGraph 兼容的 folded 格式,本质上就是“每一行表示一条调用栈 + 这条调用栈出现的次数”。

格式大概是:

1
栈底函数;中间函数;栈顶函数 次数

比如:

1
2
3
main;handle_request;parse_json;malloc 17
main;handle_request;query_db;read 9
main;background_worker;compress;zlib_deflate 4

含义是:

  • ; 用来分隔调用栈里的每一层函数
  • 左边从栈底到栈顶排列,也就是调用链从外到内
  • 最后的空格后面是采样次数 / 权重
  • FlameGraph 会把相同前缀的调用路径合并,宽度由次数决定

在你的项目里,扩展 folded 输出大概是这样生成的:

1
进程名-pid;用户态函数1;用户态函数2;内核态函数_[k] 1

例如可能输出:

1
yes-12345;main;do_output;write;entry_SYSCALL_64_after_hwframe_[k];do_syscall_64_[k] 1

这里最后的 1 表示这一行代表一次采样。后续 flamegraph.pl 会把完全相同或有共同前缀的调用栈聚合起来。

你的代码里对应逻辑在 src/event.cpphandle_fold_extend:它先把 comm-pid 放在栈底,然后把用户态栈、内核态栈反转后拼起来,最后用分号连接并输出:

1
stack;frames 1

所以 folded 格式不是给人精读的,它是给 FlameGraph 这类工具做聚合和可视化用的中间格式。

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计