概述

Alibaba Cloud Linux 2 是阿里云操作系统团队基于开源 Linux 4.19 LTS 版本打造的一款针对云应用场景的下一代 Linux OS 发行版。在首次推出一年后,阿里云操作系统团队对外正式发布了Alibaba Cloud Linux 2 LTS 版本。LTS 版本的发布是一个重要的里程碑,标志着阿里云操作系统团队将为 Alibaba Cloud Linux 2 提供长期技术支持、稳定的更新和更好的服务,为 Alibaba Cloud Linux 2 的客户提供更多保障。

Alibaba Cloud Linux 2 LTS 版本,其中一个重要的特性更新是提供了对 io_uring 的支持。io_uring 是由 block 维护者 Jens Axboe 开发的新型异步 IO 框架。io_uring 在 2019 年 1 月初提出,到 2019 年 3 月初合并到 Linux 内核主线,仅用短短的 2 个月时间就合入了 Linux v5.1,充分表明了社区对该框架的积极态度。当前社区发展非常火热,很多主流应用都开始提供对 io_uring 的支持,如 Node.js,Nginx,PostgreSQL,RocksDB,QEMU,spdk,等等。

Alibaba Cloud Linux 2 LTS 版本的 io_uring 功能同步自 Linux 内核主线 v5.4,测试过程中发现的稳定性和性能问题已得到修复,相关补丁也已被接收合入到社区上游,并持续对其进行维护和支持。

Linux IO 发展史

Linux 中有很多方式来执行文件 IO 操作。最初的 IO 系统调用需要追溯到 read(2) 和write(2),后来发展为增加 offset 参数的 pread(2) 和 pwrite(2),以及基于 vector 的版本 preadv(2) 和 pwritev(2),再扩展成允许修改 flags 的版本 preadv2(2) 和 pwritev2(2)。这些系统调用看上去多种多样,但有一个共同的特性就是同步,即系统调用需要在数据读取完成或写入完成才返回。应某些应用场景的诉求,异步 IO 接口应势而生。POSIX 对应的接口为 aio_read(3)和 aio_write(3),但由于性能不好实际使用很少。

目前异步 IO 使用最多的是 Linux Native 异步 IO,即我们通常称的 aio。不幸的是,其同样有着诸多约束:

  • 最大的限制无疑是仅支持 direct io。而 O_DIRECT 存在 bypass 缓存和 size 对齐等限制,直接影响了 aio 在很多场景的使用。而针对 buffered io,其表现为同步。
  • 即使满足了所有异步 IO 的约束,有时候还是可能会被阻塞。例如,等待元数据 IO,或者等待 request 的分配等。
  • 存在额外的拷贝开销,每个 IO 提交需要拷贝 64+8 字节,每个 IO 完成需要拷贝 32 字节,这 104 字节的拷贝在大量小 IO 的场景下影响很可观。同时,需要非常小心地使用完成事件以避免丢事件。IO 需要至少 2 个系统调用(submit + wait-for-completion),这在 spectre/meltdown 开启的前提下性能下降非常严重。

io_uring 原理介绍

为了从根本上解决当前 aio 存在的问题和约束,io_uring 全新从零开始设计的异步 IO 框架。其设计的主要目标如下:

  • 简单易用,方便应用集成。
  • 可扩展,不仅仅为 block IO 使用,同样可以用于网络/非 block IO。
  • 特性丰富,满足所有应用,如 buffered io。
  • 高效,尤其是针对大部分场景的 512 字节或 4K IO。
  • 可伸缩,满足峰值场景的性能需要。

io_uring 为了避免在提交和完成事件中的内存拷贝,设计了一对共享的 ring buffer 用于应用和内核之间的通信。其中,针对提交队列(SQ),应用是 IO 提交的生产者(producer),内核是消费者(consumer);反过来,针对完成队列(CQ),内核是完成事件的生产者,应用是消费者。 image.png

io_uring 系统调用

io_uring 一共提供了 3 个系统调用:io_uring_setup(),io_uring_enter(),以及io_uring_register(),位于 fs/io_uring.c。

/**
 * io_uring_setup - setup a context for performing asynchronous I/O
 */
int io_uring_setup(u32 entries, struct io_uring_params *p);

/**
 * io_uring_enter - initiate and/or complete asynchronous I/O
 */
int io_uring_enter(int fd, unsigned int to_submit, unsigned int min_complete,
	unsigned int flags, sigset_t *sig)

/**
 * io_uring_register - register files or user buffers for asynchronous I/O
 */
