[转帖]怎样设计异步系统: Linux Native AIO vs io_uring

怎样,设计,异步,系统,linux,native,aio,vs,io,uring · 浏览次数 : 0

小编点评

**Asynchronous I/O Support in Linux 2.5** **Linux Asynchronous I/O Design: Evolution & Challenges** **Filesystem AIO rdwr - aio readEfficient IO with io_uring** **Introduction** An AIO (Asynchronous I/O) operation is a non-blocking operation that involves transferring data between a kernel space and a user space without blocking the application. **Overview of io_uring** io_uring is a Go library that provides a convenient way to implement asynchronous I/O operations. It uses a workqueue system to efficiently manage multiple asynchronous requests. **Key Concepts** * **Workqueue:** A queue system that stores asynchronous requests. * **Submit Path:** A path where applications submit asynchronous requests. * **Kernel Space:** A space accessible by the kernel. * **User Space:** A space accessible by the application. * **IO Ring Buffer:** A buffer that provides efficient access to kernel space. **Implementation** The following steps outline the implementation of an AIO operation using io_uring: 1. Create a submit path. 2. Create an IO ring buffer. 3. Submit an asynchronous request. 4. Wait for the asynchronous request to complete. 5. Release the submit path. **Example** ```go import ( "io/uring" ) // Submit path submitPath := "my-submit-path" // Create IO ring buffer ringBuffer := io_uring.NewRingBuffer() // Submit asynchronous request request := io_uring.NewSubmitRequest(submitPath, ringBuffer) // Wait for asynchronous request to complete result := io_uring.Wait(request, 100) // Release submit path submitPath := nil ``` **Benefits of io_uring** * **Non-blocking:** Allows applications to continue performing other tasks while waiting for I/O operations to complete. * **Efficient:** Uses a workqueue system to efficiently manage multiple asynchronous requests. * **Easy to implement:** Provides a convenient Go library for implementing AIO operations. **Conclusion** io_uring is a powerful and efficient library for implementing asynchronous I/O operations. It provides a convenient and easy way to achieve non-blocking I/O performance without blocking the application.

正文

https://zhuanlan.zhihu.com/p/149836046

 

Linux native aio一方面有其实用价值, 基本满足了特别业务比如大型数据库系统对异步io的需求, 另一方面却被总是被诟病, 既不完美也不通用, 究其原因在于设计异步系统的理念.

AIO的需求

aio最核心的需求就是解偶submit和wait for completion, 简单地说就是在submit的时候不要阻塞, 这样用户在submit之后可以做其它事情或者继续submit新的io, 从而获得更高的cpu利用率和磁盘带宽. 早期aio的需求主要来源于数据库, 这对当时aio的设计起到了决定性的作用. 和大部分情况一样, 第一步先解决现有问题, 然后才去看能不能扩展到更通用的领域, 而实际情况往往是第一步就决定了后面不好扩展. 数据库比如innodb, page buffer由db自己管理, 基本不使用os提供的page cache, 只要实现异步的direct io就能满足db的需求.

AIO + DIO

因为主要目标是实现异步的direct io, 把io提交到device的request queue就可以, 后面的执行本来就是异步过程.

        do_syscall_64
        __x64_sys_io_submit
        io_submit_one
        __io_submit_one.constprop.0
        aio_write
        btrfs_file_write_iter
        generic_file_direct_write
        btrfs_direct_IO
        __blockdev_direct_IO
        do_blockdev_direct_IO
        btrfs_submit_direct
        btrfs_map_bio
        generic_make_request

大部分情况下submit direct io可以直接提交到request queue, 但是submit的路径上并不是完全没有阻塞点, 比如pin user page以及文件系统get_block, 或者超过了qdepth. 在某些编程模型下, 比如为了减少context switch的开销, 每个cpu只有一个thread, 当该thread sleep的时候, 就会直接影响到系统的吞吐. 但是db的编程模型并没有这种严格要求, 偶尔的阻塞并不一定能直接导致性能下降, 而且用户态也可以通过一些手段来降低submit阻塞的情况.

另外, 关于dio的语义也是比较模糊的, 引用 Clarifying Direct IO's Semantics 的一段话

The exact semantics of Direct I/O (O_DIRECT) are not well specified. It is not a part of POSIX, or SUS, or any other formal standards specification. The exact meaning of O_DIRECT has historically been negotiated in non-public discussions between powerful enterprise database companies and proprietary Unix systems, and its behaviour has generally been passed down as oral lore rather than as a formal set of requirements and specifications.

AIO + Buffer IO

难点

