eBPF系列学习(4)了解libbpf、CO-RE (Compile Once – Run Everywhe) | 使用go开发ebpf程序(云原生利器cilium ebpf )

一、了解libbpf

在使用libbpf前,先使用bcc对eBPF相关知识进行学习运行,学习曲线将更平滑。相对于bcc,libbpf与BPF CO-RE的实际编译部署的难度增大了。

1. BPF的可移植性CO-RE (Compile Once – Run Everywhere)

官方:BPF CO-RE (Compile Once – Run Everywhere)
参考URL: https://github.com/libbpf/libbpf#bpf-co-re-compile-once–run-everywhere
BPF的可移植性和CO-RE (Compile Once – Run Everywhere)
参考URL: https://www.cnblogs.com/charlieroro/p/14206214.html

eBPF 的内核无关是有限的,需要 eBPF 机制和开发者共同努力实现。除了 BCC 这种即时编译的方案,还有另外一种名为 CO-RE (Compile Once – Run Everywhere) 的编译方式,其核心依赖于 BTF(更加先进的 DWARF 替代方案)。

文章BPF Portability and CO-R 指出,为了提高BPF程序的便携性,即在不同内核版本上正常工作,而无需为每个特定内核重新编译的能力,社区提出了一个称为BPF CO-RE(Compile Once – Run Everywhere)的解决方案。

Libbpf+BPF CO-RE的理念是,BPF程序与任何"正常"用户空间程序没有太大区别:它们应该汇编成小型二进制文件,然后以紧凑的形式进行部署,以瞄准主机。Libbpf 扮演 BPF 程序装载机的角色,执行平凡的设置工作(重定位、加载和验证 BPF 程序、创建 BPF map、连接到 BPF 挂钩等),让开发人员只担心 BPF 程序的正确性和性能。这种方法将开销保持在最低水平,消除沉重的依赖关系,使整体开发人员体验更加愉快。

BPF CO-RE的目标是帮助BPF开发者使用一个简单的方式解决简单的可移植性问题(如读取结构体字段),并使用它来定位复杂的可移植性问题(如不兼容的数据结构,复杂的用户空间控制条件等)。使得开发者的BPF程序能够"一次编译–随处运行"

Libbpf supports building BPF CO-RE-enabled applications, which, in contrast to BCC, do not require Clang/LLVM runtime being deployed to target servers and doesn’t rely on kernel-devel headers being available.

Libbpf 支持构建 BPF CO-RE-enabled 的应用程序,与BCC相比,它不需要部署的Clang/LLVM运行时,并且不依赖 kernel-devel 内核头文件。

It does rely on kernel to be built with BTF type information, though. Some major Linux distributions come with kernel BTF already built in:

不过,它确实依靠内核来使用BTF类型信息构建。一些主要的Linux发行版本已内置的内核BTF:
Fedora 31+
RHEL 8.2+
OpenSUSE Tumbleweed (in the next release, as of 2020-06-04)
Arch Linux (from kernel 5.7.1.arch1-1)
Manjaro (from kernel 5.4 if compiled after 2021-06-18)
Ubuntu 20.10
Debian 11 (amd64/arm64)

如果您的内核不支持内置的BTF附带,则需要构建自定义内核。你需要:

  • pahole 1.16+ tool (part of dwarves package), which performs DWARF to BTF conversion;
  • kernel built with CONFIG_DEBUG_INFO_BTF=y option;
  • you can check if your kernel has BTF built-in by looking for /sys/kernel/btf/vmlinux file:
$ ls -la /sys/kernel/btf/vmlinux
-r--r--r--. 1 root root 3541561 Jun  2 18:16 /sys/kernel/btf/vmlinux

要开发和构建BPF程序,您将需要Clang/LLVM 10+。默认情况下,以下发行版具有clang/llvm 10+打包:

  • Fedora 32+
  • Ubuntu 20.04+
  • Arch Linux
  • Ubuntu 20.10 (LLVM 11)
  • Debian 11 (LLVM 11)
  • Alpine 3.13+

2. libbpf和bcc性能对比

性能优化大师 Brendan Gregg 在用 libbpf + BPF CO-RE 转换一个 BCC 工具后给出了性能对比数据:

