Victor's Code Journey
Victor's Code Journey

目录

eBPF 简介:从钩子、验证器到 BCC 实战

你有没有遇到过这种场景:线上服务偶发延迟,top 看 CPU 正常、iostat 看磁盘不忙、strace 抓不到关键调用,但应用就是慢。想看某个系统调用真实的耗时分布?想找"一闪而过"的短进程?传统工具集体失灵。

问题的根源在于:Linux 内核对你"关着门"。你想观测的所有关键路径——系统调用、网络收发、调度、文件 I/O——都在内核里,而你已经很多年没编译过内核了。

eBPF(extended Berkeley Packet Filter)就是 Linux 给开发者开的一扇窗。它让你不重启内核、不写内核模块,就能在内核的关键路径上跑一段自定义逻辑。本文用三件事讲清它:eBPF 是什么、怎么工作、怎么先用起来。

先把名字搞清楚。BPF 最早是 1992 年诞生的 Berkeley Packet Filter,顾名思义,最初就是用来过滤网络包的。2014 年,Alexei Starovoitov 大幅扩展了它,Linux 3.15 引入新版本——eBPF(extended BPF),从此它能干的事远远不止包过滤。现在 BPF 和 eBPF 两个词在社区里基本互通,老版本则被叫做 cBPF(classic BPF)以示区别。

至于 eBPF 的官方吉祥物 eBee(一只蜜蜂),纯粹是社区选出来的标志,与缩写本身无关——之所以今天还保留"BPF"三个字母,主要是因为 Linux 源码、工具链、文档里已经根深蒂固地使用着它。

那 eBPF 到底是什么?一句话定义:一种让用户编写的沙箱程序在内核特权上下文中安全运行的技术。

对比一下传统做法就更清楚。想给 Linux 内核加点新能力,过去基本只有两条路:

  • 改内核源码:发邮件给 LKML,运气好几年后合入主线,运气差被维护者打回。
  • 写内核模块:自己维护,每次内核升级都可能编译不过,还可能因为一个空指针直接搞挂整个系统。

eBPF 提供了第三条路:不改源码、不写模块,把一段程序以字节码的形式塞进内核,挂到指定钩子上。内核在加载前会做严格校验,加载后用 JIT 编译成本地指令——性能接近原生内核代码(多数 benchmark 显示在 5% 以内),安全性由验证器兜底

正因为这个特性,eBPF 在过去几年催生了一波项目:

  • 网络:Cilium 用 XDP/eBPF 替代 iptables 做 Kubernetes 的 kube-proxy;
  • 可观测性:Brendan Gregg 的整套性能分析工具链几乎都跑在 eBPF 之上;
  • 安全:Falco、Tetragon 用 eBPF 做容器运行时的入侵检测。

需要强调的是:eBPF 不是"替代内核开发",而是给内核加了一层"可编程层"。内核源码该写还得写,只是很多以前必须改内核才能做的事,现在用一段 eBPF 程序就能在用户态完成。

讲完"是什么",我们看"怎么工作"。eBPF 看似神秘,拆开看其实就是五件套的协作:钩子(Hook)验证器(Verifier)JIT 编译MapHelper 调用

eBPF 整体架构

这张图来自 eBPF.io 官方文档,把五件套的关系画得很清楚。我们按数据流方向走一遍。

eBPF 是事件驱动的。程序平时不跑,只有内核或应用程序经过某个钩子点时才被触发

钩子分两类:

  • 预定义钩子:系统调用(sys_enter_* / sys_exit_*)、函数入口/退出、网络事件(XDP、tc)、各种内核 tracepoint;
  • 动态探针:用 kprobe 探内核任意函数、uprobe 探用户态任意函数——理论上内核或应用的几乎任何位置都能挂

eBPF 钩子概览

这种"满天星"的钩子布局,正是 eBPF 能覆盖几乎所有可观测性场景的基础。

光能挂还不够。内核的特权地位决定了:任何被加载进来的代码都必须证明"我不会搞坏系统"。这就是验证器的工作。

加载一个 eBPF 程序时,验证器会做一系列静态检查:

  • 程序一定会运行到结束(不允许死循环,但允许验证器能证明"有界"的循环);
  • 不能越界访问内存,不允许用未初始化的变量;
  • 程序大小受限,不能任意膨胀;
  • 复杂度有上限,验证器必须能在限定时间内分析完所有执行路径。

如果验证不通过,程序根本进不了内核。这也是 eBPF 能在生产环境被信任的根本原因——不是"相信你不会写错",而是"在加载前就证明了不会错"

验证通过后,还会再叠一层保护:内核把 eBPF 程序存放的内存设为只读,避免运行时被篡改;常量会被"盲化"(constant blinding)以防 JIT 喷射攻击;推测执行路径也有专门的 Spectre 缓解。