异步buffer io比dio复杂很多, dio本质上使用了device的request queue来处理磁盘io的阻塞, 而buffer io因为增加了一层page cache, 多出了很多阻塞点.

以read为例, 这里的异步并不要求page cache到user buffer的copy是异步的, 毕竟这并没有阻塞, 这里关注lock_page等导致的阻塞. 比如要读2个page, 第一个在page cache中并且uptodate, 而第二个不在page cache中, 同步的read会因为第2个page没有ready从而阻塞.

Retry-Based

为了做到异步化, 内核实现了retry-based的机制:

In a retry-based model, an operation executes by running through a series of iterations. Each iteration makes as much progress as possible in a non-blocking manner and returns. The model assumes that a restart of the operation from where it left off will occur at the next opportunity. To ensure that another opportunity indeed arises, each iteration initiates steps towards progress without waiting. The iteration then sets up to be notified when enough progress has been made and it is worth trying the next iteration. This cycle is repeated until the entire operation is finished.

以内核2.6.12为例, 支持aio的文件系统实现aio_read, 如果没有读完, 需要通知后面进行retry:

aio_pread
  ret = file->f_op->aio_read(iocb, iocb->ki_buf, iocb->ki_left, iocb->ki_pos);
  return -EIOCBRETRY if (ret > 0);  // 如果没有读完

而retry的函数正是aio_pread:

io_submit_one
  init_waitqueue_func_entry(&req->ki_wait, aio_wake_function);
  aio_setup_iocb(req);
    kiocb->ki_retry = aio_pread;
  aio_run_iocb(req);
    current->io_wait = &iocb->ki_wait;
    ret = retry(iocb);
    current->io_wait = NULL;
    __queue_kicked_iocb(iocb) if (-EIOCBRETRY == ret);

当read的数据ready时, 会retry之前未完成的部分:

aio_wake_function
  kick_iocb(iocb);
    queue_kicked_iocb
      run = __queue_kicked_iocb(iocb);
      aio_queue_work(ctx) if (run);
        queue_delayed_work(aio_wq, &ctx->wq, timeout);

INIT_WORK(&ctx->wq, aio_kick_handler, ctx);
aio_kick_handler
  requeue =__aio_run_iocbs(ctx);
  queue_work(aio_wq, &ctx->wq) if (requeue);

retry机制依赖一个前提, 每一次调用ki_retry()的时候不能block, 否则就没有retry一说, 所以需要将之前的阻塞点比如lock_page改造成非阻塞点, 在资源不ready的情况返回EIOCBRETRY, 而不是同步等待资源ready. 具体可以参考Filesystem AIO rdwr - aio read

-void __lock_page(struct page *page)
+int __lock_page_wq(struct page *page, wait_queue_t *wait)
 {
    wait_queue_head_t *wqh = page_waitqueue(page);
-   DEFINE_WAIT(wait);
+   DEFINE_WAIT(local_wait);

+   if (!wait)
+       wait = &local_wait;
+       
    while (TestSetPageLocked(page)) {
-       prepare_to_wait(wqh, &wait, TASK_UNINTERRUPTIBLE);
+       prepare_to_wait(wqh, wait, TASK_UNINTERRUPTIBLE);
        if (PageLocked(page)) {
            sync_page(page);
+           if (!is_sync_wait(wait))
+               return -EIOCBRETRY;
            io_schedule();
        }
    }
-   finish_wait(wqh, &wait);
+   finish_wait(wqh, wait);
+   return 0;
+}
+EXPORT_SYMBOL(__lock_page_wq);
+
+void __lock_page(struct page *page)
+{
+   __lock_page_wq(page, NULL); 
 }
 EXPORT_SYMBOL(__lock_page);

@@ -621,7 +655,13 @@
            goto page_ok;

        /* Get exclusive access to the page ... */
-       lock_page(page);
+       
+       if (lock_page_wq(page, current->io_wait)) {
+           pr_debug("queued lock page \n");
+           error = -EIOCBRETRY;
+           /* TBD: should we hold on to the cached page ? */
+           goto sync_error;
+       }

Status

以2.6.12为例, 只有在aio_pread/aio_pwrite两个函数中返回了EIOCBRETRY, 而上面提到的lock_page等改造并没有进入内核, 所以buffer io的异步支持没有完成, 只是先加了用于支持retry的框架. 最终在这个commit中全部删掉了, btw, 下面说的kernel thread需要使用submit task的mm, 因为里面会涉及user/kernel间的内存拷贝.

