【FAST‘24论文解读】I/O直通:上游Linux一种灵活和有效的I/O路径

引言

前几天有一位朋友问我一个问题:如何优化单机存储引擎在混合读写下的吞吐和延迟?根据我的了解,单机引擎有基于文件系统的、有直接管理裸盘的,有基于KV数据库的,还有基于DPU等软硬协同设计实现的。这些年随着闪存设备等高性能介质的大量使用,传统的文件系统和I/O堆栈所带来的性能损耗越发明显,其设计抽象愈发不适应新介质标准的发展。通过调整参数一定程度上能提升性能,但总体效果有限。针对这种情况,视对当前机制继承的多寡,新的解放办法大致分成改良派和改革派,改良派在现有基础上打补丁,做适应性扩展,如:Linux内核在块层新增多队列、增加新的异步接口io_uring,文件系统增加DAX支持,针对闪存优化的kvdk等。改革派抛弃现有的机制,重头造,如:针对闪存设计的F2FS,针对Zoned SSD开发的ZenFS,Bypass Kernel的SPDK,NV的GDS, Ceph的BlueStore等。

今天给大家分享,我在FAST’24上看到的一篇论文,这是一篇关于单机引擎优化的文章,相关的实现已合并到上游的Linux内核版本,论文分析了当前I/O路径在NVMe介质使用上存在的不足,然后参考NVMe 2.0规范及演进趋势对io_uring进行了针对性的扩展,结果显示相比块I/O,新的方法取得了16-40%的性能提升。

下文非论文的逐字翻译,但不影响对原文的理解,完整的论文,请点击上文链接

io_uring在5.10内核可用,根据论文提供的信息,优化合并的上游Linux版本为5.13~6.2。而生产环境上使用的主流Linux发行版内核大部分都是在4.18及以下,基于裸盘的引擎,当前采用Bypass Kernel的SPDK实现可行性会更好些,如有实力维护高版本的Linux内核,io_uring也是不错的选项,开发集成复杂度要比spdk低很多,有资料显示io_uring在polling模式下,高并发性能可以达到spdk的80-90%。

动机和背景

论文首先介绍了NVMe 2.0规范中新增的命令集(如:Zoned Namespace,KV),目的在于说明NVMe提供了更丰富的语义,而不仅仅再是块存储标准。 接着通过一张Linux I/O栈图(如下)说明了三种I/O访问路径(A:通过文件系统,B:绕过文件系统,直接访问通用块层,C:绕过文件系统,Ioctl直接访问NVMe设备),指出现有系统调用接口及访问方式的不足,无法覆盖持续增长的NVMe命令,如:Zoned-namespace,FDP等,鉴于Linux接口的高度通用性,仅仅为NVMe设备新增接口也不具有实践性,ioctl提供的是同步接口,对高并发的NVMe设备来说,存在有效性问题。最后介绍了最新的异步I/O子系统io_uring,io_uring通过polling、mmap减少了上下文和内存拷贝的开销,还为各种同步系统调用提供了基于工作线程的异步(Worker-based Async)和异步(True-Async)支持。
Linux I/O stack

设计考量

论文首先指出上游的Linux NVMe驱动通过ioctl操作码向应用提供的直通访问接口存在的问题:

  1. 与块设备绑定
  2. Ioctl同步接口带来扩展性和有效性问题
  3. 提交命令和收割应答时的内存拷贝的效率问题
  4. 只有root可以访问

相应地,给出了新设计的目标:

  1. 不依赖于块I/O:覆盖所有的NVMe命令,而不仅仅是块语义
  2. 捕获所有的用户接口:为每个新增的NVMe命令添加一个Linux系统调用不现实,因此对于现有或者将来的NVMe命令,新的解决方案需要确保无需创建新的用户接口
  3. 有效且可扩展:新的接口需要有不低于已有块I/O的有效性和扩展性
  4. 普遍可用:不仅仅root,普通用户也可以访问
  5. 被上游Linux接受:新的解决方案应该成为Linux官方的一部分,这可以确保使用者无需重现创造或者维护第三方代码