As my colleague Jason pointed out, the memory footprint of opensnoopas CO-RE is much lower than opensnoop.py. 9 Mbytes for CO-RE vs 80 Mbytes for Python.

这句话原文暂未找到,TODO!

我们可以看到在运行时相比 BCC 版本,libbpf + BPF CO-RE 版本节约了近 9 倍的内存开销。

3. 了解libbpf

摆脱对内核头文件的依赖
除了使用内核的BTF信息进行字段的重定位意外,还可以将BTF信息生成一个大(基于5.10.1版本生成的长度有106382行)的头文件(“vmlinux.h”),其中包含了所有的内核内部类型,可以避免对系统范围的内核头文件的依赖。可以使用如下方式生成vmlinux.h:

$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

上述命令可以获得到一个可兼容的C头文件(即"vmlinux.h"),包含所有的内核类型("所有"意味着包含那些不会通过kernel-devel包暴露的头文件)。

当使用了vmlinux.h,此时就不需要依赖像#include <linux/sched.h>, #include <linux/fs.h>这样的头文件,仅需要#include "vmlinux.h"即可。该头文件包含了所有的内核类型:暴露了UAPI,通过kernel-devel提供的内部类型,以及其他一些更加内部的内核类型。

不幸的是,BTF(即DWARF)不会记录#define宏,因此在vmlinux.h中丢失一些常用的宏。但大多数通常不存在的宏可以通过libbpf的bpf_helpers.h(即libbpf提供的内核侧的库)头文件提供。

libbpf知道如何将BPF程序代码匹配到特定的内核。它会查看程序记录的BTF类型和重定位信息,然后将这些信息与内核提供的BTF信息进行匹配。 libbpf解析并匹配所有的类型和字段,更新必要的偏移以及重定位数据,确保BPF程序能够正确地运行在特定的内核上。如果一切顺利,则BPF应用开发人员会获得一个BPF程序,这种方式可以针对目标主机上的内核进行“量身定制”,就好像程序是专门针对这个内核编译的,但无需在应用程序中分发Clang以及在目标主机上的运行时中执行编译,就可以实现所有这些目标。

libbpf 是一个比BCC更新的 BPF 开发库,也是最新的 BPF 开发推荐方式!

4. libbpf 库

libbpf/bpftool 项目地址:https://github.com/libbpf/libbpf

libbpf库是基于C / C ++的通用eBPF库,提供了一些加载bpf程序的方法,封装了内核提供的bpf()系统调用。
在这里插入图片描述

eBPF 程序加载的本质是 BPF 系统调用,Linux 内核通过 BPF 系统调用提供 eBPF 相关的一切操作,比如:程序加载、map 创建删除等。常见的 loader 都是对这个系统调用的封装,部分 loader 提供更加原生接近系统调用的操作,部分 loader 则是进行了更多封装使得编程更便捷。

内核实现的 libbpf 库,封装了 BPF 系统调用,使得加载 BPF 程序更便捷。libbpf 不像 iproute2,它能够使 BPF 相关操作更为便捷,没有做过多封装。如果要将程序加载到内核,则需要自己实现一个用户态程序,调用 libbpf 的 API 去加载到内核。如果要复用 pinned 在 BPF 文件系统的 MAP,也需要用户态程序调用 libbpf 的 API,在加载程序时进行相关处理。

典型案例:Facebook 开源的 katran 项目使用 libbpf 加载 eBPF 程序。

构建基于libbpf的BPF应用需要使用BPF CO-RE的步骤

构建基于libbpf的BPF应用需要使用BPF CO-RE包含的几个步骤:

  • 生成带所有内核类型的头文件vmlinux.h;
  • 使用Clang(版本10或更新版本)将BPF程序的源代码编译为.o对象文件;
  • 使用 libbpfgo 将其编译为二进制文件,加载到内核中并监听输出。

5. libbpf-bootstrap

原文链接:Building BPF applications with libbpf-bootstrap

开始使用 BPF 在很大程度上仍然令人生畏,因为即使为简单的"Hello World"般的 BPF 应用程序设置构建工作流,也需要一系列步骤,对于新的 BPF 开发人员来说,这些步骤可能会令人沮丧和令人生畏。这并不复杂,但知道必要的步骤是一个(不必要的)困难的部分。