int io_uring_register(int fd, unsigned int opcode, void *arg,
	unsigned int nr_args)

Alibaba Cloud Linux 2 LTS 版本支持的异步操作如下:

  • IORING_OP_NOP 仅产生一个完成事件,除此之外没有任何操作。
  • IORING_OP_READV / IORING_OP_WRITEV 提交 readv() / writev() 操作,大多数场景最核心的操作。
  • IORING_OP_READ_FIXED / IORING_OP_WRITE_FIXED 使用已注册的 buffer 来提交 IO 操作,由于这些 buffer 已经完成映射,可以降低系统调用的开销。
  • IORING_OP_FSYNC 下发 fsync() 调用。
  • IORING_OP_POLL_ADD / IORING_OP_POLL_REMOVE 使用 IORING_OP_POLL_ADD 可对一组文件描述符 (file descriptors) 执行 poll() 操作;可以使用 IORING_OP_POLL_REMOVE 显式地取消 poll()。这种方式可以用来异步地监控一组文件描述符。
  • IORING_OP_SYNC_FILE_RANGE 执行 sync_file_range() 调用,是对 fsync() 的一个增强。
  • IORING_OP_SENDMSG / IORING_OP_RECVMSG 在 sendmsg() 和 recvmsg() 基础上,提供异步收发网络包功能。
  • IORING_OP_TIMEOUT 用户态程序等待 IO 完成事件时,可以通过 IORING_OP_TIMEOUT 设置一个超时时间,类似 io_getevents(2) 的 timeout 机制。

io_uring 用户态库 liburing

为了简化使用,原作者 Jens 开发了一套 liburing 库,用户无需了解诸多 io_uring 细节便可以使用起来,如无需关心 memory barrier,以及 ring buffer 的管理等。相关接口在头文件 /usr/include/liburing.h 中定义。 Alibaba Cloud Linux 2 LTS 提供了 liburing 和 liburing-devel 包供用户安装。

sodo yum install liburing liburing-devel

基于 liburing 的一个简单的示例如下:

#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <liburing.h>

#define ENTRIES	4

int main(int argc, char *argv[])
{
	struct io_uring ring;
	struct io_uring_sqe *sqe;
	struct io_uring_cqe *cqe;
	struct iovec iov = {
		.iov_base = "Hello World",
		.iov_len = strlen("Hello World"),
	};
	int fd, ret;

	if (argc != 2) {
		printf("%s: <testfile>\n", argv[0]);
		return 1;
	}

	/* setup io_uring and do mmap */
	ret = io_uring_queue_init(ENTRIES, &ring, 0);
	if (ret < 0) {
		printf("io_uring_queue_init: %s\n", strerror(-ret));
		return 1;
	}

	fd = open("testfile", O_WRONLY | O_CREAT);
	if (fd < 0) {
		printf("open failed\n");
		ret = 1;
		goto exit;
	}

	/* get an sqe and fill in a WRITEV operation */
	sqe = io_uring_get_sqe(&ring);
	if (!sqe) {
		printf("io_uring_get_sqe failed\n");
		ret = 1;
		goto out;
	}

	io_uring_prep_writev(sqe, fd, &iov, 1, 0);

	/* tell the kernel we have an sqe ready for consumption */
	ret = io_uring_submit(&ring);
	if (ret < 0) {
		printf("io_uring_submit: %s\n", strerror(-ret));
		goto out;
	}

	/* wait for the sqe to complete */
	while (1) {
		io_uring_peek_cqe(&ring, &cqe);
		if (!cqe) {
			printf("Not completed, waiting...\n");
			usleep(1);
		} else {
			printf("Completed\n");
			break;
		}
	}

	/* read and process cqe event */
	io_uring_cqe_seen(&ring, cqe);
out:
	close(fd);
exit:
	/* tear down */
	io_uring_queue_exit(&ring);
	return ret;
}

更多的示例可参考: https://github.com/axboe/liburing/tree/master/examples/ https://github.com/axboe/liburing/tree/master/test

使用 fio io_uring 测试性能

Alibaba Cloud Linux 2 LTS 版本在 experimental 源中提供支持 io_uring 的 fio-3.17,用户可通过 ioengine=io_uring 使用 fio io_uring 进行性能测试。

sudo yum install -y alinux-release-experimentals
sudo yum install -y fio-3.17

fio 示例:

