MySQL 引擎特性 InnoDB 文件系统之IO系统和内存管理(2)

首先在任务队列数组中选择一个segment;这里根据偏移量来算segment,因此可以尽可能的将相邻的读写请求放到一起,这有利于在IO层的合并操作

local_seg = (offset >> (UNIV_PAGE_SIZE_SHIFT + 6)) % m_n_segments;

从该segment范围内选择一个空闲的slot,如果没有则等待;

将对应的文件读写请求信息赋值到slot中,例如写入的目标文件,偏移量,数据等;

如果这是一次IO写入操作,且使用native aio时,如果表开启了transparent compression,则对要写入的数据页先进行压缩并punch hole;如果设置了表空间加密,再对数据页进行加密;

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

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

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

处理异步AIO请求

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

首先调用os_aio_handler来获取请求:

对于Native AIO,调用函数os_aio_linux_handle 获取读写请求。IO线程会反复以500ms(OS_AIO_REAP_TIMEOUT)的超时时间通过io_getevents确认是否有任务已经完成了(LinuxAIOHandler::collect()),如果有读写任务完成,找到已完成任务的slot后,释放对应的槽位;

对于simulated aio,调用函数os_aio_simulated_handler 处理读写请求,这里相比NATIVE AIO要复杂很多;

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

已经完成的slot需要及时被处理(SimulatedAIOHandler::check_completed,可能由上次的io合并操作完成);

如果有超过2秒未被调度的请求(SimulatedAIOHandler::select_oldest),则优先选择最老的slot,防止饿死,否则,找一个文件读写偏移量最小的位置的slot(SimulatedAIOHandler::select());

没有任何请求时进入等待状态;

当找到一个未完成的slot时,会尝试merge相邻的IO请求(SimulatedAIOHandler::merge()),并将对应的slot加入到SimulatedAIOHandler::m_slots数组中,最多不超过64个slot;

然而在5.7版本里,合并操作已经被禁止了,全部改成了一个个slot进行读写,升级到5.7的用户一定要注意这个改变,或者改为使用更好的Native AIO方式;

完成io后,释放slot; 并选择第一个处理完的slot作为随后优先完成的请求。

从上一步获得完成IO的slot后,调用函数fil_node_complete_io, 递减node->n_pending。对于文件写操作,需要加入到fil_system->unflushed_spaces链表上,表示这个文件修改过了,后续需要被sync到磁盘。

如果设置为O_DIRECT_NO_FSYNC的文件IO模式,则数据文件无需加入到fil_system_t::unflushed_spaces链表上。通常我们即时使用O_DIRECT的方式操作文件,也需要做一次sync,来保证文件元数据的持久化,但在某些文件系统下则没有这个必要,通常只要文件的大小这些关键元数据没发生变化,可以省略一次fsync。

最后在IO完成后,调用buf_page_io_complete,做page corruption检查、change buffer merge等操作;对于写操作,需要从flush list上移除block并更新double write buffer;对于LRU FLUSH产生的写操作,还会将其对应的block释放到free list上;

对于日志文件操作,调用log_io_complete执行一次fil_flush,并更新内存内的checkpoint信息(log_complete_checkpoint)。

IO 并发控制

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

对相同文件的IO操作通过大量的counter/flag来进行并发控制。

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

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

将fil_space_t::stop_new_ops设置为true;

检查是否有Pending的change buffer merge (fil_space_t::n_pending_ops);有则等待;

检查是否有pending的IO(fil_node_t::n_pending) 或者pending的文件flush操作(fil_node_t::n_pending_flushes);有则等待。

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

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

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/3fb67498486ae9aef44188984b3bf568.html