高性能存储的另一块拼图——DM 支持 IO Polling

简介: DM (Device Mapper) IO polling 新特性[1]目前正瞄准 v5.13 窗口。有了该特性的支持,DM 设备开始正式支持 io_uring 的 polling 模式,从而为 Linux 的高性能存储栈补上了另一块拼图。 > 注 > 1. io_uring 是 Linux 的新一代高性能异步 IO 框架,由 block 子系统的维护者 Jens Axboe 亲自

DM (Device Mapper) IO polling 新特性[1]目前正瞄准 v5.13 窗口。有了该特性的支持,DM 设备开始正式支持 io_uring 的 polling 模式,从而为 Linux 的高性能存储栈补上了另一块拼图。

  1. io_uring 是 Linux 的新一代高性能异步 IO 框架,由 block 子系统的维护者 Jens Axboe 亲自操刀,与 eBPF 并列近些年 Linux 的两大热点,可参考 2[4]。
  2. 目前 DM 设备中只有 dm-linear、dm-stripe 等设备类型支持 IO polling,该特性由 Ming Lei \<ming.lei@redhat.com> 与 Jeffle Xu \<jefflexu@linux.alibaba.com> 共同开发。
  3. 目前只有 DM 设备支持 IO polling,但是 MD 设备想要支持 IO polling 只需要一些小修改。

导读

DM 设备

DM (Device Mapper) 子系统用于在真实的物理磁盘之上创建虚拟磁盘,从而满足用户的特殊需求。

例如 dm-linear 能够将多块物理磁盘线性拼接为一块虚拟磁盘。

+-----------------------+-----------------------+
|         disk A        |         disk B        |
+-----------------------+-----------------------+

dm-stripe 能够将虚拟磁盘的一段连续存储空间分别映射到多块物理磁盘,例如下图中虚拟磁盘 16KB 大小的连续存储空间,前 4KB 映射到物理磁盘 A,其后的 4KB 映射到物理磁盘 B,以此类推。这样虚拟磁盘的 16KB 存储空间执行 IO 操作的时候,底层的所有物理磁盘都在工作,从而最大化利用所有物理磁盘的能力。

+-----------+-----------+-----------+-----------+
|  chunk 0  |  chunk 1  |  chunk 2  |  chunk 3  |
+-----------+-----------+-----------+-----------+
|  chunk 4  |  chunk 5  |  chunk 6  |  chunk 7  |
+-----------+-----------+-----------+-----------+
|  chunk 8  |  chunk 9  |  chunk 10 |  chunk 11 |
+-----------+-----------+-----------+-----------+

|   disk A  |   disk B  |   disk C  |   disk D  |
| (stripe 1)| (stripe 2)| (stripe 3)| (stripe 4)|

IO polling

软件与硬件设备常见的交互模型是,软件通过设置相应的寄存器,告诉硬件执行什么任务;
软件需要在硬件完成任务后获取到其结果,通常有硬件中断(IRQ)通知和软件主动轮询(polling)两种模型。

中断模型下,软件通过寄存器向硬件下发完任务后,软件就不再关心这件事,转而执行其他任务;之后硬件完成任务后,通过中断 (通过拉低处理器的 INT 引脚电平,或者在 PCIe 总线发送 MSI/MSIX 消息) 的方式通知软件。这种模型适用于硬件设备速度慢、系统的 IO 负载小的场景,此时处理器的每个时间片都在干有意义的事情,从而最大化地提升处理器的利用率。

然而当硬件设备的速度提升、系统的 IO 负载增大时,中断本身带来的开销,叠加上频繁发生的中断,反而会成为性能的瓶颈。此时轮询模型正适用于这种场景。该模型下,软件在下发任务后,会在一个循环中“询问”硬件,之前下发的任务是否已经完成,相当于是用 CPU 换 IO 性能。

由中断模型向轮询模型的更替,是硬件能力提升、对系统高性能 IO 能力诉求的必然结果,网卡的 NAPI 接口是如此,本文介绍的块设备的 IO polling 同样是如此,万变不离其宗。

下表是中断模式下 4K 随机读/写的 d2c 延时在整个 IO 延时中的占比。d2c 延时描绘的是设备侧延时,具体是指软件向硬件下发任务之后,到设备完成任务、通过中断通知到软件的这一段时间。因而 d2c 延时在总延时中的占比越小,说明设备的速度越快。从下表可以看出,对于 Optane SSD 的 4K IO 来说,设备侧的延时已经足够小,使得软件侧的开销不容忽视,其中也包括了中断以及中断处理的固有开销。

因而 IO polling 特性的支持,是建设高性能存储栈不可或缺的重要一环。

Device IO type d2c %
Optane SSD 4KB randread 82%
Optane SSD 4KB randwrite 82%

DM polling

实现原理

