MySQL数据库InnoDB存储引擎 异步IO(AIO)实现机制详解

1.InnoDB存储引擎 AIO

insert into nkeys values (71,71,71,71,71);

Innodb的异步I/O,默认情况下使用linux原生aio,libaio。关于异步I/O的优势,可参考网文[18][19];libaio的限制,可见网文[17]。下面详细分析Innodb 异步I/O的处理步骤。

 

2.聚簇索引IO

insert操作,读取聚簇索引页面,函数调用流程:

buf_page_get_gen -> buf_read_page -> buf_read_page_low -> fil_io ->

os_aio_func(type, mode) ->

  1. type = OS_FILE_READ; mode = OS_AIO_SYNC;使用os_aio_sync_array (其余的array包括:os_aio_read_array; os_aio_write_array; os_aio_ibuf_array; os_aio_log_array)
    1. 每个aio array,在系统启动时调用os0file.c::os_aio_init函数初始化

      os_aio_init(io_limit,

      srv_n_read_io_threads,

      srv_n_write_io_threads,

      SRV_MAX_N_PENDING_SYNC_IOS);

    2. io_limit: 每个线程可以并发处理多少pending I/O

      windows ->     io_limit = SRV_N_PENDING_IOS_PER_THREAD = 32

      linux ->         io_limit = 8 * SRV_N_PENDING_IOS_PER_THREAD = 8 * 32 = 256

      #define
      SRV_N_PENDING_IOS_PER_THREAD    OS_AIO_N_PENDING_IOS_PER_THREAD = 32

    3. srv_n_read_io_threads

      处理异步read I/O线程的数量

      innobase_read_io_threads/innodb_read_io_threads:通过参数控制

      因此系统可以并发处理的异步read page请求为:

      io_limit * innodb_read_io_threads

    os_aio_read_array = os_aio_array_create(n_read_segs * n_per_seg, n_read_segs);

    异步I/O主要包括两大类:

    1. 预读page

      需要通过异步I/O方式进行

    2. 主动Merge

      Innodb主线程对需要merge的page发出异步读操作,在read_thread中进行实际merge处理

注:如何确定将哪些read io请求分配给哪些read thread?

  1. 首先,每个read thread负责os_aio_read_array数组中的一部分。

    例如:thread0处理read_array[0, io_limit-1];thread1处理read_array[io_limit, 2*io_limit - 1],以此类推

  2. os_aio_array_reserve_slot函数中实现了array的分配策略(array未满时)。

    给定一个Aio read page,[space_id, page_no],首先计算local_seg(local_thd):

    local_seg = (offset >> (UNIV_PAGE_SIZE_SHIFT + 6)) % array->n_segments;

    然后从read_array的local_seg * io_limit处开始向后遍历array,直到找到一个空闲slot。

    一来保证相邻的page,能够尽可能分配给同一个thread处理,提高aio(merge io request)性能;

    二来由于是循环分配,也基本上保证了每个thread处理的io基本一致。

  1. srv_n_write_io_threads

    处理异步write I/O线程的数量

    innobase_write_io_threads/innodb_write_io_threads:通过参数控制

    因此系统可以并发处理的异步write请求为:

    io_limit * innodb_write_io_threads

    超过此限制,必须将已有的异步I/O部分写回磁盘,才能处理新的请求。

  2. SRV_MAX_N_PENDING_SYNC_IOS

    同步I/O array的slots个数,同步I/O不需要处理线程

  3. log thread,ibuf thread个数均为1