commit 41003a7bcfed1255032e1e7c7b487e505b22e298
Author: Zach Brown <zab@redhat.com>
Date:   Tue May 7 16:18:25 2013 -0700

    aio: remove retry-based AIO

    This removes the retry-based AIO infrastructure now that nothing in tree
    is using it.

    We want to remove retry-based AIO because it is fundemantally unsafe.
    It retries IO submission from a kernel thread that has only assumed the
    mm of the submitting task.  All other task_struct references in the IO
    submission path will see the kernel thread, not the submitting task.
    This design flaw means that nothing of any meaningful complexity can use
    retry-based AIO.

    This removes all the code and data associated with the retry machinery.
    The most significant benefit of this is the removal of the locking
    around the unused run list in the submission path.

小结

retry based实现的aio + buffer io使用的设计模式是:

AIO = Sync IO - wait + retry

这种模式注定了改造起来非常复杂, 首先要找到所有的同步点, 再一个一个改造成异步点, 这种try-and-fix的方法很难提供一个清晰的演进路径, 而aio + buffer io的user case不足以驱动全面改造, 最终native aio并没有真的提供buffer io的异步化. 最后我们引用 Asynchronous I/O Support in Linux 2.5 的一段话来看当时的设计目标:

Ideally, an AIO operation is completely non-blocking. If too few resources exist for an AIO operation to be completely non-blocking, the operation is expected to return -EAGAIN to the application rather than cause the process to sleep while waiting for resources to become available.
However, converting all potential blocking points that could be encountered in existing file I/O paths to an asynchronous form involves trade-offs in terms of complexity and/or invasiveness. In some cases, this tradeoff produces only marginal gains in the degree of asynchrony.
This issue motivated a focus on first identifying and tackling the major blocking points and less deeply nested cases to achieve maximum asynchrony benefits with reasonably limited changes. The solution can then be incrementally improved to attain greater asynchrony.

io_uring

小测试

我们使用fio来做个简单的测试, 看看io_uring大致的工作流程.

  • 清空page cache, 后面的read需要从磁盘load数据
# echo 3 > /proc/sys/vm/drop_caches
  • 监控io_read / page_cache_sync_readahead / page_cache_async_readahead等函数
$ sudo trace-bpfcc -K 'r::io_read (retval) "r=%x", retval' \
        'r::page_cache_sync_readahead' \
        'r::page_cache_async_readahead'
  • 使用io_uring引擎执行fio
$ ./fio --name=aaa --filename=aaa --ioengine=io_uring --rw=read --size=16k --bs=8k

根据如下收集的结果, 我们可以大致看出io_uring异步读数据流程:

  1. fio通过io_uring_enter发送异步读请求. 因为page cache中没有该数据, 肯定需要磁盘io. 但是在该submit路径上, 并未下发io请求, 而是返回了EAGAIN (fffffff5)
  2. 随后在kworker中, 真正下发io请求
  3. 第3个栈是因为我们需要读入连续的(顺序读)两个block(16k/8k), 第2个io在前一个io的readahead window
17547   17547   fio             io_read          r=fffffff5
        kretprobe_trampoline+0x0 [kernel]
        io_queue_sqe+0xd3 [kernel]
        io_submit_sqe+0x2ea [kernel]
        io_ring_submit+0xbf [kernel]
        __x64_sys_io_uring_enter+0x1aa [kernel]
        do_syscall_64+0x5a [kernel]
        entry_SYSCALL_64_after_hwframe+0x44 [kernel]

15931   15931   kworker/u64:10  page_cache_sync_readahead
        kretprobe_trampoline+0x0 [kernel]
        generic_file_read_iter+0xdc [kernel]
        io_read+0xeb [kernel]
        kretprobe_trampoline+0x0 [kernel]
        io_sq_wq_submit_work+0x15f [kernel]
        process_one_work+0x1db [kernel]
        worker_thread+0x4d [kernel]
        kthread+0x104 [kernel]
        ret_from_fork+0x22 [kernel]

17547   17547   fio             page_cache_async_readahead
        kretprobe_trampoline+0x0 [kernel]
        generic_file_read_iter+0xdc [kernel]
        io_read+0xeb [kernel]
        kretprobe_trampoline+0x0 [kernel]
        io_queue_sqe+0xd3 [kernel]
        io_submit_sqe+0x2ea [kernel]
        io_ring_submit+0xbf [kernel]
        __x64_sys_io_uring_enter+0x1aa [kernel]
        do_syscall_64+0x5a [kernel]
        entry_SYSCALL_64_after_hwframe+0x44 [kernel]

实现

具体的实现这里不一一展开, 主要看一下user space和kernel space交互的几个ring buffer. 图片来源: 

