eBPF:内核可编程的革命

传统性能分析工具分两类:一类是 topvmstatiostat 这样的"概览型"工具,告诉你系统层面发生了什么,但看不到细节;另一类是 stracegdb 这样的"跟踪型"工具,能看细节但开销巨大,生产环境基本不敢用。

eBPF(Extended Berkeley Packet Filter)改变了一切。它允许你在不修改内核源码、不加载内核模块的前提下,安全地在内核中运行沙箱程序。这些程序挂载到内核的 hook 点(kprobes、tracepoints、perf events 等),在事件发生时被触发执行,采集数据后通过 ring buffer 传递到用户态。

为什么说这是革命性的?三个原因:

  1. 安全:eBPF 程序在加载时经过验证器(verifier)检查,确保不会死循环、不会非法访问内存,不需要 root 权限加载内核模块的风险。
  2. 低开销:eBPF 程序在内核态直接执行,只有采集到的数据才通过 ring buffer 拷贝到用户态,热路径上的开销极小。
  3. 可编程:你可以针对自己的具体问题编写精确的探针程序,而不是依赖通用工具"凑合"用。

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_readsyscalls/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 工具的兼容性是最大的生产坑。关键要点:

  1. 内核 >= 5.0 时建议启用 BTF 支持。BTF 提供了内核数据结构的类型信息,使 eBPF 程序能自适应不同内核版本的结构体布局差异(CO-RE 技术)。
  2. CentOS 7 内核 3.10 下大量 bcc 工具不可用或功能受限。建议升级内核(通过 ELRepo 安装 kernel-ltkernel-ml)再使用。
  3. 内核升级后需重新安装 kernel-headers,否则 bcc 运行时会因找不到头文件而报错:
# 每次升级内核后执行
sudo apt install -y linux-headers-$(uname -r)
# 或
sudo dnf install -y kernel-devel-$(uname -r)

性能开销控制

eBPF 本身开销极小,但不代表可以无限制使用。生产环境使用要点:

  1. 避免高频事件探针:挂载到 sched_switch(上下文切换)等每秒触发数万次的探针时,即使单次开销只有 1 微秒,累积起来也会显著影响性能。在高负载系统上,优先使用统计聚合(histogram)而非逐事件输出。
  2. 使用 ring buffer 替代 perf buffer:内核 4.6+ 支持 ring buffer,批量传递数据,减少用户态/内核态切换次数。
  3. 设置超时退出:生产环境使用 bcc 工具时始终加上超时参数(如 biolatency 10),避免长时间运行遗忘关闭。
  4. 避免 -p 追踪高并发进程的所有事件:对 Nginx、MySQL 这类高并发进程,-p 追踪每个连接会产生海量事件。应缩小追踪范围,如只追踪特定端口。

安全与权限

所有 bcc 工具都需要 CAP_SYS_ADMIN 或 root 权限才能加载 BPF 程序。在生产环境中:

  • 不要给应用容器授予 CAP_BPFCAP_SYS_ADMIN
  • 在 Kubernetes 集群中,使用专门的 DaemonSet 运行 bcc 工具,通过 hostPath 挂载内核符号表和 debugfs。
  • 对于需要长期运行的 eBPF 观测程序,考虑使用 Polycube 或 Cilium 等更成熟的平台级方案,而非直接在生产节点上跑 bcc 脚本。

小结

eBPF + bcc 是 Linux 性能分析的瑞士军刀。与传统工具相比,它能以极低的开销深入内核细节,从磁盘延迟分布到 TCP 连接生命周期,从进程执行追踪到文件打开监控,覆盖了性能排查的各个层面。

掌握 eBPF 不只是学会用几个工具,更关键的是理解它提供的可编程能力——当内置工具无法回答你的问题时,你可以编写自定义探针,精确地问出你想问的问题。这正是 eBPF 被称为"内核可编程革命"的原因。

延伸阅读:Brendan Gregg 的 BPF Performance Tools 是目前最权威的 eBPF 性能分析著作,涵盖了 100+ 个工具的使用场景和输出解读。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. BPF Compiler Collection (BCC) 官方仓库 — GitHub 开源社区,参考了BPF Compiler Collection (BCC) 官方仓库相关内容
  2. Brendan Gregg 的 BPF Performance Tools — Brendan Gregg,参考了Brendan Gregg 的 BPF Performance Tools相关内容