os_aio_array_reserve_slot ->

  1. 在aio array中定位一个空闲array,aio前期准备工作
    1. array已满
      1. native aio:    os_wait_event(array->not_full); native aio,等待not_full信号
      2. 非native aio:os_aio_simulated_wake_handler_threads;模拟唤醒
    2. array未满
      1. WIN_ASYNC_IO(windows AIO)

        设置OVERLAPPED结构,使用的是Windows Overlapped I/O [5,6]

        ResetEvent(slot->handle)

      2. LINUX_NATIVE_AIO(Linux AIO)

      设置iocb结构

      然后根据type判断: io_prep_pread or io_prep_pwrite [7]

  2. 进行aio操作
    1. type = OS_FILE_READ
      1. use native aio
        1. windows: ReadFile
        2. Linux:    os_aio_linux_dispatch(array, slot);
          1. 将async io请求发送至linux kernel
          2. 调用io_submit函数进行aio发送

          iocb = &slot->control;

io_ctx_index = (slot->pos * array->n_segments) / array->n_slots;

ret = io_submit(array->aio_ctx[io_ctx_index], 1, &iocb);

  1. iocb结构在slot之中;io_context结构,相同segment共用一个
  1. use simulated aio
  1. type = OS_FILE_WRITE
    1. use native aio
      1. windows: WriteFile
      2. Linux:    os_aio_linux_dispatch(array, slot)
    2. use simulated aio

os_aio_windows_handle

  1. 特殊处理流程,windows下,若mode = OS_AIO_SYNC,则需要调用os_aio_windows_handle函数等待aio结束
    1. 判断当前是否为sync_array
      1. 若是,等待指定的slot aio操作完成:         WaitForSingleObject
      2. 若不是,等待array中所有的aio操作完成:    WaitForMultipleObjects
    2. 获取aio操作的结果
      1. GetOverlappedResult
    3. 最后释放当前slot
      1. os_aio_array_free_slot

os_aio_linux_handle

  1. 分析完os_aio_windows_handle函数,接着分析Linux下同样功能的函数:os_aio_linux_handle
    1. 无限循环,遍历array,直到定位到一个完成的I/O操作(slot->io_already_done)为止
    2. 若当前没有完成的I/O,同时有I/O请求,则进入os_aio_linux_collect函数
      1. os_aio_linux_collect:从kernel中收集更多的I/O请求
        1. 调用io_getevents函数,进入忙等,等待超时设置为OS_AIO_REAP_TIMEOUT

        /** timeout for each io_getevents() call = 500ms. */

        #define OS_AIO_REAP_TIMEOUT    (500000000UL)

        1. 若io_getevents函数返回ret > 0,说明有完成的I/O,进行一些设置,最主要是将slot->io_already_done设置为TRUE

          slot->io_already_done = TRUE;

        2. 若系统I/O处于空闲状态,那么io_thread线程的主要时间,都在io_getevents函数中消耗。

3.Unique索引IO

与聚簇索引IO完全一致,因为二者都必须读取页面,不能进行Insert Buffer优化。

4.Non-unique索引IO

与聚簇索引IO不一致,区分流程在于函数buf_page_get_gen

buf_page_get_gen

if (mode == BUF_GET_IF_IN_POOL || mode == BUF_PEEK_IF_IN_POOL || mode == BUF_GET_IF_IN_POOL_OR_WATCH)

return NULL;

对于non-unique索引,此时mode = BUF_GET_IF_IN_POOL;若page在buffer pool中,则返回page,否则不立即读取page,进行insert buffer优化。

由于不会调用buf_read_page函数,因此不会产生物理IO。那么non-unique索引的页面何时会读入buffer pool,与insert buffer进行merge呢?详见 InnoDB Insert Buffer实现详解

 

5.InnoDB存储引擎 Simulated AIO

在Linux系统上,MySQL数据库InnoDB存储引擎除了可以使用Linux自带的libaio之外,其内部还实现了一种称之为Simulated aio功能,用以模拟系统AIO实现(其实,Simulated aio要早于linux native aio使用在innodb中,可参考网文[16])。前面的章节,已经分析了InnoDB存储引擎对于Linux原生AIO的使用,此处,再简单分析一下 Innodb simulated aio的实现方式。

以下一段话摘自Transactions on InnoDB网站[16],简单说明了simulated aio在innodb中的处理方式。

