文/郑昱笙 eBPF 技术摸索 SIG Contributor、浙江大学学生
当今云原生世界中两个最热门的轻量级代码执行沙箱/虚拟机是 eBPF 和 WebAssembly。它们都运行从 C、C++ 和 Rust 等语言编译的高性能字节码程序,并且都是跨平台、可移植的。二者最大的区别在于:eBPF 在 Linux 内核中运行,而 WebAssembly 在用户空间中运行。咱们心愿能做一些将二者互相交融的尝试:应用 WASM 来编写通用的 eBPF 程序,而后能够将其散发到任意不同版本、不同架构的 Linux 内核中,无需从新编译即可运行。
WebAssembly vs eBPF
WebAssembly(缩写 Wasm)是基于堆栈虚拟机的二进制指令格局。Wasm 是为了一个可移植的指标而设计的,可作为 C/C+/RUST 等高级语言的编译指标,使客户端和服务器应用程序可能在 Web 上部署。WASM 的运行时有多种实现,包含浏览器和独立的零碎,它能够用于视频和音频编解码器、图形和 3D、多媒体和游戏、明码计算或便携式语言实现等利用。
只管 WASM 是为了进步网页中性能敏感模块体现而提出的字节码规范, 然而 WASM 却不仅能用在浏览器(broswer)中, 也能够用在其余环境中。WASM 曾经倒退成为一个轻量级、高性能、跨平台和多语种的软件沙盒环境,被使用于云原生软件组件。与 Linux 容器相比,WebAssembly 的启动速度能够进步 100 倍,内存和磁盘占用空间要小得多,并且具备更好定义的平安沙箱。然而,衡量是 WebAssembly 须要本人的语言 SDK 和编译器工具链,使其成为比 Linux 容器更受限制的开发环境。WebAssembly 越来越多地用于难以部署 Linux 容器或应用程序性能至关重要的边缘计算场景。
WASM 的编译和部署流程如下:
wasm-compile-deploy
通常能够将 C/C+/RUST 等高级语言编译为 WASM 字节码,在 WASM 虚拟机中进行加载运行。WASM 虚构机会通过解释执行或 JIT 的形式,将 WASM 字节码翻译为对应平台( x86/Arm 等)的机器码运行。
eBPF 源于 BPF,实质上是处于内核中的一个高效与灵便的虚拟机组件,以一种平安的形式在许多内核 hook 点执行字节码。BPF 最后的目标是用于高效网络报文过滤,通过从新设计,eBPF 不再局限于网络协议栈,曾经成为内核顶级的子系统,演进为一个通用执行引擎。开发者可基于 eBPF 开发性能剖析工具、软件定义网络、平安等诸多场景。eBPF 有一些编程限度,须要通过验证器确保其在内核利用场景中是平安的(例如,没有有限循环、内存越界等),但这也意味着 eBPF 的编程模型不是图灵齐备的。相比之下,WebAssembly 是一种图灵齐备的语言,具备可能突破沙盒和拜访原生 OS 库的扩大 WASI (WebAssembly System Interface, WASM零碎接口) ,同时 WASM 运行时能够平安地隔离并以靠近原生的性能执行用户空间代码。二者的畛域主体上有不少差别,但也有不少互相重叠的中央。
有一些在 Linux 内核中运行 WebAssembly 的尝试,然而基本上不太胜利。eBPF 是这个利用场景下更好的抉择。然而 WebAssembly 程序能够解决许多类内核的工作,能够被 AOT 编译成原生应用程序。来自 CNCF 的 WasmEdge Runtime 是一个很好的基于 LLVM 的云原生 WebAssembly 编译器。原生应用程序将所有沙箱查看合并到原生库中,这容许 WebAssembly 程序体现得像一个独立的 unikernel “库操作系统”。此外,这种 AOT 编译的沙盒 WebAssembly 应用程序能够在微内核操作系统(如 seL4)上运行,并且能够接管许多“内核级”工作[1]。
尽管 WebAssembly 能够降落到内核级别,但 eBPF 也能够回升到应用程序级别。在 sidecar 代理中,Envoy Proxy 创始了应用 Wasm 作为扩大机制对数据立体进行编程的办法。开发人员能够用 C、C++、Rust、AssemblyScript、Swift 和 TinyGo 等语言编写特定利用的代理逻辑,并将该模块编译到 Wasm 中。通过 proxy-Wasm 规范,代理能够在 Wasmtime 和 WasmEdge 等高性能运行机制中执行那些 Wasm 插件[2]。
只管目前有不少应用程序同时应用了二者,但大多数时候这两个虚拟机是互相独立并且没有交加的:例如在可观测性利用中,通过 eBPF 探针获取数据,获取数据之后在用户态引入 WASM 插件模块,进行可配置的数据处理。WASM 模块和 eBPF 程序的散发、运行、加载、管制互相独立,仅仅存在数据流的关联。
咱们的一次尝试
一般来说,一个残缺的 eBPF 应用程序分为用户空间程序和内核程序两局部:
- 用户空间程序负责加载 BPF 字节码至内核,或负责读取内核回传的统计信息或者事件详情,进行相干的数据处理和管制。
- 内核中的 BPF 字节码负责在内核中执行特定事件,如须要也会将执行的后果通过 maps 或者 perf-event 事件发送至用户空间。
用户态程序能够在加载 eBPF 程序前管制一些 eBPF 程序的参数和变量,以及挂载点;也能够通过 map 等等形式进行用户态和内核态之间的双向通信。通常来说用户态的 eBPF 程序能够基于 libbpf 库进行开发,来管制内核态 eBPF 程序的装载和运行。那么,如果将用户态的所有管制和数据处理逻辑全副移到 WASM 虚拟机中,通过 WASM module 打包和散发 eBPF 字节码,同时在 WASM 虚拟机外部管制整个 eBPF 程序的加载和执行,兴许咱们就能够将二者的劣势联合起来,让任意 eBPF 程序能有如下个性:
- 可移植:让 eBPF 工具和利用齐全平台无关、可移植,不须要进行从新编译即能够跨平台散发。
- 隔离性:借助 WASM 的可靠性和隔离性,让 eBPF 程序的加载和执行、以及用户态的数据处理流程更加安全可靠;事实上一个 eBPF 利用的用户态控制代码通常远远多于内核态。
- 包治理:借助 WASM 的生态和工具链,实现 eBPF 程序或工具的散发、治理、加载等工作,目前 eBPF 程序或工具生态可能不足一个通用的包治理或插件管理系统。
- 跨语言:目前 eBPF 程序由多种用户态语言开发(如 Go++等),超过 30 种编程语言能够被编译成 WebAssembly 模块,容许各种背景的开发人员(C、Go、Rust、Java、TypeScript 等)用他们抉择的语言编写 eBPF 的用户态程序,而不须要学习新的语言。
- 敏捷性:对于大型的 eBPF 应用程序,能够应用 WASM 作为插件扩大平台:扩大程序能够在运行时间接从管制立体交付和从新加载。这不仅意味着每个人都能够应用官网和未经批改的应用程序来加载自定义扩大,而且任何 eBPF 程序的谬误修复和/或更新都能够在运行时推送和/或测试,而不须要更新和/或重新部署一个新的二进制。
- 轻量级:WebAssembly 微服务耗费 1% 的资源,与 Linux 容器利用相比,冷启动的工夫是 1%:咱们兴许能够借此实现 eBPF as a service,让 eBPF 程序的加载和执行变得更加轻量级、疾速、简便易行。
eunomia-bpf 是 eBPF 技术摸索 SIG [3] [5] 中发动并孵化的我的项目,目前也曾经在 github [4] 上开源。eunomia-bpf 是一个 eBPF 程序的轻量级开发加载框架,蕴含了一个用户态动静加载框架/运行时库,以及一个简略的编译 WASM 和 eBPF 字节码的工具链容器。事实上,在 WASM 模块中编写 eBPF 代码和通常相熟的应用 libbpf 框架或 Coolbpf 开发 eBPF 程序的形式是根本一样的,WASM 的复杂性会被暗藏在 eunomia-bpf 的编译工具链和运行时库中,开发者能够专一于 eBPF 程序的开发和调试,不须要理解 WASM 的背景常识,也不须要放心 WASM 的编译环境配置。
应用 WASM 模块散发、动静加载 eBPF 程序
eunomia-bpf 库蕴含一个简略的命令行工具(ecli),蕴含了一个小型的 WASM 运行时模块和 eBPF 动静装载的性能,能够间接下载下来后进行应用:
ecli 会主动从网页上下载并加载 sigsnoop/app.wasm 这个 wasm 模块,它蕴含了一个 eBPF 程序,用于跟踪内核中过程的信号发送和接管。这里咱们能够看到一个简略的 JSON 格局的输入,蕴含了过程的 PID、信号的类型、发送者和接收者,以及信号名称等信息。它也能够附带一些命令行参数,例如:
咱们能够通过 -p 管制它追踪哪个过程,在内核态 eBPF 程序中进行一些过滤和解决。同样也能够应用 ecli 来动静加载应用其余的工具,例如 opensnoop:
opensnoop 会追踪过程的 open() 调用,即内核中所有的关上文件操作,这里咱们能够看到过程的 PID、UID、返回值、调用标记、过程名和文件名等信息。内核态的 eBPF 程序会被蕴含在 WASM 模块中进行散发,在加载的时候通过 BTF 信息和 libbpf 进行重定位操作,以适应不同的内核版本。同时,因为用户态的相干解决代码齐全由 WASM 编写,内核态由 eBPF 指令编写,因而不受具体指令集(x86、Arm 等)的限度,能够在不同的平台上运行。
应用 WASM 开发和打包 eBPF 程序
同样,以上文所述的 sigsnoop 为例,要跟踪过程的信号发送和接管,咱们首先须要在 sigsnoop.bpf.c 中编写内核态的 eBPF 代码:
这里咱们应用 tracepoint/signal/signal_generate 这个 tracepoint 来在内核中追踪信号的产生事件。内核态代码通过 BPF_MAP_TYPE_PERF_EVENT_ARRAY 往用户态导出信息,为此咱们须要在 sigsnoop.bpf.h 头文件,中定义一个导出信息的构造体:
能够间接应用 eunomia-bpf 的编译工具链将其编译为 JSON 格局,生成一个 package.json 文件,并且能够间接应用 ecli 加载运行:
咱们所有的编译工具链都曾经打包成了 docker 镜像的模式并公布到了 docker hub 上,能够间接开箱即用。此时动静加载运行的只有内核态的 eBPF 代码和一些辅助信息,帮忙 eunomia-bpf 库主动获取内核态往用户态上报的事件。如果咱们想要在用户态进行一些参数配置和调整,以及数据处理流程,咱们须要在用户态编写代码,将内核态的 eBPF 代码和用户态的代码打包成一个残缺的 eBPF 程序。
能够间接一行命令,生成 eBPF 程序的用户态 WebAssembly 开发框架:
咱们提供的是 C 语言版本的 WASM 开发框架,它蕴含如下这些文件:
- ewasm-skel.h:用户态 WebAssembly 开发框架的头文件,蕴含了预编译的 eBPF 程序字节码,和 eBPF 程序框架辅助信息,用来动静加载。
- eunomia-include:一些 header-only 的库函数和辅助文件,用来辅助开发。
- app.c:用户态 WebAssembly 程序的次要代码,蕴含了 eBPF 程序的次要逻辑,以及 eBPF 程序的数据处理流程。
以 sigsnoop 为例,用户态蕴含一些命令行解析、配置 eBPF 程序和数据处理的代码,会将依据 signal number 将信号事件的英文名称增加到事件中:
最初应用容器镜像即可一行命令实现 WebAssembly/eBPF 程序的编译和打包,应用 ecli 即可一键运行:
因为咱们基于一次编译、到处运行的 libbpf 框架实现加载和启动 eBPF 程序的操作,因而编译和运行两个步骤是齐全拆散的,能够通过网络或任意形式间接进行 eBPF 程序的散发和部署,不依赖于特定内核版本。借助 WebAssembly 的轻量级个性,eBPF 程序的启动速度也比通常的应用镜像模式散发的 libbpf 程序快上不少,通常只需不到 100 ms 的工夫即可实现,比起应用 BCC 部署启动时,应用 LLVM、Clang 编译运行耗费的工夫和大量资源,更是有了质的飞跃。
下面提及的示例程序的残缺代码,能够参考这里[6]。
演示视频
咱们也有一个在 B 站上的演示视频,演示了如何从 bcc/libbpf-tools 中移植一个 eBPF 工具程序到 eunomia-bpf 中,并且应用 WASM 或 JSON 文件来散发、加载 eBPF 程序:https://www.bilibili.com/vide...
咱们是如何做到的
ecli 是基于咱们底层的 eunomia-bpf 库和运行时实现的一个简略的命令行工具。咱们的我的项目架构如下图所示:
arch
ecli 工具基于 ewasm 库实现,ewasm 库蕴含一个 WAMR(wasm-micro-runtime) 运行时,以及基于 libbpf 库构建的 eBPF 动静装载模块。大抵来说,咱们在 WASM 运行时和用户态的 libbpf 两头多加了一层形象层(eunomia-bpf 库),使得一次编译、到处运行的 eBPF 代码能够从 JSON 对象中动静加载。JSON 对象会在编译时被蕴含在 WASM 模块中,因而在运行时,咱们能够通过解析 JSON 对象来获取 eBPF 程序的信息,而后动静加载 eBPF 程序。
应用 WASM 或 JSON 编译散发 eBPF 程序的流程图大抵如下:
flow
大抵来说,整个 eBPF 程序的编写和加载分为四个局部:
- 用 eunomia-cc 工具链将内核的 eBPF 代码骨架和字节码编译为 JSON 格局。
- 在用户态开发的高级语言(例如 C 语言)中嵌入 JSON 数据,并提供一些 API 用于操作 JSON 状态的 eBPF 程序骨架。
- 将用户态程序和 JSON 数据一起编译为 WASM 字节码并打包为 WASM 模块,而后在指标机器上加载并运行 WASM 程序。
- 从 WASM 模块中加载内嵌的 JSON 数据,用 eunomia-bpf 库动静装载和配置 eBPF 程序骨架。
咱们须要实现的仅仅是大量的 native API 和 WASM 运行时的绑定,并且在 WASM 代码中解决 JSON 数据。你能够在一个繁多的 WASM 模块中领有多个 eBPF 程序。如果不应用咱们提供的 WASM 运行时,或者想要应用其余语言进行用户态的 eBPF 辅助代码的开发,在咱们提供的 eunomia-bpf 库根底上实现一些 WebaAssembly 的绑定即可。
另外,对于 eunomia-bpf 库而言,不须要 WASM 模块和运行时同样能够启动和动静加载 eBPF 程序,不过此时动静加载运行的就只是内核态的 eBPF 程序字节码。你能够手动或应用任意语言批改 JSON 对象来管制 eBPF 程序的加载和参数,并且通过 eunomia-bpf 主动获取内核态上报的返回数据。对于初学者而言,这可能比应用 WebAssembly 更加简略不便:只须要编写内核态的 eBPF 程序,而后应用 eunomia-cc 工具链将其编译为 JSON 格局,最初应用 eunomia-bpf 库加载和运行即可。齐全不必思考任何用户态的辅助程序,包含 WASM 在内。具体能够参考咱们的使用手册[7]或示例代码[8]。
将来的方向
目前 eunomia-bpf 对于一个开发工具链来说,具体的 API 规范和相干的生态是十分重要的,咱们心愿如果有机会的话,兴许能够和 SIG 社区的其余成员一起探讨并造成一个具体的 API 规范,可能基于 eBPF 和 WASM 等技术,独特提供一个通用的、跨平台和内核版本的插件生态,为各自的利用减少 eBPF 和 WASM 的超能力。
目前 eunomia-bpf 跨内核版本的动静加载个性还依赖于内核的 BTF 信息,SIG 社区的 coolbpf 我的项目[9]自身能提供 BTF 的主动生成、低版本内核的适配性能,将来低版本内核的反对会基于 Coolbpf 的现有的局部实现。同时,咱们也会给 Coolbpf 的 API 实现、近程编译后端提供相似于 eunomia-bpf 的内核态编译和运行齐全拆散的性能,让应用 Coolbpf API 开发 eBPF 的程序,在近程编译一次过后能够在任意内核版本和架构上间接应用,在部署时无需再次连贯近程服务器;也能够将编译实现的 eBPF 程序作为 Go、Python、Rust 等语言的开发包间接应用,让开发者能轻松取得 eBPF 程序上报的信息,而齐全不须要再次进行任何 eBPF 程序的编译过程。
SIG 社区孵化于高校的 Linux Microscope (LMP) 我的项目[10]中,也曾经有一些基于 eunomia-bpf 提供通用的、规范化、能够随时下载运行的 eBPF 程序或工具库的打算,目前还在持续欠缺的阶段。
参考资料
【1】eBPF 和 WebAssembly:哪种 VM 会制霸云原生时代?
https://juejin.cn/post/704372...
【2】eBPF 和 Wasm:摸索服务网格数据立体的将来:
https://cloudnative.to/blog/e...
【3】eBPF 技术摸索 SIG 主页:
https://openanolis.cn/sig/ebp...
【4】eunomia-bpf Github 仓库:
https://github.com/eunomia-bp...
【5】eunomia-bpf 龙蜥社区镜像仓库:
https://gitee.com/anolis/eunomia
【6】sigsnoop 示例代码:
https://gitee.com/anolis/euno...
【7】eunomia-bpf 用户手册:
https://openanolis.cn/sig/ebp...
【8】更多示例代码:
https://gitee.com/anolis/euno...
【9】coolbpf 我的项目介绍:
https://openanolis.cn/sig/ebp...
【10】LMP 我的项目介绍:
https://openanolis.cn/sig/ebp...
—— 完 ——