searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

io_uring介绍

2023-06-08 08:36:37
237
0

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(&param, 0, sizeof(param));
param.flags = IORING_SETUP_SQPOLL;
param.sq_thread_idle = SQ_THREAD_IDLE;
io_uring_queue_init_params(MAX_ENTRIES, ring, &param);

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模式,如需要打开请参考

sqpoll.patch

 

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) =>
Enable IO uring support (NEW)

  fs/io_uring.c
liburing 建议最新,当前v0.7

./configure --libdir=/usr/lib64

make CFLAGS=-std=gnu99 && make install

gcc<4.9由于没有stdatomic.h需要应用

liburing_barrier.patch

 

include/liburing.h
src/queue.c
src/register.c
src/setup.c
src/syscall.c

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%;

0条评论
作者已关闭评论
袁****浩
3文章数
0粉丝数
袁****浩
3 文章 | 0 粉丝
袁****浩
3文章数
0粉丝数
袁****浩
3 文章 | 0 粉丝
原创

io_uring介绍

2023-06-08 08:36:37
237
0

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(&param, 0, sizeof(param));
param.flags = IORING_SETUP_SQPOLL;
param.sq_thread_idle = SQ_THREAD_IDLE;
io_uring_queue_init_params(MAX_ENTRIES, ring, &param);

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模式,如需要打开请参考

sqpoll.patch

 

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) =>
Enable IO uring support (NEW)

  fs/io_uring.c
liburing 建议最新,当前v0.7

./configure --libdir=/usr/lib64

make CFLAGS=-std=gnu99 && make install

gcc<4.9由于没有stdatomic.h需要应用

liburing_barrier.patch

 

include/liburing.h
src/queue.c
src/register.c
src/setup.c
src/syscall.c

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%;

文章来自个人专栏
文章 | 订阅
0条评论
作者已关闭评论
作者已关闭评论
0
0