【译】eBPF 和 Go 经验初探

简介: 【译】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 的 DropboxCiliumAquaCalico 等库中选择。另一个经常被忽视的重要领域是 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 应用程序中(见使用实例)。


6. 阅读和有趣的参考资料


通用理论:

https://github.com/xdp-project/xdp-tutorial

https://docs.cilium.io/en/stable/bpf/

https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/


BCC 和 libbpf:

https://facebookmicrosites.github.io/bpf/blog/2020/02/20/bcc-to-libbpf-howto-guide.html

https://nakryiko.com/posts/libbpf-bootstrap/

https://pingcap.com/blog/why-we-switched-from-bcc-to-libbpf-for-linux-bpf-performance-analysis

https://facebookmicrosites.github.io/bpf/blog/


eBPF/XDP 性能:

https://www.netronome.com/blog/bpf-ebpf-xdp-and-bpfilter-what-are-these-things-and-what-do-they-mean-enterprise/


Linus Kernel 代码风格:

https://www.kernel.org/doc/html/v5.9/process/coding-style.html


libbpf-go 样例程序:

https://github.com/takehaya/goxdp-template

https://github.com/hrntknr/nfNat

https://github.com/takehaya/Vinbero

https://github.com/tcfw/vpc

https://github.com/florianl/tc-skeleton

https://github.com/cloudflare/rakelimit

https://github.com/b3a-dev/ebpf-geoip-demo


bpf2go

https://github.com/lmb/ship-bpf-with-go

https://pkg.go.dev/github.com/cilium/ebpf/cmd/bpf2go


XDP 样例程序:

https://github.com/cpmarvin/lnetd-ctl

https://gitlab.com/mwiget/crpd-l2tpv3-xdp

目录
相关文章
|
10月前
|
Java Go 调度
Go语言并发编程原理与实践:面试经验与必备知识点解析
【4月更文挑战第12天】本文分享了Go语言并发编程在面试中的重要性,包括必备知识点和面试经验。核心知识点涵盖Goroutines、Channels、Select、Mutex、Sync包、Context和错误处理。面试策略强调结构化回答、代码示例及实战经历。同时,解析了Goroutine与线程的区别、Channel实现生产者消费者模式、避免死锁的方法以及Context包的作用和应用场景。通过理论与实践的结合,助你成功应对Go并发编程面试。
153 3
Go中都是值传递,切记! 你所了解的引用传递等知识经验从今天开始彻底抛弃!
Go中都是值传递,切记! 你所了解的引用传递等知识经验从今天开始彻底抛弃!
|
数据采集 缓存 NoSQL
go语言项目优化(经验之谈)
我的课题主要分为以下三章,斗鱼在GO的应用场景,GO在业务中如何优化,我们在GO中踩过了哪些坑。
go语言项目优化(经验之谈)
|
缓存 NoSQL Go
go语言项目优化(经验之谈)
我的课题主要分为以下三章,斗鱼在GO的应用场景,GO在业务中如何优化,我们在GO中踩过了哪些坑。
1328 0
|
16天前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
17天前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
22天前
|
存储 Go
Go 语言入门指南:切片
Golang中的切片(Slice)是基于数组的动态序列,支持变长操作。它由指针、长度和容量三部分组成,底层引用一个连续的数组片段。切片提供灵活的增减元素功能,语法形式为`[]T`,其中T为元素类型。相比固定长度的数组,切片更常用,允许动态调整大小,并且多个切片可以共享同一底层数组。通过内置的`make`函数可创建指定长度和容量的切片。需要注意的是,切片不能直接比较,只能与`nil`比较,且空切片的长度为0。
Go 语言入门指南:切片
|
25天前
|
算法 安全 Go
公司局域网管理系统里的 Go 语言 Bloom Filter 算法,太值得深挖了
本文探讨了如何利用 Go 语言中的 Bloom Filter 算法提升公司局域网管理系统的性能。Bloom Filter 是一种高效的空间节省型数据结构,适用于快速判断元素是否存在于集合中。文中通过具体代码示例展示了如何在 Go 中实现 Bloom Filter,并应用于局域网的 IP 访问控制,显著提高系统响应速度和安全性。随着网络规模扩大和技术进步,持续优化算法和结合其他安全技术将是企业维持网络竞争力的关键。
46 2
公司局域网管理系统里的 Go 语言 Bloom Filter 算法,太值得深挖了
|
21天前
|
开发框架 前端开发 Go
eino — 基于go语言的大模型应用开发框架(二)
本文介绍了如何使用Eino框架实现一个基本的LLM(大语言模型)应用。Eino中的`ChatModel`接口提供了与不同大模型服务(如OpenAI、Ollama等)交互的统一方式,支持生成完整响应、流式响应和绑定工具等功能。`Generate`方法用于生成完整的模型响应,`Stream`方法以流式方式返回结果,`BindTools`方法为模型绑定工具。此外,还介绍了通过`Option`模式配置模型参数及模板功能,支持基于前端和用户自定义的角色及Prompt。目前主要聚焦于`ChatModel`的`Generate`方法,后续将继续深入学习。
182 7
|
17天前
|
存储 缓存 监控
企业监控软件中 Go 语言哈希表算法的应用研究与分析
在数字化时代,企业监控软件对企业的稳定运营至关重要。哈希表(散列表)作为高效的数据结构,广泛应用于企业监控中,如设备状态管理、数据分类和缓存机制。Go 语言中的 map 实现了哈希表,能快速处理海量监控数据,确保实时准确反映设备状态,提升系统性能,助力企业实现智能化管理。
29 3