# eBPF (Extended Berkeley Packet Filter) 软件跨内核 release 版本兼容性的技术方案

## 背景

eBPF (Extended Berkeley Packet Filter) 是一种在 Linux 内核中执行的可编程的字节码。其设计初衷是提供一种安全有效的方式来定制内核行为而无需修改内核源码或加载内核模块。eBPF 的广泛应用场景包括但不限于网络监控、性能优化、系统调用跟踪和安全审计。

跨版本兼容性的重要性：

由于 Linux 内核的快速发展，新版本的内核会引入不兼容的 API 更改，以及数据结构成员命名、偏移量的变化，这对于维持 eBPF 程序的稳定性和可用性产生了挑战；同时，不同的 Linux 发行版也会使用不同版本的内核。我们不可能为每个内核发行版都编写和编译一个 eBPF 程序，这就需要同一个 eBPF 程序能够在不同版本的内核中运行。

实际需求：在生产环境中，软件的稳定性和持续可用性是至关重要的。如果 eBPF 程序不能在新版本的内核中运行，那么它就不能提供持续的服务。同时，由于内核升级的复杂性，不可能总是使用最新的内核，实践中存在大量碎片化、不同的内核版本，需要在其上部署同一个 eBPF 程序。所以 eBPF 程序需要具备跨版本的兼容性。

## 问题定义与之前探索的解决方案

问题的核心在于内核接口的变动、数据结构偏移量的变动以及虚拟机指令集的调整等。新版本的内核可能引入新的 API 或者改变现有的 API，数据结构偏移量也可能随内核版本变动，这可能导致早期的 eBPF 程序无法在新版本的内核中运行。

BCC (BPF Compiler Collection) 是一种解决方案，它使用源码方式分发和编译运行 eBPF 程序，从而在一定程度上解决了兼容性问题。然而，BCC 需要在运行时动态编译 eBPF 程序，这就要求在目标系统上安装相应的编译器和内核头文件，同时编译 eBPF 程序也会占用不少系统资源，这在很多生产环境中是不允许或者不方便的。

CO-RE (Compile Once, Run Everywhere) 则使用 BTF (BPF Type Format) 信息进行移植，它把类型信息嵌入到 eBPF 对象文件中，使得编译好的二进制 eBPF 程序可以在不同版本的内核中直接运行。然而，CO-RE 需要内核支持 BTF，而这一特性在一些较旧的内核版本中并未被支持。另外，CO-RE 需要编译器在编译时生成 BTF 信息，这对编译环境也有一定的要求。同时，CO-RE 能做到的也比较有限，对于大版本之间引入的 API 不兼容或者内核功能缺失的情况，CO-RE 也无能为力。

最小化的 BTF 是一种折中方案，它只包含运行 eBPF 程序所需的最少的 BTF 信息，这可以减小 BTF 的体积，同时也简化了用户的操作。然而，生成最小化的 BTF 需要在编译期进行额外的操作，比如分析 eBPF 程序的类型使用情况，提取所需的类型信息等。尽管存在一些工具可以完成这些操作，但是这些工具通常需要用户有一定的专业知识，使用起来并不方便。同时，由于这些工具是独立的，没有集成到一起，这就增加了用户的使用负担。

总的来说，尽管存在一些方案可以在一定程度上解决 eBPF 程序的跨版本兼容性问题，但是这些方案都有各自的限制和缺点，没有一个方案可以完全满足生产环境中对稳定性、易用性和效率的要求。因此，我们需要探索一种新的解决方案，以更好地解决 eBPF 程序的跨版本兼容性问题。

## 目标和约束 

- **目标** ：我们的目标是实现 eBPF 程序跨内核版本的兼容性，即在不同版本的 Linux 内核上都能正常运行。这包括但不限于执行，加载和卸载程序，读取和修改 map 数据，以及连接到 perf events 等操作。
- **特性抽象** ：为了实现目标，我们希望通过对特性的精细抽象，以便在不同版本的内核中使用最合适的功能 API。这意味着我们需要了解并适应各种内核版本的特性变化，如数据结构的变化，新的功能 API 的引入，以及旧的 API 的废弃等。

**约束** ：在实现目标的过程中，我们面临一些重要的约束： 
- **兼容性** ：一方面，我们需要保证兼容性，这意味着我们可能需要折中一些特性。例如，高版本内核可能支持 ring buffer，而低版本内核只能使用 perf event。我们需要设计一个能够在两种环境中都能工作的解决方案，例如使用一种 bpf buffer 作为抽象，例如：

https://github.com/iovisor/bcc/blob/master/libbpf-tools/compat.h

```c
typedef int (*bpf_buffer_sample_fn)(void *ctx, void *data, size_t size);
typedef void (*bpf_buffer_lost_fn)(void *ctx, int cpu, __u64 cnt);

struct bpf_buffer *bpf_buffer__new(struct bpf_map *events, struct bpf_map *heap);
int bpf_buffer__open(struct bpf_buffer *buffer, bpf_buffer_sample_fn sample_cb,
		     bpf_buffer_lost_fn lost_cb, void *ctx);
int bpf_buffer__poll(struct bpf_buffer *, int timeout_ms);
void bpf_buffer__free(struct bpf_buffer *);
```

- **性能** ：另一方面，我们需要保证性能，因此我们不能引入过多的抽象，这可能会导致性能下降。我们需要在保证兼容性的同时，最小化性能损失。 
- **架构差异** ：对于架构的差异（如 ARM, x86），目前我们不做额外考虑，这可能会在未来的版本中提供支持。

## 解决方案