架构及实现

论文中提出的I/O直通,如下图黄色框的路径D:一个新的字符设备/dev/ng0n1作为后端,io_uring通过新引入的io_uring_command与其对接。选择使用io_uring,是因为它是比libaio更高效、特征更丰富且在上游Linux更活跃的I/O子系统。
新的I/O直通
接下来,从三个方面来详细说明其设计和实现:

可用性

使用NVMe通用字符设备来解决块设备的可用性问题(如上所述块层无法覆盖所有的NVMe命令),修改NVMe驱动为NVMe设备的每个命名空间创建一个字符设备,不管存在任何不受块设备支持的特征都会创建。存在未知命令集的情况下,也会创建字符设备。因此,字符设备可以用于将来出现的命令集,而无需进一步修改NVMe驱动代码。下面的列表给出了字符设备的file_operations操作集,如上图,用户态可以通过ioctl向字符设备发送任何的NVMe命令,第6行是ioct在NVMe驱动中的l句柄。还可以通过io_uring与字符设备交互,这会带来一系列的优势,第8行是uring_cmd在NVMe驱动中的句柄。

1 const struct file_operations nvme_ns_chr_fops =
2 {
3  . owner = THIS_MODULE ,
4  . open = nvme_ns_chr_open ,
5  . release = nvme_ns_chr_release ,
6  . unlocked_ioctl = nvme_ns_chr_ioctl ,
7  . compat_ioctl = compat_ptr_ioctl ,
8  . uring_cmd = nvme_ns_chr_uring_cmd ,
9  . uring_cmd_iopoll = nvme_ns_chr_uring_cmd_iopoll,
10 };

有效性及扩展性

解决NMVe直通的根本是,找到一种替代ioctl的有效选项,这种选择需要足够的通用,为此在io_uring中添加了三类新设施:io_uring_cmd,大SQE(Big SQE),大CQE(Big CQE)。为进一步降低每个I/O的开销,还提出了两个新特征:固定缓冲(fixed-buffer)和完成轮询(competion-polling)。

io_uring命令

io_uring提供了工作线程异步和异步两种异步方式,根据下图的性能对比结果,我们选择向io_uring中添加异步的新设施:io_uring_cmd

下图显示io_uring在工作线程异步(Worker-based Async)和真异步(True-Async)下512kb随机读的性能扩展性,
随着队列深度的增加,真异步性能快速增长,工作线程异步增长不明显

Aync对比
用户接口通过新的操作码IORING_OP_URING_CMD准备SQE。正常的SQE有16字节的空间给用户程序使用,CQE有4字节的空间用于接受请求结果。如下给出的字符设备命令有近80字节,16字节的SQE的空间已不足,因此引入了包含80字节的大SQE(Big SQE),应用程序可以指定IORING_SETUP_SQE_128标志来创建使用大SQE的ring。相似的,NVMe命令不仅仅返回一个结果,如:zone-append命令还返回写入的位置,因此引入包含24字节的大CQE(Big CQE),应用程序可以指定IORING_SETUP_CQE_32标志来创建使用大CQE的ring。

通用的io_uring命令支持任何底层命令,命令提供者可以是任何与io_uring协作的内核组件,如:文件系统,驱动,NVMe驱动是首个进入内核的提供者,还包括ublk和网络socket。io_uring和命令提供者间的通信遵循异步(True-Async)方式,如下图:提交命令时,io_uring处理SQE,同时准备另一个io_uring_cmd结构(如下)用于后续的通信。io_uring调用file_operations操作集中的uring_cmd向命令提供者提交请求。命令提供者对提交请求任何必要的处理,然后无阻塞的返回io_uring。命令的提交与完成解耦,当命令提供者调用携带主结果和辅助结果的io_uring_cmd_done时命令才完成。主结果放在正常的CQE中,辅助结果放在大CQE中。
uring_cmd通信

