译者序
本文组合翻译了以下两篇文章的干货部分,作为 io_uring
相关的入门参考:
- How io_uring and eBPF Will Revolutionize Programming in Linux, ScyllaDB, 2020
- An Introduction to the io_uring Asynchronous I/O Framework, Oracle, 2020
io_uring
是 2019 年 Linux 5.1 内核首次引入的高性能 异步 I/O 框架,能显著加速 I/O 密集型应用的性能。 但如果你的应用已经在使用 传统 Linux AIO 了,并且使用方式恰当, 那 io_uring
并不会带来太大的性能提升 —— 根据原文测试(以及我们 自己的复现),即便打开高级特性,也只有 5%。除非你真的需要这 5% 的额外性能,否则 切换成 io_uring
代价可能也挺大,因为要 重写应用来适配 io_uring
(或者让依赖的平台或框架去适配,总之需要改代码)。
既然性能跟传统 AIO 差不多,那为什么还称 io_uring
为革命性技术呢?
-
它首先和最大的贡献在于:统一了 Linux 异步 I/O 框架,
- Linux AIO 只支持 direct I/O 模式的存储文件 (storage file),而且主要用在数据库这一细分领域;
io_uring
支持存储文件和网络文件(network sockets),也支持更多的异步系统调用 (accept/openat/stat/...
),而非仅限于read/write
系统调用。
-
在设计上是真正的异步 I/O,作为对比,Linux AIO 虽然也 是异步的,但仍然可能会阻塞,某些情况下的行为也无法预测;
似乎之前 Windows 在这块反而是领先的,更多参考:
- 浅析开源项目之 io_uring,“分步试存储”专栏,知乎
- Is there really no asynchronous block I/O on Linux?,stackoverflow
-
灵活性和可扩展性非常好,甚至能基于
io_uring
重写所有系统调用,而 Linux AIO 设计时就没考虑扩展性。
eBPF 也算是异步框架(事件驱动),但与 io_uring
没有本质联系,二者属于不同子系统, 并且在模型上有一个本质区别:
- eBPF 对用户是透明的,只需升级内核(到合适的版本),应用程序无需任何改造;
io_uring
提供了新的系统调用和用户空间 API,因此需要应用程序做改造。
eBPF 作为动态跟踪工具,能够更方便地排查和观测 io_uring
等模块在执行层面的具体问题。
本文介绍 Linux 异步 I/O 的发展历史,io_uring
的原理和功能, 并给出了一些程序示例和性能压测结果(我们在 5.10 内核做了类似测试,结论与原文差不多)。
另外,Ceph 已经支持了 io_uring
。我们对 kernel 5.10 + ceph 15.x
的压测显示, bluestore
打开 io_uring
优化之后,
- 吞吐(iops)提升
20%~30%
,同时 - 延迟降低
20~30%
。
Ceph 关于 io_uring 的资料非常少,这里提供一点参考配置:
$ cat /etc/ceph/ceph.conf [osd] bluestore_ioring = true ...
确认配置生效(这是只是随便挑一个 OSD):
$ ceph config show osd.16 | grep ioring bluestore_ioring true file
由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。
以下是译文。
很多人可能还没意识到,Linux 内核在过去几年已经发生了一场革命。这场革命源于 两个激动人心的新接口的引入:eBPF 和 io_uring。 我们认为,二者将会完全改变应用与内核交互的方式,以及 应用开发者思考和看待内核的方式。
本文介绍 io_uring(我们在 ScyllaDB 中有 io_uring 的深入使用经验),并略微提及一下 eBPF。
1 Linux I/O 系统调用演进
1.1 基于 fd 的阻塞式 I/O:read()/write()
作为大家最熟悉的读写方式,Linux 内核提供了基于文件描述符的系统调用, 这些描述符指向的可能是存储文件(storage file),也可能是 network sockets:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
二者称为阻塞式系统调用(blocking system calls),因为程序调用 这些函数时会进入 sleep 状态,然后被调度出去(让出处理器),直到 I/O 操作完成:
- 如果数据在文件中,并且文件内容已经缓存在 page cache 中,调用会立即返回;
- 如果数据在另一台机器上,就需要通过网络(例如 TCP)获取,会阻塞一段时间;
- 如果数据在硬盘上,也会阻塞一段时间。
但很容易想到,随着存储设备越来越快,程序越来越复杂, 阻塞式(blocking)已经这种最简单的方式已经不适用了。
1.2 非阻塞式 I/O:select()/poll()/epoll()
阻塞式之后,出现了一些新的、非阻塞的系统调用,例如 select()
、poll()
以及更新的 epoll()
。 应用程序在调用这些函数读写时不会阻塞,而是立即返回,返回的是一个 已经 ready 的文件描述符列表。
但这种方式存在一个致命缺点:只支持 network sockets 和 pipes —— epoll()
甚至连 storage files 都不支持。
1.3 线程池方式
对于 storage I/O,经典的解决思路是 thread pool: 主线程将 I/O 分发给 worker 线程,后者代替主线程进行阻塞式读写,主线程不会阻塞。
这种方式的问题是线程上下文切换开销可能非常大,后面性能压测会看到。
1.4 Direct I/O(数据库软件):绕过 page cache
随后出现了更加灵活和强大的方式:数据库软件(database software) 有时 并不想使用操作系统的 page cache, 而是希望打开一个文件后,直接从设备读写这个文件(direct access to the device)。 这种方式称为直接访问(direct access)或直接 I/O(direct I/O),
- 需要指定
O_DIRECT
flag; - 需要应用自己管理自己的缓存 —— 这正是数据库软件所希望的;
- 是 zero-copy I/O,因为应用的缓冲数据直接发送到设备,或者直接从设备读取。
1.5 异步 IO(AIO)
前面提到,随着存储设备越来越快,主线程和 worker 线性之间的上下文切换开销占比越来越高。 现在市场上的一些设备,例如 Intel Optane ,延迟已经低到和上下文切换一个量级(微秒 us
)。换个方式描述, 更能让我们感受到这种开销: 上下文每切换一次,我们就少一次 dispatch I/O 的机会。
因此,Linux 2.6 内核引入了异步 I/O(asynchronous I/O)接口, 方便起见,本文简写为 linux-aio
。AIO 原理是很简单的:
- 用户通过
io_submit()
提交 I/O 请求, - 过一会再调用
io_getevents()
来检查哪些 events 已经 ready 了。 - 使程序员能编写完全异步的代码。
近期,Linux AIO 甚至支持了 epoll()
:也就是说 不仅能提交 storage I/O 请求,还能提交网络 I/O 请求。照这样发展下去,linux-aio 似乎能成为一个王者。但由于它糟糕的演进之路,这个愿望几乎不可能实现了。 我们从 Linus 标志性的激烈言辞中就能略窥一斑:
Reply to: to support opening files asynchronously
So I think this is ridiculously ugly.
AIO is a horrible ad-hoc design, with the main excuse being “other, less gifted people, made that design, and we are implementing it for compatibility because database people — who seldom have any shred of taste — actually use it”.
— Linus Torvalds (on lwn.net)
首先,作为数据库从业人员,我们想借此机会为我们的没品(lack of taste)向 Linus 道歉。 但更重要的是,我们要进一步解释一下为什么 Linus 是对的:Linux AIO 确实问题缠身,
- 只支持
O_DIRECT