fio -name=fiotest -filename=/mnt/vdd/testfile -iodepth=128 -thread -rw=randread -ioengine=io_uring -sqthread_poll=1 -direct=1 -bs=4k -size=10G -numjobs=1 -runtime=600 -group_reporting

io_uring 高级特性

Fixed Files and Buffers

IORING_REGISTER_FILES / IORING_UNREGISTER_FILES,通过 io_uring_register() 系统调用提前注册一组 file,缓解每次 IO 操作因 fget() / fput() 带来的开销。 IORING_REGISTER_BUFFERS / IORING_UNREGISTER_BUFFERS,通过 io_uring_register() 系统调用注册一组固定的 IO buffers,当应用重用这些 IO buffers 时,只需要 map / unmap 一次即可,而不是每次 IO 都要去做,减少get_user_pages() / put_page() 带来的开销。

Polled IO

IORING_SETUP_IOPOLL,与非 polling 模式等待硬件中断唤醒不同,内核将采用 polling 模式不断轮询硬件以确认 IO 请求是否已经完成,这在追求低延时和高 IOPS 的应用场景非常有用。

Kernel Side Polling

IORING_SETUP_SQPOLL,当前应用更新 SQ ring 并填充一个新的 sqe,内核线程 sqthread 会自动完成提交,这样应用无需每次调用 io_uring_enter() 系统调用来提交 IO。应用可通过 IORING_SETUP_SQ_AFF 和 sq_thread_cpu 绑定特定的 CPU。 同时,为了节省无 IO 场景的 CPU 开销,该内核线程会在一段时间空闲后自动睡眠。应用在下发新的 IO 时,通过 IORING_ENTER_SQ_WAKEUP 唤醒该内核线程,该操作在 liburing 中都已封装完成。

io_uring 性能评测

Alibaba Cloud Linux 2 LTS 评测

我们基于 Alibaba Cloud Linux 2 LTS 的 fio 测试评估数据如下:

测试环境:ecs.i2.2xlarge,8 vCPU 64 GiB,I2 本地存储 1788 GiB。

  • 4k 顺序读 image.png

  • 4k 顺序写 image.png

  • 4k 随机读 image.png

  • 4k 随机写 image.png

从上述测试数据可以看出:

  • 默认模式下,略微提升。
  • 开启 sqthread_poll 后,顺序读写提升很明显,达到 160% ~ 170%;随机读写提升 30% ~ 150%。

社区性能数据

原作者 Jens 在 PATCHSET v5 中有分别对比 io_uring vs libaio,io_uring vs spdk 的 4k 随机读数据: https://lore.kernel.org/linux-block/20190116175003.17880-1-axboe@kernel.dk/

测试结果如下:

  • 非 polling 模式,io_uring 相比 libaio 有略微提升。
  • 在 polling 模式下,io_uring 与 spdk 接近,甚至在 queue depth 较高时性能更好,完胜 libaio。

社区工作

阿里云操作系统团队在 backport io_uring 特性到 Alibaba Cloud Linux 2 的过程中,进一步加固 io_uring 的稳定性,同时优化性能,相关工作以补丁的形式回馈到社区。

BugFix

  • io_uring: fix __io_iopoll_check deadlock in io_sq_thread
  • io_uring: fix poll_list race for SETUP_IOPOLL|SETUP_SQPOLL
  • io_uring: restore req->work when canceling poll request
  • io_uring: only restore req->work for req that needs do completion
  • io_uring: use cond_resched() in io_ring_ctx_wait_and_kill()
  • io_uring: fix mismatched finish_wait() calls in io_uring_cancel_files()
  • io_uring: handle -EFAULT properly in io_uring_setup()
  • io_uring: reset -EBUSY error when io sq thread is waken up

性能优化

  • engines/io_uring: delete fio_option_is_set() calls when submitting sqes
    fio io_uring 提交 IO 性能提升 30%。
  • __io_uring_get_cqe: eliminate unnecessary io_uring_enter() syscalls
    在某些场景下,减少 50% 的 io_uring_enter() 系统调用开销。
  • ext4: start to support iopoll method
  • io_uring: io_uring_enter(2) don’t poll while SETUP_IOPOLL|SETUP_SQPOLL enabled
    能带来 13% 的性能提升,同时减少 20% 的 CPU 开销。

代码优化和特性重构

  • io_uring: cleanup io_alloc_async_ctx()
  • io_uring: refactor file register/unregister/update handling & io_uring: initialize fixed_file_data lock
    重构 file register/unregister/update 特性,能更好地处理大量文件场景。
  • io_uring: do not always copy iovec in io_req_map_rw()