libbpf-bootstrap 就是这样一个 BPF 游乐场,它已经尽可能地为初学者配置好了环境,帮助他们可以直接步入到 BPF 程序的书写。它综合了 BPF 社区多年来的最佳实践,并且提供了一个现代化的、便捷的工作流。libbpf-bootstrap 依赖于 libbpf 并且使用了一个很简单的 Makefile。对于需要更高级设置的用户,它也是一个好的起点。即使这个 Makefile不会被直接使用到,也可以很轻易地迁移到别的构建系统上。

二、使用go开发ebpf程序

ebpf的核心程序是通过c编写,clang进行编译的。在编译好ebpf程序后,我们需要将其加载到内核中。目前有很多个项目对ebpf的编写调试运行的流程进行了优化,比较有名的是bcc和libbpf。很多时候我们希望能够更加方便的进行程序编写和部署,也希望程序能够在不同的linux发行版和内核上使用(即BPF CO-RE),bcc的运行依赖内核的头文件,也引入了繁重的整个clang llvm工具链,libbpf只能使用C/C++进行外部程序的开发

BCC、libbpf都主要使用 C 语言开发 eBPF 程序,而实际的应用程序可能会以多种多样的编程语言进行开发。所以,开源社区也开发和维护了很多不同语言的接口,方便这些高级语言跟 eBPF 系统进行交互。

BCC 就提供了 Python、C++ 等多种语言的接口,而使用 BCC 的 Python 接口去加载 eBPF 程序,要比 libbpf 的方法简单得多。

随着ebpf的发展,开源社区中也诞生了各种编程语言的开发库,特别是 Go 和 Rust 这两种语言,其开发库尤为丰富。

1. Go 语言开发库以及选择

使用 Go 语言管理和分发 ebpf 程序
参考URL: https://www.ebpf.top/post/ebpf_go/

目前使用 Go 开发 eBPF 程序可以使用的框架有 IO Visor-gobpf、Dropbox-goebpf和 Cilium-ebpf等,考虑到 Cilium 的社区活跃度和未来的发展,使用 Cilium 的 ebpf 是一个比较不错的选择。

在这里插入图片描述
每个库都有各自的范围和限制:

  • Calico 在用 bpftool 和 iproute2 实现的 CLI 命令基础上实现了一个 Go 包装器。
  • Aqua 实现了对 libbpf C 库的 Go 包装器。
  • Dropbox 支持一小部分程序,但有一个非常干净和方便的用户API。
  • IO Visor 的 gobpf 是 BCC 框架的 Go 语言绑定,它更注重于跟踪和性能分析。
  • Cilium 和 Cloudflare 维护一个 纯 Go 语言编写的库 (以下简称 “libbpf-go”),它将所有 eBPF 系统调用抽象在一个本地 Go 接口后面。

在使用这些 Go 语言开发库时需要注意,Go 开发库只适用于用户态程序中,可以完成 eBPF 程序编译、加载、事件挂载,以及 BPF 映射交互等用户态的功能,而内核态的 eBPF 程序还是需要使用 C 语言来开发的。

当涉及到选择库和工具来与 eBPF 进行交互时,会让人有所困惑。在选择时,你必须在基于 Python 的 BCC 框架、基于 C 的 libbpf 和一系列基于 Go 的 Dropbox、Cilium、Aqua 和 Calico 等库中选择。

库贡献者的活跃度
总结: cilium/ebpf > iovisor/gobpf > dropbox/goebpf > aquasecurity/libbpfgo

关于Cilium?

cilium
n. 纤毛;睫毛;
[例句]Cilium is the major power source of pallium to transport materials.
纤毛是外套膜进行物质运输的主要动力来源。
[其他] 复数:cilia

eBPF是一项革命性的技术,可以在Linux内核中运行沙盒程序,而无需重新编译内核或加载内核模块。在过去的几年中,eBPF已成为解决以前依赖于内核更改或内核模块的问题的标准方法。

eBPF 在动态跟踪、网络、安全以及云原生等领域的广泛应用。

Cilium是一个开源项目,它的基础是基于eBPF的Linux内核技术,用于透明地提供和保护使用Linux容器管理平台部署的应用程序服务之间的网络和API连接。以解决容器工作负载的新可伸缩性,安全性和可见性要求。Cilium超越了传统的容器网络接口(CNI),可提供服务解析,策略执行等功能。