eBPF 程序加载时还是通用字节码,每次执行都解释跑,性能撑不住。内核会调用 JIT(Just-in-Time)编译器 把字节码翻译成目标 CPU 的本地指令。翻译完,执行效率与手写内核代码几乎没有差距

这是 eBPF 能"上生产"的另一根支柱:沙箱安全归验证器,性能归 JIT,两者缺一不可。

一个 eBPF 程序光自己跑没用,它需要把数据交给用户态的工具来展示,或者和其他 eBPF 程序共享状态。这个角色由 Map 承担。

Map 本质上是一组键值数据结构,支持哈希表、数组、LRU、环形缓冲区、栈、LPM 等多种类型;既可以从 eBPF 程序访问,也可以通过 bpf() 系统调用从用户态访问。

Map 架构

举个直观的例子:execsnoop 工具就是用 Map 把每次捕获到的 exec() 调用信息聚合起来,用户态程序周期性读取并打印。

eBPF 程序不能直接调用任意内核函数——那样会把程序和特定内核版本死死绑在一起。

替代方案是 Helper 函数:内核提供一组稳定、版本兼容的 API,eBPF 程序只能通过 Helper 间接访问内核能力。常见的 Helper 包括:

  • bpf_ktime_get_ns():获取纳秒级时间戳;
  • bpf_get_current_pid_tgid():获取当前进程 PID;
  • bpf_probe_read():从任意地址安全读数据;
  • bpf_map_lookup_elem() / bpf_map_update_elem():操作 Map。

Helper 是 eBPF 的"系统调用",把"用什么"和"怎么用"解耦开,让同一份 eBPF 程序能跑在多个内核版本上。

走一遍完整的 eBPF 程序生命周期:

  1. 开发者用 C(或 Go/Rust)写 eBPF 程序,clang 编译成 BPF 字节码
  2. 用户态程序通过 bpf() 系统调用加载字节码;
  3. 验证器静态检查安全性,不通过直接拒绝;
  4. JIT 编译为目标 CPU 指令;
  5. 程序挂到指定钩子(kprobe / tracepoint / XDP 等);
  6. 触发时执行,通过 Map 与用户态交换数据
  7. 用 Helper 与内核交互,不直接调用内核函数

整个过程完全在用户态发起,不需要特权工具,但要求加载进程有 CAP_BPF 权限(或 root)。

讲完机制,回到一个更根本的问题:为什么 eBPF 这么重要?

eBPF 官网给了一个很精彩的类比——把镜头拉回 20 年前的浏览器。

你还记得 GeoCities 吗?20 年前,网页几乎完全由静态 HTML 写就,本质就是一个文档,靠浏览器渲染。看看今天的网页——它们已经是完整的应用程序,Web 技术已经取代了绝大多数需要编译语言编写的程序。

驱动这场革命的,是 JavaScript。它让浏览器从"渲染文档的工具"变成了"可编程的运行时"。三个关键能力缺一不可:

  • 安全:沙箱运行不受信任的代码;
  • 持续交付:不需要发布新浏览器版本就能演进功能;
  • 性能:JIT 编译保证可编程性的开销足够低。

GeoCities 时代

把这个故事搬到 Linux 内核,几乎一一对应

JavaScript 之于浏览器eBPF 之于 Linux 内核
沙箱隔离验证器(Verifier)
持续交付、不等浏览器版本热加载、不等内核版本
JIT 编译低开销JIT 编译接近原生性能

所以 eBPF 的真正意义,不在某个具体工具,而在于它让 Linux 内核的创新速度,第一次接近了应用层。过去需要等几年内核版本更新才能用的能力,现在写一段 eBPF 程序就能在生产环境用上。

更宏观地看,eBPF 改变了 Linux 内核的扩展方式。下图展示了 eBPF 在 Linux 内核架构中的位置——它不替换任何已有子系统,而是在它们之上加了一层动态扩展

Linux 内核架构中的 eBPF

看到这里你可能会想:“听起来不错,但总不能让我手写 BPF 字节码吧?”

实际上没人直接写字节码。社区已经构建了好几层抽象,按使用门槛从低到高排:

  • bcc:Python(或 Lua)前端 + C 内核探针,80+ 现成工具覆盖性能、网络、安全等场景。适合"先跑起来"。
  • bpftrace:类 awk 的高级跟踪语言,灵感来自 DTrace。适合临时排查,一行命令出直方图。
  • libbpf + CO-RE:C/C++ 库,配合 BTF(BPF Type Format)做"一次编译、跨内核版本运行"。生产环境首选
  • cilium/ebpf(Go 库):云原生场景用得最多,Cilium、Tetragon 都基于它。

