很多人认为融合eBPF技术会是未来计算型存储器**(Computational Storage,CSD)的一个重要发展方向。本文调研了eBPF技术的原理和应用场景**,并指出“Why eBPF+CSD”。此外,本文还调研了当下在CSD中使用eBPF技术的难点和可能的替代选项。
什么是eBPF技术?
前身:BPF(Berkeley Package Filter)
BPF背景:于1992年提出,当时unix操作系统提供了捕获数据包的设施以监控当前的网络情况。但是,网络监控程序(如tcpdump)处于用户态,数据包必须频繁地被从内核态被拷贝到用户态。因此,一些被称为【数据包过滤器】的内核代理被提出了,以尽早在内核就丢弃不想要的数据包,避免从操作系统内核向用户态复制其他对用户态程序无用的数据包,从而极大提高性能。BPF是其中一种。
图 经典的BPF
BPF实现:用户态的进程可以提供一个过滤程序,BPF功能用对于BPF虚拟机机器语言的解释器来实现。
extended BPF: 从3.18版本开始,linux内核中提供了一种扩展的BPF虚拟机(eBPF),可以被用于非网络相关的内核其他用途,比如附着在不同的tracepoint上获取内核运行的信息,eBPF也被windows操作系统所采用。
eBPF技术简介
现代eBPF:eBPF现在已经不再是任何东西的缩写,而是一项单独的技术。它可以在不修改内核源代码或者加载内核模块的情况下,在特权上下文(如操作系统内核中)运行沙盒程序,被用来安全高效地拓展内核程序。可编程的部分直接添加到现有层中。
优势:由于内核具有监督、控制整个系统的特权,操作系统一直是实现可观察性、安全性和网络功能的理想场所。但是内核由于其核心作用和对稳定性和安全性的高要求,修改内核很难。eBPF则改变了这一点,并且由操作系统来保证其安全性和执行效率。
可编程性:因为eBPF虚拟机使用的是类似于汇编语言的指令,可编程性差。现在的编译器正在兼容高级语言生成BPF字节码。例如,LLVM在3.7版本开始支持BPF字节码作为后端输出。
eBPF的实现:
图 eBPF开发流程
eBPF技术现有的应用领域
- 网络(协议解析、转发逻辑)
- 跟踪和分析(可以同时跟踪内核和用户程序的执行,提供强大的解决内核问题的能力)
- 安全性(更好的保护系统,允许结合所有方面的可靠性)
- 传统存储(实现数据过滤)
- ……
图 eBPF面向的应用场景和技术栈
为什么说eBPF+CSD是一个重要的发展方向?
当前CSD发展所目前面临的难点
目前,CSD的可编程性仍然很差:
- CSD的设计没有统一的标准。由于不了解设备内部的复杂程度,且很难去对内部编程,探索硬件模型和在主机上统一集成接口非常困难;
- CSD没有标准的编程抽象、API或者编程模型。之前的工作往往都是选定编程模型,而不是让开发者自己来自定义编程以适应当前工作负载。
可编程性意味着可以动态、安全地运行用户提供的代码,能够方便的将用户需要的任务卸载到存储器上。而eBPF可以将用户定义的任务“注入”内核,刚好符合这样的要求;又因为eBPF有现有的工具链和库,CSD研究者们希望用eBPF来为CSD提供任务卸载。
为什么选择eBPF?
- 不影响内核:CSD需要在主机上开发程序并让存储控制器执行它们,而不影响控制器上原有固件的执行;而使用eBPF技术可以将eBPF程序挂载到内核中,而无需修改内核。
- 架构无关:eBPF与架构无关。在计算型存储器中我们希望使用与硬件无关的指令集,而不是预先知道CSD上处理器的型号再来下载对应的代码(X86或ARM)。
想象一个可能的应用场景:假设我们要对压缩加密后的大型数据库表进行数据分析。首先,借助CSD的可编程部分,我们可以在离线阶段在CSD中定义一个通用的控制程序,例如将数据读取到CSD的内存缓冲区,并解压解密数据放入CSD上的一个本地位置;在线阶段用户可以通过eBPF自定义计算程序,对数据进行过滤,最后再将过滤后的数据传输到主机。这样可以通过自定义的方式,能够更好地贴合数据特征,进一步节省内存和带宽,比之前的方式灵活的多。
CSD与内核的相同点和不同点
- 相同点:eBPF指令集和工具链已经相对来说比较成熟。我们像kernel中一样地把 C 程序编译成eBPF 目标文件,就可以在CSD中使用 eBPF 生态中已有的东西。
- 不同点:eBPF发源于内核,而内核对 eBPF 程序允许执行的操作有非常多的限制。例如,eBPF 指令集是允许用户写无限循环的,但Linux内核中的虚拟机在检查时会拒绝该程序。这是由之前的应用场景导致的:因为内核在处理数据包的时候没有那么复杂的操作。但是在CSD的场景下,我们可能希望运行 eBPF 做计算,这就需要对一些复杂数据结构进行处理,要放宽原有的限制。
CSD+eBPF产生的研究性问题
- 如何让eBPF执行计算任务:eBPF目前的应用主要是一些计算复杂度低的控制类程序,且必须要通过现有的应用于Kernel场景的eBPF虚拟机进行验证。但计算程序更复杂,运行时间更长。因此,如何将eBPF应用到计算任务上,以使得程序运行效率接近基于目标硬件指令集的程序是目前eBPF开发人员关注的一个重要问题。
- 可能的方法:修改eBPF虚拟机定义的正确运行时限制、定义新的计算指令(跟CSD无关)
- 任务卸载:对于一般的大型计算任务,哪些工作负载卸载在CSD上会更有意义?我们如何为这些任务定义一个通用的 API 或框架?
- 通信问题:在存储设备中的CPU往往算力较小,因此其需要辅有其他专用硬件来完成计算。eBPF程序与存储上其他设备的通信方式也是难点之一。
一个可能的eBPF+CSD prototype
来自阿姆斯特丹自由大学的团队探索了**基于QEMU仿真ZNS,来实现eBPF+**CSD的一个prototype。(仍然在开发中,文档健全;官网给出运行的视频样例)(https://github.com/Dantali0n/OpenCSD)。我们可以通过这个项目去展望一下eBPF+CSD的设计:
图 技术报告封面
配置
- QEMU:仿真Zone NameSpace SSD(ZNS)。
- SPDK:一种与用户空间的存储设备接口(在本项目中就是指能够与QEMU仿真的ZNS)的技术。
- uBPF(user-space BPF):一种ebpf虚拟机。
图 ZCSD的工作流程
主机端
- 编写程序BPF程序:用户编写的BPF程序中应当包含所有需要在CSD上运行的API;
- 编译:Clang+LLVM编译生成bpf字节码;
- 格式转换:Bpftool转换将bpf字节码转换为要用户程序包含的头文件,其中包含有elf格式的字节码;
- 包含头文件:头文件包含在用户写的程序中,用户有了调用CSD上eBPF程序接口的能力
之后编译好的BPF程序可以通过用户写的程序连同NVMe API来使用了。
存储端
- 程序提交:用户程序以elf格式提交用户程序
- 虚拟机****执行:uBPF被用来执行BPF程序,提供BPF API的实现
- 取数据:BPF程序通过SPDK在底层的ZNS中取数据,ZNS返回的数据由驱动程序和SPDK处理。
- 自定义计算:BPF程序进行计算或过滤,将数据返回给用户程序。
- 用户接收数据:用户程序接收数据。
NVMe扩展了原有的NVMe指令集:
- 下载eBPF程序:以elf格式发送BPF程序;
- 返回处理数据:将eBPF程序的返回数据提取出来。
eBPF不是唯一的选择
现有的一些讨论将eBPF视为对CSD进行卸载的主要方案,这一观点并没有仔细考虑其他的选项。https://arxiv.org/abs/2111.01947这篇论文从**定性**和**定量**的角度来综合比较了**eBPF**和**WebAssembly(另一种易于移植的、低层次的字节码,用于基于堆栈的****虚拟机****)**的区别,给出了他们对于这个问题的见解。
CSD的其他选择
- eBPF:现在指允许在Linux内核总运行沙盒程序的技术的总称。但是我们在这里尤其对基于寄存器的eBPF字节码感兴趣。eBPF现在应用在网络包过滤以及系统监视等多种应用场景中。以下展示了用eBPF写I/O tracer的工作过程:用户用C语言写一个I/Otracer,然后用clang编译成eBPF字节码,这个字节码将会被libbpf加载进内核执行。虽然目前来看eBPF字节码常常被用于linux kernel的场景下,但是也有很多工作去尝试独立运行eBPF的字节码。
- WebAssembly:WebAssembly是一易于移植的、低层次的字节码,用于基于堆栈的虚拟机。它是为在浏览器中安全高效的运行而设计的。已经有工具从python或C语言来生成WebAssembly的代码,为了在浏览器环境之外执行这些字节码,可以使用wasmtime/wasmer/GraalVM等带有JIT的虚拟机。
计算型存储器背后的思想是代码迁移,将处理直接卸载到存储设备中。过去的方案试图为特定类型的工作负载提供专用软硬件,而缺乏对通用计算卸载的讨论,例如用户可编程的、动态可卸载的计算。
分析目标
通过定性和定量评估eBPF和WebAssembly两种卸载机制来重启关于通用计算任务卸载机制的讨论。具体来说是回答以下问题:
- 这两个卸载机制当前处于一个什么样的状态?
- 二者的优缺点分别是什么?
- 在计算型存储器的背景下怎么去改进这些两种机制?
定性分析
安全性
这里的安全性是指抵御人为错误的机制:如果卸载的bytecode里有bug,能发生的最坏结果是什么?
- WebAssembly:卸载的代码会在一个隔离的内存空间中运行,并且默认是不允许访问文件系统。因此bug可能最差会引起CPU时钟周期的浪费,但是并不会在runtime之外造成什么影响。
- eBPF:主要取决于验证器。Linux内核验证非常严谨,禁止很多类的循环和数据访问;而使用这样的内核进行数据处理显然不适用。新的验证器仍然待开发,因此还不能说eBPF的安全性如何。
工具链
- 开发工具:对于WebAssembly,有一系列叫做wabt(WebAssembly Binary ToolKit)的工具可以来检查和转换WebAssembly的二进制文件。对于eBPF,bpftool能够用来检查eBPF程序,但是本质上它与内核的使用绑定,所以在独立的场景下没法使用。Llvm-objdump对于ebpf来说是更好的选择。
- 虚拟机:如之前所提到的,WebAssembly有至少三种适合不同场景的虚拟机实现。虽然他们都有一些同样的特点例如JIT支持,但是他们也有很多独立的特征;但是eBPF却仍然还有很长的路要走。uBPF和rBPF都有JIT支持(仅x86_64),但是他们仍然缺乏分析和调试等方面的功能。
兼容性
这里的兼容性指重用现有生态中库的能力,任何外部依赖项(包括C库libc)都要被重新编译并静态链接。
- WebAssembly:使用wasi-sdk进行编译,但是也有一些函数例如fork/pthread无法build。
- eBPF:没有wasi-sdk这样的工具,由于无法调用C库,所以大量现存的库也无法使用了;除此之外,由于clang -target bpf不支持一些种类的数值操作和浮点操作,一些数据类型也可能不支持,这主要是因为eBPF缺乏合适的指令让clang来生成。因此,我们并不知道这些操作将如何被eBPF来使用除非eBPF有重大的扩展。
可移植性
由于二者都是字节码,所以都比较好移植
- eBPF:并不是大小端独立的。ebpf的大小端依赖会影响他的移植
- WebAssembly:总是用小端模式,如果host架构是大端的,runtime可以来负责转换
用户体验
- 编译:WebAssembly可以由多种语言产生,可以用两种方式(Emscripten和wasi-sdk)来编译进行C库支持。除此之外,支持链接时间优化、关闭系统C库、无main入口支持等选项,最后还可以对生成的code码来缩减大小;ebpf可以用clang的bpf选项来编译,但是实际上是对内核内部的应用进行编译的。
- 字节码检查:wabt工具链中有一个wasm2wat的工具,能够将二进制webassembly文件转换为可读的文本文件。也可以使用wasm-objdump进行反编译。ebpf虽然还没有正式的文本格式,如果ebpf二进制文件是由debugging信息编译成的,可以用交叉源代码查看反汇编的二进制文件。
语言不可知性
- webassembly:可以用大量的源语言。但是实际上,源语言必须支持导出malloc()以分配buffer,并且在主机和虚拟机之间传递程序。
- ebpf:目前只支持C语言为源语言。
定量分析
内存占用:rbpf和ubpf接近原生表现
运行时间:实际计算所花费的时间,rBPF和uBPF表现非常好,以至于能够击败host上的binary。
总时间:rBPF和uBPF在x64架构上经常击败基于host的方式
讨论
虽然eBPF虚拟机在定量分析中显示占有更少的内存、计算性能更好,但是作者从定性分析的角度,认为eBPF存在更多的缺陷,而webAssembly可能是更成熟、更容易上手的选择。
图 定量分析实验其中之一
图 对eBPF和WebAssembly定性分析的总结
下面是一些潜在的可能提升机会:
webassembly:作为一个相当成熟的技术,仍然有以下改进方向
- 实现SIMD和多线程
- 提供与host数据共享的接口
- 解决安全问题
eBPF:根据前面的评估,eBPF在成为适合的数据处理工具之前还需要做很多工作
- 成熟的虚拟机,能够支持debug和性能分析
- 扩展指令集,以支持多种数字运算
- 明确定义允许操作的规范
- 与规范同步开发的可靠验证器
- C库和其他系统服务的支持
- 本文作者: Zhang Xinmiao
- 本文链接: https://recoderchris.github.io/2022/07/11/eBPF/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!