InnoDB IO子系统介绍

本文我们来简单过一下InnoDB的IO子系统相关模块的代码逻辑。主要包括IO读写线程、预读逻辑、InnoDB读写Page以及社区的一些改进。

前言

InnoDB对page的磁盘操作分为读操作和写操作。

 

对于读操作,在将数据读入磁盘前,总是为其先预先分配好一个block,然后再去磁盘读取一个新的page,在使用这个page之前,还需要检查是否有change buffer项,并根据change buffer,进行数据变更。

 

读操作分为两种场景:普通的读page及预读操作,前者为同步读,后者为异步读

 

Page写操作也分为两种,一种是batch write,一种是single page write。写page默认受double write buffer保护,因此对double write buffer的写磁盘为同步写,而对数据文件的写入为异步写。

 

同步读写操作通常由用户线程来完成,而异步读写操作则需要后台线程的协同。

 

举个简单的例子,假设我们向磁盘批量写数据,首先先写到double write buffer,当dblwr满了之后,一次性将dblwr中的数据同步刷到Ibdata,在确保sync到dblwr后,再将这些page分别异步写到各自的文件中。注意这时候dblwr依旧未被清空,新的写Page请求会进入等待。

 

当异步写page完成后,io helper线程会调用buf_flush_write_complete,将写入的Page从flush list上移除。当dblwr中的page完全写完后,在函数buf_dblwr_update里将dblwr清空。这时候才允许新的写请求进dblwr。

 

同样的,对于异步写操作,也需要IO Helper线程来检查page是否完好、merge change buffer等一系列操作。

 

除了page的写入,还包括日志异步写入线程、及ibuf后台线程。

 

后台线程

 

* IO READ 线程 —- 后台读线程数,线程数目通过参数innodb_read_io_threads配置

 

主要处理INNODB 数据文件异步读请求,任务队列为os_aio_read_array,任务队列包含slot数为线程数 * 256(linux 平台),也就是说,每个read线程最多可以pend 256个任务;

 

* IO WRITE 线程 —- 后台写线程数,线程数目通过参数innodb_write_io_threads配置

 

主要处理INNODB 数据文件异步写请求,任务队列为os_aio_write_array,任务队列包含slot数为线程数 * 256(linux 平台),也就是说,每个read线程最多可以pend 256个任务;

 

* LOG 线程 — 写日志线程

 

只有在写checkpoint信息时才会发出一次异步写请求。任务队列为os_aio_log_array,共1个segment,包含256个slot

 

* IBUF 线程 — 负责读入change buffer页的后台线程,任务队列为os_aio_ibuf_array,共1个segment,包含256个slot

 

所有的同步写操作都是由用户线程或其他后台线程执行。上述IO线程只负责异步操作。

 

发起请求

 

入口函数:os_aio_func

 

a.首先对于同步读写请求(OS_AIO_SYNC),发起请求的线程直接调用os_file_read_func 或者os_file_write_func 去读写文件 ,然后返回

 

b.对于异步请求,用户线程从对应操作类型的任务队列中选取一个slot,将需要读写的信息存储于其中(os_aio_array_reserve_slot):
##首先在任务队列数组中选择一个segment
     local_seg = (offset >> (UNIV_PAGE_SIZE_SHIFT + 6))
% array->n_segments;
这里根据偏移量来算segment,因此可以尽可能的将相邻的读写请求放到一起,这有利于在IO层的合并操作。

 

##然后加mutex,遍历该segement,选择空闲的slot,如果没有则等待。

 