通过SQ, 把submit path的逻辑和kernel的其他模块解耦, 确保submit path上不会阻塞. 如上面的测试, 在io_uring_enter的时候不会去调用可能会导致阻塞的page_cache_sync_readahead, 而是使用workqueue的方式后台执行, 当然worker线程是可能也是可以被阻塞的. 另外SQ的可用长度在userspace是可见的, 所以也不会因为等待SQ entry而阻塞.

总结

最简单高效的异步系统是生产者将任务投递到一条队列上, 消费者处理队列上的任务. 如果在任务到达队列之前生产者就已经阻塞, 那么就不能达到异步的目的, 所以这条队列需要尽可能靠近生产者, 而不是靠近消费者. 这样的投递系统不只简单高效, 并且十分通用, 可以适用绝大部分任务, 所以io_uring不只在传统的块设备上发挥重要作用, 已经广泛应用到各种场合.

参考资料

与[转帖]怎样设计异步系统: Linux Native AIO vs io_uring相似的内容:

[转帖]怎样设计异步系统: Linux Native AIO vs io_uring

https://zhuanlan.zhihu.com/p/149836046 Linux native aio一方面有其实用价值, 基本满足了特别业务比如大型数据库系统对异步io的需求, 另一方面却被总是被诟病, 既不完美也不通用, 究其原因在于设计异步系统的理念. AIO的需求 aio最核心的需求

[转帖]全栈监控:如何设计全栈监控策略?

https://zhuanlan.zhihu.com/p/597779642 你好,我是高楼。这一篇,我们来看看怎样设计全链路压测的全局监控。 对于全链路压测来说,因为涉及到的服务比较多,所以分析逻辑难度加大,对监控的要求当然也更加复杂。 如果我们总是在性能瓶颈出现之后再去做分析,很可能会发现缺少各

[转帖]CDN调度及管理类

CDN调度及管理类 设计CDN系统最关键的两个问题是:中央怎么管?地方怎么干?那么今天,我们就来简单探讨一下"中央怎么管"的问题。 管理是为了合理的调度,合理的调度是为了提升整个组织的效益。所以提升效益才是最终目的,管理只是途径而已。那CDN系统是通过怎样的管理与调度,实现组织利益最大化的呢?请带着

[转帖]一周文章导读:架构图;服务器;CPU

Table of Contents 阿里巴巴的技术专家,是如何画好架构图的? 先厘清一些基础概念 1、什么是架构 2、什么是架构图 3、架构图的作用 4、架构图分类 怎样的架构图是好的架构图 服务器 服务器架构 1)X86架构服务器 2)RISC架构服务器 3)IA-64 服务器分类 如何设计和生产

[转帖]MySQL 内存泄露怎样检查

MySQ使用内存上升90%!在运维过程中50%的几率,会碰到这样的问题。算是比较普遍的现象。 MySQL内存使用率过高,有诸多原因。普遍的情况是因为使用不当导致的,还有mysql本身的缺陷的导致的。到底是那方面的问题,那就需要一个一个进行排查。 下面介绍排查思路: 1.参数配置需要确认。是否内存设置

[转帖]给容器设置内核参数

http://www.manongjc.com/detail/55-sgvseudnytygddj.html 本文章向大家介绍给容器设置内核参数,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。 怎么给docker容器设置内核参数? 怎么给

[转帖]限制容器中的jvm

在rancher中部署完java应用之后,需要对java程序的jvm进行设置,这个非常重要,不然可能会引起比较严重的后果:容器无限制的重启或者主机的内存被耗尽。 在开始之前,先来看一个问题: 在容器中跑了一个java应用,那怎么来限制这个jvm的memory呢? 按照传统的思路对memory进行限制

[转帖]Redis系列(十七)、Redis中的内存淘汰策略和过期删除策略

我们知道Redis是分布式内存数据库,基于内存运行,可是有没有想过比较好的服务器内存也不过几百G,能存多少数据呢,当内存占用满了之后该怎么办呢?Redis的内存是否可以设置限制? 过期的key是怎么从内存中删除的?不要怕,本篇我们一起来看一下Redis的内存淘汰策略是如何释放内存的,以及过期的key

[转帖]Java IO篇:什么是零拷贝?

在介绍零拷贝的IO模式之前,我们先简单了解下传统的IO模式是怎么样的? 一、传统的IO模式: 传统的IO模式,主要包括 read 和 write 过程: read:把数据从磁盘读取到内核缓冲区,再拷贝到用户缓冲区write:先把数据写入到 socket缓冲区,最后写入网卡设备 流程图如下: (1)用

[转帖]什么是CDN?CDN的工作原理是怎样的?

1.什么是CDN? CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术