1 struct io_uring_cmd  {
2  struct file * file ;
3  const void * cmd ;
4  union {
5  /* to defer completions to task context */
6  void (* task_work_cb ) (struct io_uring_cmd * cmd);
7  /* for polled completion */
8  void * cookie ;
9  };
10 u32 cmd_op ;
11 u32 flags ;
12 u8 pdu[32];  /* available inline for free use */
13 };

异步处理

为支持io_uring驱动的NVMe直通,在NVMe驱动中增加了用于I/O的命令:NVME_URING_CMD_IONVME_URING_CMD_IO_VEC,以及用于管理的命令:NVME_URING_CMD_ADMINNVME_URING_CMD_ADMIN_VEC。与传统的I/O用readv/writev进行批提交一样,上述命令的向量变种,允许从用户空间一次传递多个缓冲区。上述命令使用的结构如下:

1 struct nvme_uring_cmd {
2 __u8 opcode ;
3 __u8 flags ;
4 __u16 rsvd1 ;
5 __u32 nsid ;
6 __u32 cdw2 ;
7 __u32 cdw3 ;
8 __u64 metadata ;    /*元数据*/
9 __u64 addr ;
10 __u32 metadata_len ;  /*元数据长度*/
11 __u32 data_len ;
12 __u32 cdw10 ;
13 __u32 cdw11 ;
14 __u32 cdw12 ;
15 __u32 cdw13 ;
16 __u32 cdw14 ;
17 __u32 cdw15 ;
18 __u32 timeout_ms ; /*命令超时*/
19 __u32 rsvd2 ;
20 };

零拷贝:用户空间在大SQE中创建这些结构,避免了数据拷贝。辅助结果通过大CQE返回,也避免了数据拷贝。结构中没有包含result字段,确保了控制路径的零拷贝。
零内存分配:与同步直通不同,我们将命令的完成与提交解耦,这样命令的发起者就不会被阻塞。这个异步过程需要一些字段一直存在直到命令完成,动态分配这些字段会给I/O增加延迟,我们复用io_uring_cmd结构中的pdu字段的空闲空间来记录这些字段。

固定缓冲(Fixed-Buffer)

I/O缓冲必须锁定在内存中,以便进行任何数据传输。每个操作中的锁定和解锁定增加了I/O成本。如果相同的缓冲重复的用于I/O,这种情况可以被优化。io_uring可以提前用io_uring_register锁定多个缓冲,应用程序使用操作码IORING_OP_READ_FIXEDIORING_OP_WRITE_FIXED来使用这些缓冲。我们使用新的标志 IORING_URING_CMD_FIXED为uring_cmd引入这一能力。应用程序指定这个标志以及SQE中缓冲索引。在内核中,NVMe驱动检查该标志是否存在,如果存在,NVMe驱动就不会尝试锁定缓冲,而是与io_uring交互复用之前锁定的区域。最后,我们增加了一个任何命令提供者都能使用的内核API io_uring_cmd_import_fixed,来提供上述的能力。

完成轮询

io_uring允许应用程序通过轮询来完成读写I/O,这有助于减少上下文切换的开销。当使用polled_queues=N加载NVMe驱动时,给那些在命令完成时不生成中断的NVMe设备创建N个轮询队列对(SQ+CQ)。因为io_uring将命令提交和完成解耦,异步轮询完成是可能的。这比同步训练更有用,因为应用程序提交请求后可以继续做其他的工作,而不是让CPU空转。我们扩展了uring_cmd的异步轮询,因为这个原因,提交请求时,NVMe驱动中有两件事以不同的方式完成: 1) 为提交命令选择轮询队列, 2)一个提交id记录在io_uring_cmd的cookie字段。命令完成时,也需要两个id来查询命令:1)命令提交时的队列id,2)命令id。这两个id组合在一个4字节实体中,称为cookie。一个新的回调uring_cmd_iopoll被添加到字符设备的file_operations操作中,用于完成命令的循环轮询,它通过解码io_uring_cmd中的cookie,在NVMe的完成队列中查找匹配的完成项。