BCC 工具链:开发者在用户态通过高层抽象与 eBPF 交互

配图说明:上方的 bcc.webp 展示的是抽象层面的工具链分层(高层 API → 库 → 字节码 → 内核);后文 BCC 实战末尾的 bcc-tools-2019.webp 则是 bcc 仓库提供的 80+ 工具全景图,可作为具体工具的速查参考。

对绝大多数读者,我的建议是:从 bcc 开始。门槛最低、文档最全、工具最多。当 bcc 满足不了需求(比如生产环境长期部署、对性能极致敏感),再考虑 libbpf + CO-RE。

光讲理论太空,直接动手。BCC 在主流 Linux 发行版的包管理器里都有:

# Ubuntu/Debian
sudo apt install bpfcc-tools linux-headers-$(uname -r)

# CentOS/RHEL
sudo yum install bcc-tools kernel-devel-$(uname -r)

工具装好后位于 /usr/share/bcc/tools/。下面三个工具是性能分析的"瑞士军刀"。

sudo execsnoop

输出会一行行打印新进程:

PCOMM            PID    RET ARGS
supervise        9660     0 ./run
supervise        9661     0 ./run
mkdir            9662     0 /bin/mkdir -p ./main
run              9663     0 ./run

execsnoop 监听的是 exec() 系统调用——这正是 topps 这类基于快照的工具看不到的:那些生命周期极短、还没来得及被采集到就退出的进程。典型应用是发现被漏掉的 fork bomb 或反复拉起的定时任务。

sudo opensnoop
PID    COMM               FD ERR PATH
1565   redis-server        5   0 /proc/1565/stat
1603   snmpd               9   0 /proc/net/dev
1603   snmpd              -1   2 /sys/class/net/eth0/device/vendor

open() 系统调用能告诉你"这个进程到底在读什么"——配置文件、数据文件、日志文件都一览无余。如果一个应用性能差、行为诡异,先看它在反复 open 哪些不存在的文件,往往能直接定位问题。

sudo biolatency
Tracing block device I/O... Hit Ctrl-C to end.
^C
     usecs           : count     distribution
       0 -> 1        : 0        |                                      |
       2 -> 3        : 0        |                                      |
       ...
     128 -> 255      : 12       |********                              |
     256 -> 511      : 15       |**********                            |
     512 -> 1023     : 43       |*******************************       |
    1024 -> 2047     : 52       |**************************************|
    2048 -> 4095     : 47       |**********************************    |
    4096 -> 8191     : 52       |**************************************|
    8192 -> 16383    : 36       |**************************            |

iostat 给你的平均延迟会把长尾"平均"掉,biolatency直方图告诉你真实分布。多模分布、长尾、突发尖刺,一眼可见。

这三个工具覆盖了进程、文件、I/O 三个维度,是分析问题的"最小可用集"。BCC 仓库里还有更多工具,按主题归类如下:

BCC 工具全景(2019 版)

最后记住一句口诀:uptimedmesgvmstatmpstatpidstatiostatsartop(Netflix 60 秒排查法),确认大致方向后再上 BCC 工具做"显微镜"。

  1. eBPF = 内核沙箱可编程:在不改源码、不写模块的前提下,把用户态程序安全地放进内核跑。
  2. 安全靠验证器,性能靠 JIT:前者把"坏程序"挡在门外,后者让"好程序"跑得飞快。
  3. Map 是数据通道,Helper 是 API:eBPF 与用户态通信靠 Map,访问内核能力靠 Helper。
  4. bcc 是最低门槛的入门工具链:80+ 现成工具,三行命令就能看到内核真相。

eBPF 也不是银弹:

  • 不适合替换所有内核逻辑:极度延迟敏感的关键路径仍要谨慎;
  • 写 eBPF 程序比写 Go 服务门槛高:需要懂 C、内核概念、BPF 指令集;
  • 内核版本有要求:大量工具需要 Linux 4.x 及以上。

别从内核源码学起,从一行 execsnoop 开始

  1. 入门:跑一遍 BCC 工具集,读 Brendan Gregg 的 BPF Performance Tools(强烈推荐)或 O’Reilly 的 Learning eBPF
  2. 进阶:学习 bpftrace 语法,能用一两行命令解决具体问题;
  3. 生产:研究 libbpf + CO-RE,理解 BTF、ring buffer 等现代特性;
  4. 前沿:读 Cilium、Tetragon、bpfman 等项目的源码,参与 eBPF 社区。

那只叫 eBee 的蜜蜂,已经悄悄飞进了 Linux 内核的每一个角落。下次线上出问题又抓不到根因时,记得把它放出来。

相关内容