最近几年只要聊到 Linux 内核可观测性,几乎绕不开 eBPF 这个词。无论你是搞网络、搞安全、搞性能调优,还是纯粹在做云原生基础设施,都会发现 eBPF 正在悄悄改变我们和内核打交道的方式。我自己第一次真正被 eBPF 震撼到,是在一次生产环境故障排查里——那个问题用传统的 strace、perf、tcpdump 轮番上阵都看不到根因,最后是靠一个几十行的 bpftrace 脚本直接在内核态抓到了关键事件。那种感觉就像一直在门外透过猫眼往里看,突然拿到了钥匙直接走进房间。
这篇文章我想从一个实际使用者的角度,把 eBPF 的核心机制、工具链选型、真实应用场景和踩过的坑讲清楚。不会堆砌太多内核源码,尽量用说人话的方式,带你把 eBPF 这块硬骨头嚼碎。适合谁看?如果你对 Linux 内核有一定基础但一直没搞懂 eBPF 到底是什么,或者你已经在用 bcc / bpftrace 但想知道底层怎么工作的,又或者你正打算在自己的项目里引入 eBPF 做可观测性,这篇文章都能给你一个从原理到实战的完整视角。
1. 传统内核观测手段的短板:为什么 eBPF 会出现
1.1 还在用 /proc、strace、tcpdump 的痛
在 eBPF 出现之前,我们做内核态观测基本就三板斧:读 /proc 文件、用 strace 跟踪系统调用、用 tcpdump 抓网络包。这三样东西单独看都很好用,但放到真实的生产排障场景里,各自的局限就非常明显了。
/proc 这种方式本质上是内核主动把一些统计信息暴露给用户态,你看到的是内核想让你看到的东西,而不是你想看的东西。比如你想知道某个进程为什么频繁唤醒,/proc 里没有这种细粒度信息。strace 看着强大,但它走的是 ptrace 机制,每一次系统调用都要经过用户态和内核态的来回切换,性能开销大得吓人。在高并发的生产环境上跑 strace,那个进程的性能会直接被拖垮,相当于给赛车的轮子捆上沙袋。
tcpdump 的问题更隐蔽。它底层用的是 libpcap,在传统实现里,数据包需要先从网卡复制到内核缓冲区,再从内核缓冲区复制到用户态缓冲区,中间还有可能触发软中断。在万兆甚至更高带宽的链路上,这种复制带来的 CPU 开销和延迟是很多人不能接受的。更关键的是,你只能看到包进包出,看不到包为什么被丢、谁改写了包头、socket 缓冲区里到底发生了什么。
1.2 想改内核又没有胆量
那有人问了:既然用户态工具不够用,我直接把代码写进内核不就行了?理论上这是最彻底的方案——在内核里加一个 proc 文件、加一个 tracepoint、甚至直接改动某个子系统,想要什么观测能力就自己写。但现实是,内核代码的合入门槛非常高。Linux 内核的 review 机制极其严格,哪怕是一个小小的 tracepoint 改动,也要过 maintainer 的层层把关。一次提交从发送 patch 到最终合入主线,短则几个星期,长则几个月甚至一两年。
就算你不想合入主线,只想在自己的服务器上改,那也意味着你要维护一个私有内核分支。内核的编译时间动不动就是几十分钟,遇到安全补丁还要重新 merge、重新编译、重新滚动升级,运维成本直接翻倍。更麻烦的是,改出来的行为如果没经过充分测试,可能就是一场灾难。我记得业内有个笑谈:在内核里加代码,就像在飞机飞行途中换引擎——不是不行,但你要有必死的觉悟。
1.3 内核模块:看着自由,实则戴着镣铐
在内核模块(Loadable Kernel Module, LKM)出现之后,很多人觉得找到了捷径:不需要改主线内核,只需要加载一个 .ko 文件就行。但内核模块的问题在于权限太大了。模块代码运行在内核态,拥有最高权限,任何一个指针错误直接就是 kernel panic,连 oops 都可能是轻的。而且模块和具体内核版本强绑定,换个内核版本就要重新编译一遍模块,维护成本一点没少。
更麻烦的是,内核模块几乎不受任何安全约束。一个写得不严谨的模块,可能覆盖了别人的数据结构、劫持了函数指针、甚至是污染了内核的全局状态。这也是为什么很多生产环境明确禁止加载非官方内核模块——审计风险和安全风险都太高了。
传统手段各有各的不足,那有没有一种方式,既能深入内核内部看细节,又不需要改动内核源码,还能保证运行时的安全性?这正是 eBPF 登场的核心原因。它不是简单地改良了某个工具,而是从机制层面提供了一套"安全地把代码送进内核执行"的通用框架。你写的观测逻辑可以直接挂在内核的任意探针点上,但每一个指令都必须先通过内核验证器的安全检查,就像给每个进内核的"访客"都装上了强制安检门。
2. eBPF 核心机制拆解:虚拟机、验证器与钩子
2.1 从 BPF 到 eBPF 的演进
eBPF 的全称是 Extended Berkeley Packet Filter,它的前身 BPF 最初只是为了高效过滤网络数据包,设计目标非常朴素:在内核态做一个简单的指令虚拟机,对每一个到达的数据包跑一段过滤程序,决定"放行还是丢弃"。经典的 tcpdump 过滤表达式,最终都会被编译成 BPF 指令。
2014 年前后,内核社区对 BPF 做了一次全面升级,这就是 eBPF。和老的 cBPF 相比,eBPF 把指令集从 2 条扩展到了 11 条 64 位指令,引入了寄存器数量更多、更接近真实机器的虚拟机模型,还加入了 BPF Map 这种和用户态共享数据的机制。最重要的一点是,eBPF 程序不再局限于网络包过滤,它可以挂载到内核里几乎任何感兴趣的事件点上:函数入口、函数返回、tracepoint、kprobe、perf event,甚至网络报文经过的 TC 钩子和 XDP 钩子。
用一句话概括:eBPF 把内核变成了一台可以安全执行用户自定义字节码的"可编程机器",而可观测性只是它的第一个大规模落地场景。
2.2 钩子机制:决定你从哪里切入内核
eBPF 程序要发挥作用,第一步是选择一个挂载点,也就是"钩子"。钩子的选择很大程度上决定了你看到的是什么维度的数据。
- kprobe / kretprobe:动态探针,可以挂在内核函数的入口和返回处。比如你想知道每次调用
tcp_v4_connect时传入的目标地址,用 kprobe 挂上就行。kprobe 的灵活度最高,但它依赖内核函数符号,内核版本升级后函数名变了,程序就失效了。 - tracepoint:静态探针,由内核开发者主动埋点。它比 kprobe 稳定得多,因为 tracepoint 是一等公民,不会随便改。缺点是覆盖范围有限,内核没埋点的地方你无处下手。
- perf_event:用于性能事件,比如 CPU 周期、缓存命中率、内存访问延迟等,适合做 profiling。
- XDP / TC:挂在网络数据路径上,XDP 在网卡驱动收到的包的最早阶段执行,适合做高性能包处理;TC 挂在协议栈的入口和出口,适合做流量整形和观测。
选钩子的原则其实就一条:优先用 tracepoint,因为稳定;tracepoint 覆盖不到再用 kprobe;如果是网络场景,考虑 XDP 和 TC 这种专项钩子。我见过不少新手一上来就全用 kprobe,结果升了个内核版本脚本全挂,这就是没想清楚钩子稳定性的代价。
2.3 验证器:eBPF 安全的守门人
eBPF 允许普通用户加载代码进内核,这听起来非常危险。凭什么信任这段代码?答案是验证器(verifier)。验证器是整个 eBPF 机制里最复杂的组件,它会对每一个加载进来的字节码做静态分析和模拟执行,确保它不会做任何危险的事。
验证器主要检查这几类问题:
- 指令是否合法:操作码、寄存器使用是否符合规范
- 是否有越界访问:对栈、Map、数据包缓冲区的访问必须边界可控
- 是否会死循环:eBPF 程序不允许无限循环,早期的验证器甚至要求程序必须有确定的退出路径
- 空指针、未初始化变量、类型不匹配:这些在内核模块里可能导致崩溃的问题,在 eBPF 里一律禁止
验证器的检查机制直观理解,就是它真的会"模拟走一遍"你的程序,把每一步的寄存器状态、指针类型、可能的值范围都记录下来。比如你从数据包里读取了一个字段作为数组下标,验证器会尝试所有可能的值,任何一个分支越界,程序直接被拒。
所以 eBPF 程序的编写风格和普通 C 程序很不一样。你不能依赖运行时检查兜底,必须在代码里给验证器提供足够的边界信息。写过 eBPF 的人都有体会:代码逻辑本身不难,难的是怎么让验证器满意。对 Map 的大小做动态判断、对指针做复杂的条件分支,都容易撞上验证器的限制,这时候往往需要通过调整代码逻辑来"哄好"它。
2.4 BPF Map:用户态与内核态的数据桥梁
eBPF 程序跑在内核态,但它产生的数据最终要给用户态的程序用。这个数据交换的通道就是 BPF Map。Map 本质上是一组在内核内存里维护的键值存储结构,支持哈希表、数组、环形缓冲区、栈等不同形态。
Map 的设计很有意思。它不只是简单地把数据从内核复制到用户态,更像是一块双方都能读写、通过文件描述符访问的共享内存。内核态的 eBPF 程序可以往 Map 里写数据,用户态的程序可以通过bpf_map_lookup_elem这类系统调用去读。两者之间还可以建立事件通知机制,比如 map 有新数据时,用户态通过perf_event或者 ring buffer 立刻收到消息。
在实际的可观测性程序里,常见的玩法是:eBPF 程序采集事件,把聚合结果写进 Map;用户态程序周期性地从 Map 里读取统计值,做展示或告警。还有更高级的玩法,Map 可以把多个 eBPF 程序串联起来,实现类似"程序协作"的效果,比如一个程序负责过滤,另一个程序负责处理。
2.5 一个生活化的类比
如果你想快速给身边的朋友解释 eBPF 是什么,可以这样讲:传统的内核就像一栋管理森严的大楼,你只能在楼外透过窗户往里面看(/proc),或者在门口登记进出记录(strace),想知道楼里某个房间具体发生了什么几乎不可能。eBPF 相当于给你发了张临时通行证,你可以在大楼里装各种传感器,但每一个传感器的安装位置、工作方式、数据采集范围都必须经过严格审批(验证器),并且传感器只能通过固定的接口(Map)往外传数据。
这个类比虽然简单,但能抓住三个要点:深入性(能进内核内部)、安全性(审批机制)、可编程性(传感器由你自定义)。
3. 可观测性实战:从工具链选型到 Hello World
3.1 bcc、bpftrace、libbpf 怎么选
搞清楚原理之后,下一步就是上手。目前主流的 eBPF 开发方式有三条路:bcc、bpftrace 和 libbpf。很多初学者在这三者的选择上会很纠结,我的建议是先甭管底层差异,按场景来选。
bpftrace 是写一次性观测脚本的首选。它的语法像 awk,还支持类似 printf 的输出格式,把所有 eBPF 的复杂性封装得干干净净。几十行脚本就能跟踪进程系统调用、统计函数耗时、聚合 IO 延迟,简直是为排查问题量身定制。
bcc 是一个 Python / C++ 框架,它把 eBPF 程序的加载、编译、数据采集封装成了 Python 接口。如果你想做稍微复杂一点的观测工具,比如写一个持续运行的后台监控程序,bcc 会比 bpftrace 灵活得多。bcc 的问题在于运行时依赖 Python 环境,而且每个目标机上都要安装完整的 bcc 工具集,部署起来比较笨重。
libbpf 是最底层的方式,适合做生产级工具的开发。配合 CO-RE(Compile Once, Run Everywhere)机制,可以在开发机上编译出的 eBPF 程序直接拿到其他机器上跑,不依赖目标机的内核头文件和编译工具链。这解决了 eBPF 早期版本最头疼的"可移植性"问题。代价是你需要手写 C 代码,并且自己处理加载、Map、perf event 这些细节。
三条路可以同时学。日常排障用 bpftrace 提效率,做小工具用 bcc 快开发,做严肃的基础设施再用 libbpf。
3.2 环境检查与依赖准备
上手之前,先确认你的内核支持 eBPF。最直接的方式就是跑一个 bpftrace 示例脚本,能跑起来就说明环境基本没问题。如果不想先装,可以用一条命令检查内核编译选项:
zcat /proc/config.gz | grep BPF如果输出里有CONFIG_BPF=y、CONFIG_BPF_SYSCALL=y、CONFIG_BPF_JIT=y,那基础支持就有了。如果想用 CO-RE 和 BTF,还要确认/sys/kernel/btf/vmlinux这个文件存在。内核版本方面,4.x 能跑基本的 eBPF,但想要完整的可观测性和 BTF 支持,我建议至少 5.x 以上,越新体验越好。
安装工具看你用的发行版。Ubuntu 和 Debian 系:
sudo apt install bpftraceRHEL / CentOS 系:
sudo dnf install bpftracebcc 的安装也类似:
sudo apt install bpfcc-tools装完之后直接跑一下 bpftrace --version,确认命令有效就行。
3.3 用 bpftrace 数分钟跑通第一个脚本
我个人非常推荐用 bpftrace 作为第一个 eBPF 上手脚本。比如统计系统中每秒的系统调用次数:
sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'这个脚本的意思是:在每次进入系统调用的 tracepoint 上触发一段程序,按进程名计数,bpf 运行时每秒输出一次计数结果。你会在终端上看到类似这样的输出:
Attaching 1 probe... @[chrome]: 1203 @[node]: 88 @[sshd]: 12看到这个输出的那一刻,你实际已经在用 eBPF 做内核级观测了。它底层做的事是:将一个极小的 eBPF 程序挂载到内核的 syscall 入口 tracepoint,每次系统调用都执行一次哈希表计数,数据通过 BPF Map 传到用户态并打印出来。整个过程没有加载任何内核模块,没有任何代码进入内核主线,你只是利用内核提供的安全钩子做了一次观测。
再举一个更实用的例子,统计每个进程打开文件描述符的分布:
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { @[pid, comm] = count(); }'指出的是,bpftrace 的@符号代表一个全局 map 变量,count()是聚合函数,语法虽然像 awk,但背后的语义完全是 eBPF 的写法和执行流程。如果你能理解这个脚本的执行流程,就已经跨过了 eBPF 入门的门槛。
3.4 一个完整的 eBPF C 程序结构示例
如果你打算走 libbpf 路线,这里的入门例子可以保存下来。下面是一个标准的"hello world"级 eBPF 程序,用来统计进程的 exec 调用:
#include <linux/bpf.h> #include <bpf/bpf_helpers.h> struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 1024); __type(key, u32); __type(value, u64); } counts SEC(".maps"); SEC("tracepoint/syscalls/sys_enter_execve") int on_execve(void *ctx) { u32 pid = bpf_get_current_pid_tgid() >> 32; u64 *val = bpf_map_lookup_elem(&counts, &pid); if (val) __sync_fetch_and_add(val, 1); return 0; } char _license[] SEC("license") = "GPL";这段代码干了三件事:定义一个名为 counts 的哈希 Map,定义一个挂载在 execve tracepoint 上对处理器,在处理器中获取当前进程 pid 并给对应计数加一。
这里有几个写 eBPF 程序时必须养成的习惯:
- 严格使用
bpf_helpers.h里的辅助函数,不要直接访问任意内存 - 所有全局变量和函数都要用
SEC宏声明所在的 section,加载器靠 section 名识别程序类型 - Map 的定义是用户态和内核态共享的契约,字段类型必须两边一致
- 返回值语义要清楚,对 tracepoint 程序来说返回 0 表示正常
这段代码编译成字节码之后,由加载器(比如 libbpf 的 skeleton 机制)加载进内核,验证器在加载时会对它做完整的检查。整个加载流程可以用下面的伪代码理解:
open BPF object -> load programs into kernel -> attach to hooks -> 读取 Map 数据4. 现实世界的 eBPF 应用版图
4.1 网络领域:Cilium 与云原生数据路径
eBPF 在云原生领域的最大成功案例之一就是 Cilium。Cilium 把 eBPF 用在 Kubernetes 集群的网络数据路径上,实现了网络策略、负载均衡、可观测性的一体化。
传统 K8s 网络方案里,Service 的负载均衡大多依赖 kube-proxy 用 iptables 规则做转发。iptables 的处理模型是线性匹配规则,规则一多,延迟和更新开销都会上升。Cilium 的做法是把负载均衡和网络策略直接编译成 eBPF 程序,挂在 TC 或 XDP 钩子上,在包进入协议栈之前就完成决策。数据路径短了,性能自然好;策略更新也变成了直接更新 Map,不再需要像 iptables 那样重新生成一大串规则链。
我自己的经验是,在集群流量比较大的场景下,Cilium 替代 kube-proxy 之后,长连接的转发延迟和 NAT 的性能损耗都有肉眼可见的改善。这种改善不是来自某个花哨的配置,而是因为整个数据处理路径变得更短了。
4.2 安全领域:Falco 与运行时安全监控
可观测性和安全监控在 eBPF 时代几乎是同一件事——只不过安全监控关注的是"异常行为"。Falco 是 CNCF 项目里典型的基于 eBPF 的运行时安全工具。
Falco 的思路是:用 eBPF 持续采集系统调用和其他内核事件,然后对照一套规则引擎做实时判断。比如某容器突然执行了chmod +x /etc/passwd,或者某个进程想连接一个非常规端口,这些行为会被 eBPF 探针捕捉到,规则引擎再根据上下文决定是否告警。
传统安全监控方案很多依赖 agent 在用户态读取各种日志,时效性差、容易被绕过;Falco 这类 eBPF 方案直接在事件产生的源头做判断,因为探针就在内核事件点上,恶意行为基本无处躲。
4.3 性能剖析与排障:profile、runqlat 这一票工具
BCC 工具集里有一堆现成的性能剖析工具,名字很有识别度。比如profile可以做 CPU 火焰图采样,runqlat统计任务在运行队列里的等待延迟,biolatency看块设备的 IO 延迟分布。
runqlat之于 CPU 调度问题,几乎等同于抓包之于网络问题。它能告诉你在一个统计周期内,有多少任务等待了 0-1ms、1-2ms、2-4ms…… 如果发现大量任务在运行队列里等待超过 10ms,基本可以判断 CPU 资源吃紧或者调度策略有问题。
真实排障中,我常用的组合拳是:先用runqlat判断是否调度延迟,再用profile采样看热点函数,最后用trace或者funclatency跟踪某个可疑函数的调用耗时。这一套下来,大概率能找到根因。它们都是 bpftrace 或 bcc 里现成的脚本,不需要从零写 eBPF 代码,开箱即用。
4.4 云原生基础设施的隐形底座
除了 Cilium 和 Falco,还有很多基础设施工具选择 eBPF 作为底座。比如服务网格领域的链路追踪数据采集、网络策略执行的 sidecar 卸载,再比如大规模集群里的 Node 级指标采集,都有 eBPF 方案在背后做数据采集层。
这背后的原因很实际:eBPF 不需要修改业务代码,不需要注入 sidecar 进程,也不需要在宿主机上装各种内核模块。它对业务进程的干扰最小,数据采集的精度却能到内核函数级别。对于追求"零侵入"和"高精度"的基础设施团队来说,eBPF 几乎是唯一能同时满足这两个条件的方案。
如果你平时在做 Kubernetes 方面的运维,可以留意一下环境中是否已经有容器以 privileged 模式运行、但又不是业务容器的"观测类 Pod"——那很可能就是个 eBPF 采集器。这种部署模式现在越来越常见,以后也会成为基础设施的标准形态。
5. 我踩过的坑:内核版本、BTF 与验证器限制
5.1 内核版本兼容性问题
eBPF 发展速度极快,但这也带来一个非常现实的问题:新特性往往只在较新的内核里可用。比如早期的 eBPF 不支持循环、不支持读取全局变量,很多现代特性在 5.2、5.8、5.10、5.15 这些版本上都有不同的支持程度。
我早期用 kprobe 写过一个追踪脚本,在开发机的 5.4 内核上跑得很正常,部署到生产环境的 4.19 内核上直接加载失败。原因就是某个辅助函数在 4.19 里还没有。排查过程很简单,把错误信息里的函数名去include/uapi/linux/bpf.h里查一下版本注释就行,但这种坑第一次踩到的人都容易懵。
解决这类问题的标准思路是:如果工具涉及 kprobe 和 tracepoint 混用,优先用 tracepoint;如果必须用 kprobe,程序里要处理符号找不到的情况,并且确认目标机的内核版本符合 API 支持表。长期维护的工具,还是建议上 CO-RE,从机制上规避版本耦合。
5.2 BTF 与 CO-RE 的坑
CO-RE 代表"一次编译,到处运行",听起来很美,但实际用起来有不少细节。CO-RE 的核心依赖是 BTF(BPF Type Format),它描述了内核数据结构的布局信息。eBPF 程序在访问内核结构体字段时,不再写死偏移量,而是通过 BTF 在运行时解析。
但 BTF 不是天然就有的,它需要内核开启CONFIG_DEBUG_INFO_BTF。很多发行版在较老的内核上没开这个选项。你自己编译内核的时候也得注意,开启 BTF 会增加内核镜像的体积和编译时间,不少人为了省那一点编译时间就把它关了,结果后来跑 CO-RE 程序时满头问号。
另外 BTF 也是有版本差异的。如果目标机的内核太老、BTF 信息不完整,某些字段访问还是会失败。我的经验是:维护一个面向多版本内核的观测工具,必须先在各个目标版本上做一轮冒烟测试,别在开发机上跑通了就觉得万事大吉。
5.3 验证器的限制与指令复杂度
验证器最让人痛苦的限制就是指令复杂度。eBPF 程序能够处理的指令条数有限,特别是早期版本,限制很严格,超过就加载失败。哪怕现在的版本放宽了很多,你仍然会在写复杂逻辑时撞上这个上限。
我写过一次对网络数据包做多层字段解析的程序,一开始信心满满地按常规 C 语言风格写,结果验证器直接报"complexity limit exceeded"。解决办法无外乎几种:精简代码逻辑、把复杂的状态拆成多个 eBPF 程序并用 Map 串联、或者把部分逻辑挪到用户态处理。这算是对思维方式的一种改造——在内核态只做最核心的观测动作,别的都交给用户态。
还有一点值得注意:验证器对数组索引、指针边界的要求非常苛刻。如果你在内核态代码里写了一个通过变量做数组下标的操作,验证器会尝试枚举所有可能的取值范围,确保任何情况下都不会越界。所以写 eBPF 时,尽量用常量或者非常明确的边界推导,不要写那种"理论上没问题但验证器看不出来"的代码。
5.4 权限与容器环境限制
加载 eBPF 程序需要一定的权限。普通用户默认是不能加载的,要么 root,要么具有CAP_BPF和CAP_PERFMON权限。在容器环境里,还要注意容器是否允许加载 eBPF,默认的 seccomp 或 AppArmor 配置可能会拦截bpf()系统调用。
常见的表现是:容器里跑 bpftrace 脚本,报 "Operation not permitted"。这时候不要一开始就去搞什么特权模式,先确认宿主机能不能跑、容器内是不是被 seccomp 挡了。如果有权限要求,可以在容器 spec 里加CAP_BPF,或者直接跟安全团队确认观察容器的安全策略。
最终如果跑不了,一个很实用的退路是:在宿主机上跑观测工具,把数据输出共享给容器使用。虽然不如直接在容器里跑灵活,但比什么都看不到强多了。
6. eBPF 的能力边界与未来趋势
6.1 什么场景不适合用 eBPF
eBPF 虽然强大,但它不是万能钥匙。我见过不少团队刚接触 eBPF 的时候特别兴奋,什么需求都想用它来解,结果绕了一大圈又回到传统方案。
eBPF 不适合的场景主要有这么几类。第一,需要随机访问大量磁盘数据的场景。eBPF 程序运行在内核态,你不可能在里面做复杂的文件 IO 或者数据库查询,它更适合做轻量级的数据处理。第二,状态非常复杂的业务逻辑。eBPF 程序要过验证器,逻辑复杂了指令数就爆炸,最后还是得把逻辑拆出去。第三,需要跨主机架构保证字节码兼容的场景,虽然可以借助可移植性方案缓解,但实际落地还是有不小的适配工作量。
直白地说,eBPF 的定位是"观测和轻量处理",而不是"在内核里写业务系统"。把复杂逻辑放用户态、把高频决策放内核态,才是合理的设计思路。
6.2 值得关注的技术演进方向
eBPF 还在高速演进,几个方向值得跟踪。第一是 BPF 指令集的持续扩展,比如有符号整数操作、更灵活的内存模型,这些都会让 eBPF 程序的表达能力更强。第二是 BPF 在内核生态里的渗透,越来越多的内核子系统开始把 eBPF 作为官方可扩展方式,而不是社区自己拼出来的方案。第三是工具链的成熟,libbpf 和 CO-RE 已经让"写一个生产级观测工具"的门槛大幅降低,未来会有更多开箱即用的 eBPF 组件出现。
如果是做基础设施方向的读者,我建议尽早把 eBPF 纳入技术雷达。即便现阶段只是用 bpftrace 排查问题,积累的思维方式也会在将来用上。变化是常态,但内核底层的这套"安全可编程"思路,大概率会成为未来十年操作系统基础设施的核心底座之一。
我自己在一次次排障中对 eBPF 最大的感受就是:它给了普通开发者和运维人员一种"看见内核真相"的能力,而这种能力在过去几乎只有内核开发者才能拥有。与其等着别人做好现成工具,不如先动手跑通一两个脚本,在真实的场景里体会一下探针挂上去那一刻的掌控感。踩过几次坑之后,你会慢慢形成自己的判断,知道什么场景该上 eBPF,什么场景不能硬上。这就是从会用工具到理解工具的过程,也是这篇文章最想帮你迈过的那道坎。