… The query thread simply queues the request in an array and then returns to the normal working. One of the IO helper thread, which is a background thread, then takes the request from the queue and issues a synchronous IO call (pread/pwrite) meaning it blocks on the IO call. Once it returns from the pread/pwrite call, this helper thread then calls the IO completion routine on the block in question …

queue I/O request

os0file.c::os_aio_func;

os_aio_array_reserve_slot();

if (array->n_reserved == array_n_slots && !srv_use_native_aio)

os_aio_simuated_wake_handler_threads();

os_event_wait(array->not_full);

slot->… = …;

对于用户发起的read/write aio,simulated aio仍旧从对应的array中寻找空余slot,然后将aio request丢进此空闲slot,然后返回即可。

do I/O request by backend I/O thread

fil0fil.c::fil_aio_wait -> os0file.c::os_aio_simulated_handle

  • windows下处理异步I/O函数为os_aio_windows_handle;linux原生aio下为os_aio_linux_handle;而linux simulated aio下,对应的函数则是os_aio_simulated_handle
    • 步骤一,遍历array,找出请求时间超过2S的I/O,优先处理,防止饥饿
    • 步骤二,若步骤一没有定位到,则找出I/O request中,page_no最小的请求
    • 步骤三,再次遍历array,判断是否存在与步骤一或步骤二定位到的page相邻的page request,相邻的请求全部一次处理,处理函数如下:

os_file_read/write -> os_file_read/write_func -> os_file_pread/pwrite -> pread/pwrite

  • 调用pread/pwrite进行同步读/写
  • 在读写结束之后,做些后续处理,标识对应的slot,I/O操作完成

总结

Linux simulated aio,实现简单,基本采用的仍旧是同步IO的方式。相对于Linux native aio,simulated aio最大的问题在于:每个I/O请求,最终都会调用一次pread/pwrite进行处理(除非可以进行相邻page的合并),而Linux native aio,对于一个array,进行一次异步I/O处理即可。

6.AIO层次分析

  • InnoDB层面 (IO请求)

SRV_N_PENDING_IOS_PER_THREAD

一个InnoDB thread同时能够处理的异步I/O的数量

  • File System层面 (AIO queue)

    fs.aio-max-nr

    文件系统级别,可以同时进行的aio的数量上限

  • Disk层面 (IO queue)

    nr_request

单个disk,可以并发处理的I/O请求的个数

若当前磁盘为逻辑盘,那么真实的nr_request = nr_request * disks in raid

彭立勋 (13:42:00):

FusionIO处理不是普通盘那样,普通磁盘消耗IO队列是匀速的,FIO是抖动式的

何登成 (13:42:50):

难道说要将aio队列调小,消除FIO的抖动?

彭立勋 (13:43:40):

例如FusionIO的处理能力是每10ms 100个IO,IO队列里有200个IO,FusionIO会一次拿走100,10ms内不再从IO队列里拿东西,然后再拿走100个

SAS盘是匀速的,每处理完一个就拿一个

何登成 (13:44:34):

那也没法解释为什么是10S抖动一次啊?

彭立勋 (13:44:39):

然后nr_request有个算法,如果IO队列达到2/3时,就认为系统阻塞了,就没响应了

彭立勋 (13:45:49):

所以如果AIO队列太长,一次写到磁盘IO队列上的东西太多,碰上FusionIO,就容易出现达到2/3,虽然很快就消失了,但是还是能看到突然阻塞这种情况

霸爷建议,观察nr_request用了多少,AIO队列长度保持磁盘IO队列的2/3以内

何登成 (13:47:16):

哦,有点理解了

就是要aio的队列最大长度,不超过nr_request的2/3

彭立勋 (13:47:22):

aio_nr是FS级的,nr_request是Disk级的

是的

霸爷就是这意思,只对FusionIO有效,SAS盘不是这样的

