本文主要是介绍FAST‘23《λ-IO: A Unified IO Stack for Computational Storage》论文解读,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
FAST’23《λ-IO: A Unified IO Stack for Computational Storage》论文解读
Data:2023-2-27
Ref: Z. Yang et al., “λ-IO: A Unified IO Stack for Computational Storage,” in 21st USENIX Conference on File and Storage Technologies (FAST 23), Santa Clara, CA, Feb. 2023, pp. 347–362.
省流版:计算型存储通过减少数据搬运的方式提高了数据处理的性能,但是当前的计算型存储缺乏高效的软件栈,增大了开发难度,无法充分发挥性能。这个工作的核心是修改并移植eBPF到CSD和内核中,为主机和CSD提供了相同的运行时环境。为了解决不同应用在不同平台执行效率不同的问题,通过动态性能监测和时延评估模型将任务选择性地分发给主机或者CSD以最大化系统性能。同时提供了基于现有I/O栈的API。这篇论文是清华大学舒继武教授团队在NDP领域耕耘数年的最新成果,是目前近数据领域为数不多的全栈开发框架,让近数据处理技术距离广泛应用更近了一步,开源地址:https://github.com/thustorage/lambda-io
文章目录
- FAST'23《λ-IO: A Unified IO Stack for Computational Storage》论文解读
- 背景及问题
- λ-IO
- API与工作流程
- 运行时环境移植
- eBPF扩展(sBPF)
- 文件管理
- 请求分发器
- 评估模型
- 性能监测
- 实验
- 总结
背景及问题
NDP(Near-Data Processing)技术通过在存储器内部支持数据处理,减少了数据搬移的开销。支持NDP技术的SSD称为计算型存储(CSD,Computational Storage)。
但是计算型存储器内通常使用低功耗的嵌入式处理器,计算性能低。如果盲目地将任务交给CSD执行,可能会造成性能低下;同时即便有的任务在CSD内执行更快,CSD内资源紧张时将这个任务交给主机处理也更好。本文进行了2项实验证明以上2点:1)如下图a所示。Stats64和32分别是将一个文件视为64或者32位的整数并进行求和等操作。可以看出Stats64在盘内执行效率更高,32在主机执行效率更高(因为eBPF不支持32位整数计算,通过左移和右移操作来清除寄存器的前32位,因此引入了额外开销并且处理效率低)2)如下图b所示,横轴代表分别在主机和CSD执行任务的不同阶段的时延,可以看到主机在一开始时延很高但是后面时延就低于CSD了,因为主机在刚开始时并没有缓存数据。
λ-IO
本文的主要工作包含3个方面:API、eBPF运行时移植(sBPF)、请求分发调度。λ-IO的架构图如下所示。
API与工作流程
λ-IO的一个设计原则是尽量兼容以及利用现有的IO栈,因此调用方法上也尽量与现有接口保持一致以降低开发和学习成本。API列表以及示例程序如下所示,接下来介绍API含义及工作流程:
- 首先需要定义算子。用户在
compute
内自定义数据处理方法(代码第1-9行),λ-IO运行时系统会将数据放入input
内,并且会开辟一个空间用于返回数据由output
参数所指向,length_i
代表了input内的数据长度。定义好算子后编译为sBPF字节码。 - 在使用时,首先需要利用
load
加载算子文件(代码第13行)。load
根据提供的sBPF字节码程序的路径读取程序,然后分别发送给内核和CSD内的执行环境,并返回一个λ_id
。然后两个运行时环境进行程序检查后翻译为各自平台的可执行二进制程序*(这里有点不太理解为什么要CSD来执行字节码翻译,移植翻译器复杂并且CSD性能低,为什么不在主机翻译好后直接发送给CSD呢?)* - 打开与关闭文件的API与POSIX完全相同(代码第24,26行),λ-IO复用了其文件句柄。
- 读文件时,使用
pread_λ
进行读取操作代替pread
(代码第17行)。pread_λ
多出来了一个λ_id
参数,作用就是传入之前通过load
加载的算子。当数据读出来了之后,λ-IO就会调用之前定义的算子,将读出来的原始数据传入compute
,并将计算完成后存储在output
中的数据存储到pread_λ
给的buf中。这个步骤的一个核心问题是,如何在盘内进行这一过程,因为盘内并没有文件系统的语义信息,也就是说盘内无法通过句柄、路径等参数在盘内将数据加载到compute
的参数中。解决方法在下一节中介绍。 - 写文件与读文件类似,不过输入输出是反过来的。调用
pwrite_λ
时传入的buf
是作为compute
的input
,计算出需要写入的数据后写入到output
中,λ-IO会将其写入文件的offset
处。
// sum.c
ssize_t compute(void *output, void *input, size_t length_i) { int sum = 0; for (int i = 0; i < length_i / sizeof(int); i++) { sum += ((int *)input)[i]; } *output = sum; return sizeof(int); // return the output size
}// main.c
int lambda_io_sum(int fd, int file_size) {int l_id = load_l("sum.sbpf"); char buf[sizeof(int)]; int sum = 0; for (int i = 0; i < file_size; i += BUF_SIZE) {pread_l(fd, buf, BUF_SIZE, i, l_id);sum += *(int *)buf; } return sum;
}int main() { int fd = open("path/to/file"); lambda_io_sum(fd, FILE_SIZE); close(fd); return 0;
}
运行时环境移植
为了在主机和CSD两种完全不同的平台和系统内提供相同的执行环境,λ-IO选择了时下流行的eBPF技术。根据我的理解,eBPF是一种程序翻译技术,可以将用户按规范编写的程序编译为eBPF字节码,然后经过检查校验后发送到执行环境内翻译为平台特定的二进制程序。
λ-IO为了解决eBPF应用在CSD内的问题,将eBPF修改扩展为了sBPF。运行时环境的移植主要包含2部分的工作,eBPF扩展以及文件管理:
eBPF扩展(sBPF)
eBPF应用在CSD内主要面临2个问题,一个是指针检查的问题,其次是循环限制的问题。
- 指针访问:由于eBPF最初是为内核设计的,因此对指针数据访问的检查很严格。eBPF提供了函数
bpf_probe_read
来检查指针访问,但是开销很大。因此λ-IO采用一个简单的检查机制,当访问一个地址时,检查其是否是在[input,input+length_i)
或者[output,output+length_i)
范围内,如果不是的话就返回错误码。 - 循环限制:由于eBPF在内核中执行,因此需要严格控制其循环数以避免其执行时间过长。eBPF会在静态检查时检查其循环次数,超过循环次数限制时会不通过。但是在CSD内其循环次数往往难以确定,是动态变化的,无法通过静态检查解决。因此sBPF在运行时动态检查循环次数,在每个循环后添加一个计数器,当循环次数超过阈值时停止执行。通常阈值设置为输入数据长度相同数量级的数值,可以尽量避免干扰正常程序执行同时可以终止bug。
文件管理
λ-IO为了尽量少修改现有IO栈以及复用现有架构,实现对不同文件系统的兼容,基于VFS和页缓存构建了文件访问机制。文件管理的主要目的是为pread_λ
和pwrite_λ
提供input
和output
参数的地址。
在主机端,访问文件很简单,λ-IO打开文件后通过mmap
映射所需的数据到用户态,然后作为input
参数传入即可。但是在CSD端,CSD内部是没有文件索引信息的,因此需要主机先读取出所需文件的元数据(通过filefrag
工具),然后通过自定义接口发送给盘内的运行时环境。
对于写入请求,可能会需要扩展文件长度。这时需要主机先利用fallocate
提前分配好空间再发送请求给CSD。并且分配空间后需要将extent的unwritten
标志位重置,否则主机读到这个extent时会认为里面没有数据直接返回空,但是其实CSD可能已经在盘内往里面写入了数据。
对于数据一致性的问题,主要是主机和CSD间的一致性。λ-IO采取了加锁的方式,再进行λ操作时将文件锁住避免执行请求时数据被主机修改导致数据不一致。
请求分发器
一个请求可能在主机也可以在CSD中执行更快,同时随着工作负载的变化,在CSD执行更快的请求可能暂时交给主机执行也会更快。为了解决这个问题实现性能的最大化,本文设计了一个动态请求分发器。
动态请求分发器的核心思想是将一个请求切分为多个子请求,并且实时获取系统性能参数,然后通过一个简单的时延评估模型定期评估在主机和在CSD的执行时间,然后分发到耗时最短的平台上。
评估模型
λ-IO定义了几个模型参数:
- D D D:需要从存储单元中读取出来的数据大小
- B s B_s Bs:存储单元到CSD控制器的传输带宽,也就是存储单元的速度
- B d B_d Bd:CSD到主机间的传输带宽,也就是硬盘接口的速度
- B h B_h Bh:主机的计算带宽,也就是主机端的数据处理速度
- t h , t d t_h,t_d th,td:主机与CSD的执行时间
- α \alpha α:数据放大率,也就是原始数据经过处理后结果数据的大小比值。例如在盘内进行数据处理后,传输到主机的数据量就为 α D \alpha D αD
- β \beta β:性能比率,CSD相比主机性能的比值。即CSD的处理能力为 β B h \beta B_h βBh
- c c c:缓存命中率
根据这些参数,评估模型如下:
t h = ( 1 − c ) D B s + ( 1 − c ) D B d + D B h t_h=\frac{(1-c)D}{B_s} + \frac{(1-c)D}{B_d} + \frac{D}{B_h} th=Bs(1−c)D+Bd(1−c)D+BhD
t d = D B s + α D B d + D β B h t_d=\frac{D}{B_s} + \frac{\alpha D}{B_d} + \frac{D}{\beta B_h} td=BsD+BdαD+βBhD
性能监测
只要获取到模型中各个参数的值,即可评估出某个任务在某一端上的执行时间。但是这些值有的并不是能快速获得精确值的,本文将这些参数分为了2个部分来获取:
-
直接获取参数
有一些参数是可以直接获取的,包括数据量
D
可以直接从请求中获取的,缓存命中率c
可以通过查询page cache获取(不断遍历访问page cache个人认为开销有点高了) -
估计参数
有一些参数是没法直接获取的,因此需要通过监测系统状态得到,包括各个带宽和比率。λ-IO并不测量整个系统的可用资源,而是以请求为单位分别测量他们的参数。具体的测量方法文中没有提及。为了避免为每个请求都测量参数而引发巨大的开销,λ-IO采取的周期测量的方法。在一个时间段内的前数个请求中,将请求同时发送给主机和CSD以获取两端的参数,从而进行评估后决定这个周期剩余的请求应该交给哪端执行。在一个周期结束后重新进行评估。
实验
本文的实验基于可编程SSD Daisy开发板完成,但是采用板上的64G DRAM来模拟闪存。盘内带宽(闪存到控制器)是3.52GB/s,接口带宽是3.22GB/s。实验部分简单介绍,详情大家可以去看论文。
在单应用的测试中,λ-IO分别与有缓存的IO(B),无缓存的直接IO(I),MMAP(M),全部在主机端执行(K),全部在设备端执行(D),动态执行(λ)几种方式进行了对比。并且在进行了预热有缓存数据(w/o)和没有预热没有缓存数据(w/)的2种情况下进行了对比,结果表明在各种情况下λ-IO都能取得几乎最佳的性能。
同时本文将λ-IO移植到了Spark SQL中分析在实际系统中的性能。可以看到在IO密集的查询中能够取得比较好的效果,在CPU密集型任务中能够获得与主机近似的性能。
总结
这篇文章为计算型存储设计了一个完整的I/O栈,涉及到用户态、内核态、存储器固件等多个层面的开发与移植工作,做的工作非常多,具备较高的实用价值。我认为这项工作还能从以下几个方面进一步研究或者优化:
-
文件系统部分为了与现有IO栈兼容,牺牲了部分的性能可优化空间。例如文件系统的元数据仍然需要在主机端进行管理,在存储器数量较多或者高并发请求时可能会成为瓶颈。采取盘内文件系统(DevFS,FAST‘18)的方式管理也许可以解决这一问题。
-
性能评估的准确性应该可以进一步优化。首先对于单个请求来说,需要两端同时执行一个任务以获取参数,但是同时执行时两端发出的IO请求会相互影响;其次对于多应用并发时,评估的结果与两端分别正在执行的任务息息相关,可能评估结束后系统状态就变化了。获取的性能评估结果可能偏差较大。
-
API不支持多文件的数据联合处理。输入输出数据都来自于单个文件的读写操作,某些任务(例如将多个文件的内容读取出来合并到一个文件中)无法在CSD内完成,限制了能处理的任务类型。
-
当文件规模很大时,输入输出缓冲区可能会放不下。主机端可以采用虚拟内存的方式进行swap操作,但是CSD内没有OS的支持,物理内存的大小限制了能处理的数据大小。
这篇关于FAST‘23《λ-IO: A Unified IO Stack for Computational Storage》论文解读的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!