-
【译】eBPF 和 Go 经验初探
原文地址:https://networkop.co.uk/post/2021-03-ebpf-intro/
首发地址: 【译】eBPF 和 Go 经验初探
本站相关文档:使用 Go 语言管理和分发 ebpf 程序
1. 前言
eBPF 的生态欣欣向荣,无论是 eBPF 本身及其各种应用(包括 XDP) 方面都有大量的学习资源。但当涉及到选择库和工具来与 eBPF 进行交互时,会让人有所困惑。在选择时,你必须在基于 Python 的 BCC 框架、基于 C 的 libbpf 和一系列基于 Go 的 Dropbox、Cilium、Aqua 和 Calico 等库中选择。另一个经常被忽视的重要领域是 eBPF 代码的 "生产化",即从手动编写的样例到生产级应用(例如 Cilium)。在本篇文章中,我将记录相关的经验,特别是在网络(XDP)应用程序场景中,使用 Go 编写的用户空间控制程序。
2. 选择 eBPF 库
在大多数情况下,eBPF 库主要协助实现两个功能:
-
将 eBPF 程序和 Map 载入内核并执行重定位,通过其文件描述符将 eBPF 程序与正确的 Map 进行关联。
-
与 eBPF Map 交互,允许对存储在 Map 中的键/值对进行标准的 CRUD 操作。
部分库也可以帮助你将 eBPF 程序附加到一个特定的钩子,尽管对于网络场景下,这可能很容易采用现有的 netlink API 库完成。
当涉及到 eBPF 库的选择时,我并不是唯一感到困惑的人(见[1], [2])。事实是每个库都有各自的范围和限制。
- Calico 在用 bpftool 和 iproute2 实现的 CLI 命令基础上实现了一个 Go 包装器。
- Aqua 实现了对 libbpf C 库的 Go 包装器。
- Dropbox 支持一小部分程序,但有一个非常干净和方便的用户API。
- IO Visor 的 gobpf 是 BCC 框架的 Go 语言绑定,它更注重于跟踪和性能分析。
- Cilium 和 Cloudflare 维护一个 纯 Go 语言编写的库 (以下简称 "libbpf-go"),它将所有 eBPF 系统调用抽象在一个本地 Go 接口后面。
基于我的网络特定用例,我最终选择了 libbpf-go
,因为其被 Cilium 和 Cloudflare 使用,并且有一个活跃的社区,尽管我也非常喜欢简单易用的 Dropbox 库,并且也可以使用它。
为了熟悉开发过程,我决定实现一个 XDP 交叉连接的应用,它在网络拓扑模拟方面有一个非常小众但重要的用例。我们的目标是要有一个应用程序来观察一个配置文件,并确保本地接口根据该文件的 YAML 规范进行互连。下面是对 xdp-xconnect
工作高层次概述。
下面的章节将逐步描述应用的构建和交付过程,更多的是关注集成,而不是实际的代码。xdp-xconnect
的完整代码在Github上可用。
3. 步骤1 - 编写 eBPF 代码
通常情况下,这将是任何 "eBPF 入门" 文章的主要部分,然而这一次它并不是重点。我并不认为自己可以帮助别人学习如何编写eBPF,然而,我可以参考一些非常好的资源。
- 通用的 eBPF 理论在网站 ebpf.io 和 Cilium 的 eBPF 和 XDP 参考指南中有大量的细节。
- 对 eBPF 和 XDP 进行实践的最好地方是 xdp-tutorial。这是一个了不起的资源,即使你最终选择不完成作业,也绝对值得阅读。
- Cilium 的源代码和其在 [1] 和 [2] 的分析。
我的 eBPF 程序非常简单,它包括对 eBPF 帮助函数的一次调用,可根据传入接口的索引将所有数据包从一个接口重定向到另一个。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
SEC("xdp")
int xdp_xconnect(struct xdp_md *ctx)
{
return bpf_redirect_map(&xconnect_map, ctx->ingress_ifindex, 0);
}
为了编译上述程序,我们需要为所有包含的头文件提供包含路径。最简单的方法是在 linux/tools/lib/bpf/ 下复制所有文件,然而,这将包括很多不必要的文件。因此,另一种方法是创建一个依赖性列表。
$ clang -MD -MF xconnect.d -target bpf -I ~/linux/tools/lib/bpf -c xconnect.c
现在我们可以只对 xconnect.d
中指定的少量文件进行本地拷贝,并使用以下命令为本地 CPU 架构编译 eBPF 代码。
$ clang -target bpf -Wall -O2 -emit-llvm -g -Iinclude -c xconnect.c -o - | \
llc -march=bpf -mcpu=probe -filetype=obj -o xconnect.o
The resulting ELF file is what we’d need to provide to our Go library in the next step.
编译生成的 ELF 文件就是我们在下一步需要提供给 Go 库的程序。
4. 步骤 2 - 编写 Go 代码
编译好的 eBPF 程序和 Map 可以通过 libbpf-go
加载,这只需几个指令。通过添加带有 ebpf
标签的结构,我们可以自动进行重定位程序,并且知道何处发现 Map。
spec, err := ebpf.LoadCollectionSpec("ebpf/xconnect.o")
if err != nil {
panic(err)
}
var objs struct {
XCProg *ebpf.Program `ebpf:"xdp_xconnect"`
XCMap *ebpf.Map `ebpf:"xconnect_map"`
}
if err := spec.LoadAndAssign(&objs, nil); err != nil {
panic(err)
}
defer objs.XCProg.Close()
defer objs.XCMap.Close()
ebpf.Map
类型有一组方法,可对加载的 Map 内容进行标准的 CRUD 操作:
err = objs.XCMap.Put(uint32(0), uint32(10))
var v0 uint32
err = objs.XCMap.Lookup(uint32(0), &v0)
err = objs.XCMap.Delete(uint32(0))
唯一没有被 libbpf-go
包含的步骤是将程序附加到网络钩子上。然而,这可以通过任何现有的 netlink 库轻松实现,例如vishvananda/netlink,通过将网络连接与加载程序的文件描述符联系起来:
link, err := netlink.LinkByName("eth0")
err = netlink.LinkSetXdpFdWithFlags(*link, c.objs.XCProg.FD(), 2)
请注意,我使用 SKB_MODE XDP 标志来绕过退出的 veth 驱动程序 caveat 。尽管本地 XDP 模式比任何其他 eBPF 钩子快得多,但 SKB_MODE 可能没有那么快,因为数据包头必须由网络栈预先解析(见视频)。
5. 步骤 3 - 代码分发
在这一点上,如果不是因为一个问题 -- eBPF 代码可移植性,一切都应该已经准备好打包和发布应用。历史上,这个过程涉及将 eBPF 源代码复制到目标平台,拉取所需的内核头文件,并为特定的内核版本进行编译。这个问题对于追踪/监控/跟踪的用例尤其明显,因为这些用例可能需要访问几乎所有的内核数据结构,所以唯一的解决办法是引入中介层(见 CO-RE)。
另一方面,网络用例依赖于一个相对较小且稳定的内核类型子集,所以它们不会像跟踪和性能分析程序那样遇到同样的问题。根据我目前看到的情况,两种最常见的代码打包方法是:
- 将 eBPF 代码与所需的内核头文件放在一起,假设它们与底层内核相匹配(见Cilium)。
- 分发 eBPF 代码并在目标平台上拉取内核头文件。
在这两种情况下,eBPF 代码仍然需要在目标平台上编译,这是一个额外的步骤,需要在用户空间应用程序启动之前进行。然而,还有一个选择,那就是预先将 eBPF 代码编译成 ELF 格式文件,最终只分发 ELF 文件。这正是 bpf2go
可以做到的,它可以将编译后的代码嵌入到 Go 包中。其依靠 go generate
注解指令产生一个新的文件,其中包含编译好的 eBPF 和 libbpf-go
脚手架代码,唯一的要求是 //go:generate
指令。一旦生成,我们的 eBPF 程序只需几行就可以被加载(注意没有任何参数)。
specs, err := newXdpSpecs()
objs, err := specs.Load(nil)
这种方法明显的优点是,我们不再需要在目标机器上编译,可以在一个软件包或 Go 二进制文件中同时运送 eBPF 和用户空间 Go 代码。这很好,因为它允许我们不仅将应用程序作为二进制文件使用,还可以将其导入任何第三方 Go 应用程序中(见使用实例)。