LabHub

博客

eBPF 正在吞噬可观测性

한국어English日本語中文

引言 — 在内核里跑代码这一想法

只要搭建过可观测性(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 的力量来自「能挂在哪里」。钩子点的种类,直接决定了能观测到的对象范围。来看四种最具代表性的。

贯穿这份清单的洞察只有一个:系统里几乎所有有意思的事情都会经过内核。不管是打开文件、创建 socket、fork 进程还是发送数据包,那一刻都会触发内核里的某个函数或某个 tracepoint。eBPF 恰好就坐在这些点上,从一个地方就能看住整个系统的活动。

Verifier — 内核为什么不会崩溃

这里自然会有个疑问:既然在内核里跑任意代码,会不会陷入死循环,或者碰到不该碰的内存,把内核弄崩?eBPF 之所以在生产环境里被信任,答案正是这个问题的答案 — verifier。

verifier 在程序被加载进内核的那一刻、在它真正运行之前,就对它做静态分析。它会沿着程序可能经过的每一条执行路径,尝试证明以下几点。

只要没通过这个过程,内核就会拒绝加载该程序。也就是说,不安全的 eBPF 程序从一开始就得不到运行的机会。这正是它和内核模块的决定性差别 — 内核模块是「信了就加载」,而 eBPF 是「证明了才能加载」。

当然,verifier 并不完美。有时候一个程序实际上是安全的,却因为 verifier 无法证明其安全性而被拒绝。eBPF 开发者会开玩笑说自己在「和 verifier 打架」,原因正在这里。为了让 verifier 满意,把本来好好的代码扭来扭去改一通,这种经历只要认真碰过 eBPF 的人都体会过。但这份严苛,正是内核不会崩溃所付出的代价。

零插桩追踪 — 不改代码

如果要用一句话概括 eBPF 在可观测性上的革命性,那就是「零插桩(zero-instrumentation)」。传统的可观测性,起点是往应用代码里埋插桩。要做 trace,就得给每个请求加一段打开 span 的代码;要拿 metrics,就得加一段递增计数器的代码。每种语言的 SDK 都不一样,每次升级库还得重新改一遍。

eBPF 把这个前提整个翻转过来。应用保持原样,需要的信息从内核钩子那里观测就行。想追踪 HTTP 请求,就把钩子挂在 socket 相关的内核函数上;想测 gRPC 延迟,就挂在相关的 uprobe 上。应用不需要重新编译,不需要重启,甚至不需要知道自己正被观测。

这在实务上意味着不小的事情。

不过「零插桩」这个说法容易引起误解,这里要说清楚。eBPF 在内核层面自动看到的,是系统调用、网络流、函数调用这类低层信号。「这个请求属于哪个业务事务」这种应用专属的语义,内核是看不懂的。所以像高层分布式追踪里的上下文传播,很多时候仍然需要应用配合。eBPF 消除了插桩工作的很大一部分,但并不能消除全部。

工具们 — Cilium、Falco、Pixie、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 的问题。

eBPF 从根本上改变了这个结构。它不再把代理放在每个 Pod 旁边,而是在内核里观测一次、应用一次策略。不是每个节点部署 1000 个代理,而是在内核这一个共享点上统一处理。流量不需要绕道去用户空间的代理再绕回来,延迟自然也会降低。

当然,这不代表「sidecar 已死」。sidecar 在处理复杂的应用层逻辑(高级路由规则、按协议做的精细操作)上依然有优势。eBPF 的强项在低层的快路径,sidecar 的强项在高层的灵活路径,各有各的地盘。实际上,业界正在朝着把两者结合起来的混合方案收敛 — 「无 sidecar 的数据平面 + 只在需要的地方放代理」。

eBPF 做不到的事 — 局限与陷阱

eBPF 很强大,但并非万能。在引入它之前,有几条明确的边界需要了解。

综合这些局限,eBPF 最适合的场景是「在 Linux 之上做低层、高性能、系统全局的观测与网络处理」。如果需要跨平台、需要深层的应用语义、或者逻辑复杂到 verifier 承受不了,就该考虑别的方案。

结语 — 可观测性的重心正在下沉

eBPF 带来的变化,本质上是可观测性的重心从应用下沉到了内核。过去为了观测,要给每个应用埋插桩、给每个 Pod 挂 sidecar;现在只需要在内核这个共享点上挂一次钩子,不管上面跑的是什么都能看住。

这次转移之所以有吸引力,原因很清楚:语言无关、不用改代码、开销小、难以绕过。Cilium 重新定义服务网格、Falco 重新定义运行时安全、Pixie 重新定义自动观测、bpftrace 重新定义即时调试,各走各的路,理由都在这里。

同时也需要保持清醒。eBPF 被绑定在 Linux 上,verifier 挑剔,内核能看到的东西也有语义上的边界。「eBPF 正在吞噬可观测性」这句话并不夸张,但它并不意味着它会吞掉一切。在内核能看清楚的低层世界里,eBPF 是压倒性的;而在它之上的高层世界里,依然要和其他工具共存。

下次设计可观测性技术栈时,在往应用上再多挂一个东西之前,不妨先问一句:「这个东西内核不是已经能看到了吗?」出人意料的是,很多时候答案就是「是的」。

参考资料

评论

还没有评论。

登录后即可发表评论