eBPF:内核可编程的革命
传统性能分析工具分两类:一类是 top、vmstat、iostat 这样的"概览型"工具,告诉你系统层面发生了什么,但看不到细节;另一类是 strace、gdb 这样的"跟踪型"工具,能看细节但开销巨大,生产环境基本不敢用。
eBPF(Extended Berkeley Packet Filter)改变了一切。它允许你在不修改内核源码、不加载内核模块的前提下,安全地在内核中运行沙箱程序。这些程序挂载到内核的 hook 点(kprobes、tracepoints、perf events 等),在事件发生时被触发执行,采集数据后通过 ring buffer 传递到用户态。
为什么说这是革命性的?三个原因:
- 安全:eBPF 程序在加载时经过验证器(verifier)检查,确保不会死循环、不会非法访问内存,不需要 root 权限加载内核模块的风险。
- 低开销:eBPF 程序在内核态直接执行,只有采集到的数据才通过 ring buffer 拷贝到用户态,热路径上的开销极小。
- 可编程:你可以针对自己的具体问题编写精确的探针程序,而不是依赖通用工具"凑合"用。
eBPF 的完整技术文档可参考 BPF Compiler Collection (BCC) 官方仓库,本文所有工具均来自该项目。
bcc 工具集安装
bcc(BPF Compiler Collection)是基于 eBPF 的性能分析工具集,由 iovisor 项目维护,提供了数十个开箱即用的追踪工具。
Ubuntu / Debian
# Ubuntu 20.04+ 推荐方式
sudo apt update
sudo apt install -y bpfcc-tools linux-headers-$(uname -r)
# 验证安装
/usr/share/bcc/tools/biolatency --help
CentOS / RHEL
# CentOS 8 / RHEL 8+
sudo dnf install -y bcc-tools kernel-devel-$(uname -r)
# CentOS 7 需通过 ELRepo
sudo yum install -y epel-release
sudo yum install -y bcc-tools kernel-devel-$(uname -r)
# 工具默认安装在 /usr/share/bcc/tools/ 目录下
ls /usr/share/bcc/tools/
安装后建议将工具路径加入 PATH:
echo 'export PATH=$PATH:/usr/share/bcc/tools' >> ~/.bashrc
source ~/.bashrc
内核版本要求
bcc 依赖的 eBPF 特性随内核版本演进而增强。以下是关键功能与内核版本的对应关系:
| 功能特性 | 最低内核版本 |
|---|---|
| 基础 kprobe 追踪 | 4.1+ |
| perf event 追踪 | 4.3+ |
| 环形缓冲区(ring buffer) | 4.6+ |
| BTF(BPF Type Format)支持 | 5.0+ |
| CO-RE(Compile Once - Run Everywhere) | 5.0+ |
核心工具实战
biolatency:磁盘 I/O 延迟分布
biolatency 追踪块设备 I/O 的延迟分布,输出直方图。它比 iostat 更有价值——iostat 给的是平均值,而 biolatency 给的是分布,能帮你发现尾部延迟问题。
# 追踪所有块设备 10 秒
sudo biolatency 10
# 输出示例
# usecs : count distribution
# 0 -> 1 : 0 | |
# 2 -> 3 : 0 | |
# 4 -> 7 : 0 | |
# 8 -> 15 : 12 | |
# 16 -> 31 : 45 |* |
# 32 -> 63 : 128 |**** |
# 64 -> 127 : 340 |*********** |
# 128 -> 255 : 890 |***************************** |
# 256 -> 511 : 567 |****************** |
# 512 -> 1023 : 234 |******* |
# 1024 -> 2047 : 89 |** |
# 2048 -> 4095 : 23 | |
# 4096 -> 8191 : 5 | |
从上面的输出可以看到,大部分 I/O 延迟在 128-255 微秒区间,但有少量 I/O 超过 2ms,这些尾部延迟就是需要关注的对象。
如果需要按磁盘区分,加上 -d 参数:
# 按磁盘分别统计
sudo biolatency -d 10
# 如果想追踪特定进程的 I/O 延迟
sudo biolatency -p $(pidof mysqld) 10
execsnoop:进程执行追踪
execsnoop 追踪新进程的 exec() 系统调用,实时输出每个新进程的 PID、PPID 和命令行。在排查"短命进程"(如 cron 任务、脚本调用链)时极为有用。
sudo execsnoop
# 输出示例
# PCOMM PID PPID RET ARGS
# bash 4567 4566 0 /bin/bash
# ls 4568 4567 0 /bin/ls --color=auto -la
# grep 4569 4567 0 /bin/grep --color=auto foo
# python3 4570 1 0 /usr/bin/python3 /opt/app/main.py
一个典型场景:线上服务器 CPU 突然飙升但 top 看不到对应进程——很可能是有短命进程在反复启动。execsnoop 能瞬间抓到它们:
# 统计 10 秒内执行频率最高的程序
sudo execsnoop 2>&1 | tail -n +2 | awk '{print $1}' | sort | uniq -c | sort -rn | head
tcplife:TCP 连接生命周期
tcplife 追踪 TCP 连接的生命周期,输出每次连接的源/目的地址、端口、传输字节数和持续时间。在排查"连接耗时到底花在哪"时非常有效。
sudo tcplife
# 输出示例
# PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS
# 4567 curl 10.0.1.5 43822 93.184.216.34 443 2 12 145
# 4568 nginx 10.0.1.5 80 192.168.1.100 54321 0 45 2300
# 4569 mysqld 10.0.1.5 3306 192.168.1.200 48932 1 8 56
关注 MS(毫秒)列,可以快速定位慢连接。例如一条 MySQL 连接如果持续了几十秒但传输量很小,可能是在等待锁。
opensnoop:文件打开追踪
opensnoop 追踪所有进程的 open() 系统调用,输出正在被打开的文件路径和返回值。排查"程序在读取哪个配置文件"或"哪个进程在疯狂打开文件"时一击必中。
sudo opensnoop
# 只追踪特定进程
sudo opensnoop -p $(pidof nginx)
# 只追踪打开失败的请求(排查权限问题)
sudo opensnoop -e
# 输出示例
# PID COMM FD ERR PATH
# 4567 nginx 12 0 /etc/nginx/nginx.conf
# 4567 nginx 13 0 /etc/nginx/conf.d/default.conf
# 4567 nginx 14 -1 /etc/nginx/ssl/private.key
# 4568 mysqld 15 0 /var/lib/mysql/ibdata1
上面 ERR 列为 -1 表示打开失败,PATH 指明了失败的文件——这就是权限问题的现场。
自定义 BPF trace 脚本
当内置工具无法满足需求时,可以编写自定义 BPF 脚本。bcc 支持用 Python 编写用户态逻辑,用内嵌的 C 代码编写内核态探针。
下面是一个统计各进程 read() 系统调用次数和读取字节数的自定义脚本:
#!/usr/bin/env python3
"""
readsize.py - 统计各进程 read() 调用次数和读取字节数
用法: sudo python3 readsize.py [持续时间秒数]
"""
from bcc import BPF
from time import sleep
# BPF C 代码:挂载到 read 系统调用的入口和返回
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
// 用于在入口和返回之间传递 fd 参数
struct val_t {
u64 fd;
};
// 用于在入口和返回之间传递 fd 参数
BPF_HASH(args, u32, struct val_t);
// 统计结果:进程名 -> [调用次数, 读取字节数]
struct info_t {
u64 count;
u64 bytes;
};
BPF_HASH(info, u32, struct info_t);
// read 入口探针:记录 fd
TRACEPOINT_PROBE(syscalls, sys_enter_read) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
struct val_t val = {.fd = args->fd};
args.update(&pid, &val);
return 0;
}
// read 返回探针:统计返回值(即读取的字节数)
TRACEPOINT_PROBE(syscalls, sys_exit_read) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
struct val_t *valp = args.lookup(&pid);
if (valp == 0) {
return 0; // 没有对应的入口记录,跳过
}
args.delete(&pid);
// 获取返回值(读取的字节数)
int ret = args->ret;
if (ret < 0) {
return 0; // read 失败,不计入
}
// 更新统计信息
struct info_t zero = {0, 0};
struct info_t *infop = info.lookup_or_init(&pid, &zero);
infop->count += 1;
infop->bytes += ret;
return 0;
}
"""
b = BPF(text=bpf_text)
# 前端:周期性输出统计结果
print("Tracing read() syscall... Hit Ctrl-C to end.")
try:
sleep(99999)
except KeyboardInterrupt:
print("\n%-16s %-8s %12s %12s" % ("COMM", "PID", "CALLS", "BYTES"))
print("-" * 52)
info = b.get_table("info")
for k, v in sorted(info.items(), key=lambda x: x[1].bytes, reverse=True):
comm = b.get_kprobe_functions # placeholder
# 获取进程名
try:
comm_str = b.ksymname(k.value) or b"<unknown>"
except Exception:
comm_str = b"<unknown>"
print("%-16s %-8d %12d %12d" % (comm_str[:16], k.value, v.count, v.bytes))
上述脚本使用 tracepoint 探针(
syscalls/sys_enter_read和syscalls/sys_exit_read),相比 kprobe 更稳定——kprobe 绑定内核函数符号,内核版本升级后符号可能变化;tracepoint 绑定的是内核约定的事件名称,跨版本兼容性更好。
脚本使用方式:
sudo python3 readsize.py
# 输出示例
# Tracing read() syscall... Hit Ctrl-C to end.
# ^C
# COMM PID CALLS BYTES
# ----------------------------------------------------
# mysqld 4567 89023 234567890
# nginx 4589 23456 89012345
# python3 4590 1234 5678901
更简洁的写法可以使用 bcc 提供的 BPF.trace_print() 或直接利用 @ 语法(BPF C 中使用 map 的简化写法),但上面的完整写法更适合理解底层原理。
生产环境注意事项
内核版本兼容性
bcc 工具的兼容性是最大的生产坑。关键要点:
- 内核 >= 5.0 时建议启用 BTF 支持。BTF 提供了内核数据结构的类型信息,使 eBPF 程序能自适应不同内核版本的结构体布局差异(CO-RE 技术)。
- CentOS 7 内核 3.10 下大量 bcc 工具不可用或功能受限。建议升级内核(通过 ELRepo 安装
kernel-lt或kernel-ml)再使用。 - 内核升级后需重新安装
kernel-headers,否则 bcc 运行时会因找不到头文件而报错:
# 每次升级内核后执行
sudo apt install -y linux-headers-$(uname -r)
# 或
sudo dnf install -y kernel-devel-$(uname -r)
性能开销控制
eBPF 本身开销极小,但不代表可以无限制使用。生产环境使用要点:
- 避免高频事件探针:挂载到
sched_switch(上下文切换)等每秒触发数万次的探针时,即使单次开销只有 1 微秒,累积起来也会显著影响性能。在高负载系统上,优先使用统计聚合(histogram)而非逐事件输出。 - 使用 ring buffer 替代 perf buffer:内核 4.6+ 支持 ring buffer,批量传递数据,减少用户态/内核态切换次数。
- 设置超时退出:生产环境使用 bcc 工具时始终加上超时参数(如
biolatency 10),避免长时间运行遗忘关闭。 - 避免
-p追踪高并发进程的所有事件:对 Nginx、MySQL 这类高并发进程,-p追踪每个连接会产生海量事件。应缩小追踪范围,如只追踪特定端口。
安全与权限
所有 bcc 工具都需要 CAP_SYS_ADMIN 或 root 权限才能加载 BPF 程序。在生产环境中:
- 不要给应用容器授予
CAP_BPF或CAP_SYS_ADMIN。 - 在 Kubernetes 集群中,使用专门的 DaemonSet 运行 bcc 工具,通过 hostPath 挂载内核符号表和 debugfs。
- 对于需要长期运行的 eBPF 观测程序,考虑使用 Polycube 或 Cilium 等更成熟的平台级方案,而非直接在生产节点上跑 bcc 脚本。
小结
eBPF + bcc 是 Linux 性能分析的瑞士军刀。与传统工具相比,它能以极低的开销深入内核细节,从磁盘延迟分布到 TCP 连接生命周期,从进程执行追踪到文件打开监控,覆盖了性能排查的各个层面。
掌握 eBPF 不只是学会用几个工具,更关键的是理解它提供的可编程能力——当内置工具无法回答你的问题时,你可以编写自定义探针,精确地问出你想问的问题。这正是 eBPF 被称为"内核可编程革命"的原因。
延伸阅读:Brendan Gregg 的 BPF Performance Tools 是目前最权威的 eBPF 性能分析著作,涵盖了 100+ 个工具的使用场景和输出解读。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- BPF Compiler Collection (BCC) 官方仓库 — GitHub 开源社区,参考了BPF Compiler Collection (BCC) 官方仓库相关内容
- Brendan Gregg 的 BPF Performance Tools — Brendan Gregg,参考了Brendan Gregg 的 BPF Performance Tools相关内容