内核在 v4.4 版本已经引入 IO polling 的支持,但是彼时该特性只支持 NVMe 设备,而并不支持 DM 这类虚拟设备。

其中缘由涉及到 NVMe 设备与 DM 设备的 IO 下发模式的差异。DM 设备发送的一个 IO 有可能会同时转发给底层的多个物理设备,例如上述 dm-stripe 设备下发的 16K IO,实际上就会分解并转发给底层的四个物理设备 (4K * 4)。为方便理解 DM IO polling 特性,我们将 IO 下发的过程与生活中的网购打比方。

如果是在天猫超市购物 (向 NVMe 设备发送 IO),清空购物车的时候,所有购买的商品只对应一个订单号 (下发 IO 返回的 cookie),之后大家焦急等待的过程中只需要根据这一个订单号,反复查看商品有没有发货 (轮询)。

而如果是在淘宝购物 (向 DM 设备发送 IO),清空购物车的时候,如果所有购买的商品对应多个店铺,那么就会产生多个订单号 (DM 设备发送的 IO 可能会转发给底层的一个或多个物理设备,从而产生多个 cookie)。此时就不能只通过一个序列号 (订单号) 查询所有的商品是否已经发货。

这就是 DM 设备支持 IO polling 特性时遇到的困难,即如何用一个序列号描述“清空购物车”这个动作 (上层下发 IO) 触发的所有订单 (IO 请求)。

而此次 DM IO polling 特性的实现,简单地说就是返回用户的淘宝账号 (执行 IO 下发操作的进程的 PID) 作为序列号,通过这个序列号可以追踪到用户的所有订单号 (DM 设备的一个 IO 可能转发给底层的多个物理设备,具体转发给哪个物理设备,这些信息都会记录在下发 IO 的进程的 io_context 中),进而查询用户的所有商品是否已经发货。

实现细节

控制路径

对于 DM 这类建立在物理设备之上的虚拟设备来说,只有当底层的所有物理设备都支持 IO polling 时,DM 设备才支持 IO polling。目前只有 NVMe 设备支持 IO polling,因而只有 NVMe 设备之上构建的 DM 设备才支持 IO polling。

此外对于异步 IO,只有支持 NOWAIT 的块设备才能执行 polling 操作,这是为了防止潜在的死锁风险。由于硬件限制,一个块设备能够同时处理的 IO 数量是有限的,在寻常 IO 的下发过程中,硬件资源用尽时,当前进程上下文会睡眠等待,直到之前下发的 IO 完成时腾出相应的硬件资源;而对于 NOWAIT 标记的 IO,下发过程中硬件资源用尽时,当前进程上下文会直接返回错误,而不是睡眠等待。

如果 IO 下发、轮询在同一个进程上下文中执行,不添加 NOWAIT 标记的 polling 操作有可能导致死锁。试想以下场景,如果同一个进程上下文先下发 IO,再作轮询处理,那么下发过程中异步 IO 通常会一次性批量下发多个 IO,因而很容易将设备的硬件资源用尽,此时如果没有 NOWAIT 标记,当前进程上下文就必须睡眠等待,等待之前下发的 IO 完成从而返还一些硬件资源;而如果设备工作在 polling 模式,即 IO 的完成、继而硬件资源的返还都依赖于进程的轮询操作,那么此时就会形成死锁,即当前进程卡在 IO 下发阶段,而无法执行轮询操作。

因而 io_uring 要求执行 polling 操作的块设备必须支持 NOWAIT,即设备驱动在 IO 下发过程中遇到阻塞时必须立即返回。对于 DM 设备来说,dm-linear/dm-stripe 这类设备由于原理简单,只涉及到简单的地址转换,因而下发过程中并不存在阻塞点,因而是支持 NOWAIT 的[5];而其他 DM 设备类型 IO 下发过程更为复杂,其中含有阻塞点,因而不支持 NOWAIT。

因而目前 DM 设备中只有 dm-linear/dm-stripe 这些设备类型是支持 IO polling 的。

数据路径

由于 DM 设备的一个 IO 有可能分解并转发给底层的多个物理设备,因而这一个 IO 就可能对应多个 cookie (每个 cookie 描述发往的一个底层物理设备),内核正是通过这些 cookie 作为输入实现轮询操作的,这些 cookie 实际上保存在当前下发 IO 的进程的 poll context 中。

为了支持 DM IO polling,每个进程的 io_context 中维护一个 poll context。如之前所述,poll context 用于缓存 DM 设备下发的 IO 对应的 cookie 信息,其实际上由 sq (submission queue) 与 pq (polling queue) 两个链表组成。

IO 下发过程中,下发的 IO 对应的所有 cookie 会缓存在当前进程的 poll context 的 sq 链表中;IO polling 过程中,就会对 sq 链表中的所有 IO 作轮询处理,检查其是否完成,已经完成的 IO 会从链表中移除,最后剩余未完成的 IO 会返还给当前进程的 poll context 的 pq 链表中;再下一次 IO polling 过程中,会优先对 pq 链表中的 IO 作轮询处理,再处理 sq 链表中的 IO。

