新一代异步 IO 框架 io_uring | 得物技术
1.Linux IO 模型分类
相比于 kernel bypass 模式需要结合具体的硬件支撑来讲,native IO 是日常工作中接触到比较多的一种,其中同步 IO 在较长一段时间内被广泛使用,通常我们接触到的 IO 操作主要分为网络 IO 和存储 IO。在大流量高并发的今天,提到网络 IO,很容易想到大名鼎鼎的 epoll 以及 reactor 架构。但是 epoll 并不属于异步 IO 的范畴。本质上是一个同步非阻塞的架构。关于同步异步,阻塞与非阻塞的概念区别这里做简要概述:
什么是同步
指进程调用接口时需要等待接口处理完数据并相应进程才能继续执行。这里重点是数据处理活逻辑执行完成并返回,如果是异步则不必等待数据完成,亦可以继续执行。同步强调的是逻辑上的次序性;
什么是阻塞
当进程调用一个阻塞的系统函数时,该进程被 置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比 如网络上接收到数据包,或者调用 sleep 指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在 Linux 内核中,处于运行状态的进程分为两种情况,一种是进程正在被 CPU 调度,另一种是处于就绪状态随时可能被调度的进程;阻塞强调的是函数调用下进程的状态。
2.Linux 常见文件操作方式
2.1 open/close/read/write
基本操作 API 如下:
在打开文件时可以指定为,只读,只写,读写等权限,以及阻塞或者非阻塞操作等;具体通过 open 函数的 flags 参数指定 。这里以打开一个读写文件为例,同时定义了写文件的方式为追加写,以及使用直接 IO 模式操作文件,具体什么是直接 IO 下文会细述。open("/path/to/file", O_RDWR|O_APPEND|O_DIRECT);flags 可选参数如下:
通常读写操作的数据首先从用户缓冲区进入内核缓冲区,然后由内核缓冲区完成与 IO 设备的同步:
2.2 Mmap
Mmap 是一种内存映射方法,通过将文件映射到内存的某个地址空间上,在对该地址空间的读写操作时,会触发相应的缺页异常以及脏页回写操作,从而实现文件数据的读写操作;
2.3 直接 IO
直接 IO 的方式比较简单,直接上文提及的 open 函数入参中指定 O_DIRECT 即可,相比普通 IO 操作,略过了内核的缓冲区直接操作下一层的文件文件。该操作比较底层,相比普通的文件读写少了一次数据复制,一般需要结合用户态缓存来使用;下图所示为 DIO 透过 buffer 层直接操作磁盘文件系统:
2.4 sendFile
严格来讲,sendfile 并不提供完整的读写能力,仅用于加速读取数据到网络的能力,由于数据不经过用户空间,因此无法对数据进行二次处理,也就是说从磁盘中读出来原封不动的发给网卡,下图展示了 sendFile 的工作流程,
数据首先以 DMA 的方式从磁盘上读取到内核的文件缓冲区,
然后再从文件缓冲区读取到了 socket 的缓冲区,该过程由 CPU 负责完成。
接着网卡再以 DMA 的方式从 socket 缓冲区 拷贝到自己网卡缓冲区,然后进行发送
Linux 内核 2.4 版本以后对 sendFile 进行了进一步优化,提供了带有 scatter/gather
的 sendfile 操作,将仅有一次的 CPU 参与 copy 环节去掉,该操作需要网卡硬件的支持。其原理就是在内核空间 Read Buffer 和 Socket Buffer 不做数据复制,而是将 Read Buffer 的内存地址、偏移量记录到相应的 Socket Buffer 中。其本质和虚拟内存的解决方法思路一致,就是内存地址的记录。
2.5 splice
splice 调用和 sendfile 很相似,应用程序必须拥有两个已经打开的文件描述符,一个表示输入设备,一个表示输出设备。splice 允许任意两个文件互相连接,而并不只是文件与 socket 进行数据传输。对于从一个文件描述符发送数据到 socket 这种特例来说,简化为使用 sendfile 系统调用,splice 适用范围更广且不需要硬件支持, sendfile 是 splice 的一个子集。
用户进程调用 pipe()陷入内核态;创建匿名单向管道 pipe() 返回,从内核态切换回用户态;
用户进程调用 splice()从用户态陷入内核态,DMA 控制器将数据从硬盘拷贝到内核缓冲区,从管道的写入端"拷贝"进管道,splice() 返回,从内核态切换为用户态;
用户进程再次调用 splice(),从用户态陷入内核态,内核把数据从管道的读取端拷贝到 socket 缓冲区,DMA 控制器将数据从 socket 缓冲区拷贝到网卡,splice() 返回,上下文从内核态切换回用户态。
3.IO_URING 是什么
io_uring
是 Linux 提供的一个异步非阻塞 I/O 接口,他既能支持磁盘 IO 也能支持网络 IO,只是存储 IO 支持的比较早较为成熟。IO_URING 的使用需要较高的 linux 内核版本,一般建议 5.12 版本以后。下面会分别从存储和网络两个角度来介绍 IO_URING 。
3.1 IO_URING 架构
应用程序提交的 IO 请求会直接进入 submission queue 队列的尾部,内核进程会不断的从 SQ 队列的头部消费请求
内核处理完的 SQ 后会更新 CQ tail 部分 ,应用程序读取到 CQ 的 head 时,会更新 CQ 的 head
SQ 中的任务称之为 SQE(entry), CQ 中的任务称之为 CQE
3.2 系统调用 API
3.3 三种工作模式
3.3.1 中断驱动模式:
默认模式。可通过 io_uring_enter() 提交 I/O 请求,然后直接检查 CQ 状态判断是否完成。也可以通过 min_complete 来睡在 enter 方法上,等待完成事件到达 ;
3.3.2 轮询模式
相比中断驱动方式,这种方式延迟更低, 但是会消耗更多的 CPU,应用线程需要不断的调用 enter 函数,然后陷入内核态后持续地 polling,等到一个 min_complete 到达。但是注意的是此时 polling 关注的是完成事件。3.3.3 内核轮询模式这种模式中,会创建一个内核线程(kernel thread)来执行 SQ 的轮询工作( 是否有新的 SQE 提交 )。使用这种模式应用无需切到到内核态 就能触发(issue)I/O 操作。应用线程通过 mmap 机制更新 SQ 来提交 SQE,以及监控 CQ 的完成状态,应用无需任何系统调用,就能提交和收割 I/O(submit and reap I/Os)。如果内核线程的空闲时间超过了用户的配置值,它会通知应用,然后进入 idle 状态。这种情况下,应用必须调用 io_uring_enter()
来唤醒内核线程。如果 I/O 一直很繁忙,内核线程是不会 sleep 的。在日常的使用中一般建议选择后两种轮训模式,用户线程轮存在用户态到内核态的切换,相比内核轮询存在一定的性能损耗;io_uring 之所以能达到超高性能的原因主要在以下几个方面:
Mmap 机制减少了 内存复制
内核轮询模式下,没有用户态和内核态的切换降低了损耗
基于 SQ 和 CQ 机制下的数据竞争消除,即没有并发竞争损耗
3.4 liburing
io_uring 的核心系统调用只有三个,但使用起来较为复杂,开发者在 io_uring 之上封装了新的 liburing 库,简化使用。
liburing github 地址 : https://github.com/axboe/liburing
3.5 使用方式
3.5.1 读取文件
调用 io_uring_queue_init 初始化
获取一个空 SQE 用于提交任务
io_uring_prep_readv 方法填充 SQE 任务内容
io_uring_submit 提交 SQE
io_uring_wait_cqe 获取已完成的 CQE
io_uring_cqe_seen 更新 CQ 队列的 head ,避免 CQE 被重复处理
io_uring_queue_exit 退出 io_uring
下面是 liburing github 上的 example 代码适当精简后的代码
3.5.2 网络服务
网络服务这里直接参考 Github 地址:GitHub - frevib/io_uring-echo-server: io_uring echo server
4.性能对比
4.1 存储 IO
Synchronous I/O、 Libaio 和 IO_uring 特性对比
io_uring 和 spdk 的特性对比
SPDK 全名 Storage Performance Development Kit,是一种存储性能开发套件 。针对于支持 nvme 协议的 SSD 设备。是一种高性能的解决方案。
io_uring 和 spdk 的性能对比
非 polling 模式,io_uring 相比 libaio 提升不是很明显;在 polling 模式下,io_uring 能与 spdk 接近,甚至在 queue depth 较高时性能更好,性能超越 libaio。
在 queue depth 较低时有约 7%的差距,但在 queue depth 较高时基本接近。
对比结论:
*io_uring 在非 polling 模式下,相比 libaio,性能提升不是非常显著。 *io_uring 在 polling 模式下,性能提升显著,与 spdk 接近,在队列深度较高时性能更好。
4.2 网络 IO
Epoll 性能对比
与 epoll 的性能对比差异还是很大的,参考这篇文章的数据 https://juejin.cn/post/7074212680071905311测试环境:wsl2,内核版本5.10.60.1,发行版为Debian硬件:I5-9400,16gDDR4使用webbench进行简易测试,模拟10500、30500台客户端,持续时间为5s,分别在正常访问和不等待返回两种模式下进行测试,两个客户端均关闭日志记录,epoll开启双ET模式,比较每分钟发送页面数,结果如下:
对比结论:
毋庸置疑,碾压性的结果。
5.总结
得益于精妙的设计,io_uring 的性能基本超越 linux 内核以往任何软件层面的 IO 解决方案,达到了与硬件级解决方案媲美的性能。io_uring 需要较高版本的内核支持,目前还没有大面积普及,但可以预料他是 linux 内核 IO 未来的核心发展方向。
版权声明: 本文为 InfoQ 作者【得物技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/b9a23dd5961b7c46daa32cb5f】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论