可访问性

Linux默认采用DAC来管理对象的访问。文件的mode是数字化的表示,指定谁(user+group+other)允许做什么(read+write+execute)。当应用程序请求打开文件时,VFS使用文件mode来做第一层权限检查。当应用程序用打开的文件发起命令时,NVMe驱动完成第二层权限检查。但是,NVMe驱动使用粗粒度的CAP_SYS_ADMIN检查来保护所有的直通操作,完全忽略文件的mode。如:

$ ls -l --time - style =+ / dev / ng *
2 crw -rw -rw - 1 root root 242 , 0 / dev / ng0n1
3 crw ------- 1 root root 242 , 1 / dev / ng0n2

即使字符设备/dev/ng0n1被设置为任何人可以读写,也没有用,表现得和/dev/ng0n2一样。全有或者全无的CAP_SYS_ADMIN检查将直通接口限制root用户。我们修改了NVMe的驱动,实现了一个细粒度的策略,在访问控制中将文件mode和命令类型都考虑进来,如下:

  • CAP_SYS_ADMIN被设置,一切照常。否则检查命令的类型(I/O命令,管理命令)
  • 对于任何可以写/修改设备的写命令,只有文件mode包含写权限才被允许
  • 对于任何只读/从设备获取信息的命令都是允许的
  • 管理命令,如:识别命名空间,识别控制器是允许的。但是其他的管理命令不被允许。
    除了DAC,uring_cmd还支持MAC。为uring_cmd定义了新的LSM钩子,SELinux和Smack为钩子实现相应的策略。

块层

NVMe直通并不是完全绕过块层,而是不在设备上再增加一层。NVMe通用字符设备完全摒弃了块抽象,提供了比通过块设备直通更清晰的语义。下图显示了命令提交时,I/O直通与块层的交互,块层实现了很多的公共功能:
与块层的集成
下表总结了块I/O和直通I/O在块层的功能差异:
块层功能

  • 抽象设备限制:如,块层使得那些不支持单次读取超过64KB的设备支持发送更大的I/O成为可能,块层将大的读分割成64KB的命令。自然,直通无法抽象设备限制。
  • I/O调度:I/O调度能够合并进来的I/O,直通I/O跳过了它,因为不适用I/O调度NVMe SSD表现的更好。通常,NVMe SSD有很深的队列,有很好的内部I/O调度来满足SLA。有研究表明,Linux的I/O调度会带来50%以上的损耗以及影响扩展性。Linux企业发行版,为NVMe的I/O调度默认设置为none
  • 多队列:块层在Blk-MQ设施中抽象了设备队列,使得他们可以在CPU cores间共享。直通也使用这个设施。
  • 标签管理:块层为每个硬件队列管理未完成的命令。它管理命令id的分配和释放,因此驱动不用实现流量控制。
  • 命令取消和终止:如果一个命令执行得比预期的时间长,块层能够检测到超时并终止未完成的命令。直通支持用户指定的超时,然后块I/O使用硬编码的超时。

上游Linux的支持情况

