- 引言 — 在内核里跑代码这一想法
- 什么是 eBPF — 内核中的小型虚拟机
- 能在哪里挂钩 — kprobe、uprobe、tracepoint、XDP
- Verifier — 内核为什么不会崩溃
- 零插桩追踪 — 不改代码
- 工具们 — Cilium、Falco、Pixie、bpftrace
- 为什么能赢过 sidecar
- eBPF 做不到的事 — 局限与陷阱
- 结语 — 可观测性的重心正在下沉
- 参考资料
引言 — 在内核里跑代码这一想法
只要搭建过可观测性(observability)工具的人,都会有一种熟悉的疲惫感。给每个应用装一个 agent,给每种语言接一个计装库,给每个 Pod 起一个 sidecar,然后这些附加物又反过来吃掉 CPU 和内存。为了观测系统,反倒把系统变得越来越重、越来越复杂 — 这就是那个悖论。
eBPF 从完全不同的角度攻克这个问题。它不去碰应用本身,而是下沉到内核里,在下面看着一切。网络包、系统调用、函数调用、磁盘 I/O 全都要经过内核,所以只要在内核这一处观测一次,不管上面跑的是什么都能被计装到。应用甚至根本不知道自己正被观测。
本文要讲的是:eBPF 究竟是什么,它如何能在内核里安全地运行任意代码,以及它为什么正在把可观测性工具一个个吞掉。同时也会坦率地指出,eBPF 并非万能,它的边界在哪里。
什么是 eBPF — 内核中的小型虚拟机
eBPF(extended Berkeley Packet Filter)这个名字虽然还带着「包过滤器」的字样,但早已不再局限于包过滤。今天的 eBPF 更接近于内嵌在 Linux 内核里的一个小型虚拟机。你写一段小程序,内核会把它挂到某个特定事件上,此后每当那个事件发生,就会在内核上下文里执行这段程序。
核心是这样的:传统上,要改变或窥探内核的行为,就得写一个内核模块。内核模块很强大,但也很危险 — 一个 bug 就能把整个内核拖进 panic,写得不好还会让系统卡死。所以把模块随意加载到生产内核上,从来都不是一个可以轻易做的决定。
eBPF 消除了这种风险。eBPF 程序确实在内核里跑,但会被严格约束,使其无法搞垮内核。强制这套约束的,正是稍后会细讲的 verifier(验证器)。正因如此,我们才能在接近内核模块威力的同时,不必冒着让系统卡死的风险,在生产环境里随时挂上或摘下程序。
整体运作方式如下。
用户空间
+------------------------------+
| 1. 用 C/Rust 等编写 eBPF |
| 2. LLVM 编译为 eBPF 字节码 |
+--------------+---------------+
| 通过 bpf() 系统调用加载
v
内核空间
+------------------------------+
| 3. verifier 检查安全性 |
| 4. JIT 转换为原生代码 |
| 5. 挂接到钩子点 |
+--------------+---------------+
| 事件发生时执行
v
+------------------------------+
| 6. 结果写入 map |
| 用户空间读取 map |
+------------------------------+
程序在用户空间编写、编译,然后通过 bpf() 系统调用加载进内核。一旦通过 verifier,JIT 编译器就会把它转换成原生机器码,挂接到指定的钩子点上。程序收集到的数据,会通过一种叫 map 的共享数据结构传回用户空间。
能在哪里挂钩 — kprobe、uprobe、tracepoint、XDP
eBPF 的力量来自「能挂在哪里」。钩子点的种类,直接决定了能观测到的对象范围。来看四种最具代表性的。
- kprobe(内核探针):把程序挂在内核函数的入口或返回处。比如可以设成每次调用
tcp_connect时都执行。它几乎能挂钩任何内核函数,是最灵活的一种,但依赖内核内部的函数名,内核版本一变就可能失效。 - uprobe(用户探针):挂在用户空间程序的函数上,在应用二进制里某个特定函数被调用时执行。这正是「不改代码」窥探应用内部的关键手段。举例来说,可以在 SSL 库的加密函数上挂 uprobe,来观测明文流量。
- tracepoint(追踪点):内核开发者预先埋好的稳定计装点。和 kprobe 不同,它是内核官方维护的 API,内核版本升级也不容易失效。看重稳定性时,会优先选 tracepoint 而不是 kprobe。
- XDP(eXpress Data Path):跑在网络栈的最前端 — 数据包刚到达驱动的那一刻就执行,甚至还没经过内核网络栈,所以极快。每秒处理数百万个包的 DDoS 防护或负载均衡,就发生在这里。
贯穿这份清单的洞察只有一个:系统里几乎所有有意思的事情都会经过内核。不管是打开文件、创建 socket、fork 进程还是发送数据包,那一刻都会触发内核里的某个函数或某个 tracepoint。eBPF 恰好就坐在这些点上,从一个地方就能看住整个系统的活动。
Verifier — 内核为什么不会崩溃
这里自然会有个疑问:既然在内核里跑任意代码,会不会陷入死循环,或者碰到不该碰的内存,把内核弄崩?eBPF 之所以在生产环境里被信任,答案正是这个问题的答案 — verifier。
verifier 在程序被加载进内核的那一刻、在它真正运行之前,就对它做静态分析。它会沿着程序可能经过的每一条执行路径,尝试证明以下几点。
- 终止保证:程序必须能结束。以前无限循环是被禁止的,现在则只允许 verifier 能证明存在上界的有界循环。永远不会停下来的程序,连加载都做不到。
- 内存安全:程序只能读写被允许的内存区域。指针在被解引用之前是否经过了空值检查、数组访问是否越界,verifier 都会去追踪。
- 指令数上限:程序复杂度有上限,好让 verifier 能在现实的时间内分析完整个程序。
只要没通过这个过程,内核就会拒绝加载该程序。也就是说,不安全的 eBPF 程序从一开始就得不到运行的机会。这正是它和内核模块的决定性差别 — 内核模块是「信了就加载」,而 eBPF 是「证明了才能加载」。
当然,verifier 并不完美。有时候一个程序实际上是安全的,却因为 verifier 无法证明其安全性而被拒绝。eBPF 开发者会开玩笑说自己在「和 verifier 打架」,原因正在这里。为了让 verifier 满意,把本来好好的代码扭来扭去改一通,这种经历只要认真碰过 eBPF 的人都体会过。但这份严苛,正是内核不会崩溃所付出的代价。
零插桩追踪 — 不改代码
如果要用一句话概括 eBPF 在可观测性上的革命性,那就是「零插桩(zero-instrumentation)」。传统的可观测性,起点是往应用代码里埋插桩。要做 trace,就得给每个请求加一段打开 span 的代码;要拿 metrics,就得加一段递增计数器的代码。每种语言的 SDK 都不一样,每次升级库还得重新改一遍。
eBPF 把这个前提整个翻转过来。应用保持原样,需要的信息从内核钩子那里观测就行。想追踪 HTTP 请求,就把钩子挂在 socket 相关的内核函数上;想测 gRPC 延迟,就挂在相关的 uprobe 上。应用不需要重新编译,不需要重启,甚至不需要知道自己正被观测。
这在实务上意味着不小的事情。
- 语言无关:不管是 Go、Python、Rust 还是 Java,从内核的角度看都是同样的系统调用和网络事件,不需要为每种语言单独维护 SDK。
- 覆盖遗留系统:没有源码、或者无法修改的第三方二进制也能被观测。在代码改不了的情况下尤其有力。
- 性能开销最小:插桩代码不会进入应用的热路径,而是在内核里以 JIT 编译的方式运行,因此开销很小。
不过「零插桩」这个说法容易引起误解,这里要说清楚。eBPF 在内核层面自动看到的,是系统调用、网络流、函数调用这类低层信号。「这个请求属于哪个业务事务」这种应用专属的语义,内核是看不懂的。所以像高层分布式追踪里的上下文传播,很多时候仍然需要应用配合。eBPF 消除了插桩工作的很大一部分,但并不能消除全部。
工具们 — Cilium、Falco、Pixie、bpftrace
不只是理论,eBPF 已经是生产环境里被广泛使用的一批工具的引擎。来看四个代表。
- Cilium:用 eBPF 实现 Kubernetes 的网络与安全。传统的 Kubernetes 网络依赖 iptables 规则,Pod 一旦涨到几千个,iptables 规则也会跟着爆炸式增长,性能随之崩溃。Cilium 用 eBPF 程序取而代之,把路由、策略执行、负载均衡处理得高效得多。它的服务网格功能也在朝着不用 sidecar 代理、直接用 eBPF 实现的方向发展。
- Falco:一款运行时安全观测工具。它实时监视系统调用,一旦检测到可疑行为(比如容器里冒出一个 shell、读取敏感文件、出现预期之外的网络连接)就发出告警。因为直接在内核层观测,攻击者很难绕过。
- Pixie:一个把 eBPF 的零插桩推到台前的 Kubernetes 可观测性平台。不写任何插桩代码,就能自动捕获集群里的 HTTP、gRPC、数据库流量,展示服务间的延迟和错误率。「装上就能看见」这种体验,正是 eBPF 才使之成为可能的代表案例。
- bpftrace:eBPF 的瑞士军刀。它是一种高层追踪语言,一行脚本就能当场观测内核,常用于在生产环境里立刻扎进「现在到底在发生什么」。
举例来说,用 bpftrace 统计某个进程调用了多少次哪种系统调用,就是这么简单。
# 按进程统计系统调用次数
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
这一行代码挂钩到内核的 tracepoint,每次系统调用发生时都按进程名(comm)递增计数器,退出时打印统计结果。不需要额外的 agent,也不需要改应用。这种即时性正是 bpftrace 在生产调试中广受喜爱的原因。
如果想在浏览器里概念性地体验这些工具,可以在 eBPF 演练场通过模拟来学习 eBPF 指令集和 verifier 的行为,Kubernetes 网络方面的语境则可以在 Kubernetes 网络实验室里查看。
为什么能赢过 sidecar
最近这几年,云原生世界的默认模式一直是 sidecar。服务网格给每个 Pod 旁边都挂一个代理容器,用它来拦截和观测流量。这个模型确实好用,但代价也不小。
来梳理一下 sidecar 的问题。
- 资源翻倍:每个 Pod 挂一个代理,1000 个 Pod 就有 1000 个代理。每个代理都要吃 CPU 和内存,加起来的量不容小觑。
- 延迟增加:每个请求都要走应用 → sidecar → 网络 → 对端 sidecar → 对端应用这条路径。每一跳都会叠加延迟。
- 运维复杂度:注入 sidecar、对齐版本、做升级,这些事本身就是一笔管理负担。
eBPF 从根本上改变了这个结构。它不再把代理放在每个 Pod 旁边,而是在内核里观测一次、应用一次策略。不是每个节点部署 1000 个代理,而是在内核这一个共享点上统一处理。流量不需要绕道去用户空间的代理再绕回来,延迟自然也会降低。
当然,这不代表「sidecar 已死」。sidecar 在处理复杂的应用层逻辑(高级路由规则、按协议做的精细操作)上依然有优势。eBPF 的强项在低层的快路径,sidecar 的强项在高层的灵活路径,各有各的地盘。实际上,业界正在朝着把两者结合起来的混合方案收敛 — 「无 sidecar 的数据平面 + 只在需要的地方放代理」。
eBPF 做不到的事 — 局限与陷阱
eBPF 很强大,但并非万能。在引入它之前,有几条明确的边界需要了解。
- 以 Linux 为中心:eBPF 本质上是 Linux 内核技术。虽然 Windows 版 eBPF 的移植工作正在推进,但成熟度和生态都远远落后于 Linux。在非 Linux 环境下,情况完全是另一回事。
- 依赖内核版本:像 kprobe 这样依赖内核内部实现的钩子,内核版本一变就可能失效。CO-RE(Compile Once, Run Everywhere)之类的技术大大缓解了这个问题,但在较旧的内核版本上,某些功能依然用不了。
- verifier 这堵墙:前面提过,verifier 安全但挑剔。想实现复杂逻辑时常常被 verifier 卡住,为了绕过去,代码会变得不自然。程序体积和复杂度的上限也是实际的约束。
- 需要高权限:加载 eBPF 程序通常需要强大的权限(CAP_BPF 等)。这也意味着 eBPF 本身可能成为攻击面 — 一旦攻击者获得加载恶意或有缺陷的 eBPF 程序的权限,就能深度观测甚至操纵系统。
- 语义上的边界:前面反复强调过,内核只看得到低层事件。应用的业务含义(这个请求属于哪个用户的哪笔订单)内核是不知道的。需要高层语境的观测,依然离不开应用的配合。
综合这些局限,eBPF 最适合的场景是「在 Linux 之上做低层、高性能、系统全局的观测与网络处理」。如果需要跨平台、需要深层的应用语义、或者逻辑复杂到 verifier 承受不了,就该考虑别的方案。
结语 — 可观测性的重心正在下沉
eBPF 带来的变化,本质上是可观测性的重心从应用下沉到了内核。过去为了观测,要给每个应用埋插桩、给每个 Pod 挂 sidecar;现在只需要在内核这个共享点上挂一次钩子,不管上面跑的是什么都能看住。
这次转移之所以有吸引力,原因很清楚:语言无关、不用改代码、开销小、难以绕过。Cilium 重新定义服务网格、Falco 重新定义运行时安全、Pixie 重新定义自动观测、bpftrace 重新定义即时调试,各走各的路,理由都在这里。
同时也需要保持清醒。eBPF 被绑定在 Linux 上,verifier 挑剔,内核能看到的东西也有语义上的边界。「eBPF 正在吞噬可观测性」这句话并不夸张,但它并不意味着它会吞掉一切。在内核能看清楚的低层世界里,eBPF 是压倒性的;而在它之上的高层世界里,依然要和其他工具共存。
下次设计可观测性技术栈时,在往应用上再多挂一个东西之前,不妨先问一句:「这个东西内核不是已经能看到了吗?」出人意料的是,很多时候答案就是「是的」。
参考资料
- eBPF 官方网站: https://ebpf.io/
- Cilium: https://cilium.io/
- Falco: https://falco.org/
- Pixie: https://px.dev/
- bpftrace: https://github.com/bpftrace/bpftrace
- Brendan Gregg 的 eBPF 资料: https://www.brendangregg.com/ebpf.html
- Linux 内核 BPF 文档: https://docs.kernel.org/bpf/
현재 단락 (1/81)
只要搭建过可观测性(observability)工具的人,都会有一种熟悉的疲惫感。给每个应用装一个 agent,给每种语言接一个计装库,给每个 Pod 起一个 sidecar,然后这些附加物又反过来吃...