1.背景
对于IO读写,常见的API有read/write,readv/writev,pread/pwrite,preadv/pwritev等,但这些接口都是同步的,只有在数据ready或written后才会返回,这在很多高并发或大量读写的场景下都不是最优选择,我们迫切需要异步的IO读写接口。目前已有的异步IO读写接口有posix aio和native aio。posix aio是在用户态使用多线程+阻塞IO来模拟异步IO,性能比较差;native aio则是linux kernel原生提供的接口,libaio库封装了相关接口,但native aio只支持O_DIRECT方式打开的文件,非O_DIRECT方式则退化为同步阻塞IO(所以qemu下只有none/directsync模式可以使用native aio),另外native aio对buffer有对齐的要求,即使这样,每次提交io仍然需要拷贝64+8bytes (struct iocb + struct iocb*),收割需要拷贝32bytes (struct io_event),且至少两次syscall(io_submit + io_getevents)。在这情况下,Jens Axboe设计了一组新的接口io_uring,新的接口特性丰富,易于扩展,效率高。
2.原理
用户态程序和内核内存共享Submission Queue(SQ) 和 Completion Queue(CQ) 两个环形队列,利用SQ/CQ队列进行无锁通信,进而高效地提交和收割IO。需要注意的是,真正的IO Request并没有存放在SQ队列中,而是存放在一个Submission Queue Entries(SQEs)数组中,SQ队列中则保存SQEs array中的相应entry的index。
io_uring的简要流程如下:
a.调用setup接口在内核中分配指定entry数的SQ/CQ/SQEs array内存,并返回fd;
b.用户态程序调用mmap将上述内存映射到用户态,并获取访问SQ/CQ/SQEs array必要的offset;
c.将需要提交的IO request填充到SQEs array中,index填充到SQ中,然后更新SQ tail指针;
d.调用enter接口将添加的IO request提交给内核处理,同时等待一定数量的IO完成;
e.内核从SQEs array中取出IO request,提交给block层,然后更新SQ head指针;
f.block层完成IO request后内核将返回值和userdata加入CQ队列中,并更新CQ tail指针;
g.用户态程序收割CQ队列中完成的IO request,并更新CQ head指针;
3.接口
io_uring 提供了如下3个系统调用:
# define __NR_io_uring_setup 425
# define __NR_io_uring_enter 426
# define __NR_io_uring_register 427
int io_uring_setup(unsigned entries, struct io_uring_params *p) { return syscall(__NR_io_uring_setup, entries, p); }
int io_uring_enter(unsigned int fd, unsigned to_submit, unsigned min_complete, unsigned flags, sigset_t *sig, int sz) { return syscall(__NR_io_uring_enter, fd, to_submit, min_complete, flags, sig, sz); }
int io_uring_register(unsigned int fd, unsigned opcode, const void *arg, unsigned nr_args) { return syscall(__NR_io_uring_register, fd, opcode, arg, nr_args); }
3.1 io_uring_setup
io_uring_setup(unsigned entries, struct io_uring_params *p) 用于建立SQ、CQ队列及SQEs数组
参数unsigned entries用于指定队列长度(SQ实际分配大小为entries向上取2^n,CQ entries一般为SQ两倍);
参数struct io_uring_params *p用于传入&传出配置参数;
该系统调用成功后返回fd,用于标识该io_uring实例,后续IO提交或收割或注册时需要用到;
由于队列和数组是由内核分配,用户态无法直接访问,需要使用返回的fd调用mmap将SQ、CQ和SQE数组映射到用户态,注意mmap需要传入MAP_POPULATE 以防止产生page fault。
struct io_sqring_offsets {
u32 head; //【out offset】SQ队列的head指针
u32 tail; //【out offset】SQ队列的tail指针
u32 ring_mask; //【out offset】与运算用于获取head/taill指针对应的index, mask = sq_entries - 1
u32 ring_entries; //【out offset】SQ队列的entry数
u32 flags; //【out offset】当内核sqpoll线程睡眠后,会置IORING_SQ_NEED_WAKEUP位,用于通知用户程序下次提交IO时需要wakeup内核线程
u32 dropped; //【out offset】SQ队列中累计丢弃的错误请求数
u32 array; //【out offset】SQ起始地址
u32 resv[3];
};
struct io_cqring_offsets {
u32 head; //【out offset】CQ队列的head指针
u32 tail; //【out offset】CQ队列的tail指针
u32 ring_mask; //【out offset】与运算用于获取CQ队列head/taill指针对应的index,mask = cq_entries - 1
u32 ring_entries; //【out offset】CQ队列的entry数
u32 overflow; //【out offset】CQ队列中累计溢出的event数,当用户态程序收割不及时,CQ队列溢出时,内核使用链表保存或直接丢弃多余的event
u32 cqes; //【out offset】CQ起始地址
u32 flags; //【out offset】取值有IORING_CQ_EVENTFD_DISABLED(应用程序通知内核关闭eventfd)
u32 resv[3];
};
struct io_uring_params {
u32 sq_entries; //【out】SQ队列的entry数
u32 cq_entries; //【in&out】CQ队列的entry数,当flags置IORING_SETUP_CQSIZE位时指定CQ entry数
u32 flags; //【in】传给内核的flags,如IORING_SETUP_IOPOLL/IORING_SETUP_SQPOLL/IORING_SETUP_SQ_AFF/IORING_SETUP_CQSIZE/IORING_SETUP_ATTACH_WQ等
u32 sq_thread_cpu; //【in】SQPOLL模式下,内存线程绑定sq_thread_cpu指定的cpu,需要flags置IORING_SETUP_SQ_AFF位
u32 sq_thread_idle; //【in】SQPOLL模式下,内核线程idle sq_thread_idle ms会进入睡眠,如果为0,则默认1000ms
u32 features; //【out】内核使用或支持的特性,取值有IORING_FEAT_SINGLE_MMAP/IORING_FEAT_NODROP/IORING_FEAT_CUR_PERSONALITY等
u32 wq_fd; //【in】用于ATTACH_WQ,和wf_fd对应的io_uring共享同一个内核线程
u32 resv[3]; // 预留,必须全0,否则报错
struct io_sqring_offsets sq_off; //【out】用于用户态访问SQ需要的偏移信息,如head、tailf指针的偏移等
struct io_cqring_offsets cq_off; //【out】用于用户态访问CQ需要的偏移信息,如head、tailf指针的偏移等
};
队列内存映射:
SQ/CQ/SQEs在内核中布局如下图所示,CQ队列作为struct io_rings的可变长数组成员位于其尾部,SQ队列在CQ队列之后(低版本内核上SQ和CQ独立分配内存,高版本上引入了IORING_FEAT_SINGLE_MMAP feature后则合并了SQ&CQ队列),SQEs array则单独分配内存;mmap映射SQ和CQ时实际是从io_rings的起始地址开始映射,io_sqring_offsets/io_cqring_offsets主要用于描述该布局中相应的变量对于io_rings起始地址的偏移,如
sq_off.head表示io_rings.sq.head对于io_rings的offset;
sq_off.tailf表示io_rings.sq.tail对于io_rings的offset;
sq_off.array表示SQ队列起始地址对于io_rings的offset;
cq_off.cqes表示CQ队列起始地址对于io_rings的offset;
因此,CQ队列的起始地址为mmap的地址+cq_off.cqes,SQ队列的起始地址为mmap的地址+sq_off.array;
mmap映射SQEs则是从SQEs的起始地址开始映射,访问时不需要额外的offset。
3.2 io_uring_enter
int io_uring_enter(unsigned int fd, u32 to_submit, u32 min_complete, u32 flags)用于IO提交或收割。
参数unsigned int fd为io_uring_setup返回的fd;
参数unsigned int to_submit为需要提交的请求数;
参数unsigned int min_complete需要等待完成的请求数;
参数unsigned int flags取值有IORING_ENTER_GETEVENTS(等待min_complete个请求完成)、IORING_ENTER_SQ_WAKEUP(唤醒sqpoll内核线程)、IORING_ENTER_SQ_WAIT(等待可用的sqe entry);
参数sigset_t *sig为signal mask,用于block某些signal;
参数int sz为signal mask的size;
IO提交:
SQ及SQEs用于保存用户态程序向内核提交的IO Request,用户程序提交IO前需要从SQEs中找到一个空闲的SQE,如果没有则需要等待。SQE的数据结构为struct io_uring_sqe,由于io_uring不仅支持IO读写,还支持socket/poll等丰富的接口,因此需要opcode加以区分,对于不同的操作,Request的定义也不尽相同,所以struct io_uring_sqe中有多个union,io_uring_sqe中的user_data成员会在完成后被拷贝到struct io_uring_cqe中,可以用来存放收割时需要访问的与Request相关的数据。根据Request填充完SQE后,将这个 SQE 的索引放到 SQ 队列中。SQ 是一个典型的 RingBuffer,有 head,tail 两个成员,SQE 设置完成后,需要修改 SQ 的 tail,以表示向 RingBuffer 中插入一个请求。当所有请求都加入 SQ 后,就可以使用 io_uring_enter来提交 IO 请求。调用后io_uring_enter 会陷入到内核,内核将 SQ 中的请求提交给 Block 层,提交的请求数为to_submit,一般以异步方式提交,但少数情况是同步执行,比如 IORING_OP_FSYNC。内核从SQ中head指针取出请求后,需要更新head指针。
struct io_uring_sqe {
u8 opcode; /* 该请求的操作类型,如READV/WRITEV/ACCEPT/CONNECT/POLL_ADD等 */
u8 flags; /* IOSQE_ flags */
u16 ioprio; /* request优先级 */
s32 fd; /* 需要操作的fd或fd index */
union {
u64 off; /* offset into file */
u64 addr2;
};
union {
u64 addr; /* pointer to buffer or iovecs */
u64 splice_off_in;
}
u32 len; /* buffer size or number of iovecs */
union {
kernel_rwf_t rw_flags;
u32 fsync_flags;
u16 poll_events; /* compatibility */
u32 poll32_events; /* word-reversed for BE */
u32 sync_range_flags;
u32 msg_flags;
u32 timeout_flags;
u32 accept_flags;
u32 cancel_flags;
u32 open_flags;
u32 statx_flags;
u32 fadvise_advice;
u32 splice_flags;
};
u64 user_data; /* data to be passed back at completion time */
union {
struct {
/* index into fixed buffers, if used */
union {
/* index into fixed buffers, if used */
u16 buf_index;
/* for grouped buffer selection */
u16 buf_group;
}
/* personality to use, if used */
u16 personality;
s32 splice_fd_in;
};
u64 __pad2[3];
};
};
其中,opcode的取值有:
IORING_OP_NOP 空操作,一般用来衡量io_uring框架的性能
IORING_OP_READ/IORING_OP_WRITE IO读写
IORING_OP_READV/IORING_OP_WRITEV 基于向量的IO读写
IORING_OP_READ_FIXED/IORING_OP_WRITE_FIXED 固定buffer的IO读写
IORING_OP_FSYNC/IORING_OP_SYNC_FILE_RANGE 文件同步
IORING_OP_POLL_ADD/IORING_OP_POLL_REMOVE poll add/remove
IORING_OP_EPOLL_CTL epoll ctl
IORING_OP_SENDMSG/IORING_OP_RECVMSG sendmsg/recvmsg
IORING_OP_SEND/IORING_OP_RECV/IORING_OP_ACCEPT/IORING_OP_CONNECT socket相关操作
IORING_OP_TIMEOUT/IORING_OP_TIMEOUT_REMOVE timer
IORING_OP_ASYNC_CANCEL 取消已经提交的Request,通过user_data来匹配
IORING_OP_LINK_TIMEOUT 关联另外一个的Request,如果被关联的Requst超时则被取消
IORING_OP_FALLOCATE/IORING_OP_FADVISE/IORING_OP_MADVISE 内存操作
IORING_OP_OPENAT/IORING_OP_OPENAT2/IORING_OP_RENAMEAT/IORING_OP_UNLINKAT 目录下文件操作
IORING_OP_SPLICE/IORING_OP_TEE pipe操作
IORING_OP_CLOSE close
IORING_OP_STATX stat
IORING_OP_FILES_UPDATE 更新注册的fd
IORING_OP_PROVIDE_BUFFERS/IORING_OP_REMOVE_BUFFERS 更新注册的buffer
IORING_OP_SHUTDOWN
IO收割:
struct io_uring_cqe {
u64 user_data; /* sqe->user_data */
s32 res; /* 该请求的返回值 */
u32 flags;
};
CQ队列用于保存内核向用户态返回的completion event,CQE的数据结构为struct io_uring_cqe。在某个Request完成后,内核用其返回值和user_data填充CQE,然后加入到CQ 队列中。用户态收割时,遍历 [CQ->head, CQ->tail) 区间,然后处理CQE相应的Request,最后移动 head 指针到 tail,IO 收割就到此结束了。如果 flags 设置了 IORING_ENTER_GETEVENTS,并且 min_complete > 0,那么这个系统调用会同时处理IO提交和IO 收割,调用会block直到 min_complete 个 IO 已经完成。由于 IO 在提交的时候可以顺便返回完成的 IO,所以理想情况下收割 IO 不需要额外的系统调用,这相对于 libaio省去了一次系统调用。
3.3 io_uring_register
int io_uring_register(unsigned int fd, unsigned opcode, const void *arg, unsigned nr_args)主要用于注册一些主要资源(如buffer/files/eventfd/personality/restrictions等),以便io_uring可以长期引用这些资源,而不用频繁的传递或获取释放,减小了额外的开销。这些API并不是必须的,属于advanced features。
参数unsigned int fd为io_uring_setup返回的fd;
参数unsigned int opcode为具体的操作码,取值如IORING_REGISTER_BUFFERS/IORING_REGISTER_FILES;
参数const void *arg为需要传给内核的参数,其具体含义取决于opcode;
参数unsigned int nr_args为arg数组的entry数;
其中,opcode主要有以下取值:
IORING_REGISTER_BUFFERS/IORING_UNREGISTER_BUFFERS(>=kernel 5.1)
如果应用提交到内核的虚拟内存地址是固定的,那么可以提前完成虚拟地址到物理pages的映射,避免在IO路径上进行转换,从而优化性能。用法是,在 setup io_uring 之后,调用 io_uring_register,传递 IORING_REGISTER_BUFFERS 作为 opcode,参数是一个指向 iovec 的数组,表示这些地址需要 map 到内核。在做 IO 的时候,使用带 FIXED 版本的opcode(IORING_OP_READ_FIXED /IORING_OP_WRITE_FIXED)来操作 IO 即可。内核在处理 IORING_REGISTER_BUFFERS 时,提前使用 get_user_pages 来获得 userspace 虚拟地址对应的物理 pages。在做 IO 的时候,如果提交的虚拟地址曾经被注册过,那么就免去了虚拟地址到 pages 的转换。
IORING_REGISTER_FILES/IORING_UNREGISTER_FILES(>=kernel 5.1)
该feature的作用是避免每次 IO 对文件做 fget/fput 操作,当批量 IO 的时候,这组原子操作可以避免掉。用法是,在 setup io_uring 之后,调用 io_uring_register,传递 IORING_REGISTER_FILES作为 opcode,参数是一个指向 fd的数组。在做 IO 的时候,io_uring_sqe->flags需要置IOSQE_FIXED_FILE位,并且io_uring_sqe->fd值为待操作fd在fd数组中的index。
IORING_REGISTER_EVENTFD/IORING_UNREGISTER_EVENTFD(>=kernel 5.2)
该feature用于注册eventfd来通知用户态程序IO request已完成,需要提前获取eventfd,然后将eventfd注册到内核,nr_args必须为1。
IORING_REGISTER_PROBE (>=kernel 5.6)
该接口用于探测当前内核对register feature支持的情况。由于io_uring接口随着kernel版本在不停的扩展更新,因此在使用某个feature前需要探测当前内核版本是否支持该feature。使用时传入io_uring_probe数组,由内核填充该数组,如果io_uring_probe->flags置IO_URING_OP_SUPPORTED位则表示支持。
IORING_REGISTER_RESTRICTIONS(>=kernel 5.10)
该feature用于禁用或允许某种opcode及flag,当内核处理到被禁用的opcode request时返回-EACCES错误。
3.4 POLLING模式
上文提到的用法为io_uring的常规用法,io_uring的精髓在于其polling模式(即submission offload),主要涉及sqpoll和iopoll机制。
sqpoll机制:在调用io_uring_setup初始化io_uring队列时指定flags |= IORING_SETUP_SQPOLL,内核会创建一个内核线程io_uring-sq,可根据需要绑定指定cpu(sq_thread_cpu),该内核线程会不停poll SQ队列,一旦有Request提交到SQ队列,该线程便会取出Request并提交到block层,因此用户态程序只需要填充SQEs和SQ即可,不再调用io_uring_enter将Request提交给内核,收割时也不用调用syscall。为了防止没有请求提交时线程空转,该线程在idle一定时间(sq_thread_idle ms)后进入sleep状态,同时将sq_off.flags置IORING_SQ_NEED_WAKEUP位,因此用户态程序在填充完SQ后一般需要检查sq_off.flags,如果存在IORING_SQ_NEED_WAKEUP位,则需要调用io_uring_enter(flags = IORING_ENTER_SQ_WAKEUP)唤醒内核io_uring-sq线程。
iopoll机制:内核采用 Polling 的模式收割 Block 层的请求。iopoll和sqpoll可以独立使用,当没有使用 sqpoll时,io_uring_enter(flags |= IORING_ENTER_GETEVENTS)会主动 Poll,如果所有Request只涉及一个file,则进行spin poll,以检查提交给 Block 层的请求是否已经完成,而非iopoll的情况则是挂起等待 Block 层完成后再被唤醒;使用 sqpoll时,则是由io_uring-sq内核线程负责poll。
此图片来自外部,暂时无法显示
4.内存屏障
由于cpu乱序执行机制,必须正确的使用内存屏障才能保证用户态和内核态同时正确的访问SQ/CQ/SQEs。
read_barrier(): 确保之前的写内存对后续的读内存可见
write_barrier(): 确保之前的写内存对后续的写内存可见
例如,当某应用程序需要提交IO request时,需要执行两步操作:1.获取一个空闲的struct io_uring_sqe并填充,然后加入SQEs array中,并将index加入SQ中,2.更新SQ的tail指针以便通知内核
新的entry加入,其步骤简化如下:
1: sqe->opcode = IORING_OP_READV;
2: sqe->fd = fd;
3: sqe->off = 0;
4: sqe->addr = &iovec;
5: sqe->en = 1;
6: sqe->user_data = some_value;
7: sqring->tail = sqring->tail + 1;
其中step 7用于更新tail指针,以便新加入的sqe对内核可见;如果step 7在step 6之前执行,则内核看见的就是一个没有填充完成的sqe,导致内核访问错误的数据。
因此正确的做法如下:
1: sqe->opcode = IORING_OP_READV;
2: sqe->fd = fd;
3: sqe->off = 0;
4: sqe->addr = &iovec;
5: sqe->len = 1;
6: sqe->user_data = some_value;
write_barrier(); /*确保之前的写内存在更新tail之前可见*/
7: sqring->tail = sqring->tail + 1;
write_barrier(); /*确保tail更新可见*/
对于内核,在读取sqring->tail之前,也需要执行read_barrier()确保tail更新对内核可见。
同理,在访问CQ时也需要类似的逻辑保证数据的正确性。
编译器优化
为了提升程序运行性能,编译器也会对内存访问进行一定的优化,然而有些优化并不是我们想要;
例如,由于SQ/CQ/SQEs在用户态和内核态同时被访问,因此有些时候访问这些数据时不允许gcc将其优化为访问寄存器中暂存的值,必须从内存中重新读取,此时需要使用READ_ONCE()/WRITE_ONCE()宏,
该宏的作用就是单次以volatile方式访问变量,既能保证程序逻辑正常,又能避免将变量定义为volatile而完全不优化,其定义如下:
#define WRITE_ONCE(var, val) (*((volatile __typeof(val) *)(&(var))) = (val))
#define READ_ONCE(var) (*((volatile __typeof(var) *)(&(var))))
5.liburing API
为了方便使用io_uring,Jens Axboe把io_uring syscall进行了封装,提供了liburing库,因此使用者不用再关注ring buffer的管理和内存屏障,qemu就直接使用了liburing的接口。
以下简要介绍liburing API的使用。
a.初始化队列并完成SQ/CQ/SQEs array的映射:
struct io_uring ring;
io_uring_queue_init(ENTRIES, &ring, 0);
如果需要配置更多的参数,则可以调用io_uring_queue_init_params:
struct io_uring_params param;
memset(¶m, 0, sizeof(param));
param.flags = IORING_SETUP_SQPOLL;
param.sq_thread_idle = SQ_THREAD_IDLE;
io_uring_queue_init_params(MAX_ENTRIES, ring, ¶m);
b.销毁io_uring实例并释放资源时调用:
io_uring_queue_exit(&ring);
c.提交request:
struct io_uring_sqe sqe;
/* get an sqe and fill in a READV operation */
sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &iovec, 1, offset);
/* tell the kernel we have an sqe ready for consumption */
io_uring_submit(&ring);
d.等待request完成并收割:
struct io_uring_cqe cqe;
/* wait for the sqe to complete */
io_uring_wait_cqe(&ring, &cqe);
/* read and process cqe event */
app_handle_cqe(cqe);
io_uring_cqe_seen(&ring, cqe);
6.版本及编译
SUPPORTED 最低版本 |
编译选项 | 备注 | 主要代码 | |
---|---|---|---|---|
libvirt | 6.3 | 不涉及 | ||
qemu | 5.0 |
--enable-linux-io-uring 依赖liburing库,默认自动probe,存在liburing库则编译 |
请先编译liburing并安装 qemu代码不支持sqpoll模式,如需要打开请参考
|
block/io_uring.c util/fdmon-io_uring.c |
kernel | 5.1 |
CONFIG_IO_URING=y menuconfig General setup => Configure standard kernel features (expert users) => |
fs/io_uring.c | |
liburing | 建议最新,当前v0.7 |
./configure --libdir=/usr/lib64 make CFLAGS=-std=gnu99 && make install |
gcc<4.9由于没有stdatomic.h需要应用
|
include/liburing.h |
7.性能测试
环境:内蒙测试环境
存储:Intel S4510 480 GB SSD 接口SATA3(6Gbps),官方标称IOPS 36k
虚拟机:8CPU + 8G MEM CentOS7.8
软件版本:kernel-5.7 qemu-5.0 libvirt-6.9
测试内容:
1.测试对比宿主机上fio使用io_uring、io_uring+sqpoll、libaio三种engine的性能;
2.测试对比虚拟机使用io_uring、io_uring+sqpoll、libaio三种aio时guest的fio性能;
xml配置:
io_uring:
<disk type='file' device='disk'>
<driver name='qemu' type='raw' cache='none' io='io_uring' discard='unmap'/>
libaio:
<disk type='file' device='disk'>
<driver name='qemu' type='raw' cache='none' io='native' discard='unmap'/>
fio命令:
host: fio -name=fiotest -filename=/dev/sda2 -iodepth=128 -thread -rw=xxxx -ioengine=libaio -direct=1 -bs=4k -size=50G -numjobs=1/10 -runtime=150 -ramp_time=50 -group_reporting
fio -name=fiotest -filename=/dev/sda2 -iodepth=128 -thread -rw=xxxx -ioengine=io_uring -direct=1 -bs=4k -size=50G -numjobs=1/10 -runtime=150 -ramp_time=50 -group_reporting
fio -name=fiotest -filename=/dev/sda2 -iodepth=128 -thread -rw=xxxx -ioengine=io_uring -sqthread_poll=1 -direct=1 -bs=4k -size=50G -numjobs=1/10 -runtime=150 -ramp_time=50 -group_reporting
guest: fio -name=fiotest -filename=/dev/vdb -iodepth=128 -thread -rw=xxxx -ioengine=psync -direct=1 -bs=4k -size=50G -numjobs=10 -runtime=150 -ramp_time=50 -group_reporting
fio 1job | fio 10job | qemu guest none cache | ||||||||
io_uring | sqpoll | libaio | io_uring | sqpoll | libaio | io_uring | sqpoll | libaio | ||
4K随机读写 | read iops | 48.1k | 46.5K | 50.5k | 47.0k | 47.0k | 47.0k | 20.7k | 25.5k | 20.8k |
read bw | 188 | 182 | 197 | 184 | 184 | 184 | 80.8 | 99.7 | 81.4 | |
write iops | 20.6k | 19.9K | 21.6k | 20.1k | 20.1k | 20.2k | 8870 | 10900 | 8930 | |
write bw | 80.5 | 77.8 | 84.5 | 78.6 | 78.7 | 78.7 | 34.6 | 42.8 | 34.9 | |
4K随机写 | iops | 63.2k | 63.6K | 63.7k | 65.4k | 65.1k | 64.7k | 38.8k | 52.3k | 40.1k |
bw | 247 | 248 | 249 | 255 | 255 | 253 | 151 | 204 | 157 | |
4K顺序读 | iops | 89.2k | 84.2K | 85.7k | 116k | 118k | 114k | 29.5k | 38.9k | 28.7k |
bw | 348 | 329 | 335 | 452 | 462 | 446 | 115 | 152 | 112 | |
4K顺序写 | iops | 98.6k | 113K | 98.4k | 113k | 114k | 115k | 14.2k | 15.9k | 15.0k |
bw | 385 | 441 | 384 | 442 | 444 | 449 | 55.6 | 62.1 | 58.7 | |
1M随机读写 | read iops | 342 | 341 | 352 | 327 | 322 | 329 | 314 | 323 | 325 |
read bw | 342 | 341 | 352 | 327 | 322 | 329 | 314 | 323 | 325 | |
write iops | 145 | 145 | 150 | 141 | 139 | 142 | 136 | 140 | 141 | |
write bw | 145 | 145 | 150 | 141 | 139 | 142 | 136 | 140 | 141 | |
1M随机写 | iops | 462 | 460 | 458 | 467 | 456 | 461 | 466 | 454 | 466 |
bw | 462 | 460 | 458 | 467 | 456 | 461 | 466 | 454 | 466 | |
1M顺序读 | iops | 516 | 516 | 515 | 510 | 504 | 511 | 483 | 503 | 485 |
bw | 516 | 516 | 515 | 510 | 504 | 511 | 483 | 503 | 485 | |
1M顺序写 | iops | 458 | 458 | 458 | 455 | 440 | 446 | 463 | 463 | 463 |
bw | 458 | 458 | 458 | 455 | 440 | 446 | 463 | 463 | 463 |
结论:
1.本测试场景下,宿主机上三种engine性能没有明显区别;
2.在SATA SSD环境下,使用io_uring的虚拟机性能与libaio接近,无明显提升;使用io_uring + sqpoll后iops提升约20%~30%;