UringNet设计的思路和参考
在2019年,从Linux内核5.1开始,引入了io_uring这样的异步框架,io_uring的设计非常精巧,经过验证,其性能极其强悍,在文件读写的领域已经证明了其巨大的价值。很多数据库系统的底层已经引入并采用了io_uring这个组件。其文件读写性能远远超过了原来Linux中的AIO异步接口。下图是io_uring的4k顺序读和写与AIO顺序读写的性能差异。
不过很长一段时间,io_uring一直被认为只是在文件读写这种不需要缓存的场景下有非常优秀的表现。而且,在相当长的一段时间内,还没有公开发布真正用io_uring实现,并且能够大幅领先目前以select/epoll接口为基础基于reactor模式的网络I/O框架。从经验上判断,通过内核态环形缓存的方式去发送和接收I/O事件的方式理论上肯定是要比传统的不断调用epoll系统调用的性能要优秀。经过反复验证,觉得目前io_uring 网络I/O的瓶颈应该还是出在并发性上面。大多数时候,一个io_uring实例只会占用一个CPU内,要加速带有缓存的I/O,那么多线程肯定是必选项,因为I/O访问中有大量的时间是用在等待缓存的读写和拷贝,以及网络传输延迟上面了。这些系统等待时间肯定应该用其他的线程/进程进行填补。
io_uring对于I/O类型分为两种,分别叫bounded I/O
和unbounded I/O
这两种类型。其实,bounded I/O
就是指的读写时间是有一个固定的时间范围的输入和输出方式,比如文件读写和块设备的输入输出。unbounded I/O
指的是像网络socket通信这样的不确定性比较大的输入和输出方式。网络socket通信每次的读写完成的时间不确定,甚至最后无法完成。
By default, io_uring limits the unbounded workers created to the maximum processor count set by RLIMIT_NPROC and the bounded workers is a function of the SQ ring size and the number of CPUs in the system.
io_uring的文档中提到,如果是unbounded情况下,最大的I/O并发数由RLIMIT_NPROC
这个系统参数设定。而在bounded这种情况下的并发数量是由SQ ring的大小和CPU数量决定的。
对于我们比较关心的网络I/O的情况,具体是怎样的呢?事实上,socket I/O默认是采用的Non-block方式,也就是传统的Epoll这样的模式处理,在这种情况下,io_uring本质上还是进行的epoll操作。为了能够在网络I/O中采用异步多线程模式,我们必须强制网络请求通过block的方式,也就是阻塞模式进行访问。在创建和提交SQ的时候,可以加入IOSQE_ASYNC
参数,这样就会通过线程池处理每个请求了。这样的I/O性能实际测试下来还是比epoll的模式稍差一些的。同时和普通模式下的io_uring的网络I/O相比,CPU的占用率也没有显著的改善。因此这种模式在网络I/O应用显得比较鸡肋。
对于epoll模式,单纯的提高并发数量并不能有效地提升IO性能,尤其像io_uring这样已经将系统调用频次降