##将对应的文件读写请求信息赋值到slot中,例如写入的目标文件,偏移量,数据等
     slot->is_reserved = true;
     slot->reservation_time = ut_time();
     slot->message1 = message1;
     slot->message2 = message2;
     slot->file     = file;
     slot->name     = name;
     slot->len      = len;
     slot->type     = type;
     slot->buf      = static_cast<byte*>(buf);
     slot->offset   = offset;
     slot->io_already_done = false;
     ……
     //对于Native AIO 还需要调用如下逻辑
   
     aio_offset = (off_t) offset;

     ut_a(sizeof(aio_offset) >= sizeof(offset)
          || ((os_offset_t) aio_offset) == offset);

     iocb = &slot->control;

     if (type == OS_FILE_READ) {
          io_prep_pread(iocb, file, buf, len, aio_offset);
     } else {
          ut_a(type == OS_FILE_WRITE);
          io_prep_pwrite(iocb, file, buf, len, aio_offset);
     }

     iocb->data = (void*) slot;
     slot->n_bytes = 0;
     slot->ret = 0;

 

c.对于Native AIO (使用linux自带的LIBAIO库),调用函数os_aio_linux_dispatch,将IO请求分发给kernel层。

 

d.如果没有开启Native AIO,且没有设置wakeup later 标记,则会去唤醒io线程(os_aio_simulated_wake_handler_thread),这是早期libaio还不成熟时,InnoDB在内部模拟aio实现的逻辑。

 

Tips:编译Native AIO需要安装Libaio-dev包,并打开选项srv_use_native_aio

处理异步AIO请求

 

IO线程入口函数为io_handler_thread –> fil_aio_wait

 

a. 对于Native AIO,调用函数os_aio_linux_handle 获取读写请求

 

IO线程会反复以500ms的超时时间通过io_getevents确认是否有任务已经完成了(函数os_aio_linux_collect),如果有读写任务完成,则返回上层函数

 

逻辑中还处理了AIO部分读写的场景,这里会再次提交aio请求。(什么场景会这样 ??)

 

找到已完成任务的slot后,释放对应的槽位。(os_aio_array_free_slot)

 

b.对于simulated aio,调用函数os_aio_simulated_handle 获取读写请求,这里相比NATIVE AIO要复杂些

 

##首先,如果这是异步读队列,并且os_aio_recommend_sleep_for_read_threads被设置,则暂时不处理,而是等待一会,让其他线程有机会将更过的IO请求发送过来。目前linear readhaed 会使用到该功能。这样可以得到更好的IO合并效果。

 

##如果有超过2秒未被调度的请求,则选择最老的slot,防止饿死,否则,找一个文件读写偏移量最小的位置的slot.

 

##根据上一步找到的slot,遍历其他操作,找到与其连续的IO请求,加入数组consecutive_ios中。直到遍历完成,后者数组中slot个数超过64
## 根据连续IO的slot数,分配新的内存块,并进行一次IO读或写。

 

c. 调用函数fil_node_complete_io, 递减node->n_pending, 对于文件写操作,需要加入到fil_system->unflushed_spaces链表上,表示这个文件修改过了,后续需要被sync到磁盘。

 

如果设置为O_DIRECT_NO_FSYNC,对于数据文件,无需加入到unflushed_spaces链表上。这在某些文件系统上是可行的。(fil_buffering_disabled)

 

d. 对于数据文件读写或IMPORT操作,调用buf_page_io_complete,做page corruption检查、change buffer merge等操作;对于LRU FLUSH产生的写操作,还会将其对应的block释放到free list上;对于日志文件操作,调用log_io_complete执行一次fil_flush,并更新内存内的checkpoint信息(log_complete_checkpoint)

 

并发控制

 

a. 由于文件底层使用pwrite/pread来进行文件I/O,因此用户线程对文件普通的并发I/O操作无需加锁。但在windows平台下,则需要加锁进行读写。

 

b. 当文件处于扩展阶段时(fil_space_extend),将fil_node的being_extended设置为true,避免产生并发extend,或其他关闭文件或者rename操作等

 

c. 当正在删除一个表时,会检查是否有pending的操作(fil_check_pending_operations)

 

将fil_space_t::stop_new_ops设置为true;
检查是否有Pending的change buffer merge (space->n_pending_ops);有则等待
检查是否有pending的IO(fil_node_t::n_pending) 或者pending的flush操作(fil_node_t::n_pending_flushes);有则等待

 

 