在这里插入图片描述
Cilium已迅速成为Kubernetes生态系统中的领先技术,为Google Kubernetes Engine(GKE)提供了网络数据平面,并在其他领先的云原生最终用户(包括Adobe,DataDog,GitLab和DigitalOcean)中得到采用。

Cilium 母公司 Isovalent
Liz Rice,是Isovalent的首席开源官,这家公司是Cilium网络项目的幕后推手。也是CNCF技术监督委员会主席

Cilium 是一个用于容器网络领域的开源项目,主要是面向容器而使用,用于提供并透明地保护应用程序工作负载(如应用程序容器或进程)之间的网络连接和负载均衡。

2. cilium/ebpf库实践

cilium/ebpf 纯 Go 程序编写,从而实现了程序最小依赖;与此同时其还提供了 bpf2go 工具,可用来将 eBPF 程序编译成 Go 语言中的一部分,使得交付更加方便。

因此,本文也选择基于 cilium/ebpf 库来开发和实践。

cilium/ebpf 库

cilium/ebpf库
github: https://github.com/cilium/ebpf

ebpf的核心程序是通过c编写,clang进行编译的。在编译好ebpf程序后,我们需要将其加载到内核中。目前有很多个项目对ebpf的编写调试运行的流程进行了优化,比较有名的是bcc和libbpf。很多时候我们希望能够更加方便的进行程序编写和部署,也希望程序能够在不同的linux发行版和内核上使用(即BPF CO-RE),bcc的运行依赖内核的头文件,也引入了繁重的整个clang llvm工具链,libbpf只能使用C/C++进行外部程序的开发。

如果想使用go编写,有两个选择:cilium的ebpf项目和libbpf-go,考虑的社区活跃度和未来的发展,也许使用cilium的ebpf工具比较合适。

eBPF 程序加载的本质是 BPF 系统调用,Linux 内核通过 BPF 系统调用提供 eBPF 相关的一切操作,比如:程序加载、map 创建删除等。常见的 loader 都是对这个系统调用的封装,部分 loader 提供更加原生接近系统调用的操作,部分 loader 则是进行了更多封装使得编程更便捷。

cilium/ebpf 库是一个 GO 语言版本的 libbpf 库,它封装了 BPF 系统调用,与内核提供的 libbpf 类似。使用 cilium/ebpf 库实现用户态程序加载 eBPF 到内核,在很多方面都类似 libbpf,区别在于这个库是 GO 语言的,更加方便使用 GO 语言构建一套 eBPF 程序的控制面方案。

纯go库用于读取,修改和加载EBPF程序,并将其连接到Linux内核中的各种钩子上。

使用go开发ebpf程序思路

官方github: https://github.com/cilium/ebpf
使用 Go 语言开发 ebpf 程序
https://houmin.cc/posts/adca5ae5/

开发者只需要实现内核态C文件,用户态go文件,用户态event消息结构体三个文件即可,框架会自动加载执行。

  • 运行在内核态用C写eBPF代码,llvm编译为eBPF字节码。
  • 用户态使用golang编写,cilium/ebpf纯go类库,做eBPF字节码的内核加载,kprobe/uprobe HOOK对应函数。
  • 用户态使用golang做事件读取、解码、处理。

cilium/ebpf是一个纯GO库,可提供用于加载,编译和调试EBPF程序的实用程序。它具有最小的外部依赖性,旨在用于长期运行的过程中。

官方demo: tracepoint_in_go

官方demo参考路径:
examples/tracepoint_in_go/main.go

//此程序演示如何将eBPF程序附加到跟踪点。
//程序附加到syscall/sys_enter_openat跟踪点,并且
//每次 syscall 时,打印出整数123。

//基于预先存在的内核hook(tracepoint)打开跟踪事件。
//每次用户空间程序使用’openat()’ 系统调用时,eBPF
//将执行上面指定的程序,并显示“123”值 在 perf ring 中

纯go库用于读取,修改和加载EBPF程序,并将其连接到Linux内核中的各种钩子上。
这是官方完全go写的demo,演示了 syscall/sys_enter_openat跟踪点。

猜你喜欢

转载自blog.csdn.net/inthat/article/details/125170099