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为单位来写入的话,直接完整覆盖写入即可。

时间: 2025-01-27 23:00:46

InnoDB IO子系统介绍的相关文章

MySQL · 引擎特性 · InnoDB IO子系统

前言 InnoDB做为一款成熟的跨平台数据库引擎,其实现了一套高效易用的IO接口,包括同步异步IO,IO合并等.本文简单介绍一下其内部实现,主要的代码集中在os0file.cc这个文件中.本文的分析默认基于MySQL 5.6,CentOS 6,gcc 4.8,其他版本的信息会另行指出. 基础知识 WAL技术 : 日志先行技术,基本所有的数据库,都使用了这个技术.简单的说,就是需要写数据块的时候,数据库前台线程把对应的日志先写(批量顺序写)到磁盘上,然后就告诉客户端操作成功,至于真正写数据块的操作

MySQL · 引擎特性 · InnoDB 事务子系统介绍

前言 在前面几期关于 InnoDB Redo 和 Undo 实现的铺垫后,本节我们从上层的角度来阐述 InnoDB 的事务子系统是如何实现的,涉及的内容包括:InnoDB的事务相关模块.如何实现MVCC及ACID.如何进行事务的并发控制.事务系统如何进行管理等相关知识.本文的目的是让读者对事务系统有一个较全面的理解. 由于不同版本对事务系统都有改变,本文的所有分析基于当前GA的最新版本MySQL5.7.9,但也会在阐述的过程中,顺带描述之前版本的一些内容.本文也会介绍5.7版本对事务系统的一些优

Linux性能优化之IO子系统介绍

本文的大部分内容来自 IBM Redbook - Linux Performance and Tuning Guidelines . FileSystem VFS(Virtual FileSystem) 虚拟文件系统 文件系统是内核的功能,是一种工作在内核空间的软件,访问一个文件必须需要文件系统的存在才可以.Linux 可以支持多达数十种不同的文件系统,它们的实现各不相同,因此 Linux 内核向用户空间提供了虚拟文件系统这个统一的接口用来对文件系统进行操作. 虚拟文件系统是位于用户空间进程和内

MySQL · 引擎特性 · Innodb change buffer介绍

前言 在前面几期月报我们介绍了undo log.redo log以及InnoDB如何崩溃恢复来实现数据ACID的相关知识.本期我们介绍另外一种重要的数据变更日志,也就是InnoDB change buffer. Change buffer的主要目的是将对二级索引的数据操作缓存下来,以此减少二级索引的随机IO,并达到操作合并的效果. 在MySQL5.5之前的版本中,由于只支持缓存insert操作,所以最初叫做insert buffer,只是后来的版本中支持了更多的操作类型缓存,才改叫change

Android软件架构及子系统介绍

IO 使用说明介绍_java

在判断文件对象是否是文件或者目录时,必须要先判断该文件对象封装的内容是否存在,通过exists判断: 在文本文件操作流构造的时候还可指定编码方式: File f; f.exists(); f.isDirectory(); f.isFile(); f.deleteOnExit();// 在程序退出的时候将指定文件删除: f.createNewFile();// 在指定位置创建文件,如果文件已经存在,则返回false: f.getParent();//此方法返回的是绝对路径中的父目录,如果获取的是相

MySQL · 源码分析 · InnoDB 异步IO工作流程

之前的一篇内核月报InnoDB IO子系统 中介绍了InnoDB IO子系统中包含的同步IO以及异步IO.本篇文章将从源码层面剖析一下InnoDB IO子系统中,数据页的同步IO以及异步IO请求的具体实现过程. 在MySQL5.6中,InnoDB的异步IO主要是用来处理预读以及对数据文件的写请求的.而对于正常的页面数据读取则是通过同步IO进行的.到底二者在代码层面上的实现过程有什么样的区别? 接下来我们将以Linux native io的执行过程为主线,对IO请求的执行过程进行梳理. 重点数据结

MySQL · 引擎特性 · InnoDB Buffer Pool

前言 用户对数据库的最基本要求就是能高效的读取和存储数据,但是读写数据都涉及到与低速的设备交互,为了弥补两者之间的速度差异,所有数据库都有缓存池,用来管理相应的数据页,提高数据库的效率,当然也因为引入了这一中间层,数据库对内存的管理变得相对比较复杂.本文主要分析MySQL Buffer Pool的相关技术以及实现原理,源码基于阿里云RDS MySQL 5.6分支,其中部分特性已经开源到AliSQL.Buffer Pool相关的源代码在buf目录下,主要包括LRU List,Flu List,Do

[2016-03]MySQL · TokuDB · 事务子系统和 MVCC 实现

前言 之前有篇月报是关于innodb事务子系统的<MySQL · 引擎特性 · InnoDB 事务子系统介绍> 里面较详细的讲述了 MySQL 如何开启一个事务,感兴趣的同学可以先阅读那篇温习一下. TokuDB 引擎也支持事务,保证一个事务内的所有操作都执行成功或者都未被执行.TokuDB中的事务由数据结构 tokutxn 表示.当开启一个 txn 时,TokuDB会创建一个 tokutxn 实例,下面只显示比较重要的字段. struct tokutxn { TXNID_PAIR txnid