译自:BPF for storage: an exokernel-inspired approach
BPF主要用于报文处理,通过绕过网络栈提高报文的处理速度。本文则用于通过绕过存储栈(文件系统、BIO等层)来提高存储的读写效率,但在实现过程中也遇到了相应的挑战,如文件和块的映射关系,多进程共享存储块以及进程间的QoS等。
概要内核存储路径开销占新式NVMe存储设备访问延迟的一半。本文中我们将探究使用BPF在内核的I/O处理栈中注入用户定自定义函数来降低这种开销。当发起一系列独立的I/O请求时,这种方式可以(通过绕过内核层以及避免跨内核边界)增加2.5倍的IPOS,并降低一半延迟。但在绕过文件系统和块设备层的同时时应该避免丢失重要的属性,如文件系统的安全性和物理块地址和文件偏移之间的转换。我们从90年代后期的外核文件系统中汲取灵感,为这些问题提供了可能的解决方案。
简介历史上,存储设备在实现高带宽和低延迟的目标上要远低于网络设备,现在很多网卡(NICs)都可以支持100 Gb/s的带宽,而物理层的存储设备仅支持2-7GB/s的带宽,以及4-5us的延迟[8, 13, 19, 20]。当使用这些存储设备时,软件栈会在每个存储请求上花费大量开销,在我们的实验中占存储请求占了一半的I/O操作延迟,且可能对吞吐量造成更大的影响。
内核旁路(kernel -bypass)框架(如SPDK[44])以及靠近存储的处理方式可以降低内核开销。但两者同样都有明显的缺点,例如通常需要定制,需要对应用程序进行更改[40,41],缺乏隔离性,在I/O使用率不高时会浪费大量等待时间,以及在计算存储中需要定制的硬件[10, 16, 42]。因此,我们期望使用一个支持标准OS的机制来降低高速存储设备的软件开销。
为此,我们参考了很早就支持高速带宽设备的网络通信。Linux的eBPF[6]为应用提供了一种直接在内核中嵌入简单函数的接口。当用于拦截I/O时,可以在应用中使用这些函数进行处理,以此来避免内核和用户空间的上下文切换和数据拷贝。Linux eBPF通常用于报文处理和过滤[5,30]、安全[9]和追踪[29]。
BPF在内核中普遍存在,并被广泛用于在网络栈外针对特定应用进行内核侧扩展。BPF可以用于链式依赖的I/Os,消除内核存储栈传输以及与用户空间的数据转换造成的开销。例如,它可以遍历一个基于磁盘的数据结构(B树,该数据结构中一个块会引用另一个块)。通过在内核中嵌入这些BPF函数,就可以像内核旁路一样消除发起I/Os造成的开销,但与内核旁路不同的是,它不需要轮询以及浪费CPU时间。
为了了解BPF可以获得的性能,我们确立了四个与存储使用有关的开放研究。首先,为了便于采纳研究结果,在架构上必须支持标准的Linux文件系统,并尽量减少对应用的修改,同时应该兼具灵活性,且能够高效运行并绕过大量软件层;其次,存储使BPF超出了当前的简单报文处理的范畴,由于报文是自描述的,这使得BPF对它们的处理在大部分场景下是相互隔离的。而硬盘上的数据结构的传输通常是有状态的,且经常需要查询外部状态。存储BPF需要了解应用的磁盘格式,并访问外部的应用状态或内核状态。例如,为了并发访问或访问内存的结构的元数据;再者,我们需要保证BPF存储函数在允许应用间共享磁盘容量时不会违背文件系统的安全性。存储块本身通常不会记录块的所有者或访问控制属性,与网络报文相比,报文的首部指定了该报文所属的流。因此我们需要一种有效的方案来实施访问控制,且不会在内核文件系统和块层引入开销。最后,我们需要保证并发性。应用程序需要通过细粒度同步(如锁耦合[28])来支持并发访问,以此避免读写对高吞吐量的影响,因此BPF函数可能需要同步功能。
我们的灵感来源于外核文件系统。用户定义的内核扩展是XN的基石(用于Xok 外核),它通过从用户进程下载到内核[32]的代码来支持互不信任的"libfs"es。在XN中,这些不可信的功能被解释为让内核按照用户定义的方式来理解文件系统元数据。这种方式不会影响应用和内核设计,且满足了文件系统布局的灵活性目标。虽然我们需要一个类似的机制来允许用户让内核理解他们的数据布局,但实现的目标是不同的:我们希望在设计上允许应用使用自定义的兼容BPF的数据结构以及传输和过滤磁盘数据的功能,且能够与Linux现有的接口和文件系统一起运作,通过大幅削减每个I/O执行所需要的内核代码量来驱动百万级的IOPS。
软件是存储的瓶颈在过去的几年中,使用NVMe连接到高带宽PCIe的SSD中出现了新的内存技术,这使得存储设备在性能上可以与高速网络设备[1]相媲美,具有毫秒级的访问延迟以及每秒GB级别的带宽[8, 13, 19, 20]。现在内核存储栈成为了这些新式设备的瓶颈。