内核态的支持情况如下表:各部分均已提交到Linux官方仓库
内核支持
用户态支持情况,包括:

  • xNVMe集成:xNVMe是一个跨平台的用户态I/O库,它抽象了多种同步和异步后端,包括:io_uring, libaio以及spdk。使用xNVMe编写的应用程序可以无缝在各后端切换。我们扩展了xNVMe,添加了一个新的异步后端:io_uring_cmd, 这个后端与NVMe字符设备一起工作。
  • SPDK集成:SPDK包含一个块设备层bdev,为下层设备实现了一致的块设备API,如:NVMe bdev基于SPDK的NVMe驱动实现,AIO bdev和uring bdev分别基于linux aio和io_uring实现。我们添加了一个新的xNVMe bdev,如下图,这个bdev允许在AIO,io_uring, io_uring_cmd间切换。这个新的bdev从22.09版本开始成为SPDK的一部分。
    SPDK集成
  • nvme-cli:修改支持枚举字符设备,任何能在nvme块设备上执行的操作,也能在nvme字符设备上执行。
  • Fio:增加了新的ioengine引擎io_uring_cmd,使用该引擎是需要指定cmd_type,这给支持将来其他的直通设备提供了灵活性,对NVMe直通,cmd_type设置为nvme,同时filename应该设置为/dev/ngXnY。这个新引擎从3.31版本依赖成为Fio的一部分。Fio仓库包含一个工具t/io_uring, 可以方便的评估io_uring的峰值性能,我们扩展了这个工具,因此可以用来评估NVMe直通的峰值性能。
  • Liburing:给应用程序提供了更简单的io_uring接口,我们扩展了它以支持大SQE和大CQE。

启动NVMe接口的I/O直通

通过两个例子来说明I /O直通的灵活性和有效性

灵活的数据放置(Flexible Data Placement)

FDP是NVMe标准中最新的主机控制的数据放置方法。已经通过的标准提案增加了新的概念,如:回收单元(RU),放置标识(PID)。RU与SSD的GC单元类似,通过在写命令中指定PID,主机可以将逻辑块地址(LBA)放入RU。使用一个PID写入的LBA地址不会与使用领一个PID写入的LBA地址混在一起,这有助于区分SSD中不同的数据生命期以及减少写放大。

当多流支持成为NVMe标准指令时,Linux内核开发了基于写入提示(write-hint-based)设施,允许应用程序随着写请求发送放置提示。然而,这项设施不再有效,因为它的核心代码已经被从Linux主线删除。I/O直通来救场了,应用程序可以发送放置提示而无需担心要将FDP集成到内核存储栈的各部分。论文中提供了Cachelib通过I/O直通使用FDP的实验,另外,从FIO 3.34版本开始io_uring_cmd引擎已经支持FDP。

可计算存储

可计算存储允许主机将各种计算操作卸载到存储上,减少数据移动以及资源消耗。可计算存储的NVMe标准正在制定中,它涉及两个新的命名空间:

  • 内存命名空间: 指子系统的本地内存(SLM),字节寻址的内存允许在本地处理SSD数据,主机需要发起新的NVMe命令,1)在主机内存和SLM间传输数据, 2)在NVM命名空间和SLM间拷贝数据。
  • 计算命名空间:表示运行在SLM数据上的各种计算程序,主机使用新的NVMe命令编排本地数据的处理,如:执行程序,加载程序,活动程序等。

在内核中支持可计算存储很有挑战,因为这些命名空间带来非块设备语义以及很多新的非传统命令。然而,通用字符接口对于SLM和计算命名空间都能很好支持。通过I/O直通接口所有新的NVMe命令都能有效发起。

端到端数据保护

很多NVMe SSD都具备随着数据携带额外的元数据的能力。这些元数据可以跟在数据后面(DIF)或者独立的空间(DIX)。这种能力也很容易支持EC。元数据的全部或部分可以包含保护信息(PI),PI包括校验和,引用标志以及应用标志。NVMe SSD控制器在读写时检查PI内容。

如下图,内核对E2E数据包含的支持有限,DIF不被支持因为数据缓冲区不对齐。块层支持DIX,但只支持PI放在元数据第一个字节的方式。另外,PI是块层生成的,因为缺少接口用户无法指定。
内核E2E支持
I/O直通不受缓冲区对齐以及缺少用户接口的影响。直通命令结构允许应用传递元数据缓冲区级长度(见上述的nvme_uring_cmd 结构)。我们也给FIO的io_uring_cmd引擎添加了DIF和DIX功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值