实际上 IO polling 过程中,并不是直接对 pq/sq 链表中的 IO 作轮询处理,而是先将 pq/sq 链表中的 IO 转移到当前栈上的本地链表中,之后再作轮询处理。这里的本地链表、以及将 poll context 拆分为 sq/pq 两个链表,都是为了减缓锁竞争。因为 IO 的下发与轮询可能是在两个不同的进程上下文中执行的,同时多个进程可能会共享同一个 io_context,这都导致多个进程可能同时并发访问这些链表,此时 IO 的入链表、出链表操作都必须使用锁进行保护,将原来一个大的链表拆分为三个链表 (即 sq/pq 以及栈上的本地链表),可以减小锁竞争的粒度,从而减小竞争。

undefined

此外由于 poll context 是 per 进程的,因而一个进程下发的所有 IO 都会缓存到一个 poll context 中,这样在执行轮询操作的时候,会一次性对同一个进程下发的所有 IO 作轮询处理,也就是批量处理。

以下简要描述了 IO 的下发和轮询过程中,链表的动态变化过程。

dm-polling.gif

性能提升

我们测试了 dm-linear/dm-stripe 的 4k randread 在 IO polling 模式下相较于 IRQ 模式的性能提升。下表是设备在这两种模式下的 IOPS 数据,可以看到 IO polling 存在 ~30% 的性能提升。

IRQ IOPOLL ratio
dm-linear 639K 835K ~30%
dm-stripe 314K 408K ~30%

undefined

测试环境中
dm-linear 构建在三个 NVMe 设备之上,bs=4k, iopoll=1, iodepth=128, numjobs=3, direct, randread, ioengine=io_uring
dm-stripe 构建在三个 NVMe 设备之上,chunk_size=4k, bs=12k, iopoll=1, iodepth=128, numjobs=3, direct, randread, ioengine=io_uring

结语

始于两年前的 io_uring 目前来看大有席卷之势,然而想要解锁 io_uring 的完整形态,底层的块设备必须支持 IO polling 特性。目前各种设备类型也都开始支持这一特性,NVMe 最早支持这一特性,DM IO polling 特性在可预见的未来也即将合入主线,同时社区也有 virtio-blk、SCSI 支持 IO polling 的讨论与尝试。

而正如文章标题所示,块设备对 IO polling 特性的支持,实际上是为 Linux 原生的高性能存储栈拼上了另一块重要的拼图,有助于提升 io_uring 在高性能存储领域的竞争力。

Reference

[1] block: support bio based io polling
[2] Linux异步IO新时代:io_uring
[3] Alibaba Cloud Linux 2 LTS 率先提供支持 io_uring
[4] io_uring 与 spdk 性能测试对比
[5] dm: add support for REQ_NOWAIT to various targets

相关文章
|
6月前
|
存储 Java 芯片
BIO 阻塞IO流 数据存储
为了弄清楚 I/O 流 的执行原理,首先需要了解数据在磁盘中的存储方式。目前进行数据存储的磁盘分为两种:机械磁盘、固态硬盘。
|
NoSQL Unix Linux
Redis核心技术与实践 03 | 高性能IO模型:为什么单线程Redis能那么快?
Redis核心技术与实践 03 | 高性能IO模型:为什么单线程Redis能那么快?
|
5月前
|
存储 缓存 NoSQL
Redis为什么速度快:数据结构、存储及IO网络原理总结
Redis为什么速度快:数据结构、存储及IO网络原理总结
|
6月前
|
存储 搜索推荐 文件存储
IO、存储、硬盘、文件系统相关常识
IO、存储、硬盘、文件系统相关常识
52 2
|
存储 网络协议 Linux
2.10 高性能异步IO机制:io_uring
2.10 高性能异步IO机制:io_uring
666 0
|
6月前
|
NoSQL Java Linux
【Linux IO多路复用 】 Linux 网络编程 认知负荷与Epoll:高性能I-O多路复用的实现与优化
【Linux IO多路复用 】 Linux 网络编程 认知负荷与Epoll:高性能I-O多路复用的实现与优化
184 0
|
6月前
|
存储 监控 网络协议
怎么做到高性能网络IO?
怎么做到高性能网络IO?
|
存储 Cloud Native 安全
高性能存储SIG月度动态:受邀分享 erofs 云原生进展,io_uring 基线完成更新
高性能存储SIG月度动态:受邀分享 erofs 云原生进展,io_uring 基线完成更新
|
NoSQL 网络协议 Redis
Redis学习笔记-高性能IO模型&Redis6.0多线程
Redis学习笔记-高性能IO模型&Redis6.0多线程
134 0