**设计** ：我们提出一种新的解决方案，该方案使用脚本或命令行工具在编译时生成一个包含 eBPF 对象和针对各个常见内核版本的 minimal BTF 信息的 tar 包。这个 tar 包可以作为 eBPF 程序的二进制分发。尽管它包含数百个发行版的最小化 BTF 信息，但总大小仅约为 10MB 左右.不过相对而言，构建包含最小化 BTF 信息的 eBPF 程序会使编译时间显著增加，但考虑到方便性和兼容性，这是一个可接受的折中。

**运行时库** ：我们会提供一个 C 或 Rust 库，该库可以在运行时解压并加载这个 tar 包。这个库需要处理各种可能的错误和异常情况，例如内核版本和 BTF 信息不匹配，BTF 信息缺失，eBPF 对象加载失败等。

**完整的 BTF 信息** ：为了更好地支持 eBPF 程序的运行，我们会提供一个在线的 BTF 信息库。这个库可以是一个 Git 仓库，也可以是一个 OCI 存储库。用户可以从

##  目标和约束

- 我们的目标是实现 eBPF 程序跨内核版本的兼容性，即在不同版本的 Linux 内核上都能正常运行。
- 同时，我们希望通过对特性的精细抽象，以便在不同版本的内核中使用最合适的功能 API。

约束：一方面，我们需要保证兼容性，这意味着我们可能需要折中一些特性。例如，高版本内核可能支持 ring buffer，而低版本内核只能使用 perf event。我们需要设计一个能够在两种环境中都能工作的解决方案，例如使用一种 bpf buffer 作为抽象；另一方面，我们需要保证性能，因此我们不能引入过多的抽象，这可能会导致性能下降。

对于架构的差异（arm,x86），目前不做额外考虑。

## 解决方案

设计：我们提出一种新的解决方案，该方案使用脚本或命令行工具在编译时生成一个包含 eBPF 对象和针对各个常见内核版本的 minimal BTF 信息的 tar 包。这个 tar 包可以作为 eBPF 程序的二进制分发包。包含数百个发行版的最小化 BTF 信息，约为 10MB 左右，但编译时间会显著增加很多。

运行时库：我们会提供一个 C 或 Rust 库，该库可以在运行时解压并加载这个 tar 包。通过这种方式，我们可以保证 eBPF 程序在不同版本的内核中都能正常运行。

完整的 BTF 信息：为了更好地支持 eBPF 程序的运行，我们会提供一个在线的 BTF 信息库。这个库可以是一个 Git 仓库，也可以是一个 OCI 存储库。用户可以从这个库中下载他们需要的 BTF 信息。这样，如果用户的环境中的内核版本在我们的 tar 包中没有包含，或者用户希望使用某些特殊的内核版本，他们可以从这个库中获取对应版本的 BTF 信息。

兼容性 API：我们可以提供一套兼容性 API，开发者可以使用这套 API 来编写兼容不同内核版本的 eBPF 程序。这套 API 提供了一组对常见版本的内核功能的简单抽象，例如数据结构的访问，功能 API 的调用等。开发者可以使用这套 API 来简化 eBPF 程序的开发和维护工作。

## 备选方案

是否可以有别的打包方式？比如将最小化 BTF 镶嵌在 bpf.o 里面：这是一种可能的打包方式，它可以将 BTF 信息直接嵌入到 eBPF 对象文件中，从而简化分发和加载的过程。然而，这种方式可能会增加 eBPF 对象文件的大小，尤其是当 BTF 信息非常复杂的时候。此外，这种方式也需要对现有的 eBPF 工具链进行修改，以支持嵌入和提取 BTF 信息。

eunomia-bpf 方案：这是一种内置了对应的编译和运行库的编译工具链和运行时方案。用户只需直接编译运行即可。然而，它的缺点是，它不能更好的嵌入现有的其他编译工具链中，例如 Cilium 和 aya-rs 等。

远程编译或者 BCC 方案：这是一种以源代码方式分发脚本，然后运行的时候远程或者本地编译运行的方案。这个方案的主要优点是它可以保证源代码的实时更新，但主要缺点是它需要额外的编译步骤，这可能会增加部署的复杂性。

## 实施计划与风险

初步计划：我们已经在 eunomia-bpf 中实现了一个 POC。在这个 POC 中，我们已经验证了我们的解决方案是可行的。

接下来应该会先实现基于最小化 BTF 和 btfhub 的开发库，再考虑兼容性 API 的问题。

下一步，我们将开始进一步的开发工作，包括完善我们的编译时工具和运行时库，以及实现在线 BTF 信息库和工具链集成等功能。
下一步等具体讨论之后再确定。

风险评估

技术风险：虽然我们已经验证了我们的解决方案是可行的，但在实际部署中可能还会遇到一些技术问题。例如，可能存在一些我们尚未发现的兼容性问题，或者我们的解决方案可能会对性能产生影响。为了降低这些风险，我们将在开发过程中进行详尽的测试，以及及时修复发现的问题。

项目风险：我们的实施计划可能会受到资源限制或其他未知因素的影响。例如，如果我们无法获取足够的开发资源，那么我们可能无法按计划完成我们的产品。为了降低这些风险，我们将在项目开始阶段进行详细的资源规划，以及定期进行项目审查，以确保项目的进度。

## 结论

我们提出了一种新的解决方案来解决 eBPF 程序跨内核版本的兼容性问题。通过在编译时生成包含 eBPF 对象和 minimal BTF 信息的 tar 包，以及提供一个运行时库和一个在线的 BTF 信息库，我们的解决方案能够保证 eBPF 程序在不同版本的内核中都能正常运行。
我们的解决方案还提供了一套兼容性 API，通过这套 API，开发者可以更容易地编写兼容不同内核版本的 eBPF 程序。
我们的目标是提供一个稳定、高效、易用的解决方案，以满足生产环境中对 eBPF 程序跨内核版本兼容性的要求。