d. 当truncate一张表时,和drop table类似,也会调用函数fil_check_pending_operations,检查表上是否有pending的操作,并将space->is_being_truncated设置为true

 

e. 当rename一张表时(fil_rename_tablespace),将文件的stop_ios标记设置为true,阻止其他线程所有的I/O操作

 

=====
当进行文件读写操作时,如果是读操作,发现stop_new_ops或者被设置了但is_being_truncated未被设置,会返回报错;但依然允许写操作(why ? 函数fil_io)

 

当进行文件flush操作时,如果发现stop_new_ops 或者is_being_truncated被设置了,则忽略文件flush操作 (fil_flush_file_spaces)。

文件预读

 

文件预读是一项在SSD普及前普通磁盘上比较常见的技术,通过预读的方式进行连续IO而非带价高昂的随机IO

 

InnoDB有两种预读方式:随机预读及线性预读; Facebook另外还实现了一种逻辑预读的方式

a.随机预读

 

入口函数:buf_read_ahead_random

 

以64个Page为单位(这也是一个extend的大小),当前读入的page no所在的64个pagno 区域[ (page_no/64)*64, (page_no/64) *64 + 64],如果最近被访问的Page数超过BUF_READ_AHEAD_RANDOM_THRESHOLD(通常值为13),则将其他Page也读进内存。这里采取异步读。

 

随机预读受参数innodb_random_read_ahead控制

b.线性预读

 

入口函数:buf_read_ahead_linear

 

所谓线性预读,就是在读入一个新的page时,和随机预读类似的64个连续page范围内,默认从低到高Page no,如果最近连续被访问的page数超过innodb_read_ahead_threshold,则将该extend之后的其他page也读取进来。

c.逻辑预读

 

由于表可能存在碎片空间,因此很可能对于诸如全表扫描这样的场景,连续读取的page并不是物理连续的,线性预读不能解决这样的问题,另外一次读取一个extend对于需要全表扫描的负载并不足够。因此facebook引入了逻辑预读。

 

其大致思路为,扫描聚集索引,搜集叶子节点号,然后根据叶子节点的page no (可以从非叶子节点获取)顺序异步读入一定量的page。

 

由于Innodb aio一次只支持体检一个page读请求,虽然Kernel层本身会做读请求合并,但那显然效率不够高。他们对此做了修改,使INNODB可以支持一次提交(io_submit)多个aio请求。

 

入口函数:row_search_for_mysql –> row_read_ahead_logical

 

具体参阅这篇博文:http://planet.mysql.com/entry/?id=516236

 

或者webscalesql上的几个commit:
git show 2d61329446a08f85c89a4119317ae85baacf2bbb   // 合并多个AIO请求,对所有的预读逻辑(上述三种)采用这种方式
git show 9f52bfd2222403f841fe5fcbedd1333f78a70a4b     //  逻辑预读的主要代码逻辑
git show 64b68e07430b50f6bff5ed67374b336623db24b6   // 防止事务在多个表上读取操作时预读带来的影响

日志填充写入

由于现代磁盘通常的block size都是大于512字节的,例如一般是4096字节,为了避免 “read-on-write” 问题,在5.7版本里添加了一个参数innodb_log_write_ahead_size,你可以通过配置该参数,在写入redo log时,将写入区域配置到block size对齐的字节数。

 

在代码里的实现,就是在写入redo log 文件之前,为尾部字节填充0,(参考函数log_write_up_to)

 

Tips:所谓READ-ON-WRITE问题,就是当修改的字节不足一个block时,需要将整个block读进内存,修改对应的位置,然后再写进去;如果我们以block为单位来写入的话,直接完整覆盖写入即可。

原创文章,转载请注明: 转载自Simple Life

本文链接地址: InnoDB IO子系统介绍

Post Footer automatically generated by wp-posturl plugin for wordpress.


Comments

Leave a Reply

Your email address will not be published. Name and email are required


Current month ye@r day *