SAS盘尽管往里面堆

7.Linux AIO增强

InnoDB使用的linux native aio,仅仅支持以O_DIRECT方式打开的文件。针对此情况,大神Jens Axboe做了进一步的优化,提供了Buffered async IO,具体情况可参考其个人主页上的一篇文章:Buffered async IO

 

8.软中断的影响

上面提到的两个问题点,是InnoDB引擎内部的,其实mysql在运行过程中,还有一个极大的与操作系统与硬件相关的问题点——(软)中断的处理。关于软中断的原理以及Linux下软中断的实现,可参考[25][26]。

在mysql系统中,最主要的软中断有两类:Disk软中断;网卡软中断。而软中断带来的问题是:所有的软中断由一个CPU集中处理,导致单个CPU利用率增加,同时处理软中断的性能下降。一个典型的例子如下:

mpstat -P ALL 2 3

Average: CPU %user %nice %sys %iowait %irq %soft %steal %idle intr/s

Average: all 1.07 0.00 0.37 0.12 0.08 0.04 0.00 98.31 1555.33

Average: 0 6.28 0.00 0.83 0.99 0.66 0.17 0.00 91.07 1555.00

Average: 1 0.00 0.00 0.17 0.00 0.00 0.00 0.00 99.83 0.00

Average: 2 0.00 0.00 0.17 0.00 0.00 0.00 0.00 99.83 0.00

Average: 3 0.82 0.00 1.31 0.00 0.00 0.00 0.00 97.88 0.00

上例中,intr/s列,表示CPU在internal时间段(2S)内,每秒CPU接收的中断的次数(intr /total)*100;%soft列,表示在internale时间段内,软中断时间(softirq/total)*100。可以看出,每秒总得中断 次数为1555.33,而CPU 0就处理了1555个,中断的处理分配极度不均衡。

解决的方案也很明确,将软中断处理分布到系统所有的CPU中即可。

关于网卡软中断对于MYSQL数据库性能的影响及解决方案,可参考网文 MYSQL数据库网卡软中断不平衡问题及解决方案 [22]。除此之外,Google与Facebook都曾经针对网卡软中断,做过相应的优化[23][27].

而对于Disk软中断,据我所知2002年一本漫画闯天涯同学,也对Linux做了优化,能够保证Disk软中断的处理在所有CPU中平均分布:block: improve rq_affinity placement

关于中断(包括软硬中断),Linux系统已经做了大量的优化,简单归纳起来,主要有以下一些:

  • 高级可编程中断控制(Advanced Programmable Interrupt Controller, APIC) [30]
  • 中断亲和力(SMP IRP Affinity) [28][29]
  • IRQ Balance [32]
  • MSI & MSI-X [34][35][36]
  • 网卡软中断处理[22][23][27]
  • 磁盘软中断处理[37]
  • 中断监控 [33]

 

推荐阅读的关联文章:
MySQL数据库分布式事务XA的实现原理分析

MySQL数据库InnoDB存储引擎Log漫游(1)
MySQL数据库InnoDB存储引擎Log漫游(2)
MySQL数据库InnoDB存储引擎Log漫游(3)

MySQL数据库InnoDB存储引擎原生Checkpoint策略及各版本优化详解
MySQL5.5数据库innodb_change_buffering怪异问题分析

MySQL数据库InnoDB存储引擎中的锁机制
MySQL数据库InnoDB存储引擎多版本控制(MVCC)实现原理分析

MySQL数据库分布式事务XA的实现原理分析
MySQL数据库分布式事务XA优缺点与改进方案

MySQL数据库InnoDB存储引擎 Insert Buffer实现机制详解

One thought on “MySQL数据库InnoDB存储引擎 异步IO(AIO)实现机制详解

  1. Pingback: MySQL数据库InnoDB存储引擎源码解读 Buffer Pool Flush List详解 | MySQLOPS 数据库与运维自动化技术分享

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>