InnoDB关键特性包括:

  • 插入缓冲(Inster Buffer)
  • 两次写(Double Write)
  • 自适应哈希索引(Adaptive Hash Index)
  • 异步IO(Async IO)
  • 刷新邻接页(Flush Neighbor Page)

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_缓冲池

一、插入缓冲

  • 下面介绍的插入缓冲有:
  • Insert Buffer
  • Change Buffer

聚集索引与辅助(非聚集/二级)索引

  • 在介绍Insert Buffer之前,先介绍一些聚集索引与辅助
  • 在InnoDB中,主键是行唯一的标识符。通常应用程序中行记录的插入顺序是按照主键递增的顺序进行插入的。因此,插入聚集索引(Primary Key)一般是顺序的,不需要磁盘的随机读取。比如按下列SQL定义表:

create table t( a int auto_increment, b varchar(30), primary key(a) );

  • 其中a列是自增长的,若对a列插入NULL值,则由于其具有auto_increment属性,其值会自动增长。同时页中的行记录按a的值进行顺序存放。在一般情况下,不需要随机读取另一个页中的记录。因此,对于这类情况下的插入操作,速度时非常快的

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_子节点_02

  • 但是不可能每张表上只有一个聚集索引,更多情况下,一张表上有多个非聚集的辅助索引(secondary index)。比如,用户需要按照b这个字段进行查找,并且b这个字段不是唯一的,即表按照下面的SQL语句进行定义:

create table t( a int auto_increment, b varchar(30), primary key(a), key(b) );

  • 在这种情况下产生了一个非聚集的且不是唯一的索引。在进行插入操作时,数据页的存放还是按主键a进行顺序存放的,但是对于非聚集索引叶子节点的插入不再是顺序的了,这时就需要离散地访问非聚集索引页,由于随机读取的存在而导致了插入操作性能下降。当然这并不是这个b字段上索引的错误,而是因为B+树的特性决定了非聚集索引插入的离散性
  • 需要注意的是,在某些情况下,辅助索引的插入依然是顺序的,或者说是比较顺序的,比如用户购买表中的时间字段。在通常情况下,用户购买时间是一个辅助索引,用来根据时间条件进行查询。但是在插入时却是根据时间的递增而插入的,因此插入也是“较为”顺序的

①Insert Buffer

  • InnoDB存储引擎开创性地设计了Insert Buffer
  • 工作原理:于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中若在,则直接插入;若不在,则先放入到一个Insert Buffer对象中
  • 使用者认为这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放到另一个位置。然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能
  • Insert Buffer的使用需要同时满足以下两个条件:
  • 索引是辅助索引(secondary index)
  • 索引不是唯一的
  • 一个缺陷:应用程序进行大量的插入操作,这些都涉及了不唯一的非聚集索引,也就是使用了Insert Buffer。若此时MySQL数据库发生了宕机,这时势必有大量的Insert Buffer并没有合并到实际的非聚集索引中去。因此这时恢复可能需要很长的时间,在极端情况下甚至需要几个小时
  • 辅助索引不能使唯一的,因为在插入缓冲时,数据库并不去查找索引页来判断插入的记录的唯一性。如果去查找肯定又会有离散读取的情况发生,从而导致Insert Buffer失去了意义
  • 用户可以在下面命令的输出信息中查看插入缓冲的信息:
  • seg size:显示当前Insert Buffer的大小为2*16KB
  • free list len:代表了空闲列表的长度
  • size:代表了已经合并记录页的数量

show engine innodb status\G;

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_InnoDB关键特性_03

  • Insert Buffer的另一个问题:
  • 在写密集的情况下,插入缓冲会占用过多的缓冲池内存(inoodb_buffer_pool),默认最大可以占用到1/2的缓冲池内存
  • 下图是InnoDB存储引擎源代码中对于Insert Buffer的初始化操作:
  • 这对于其他的操作可能会带来一定的影响。Percona上发布一些补丁来修正插入缓冲占用太多缓冲池内存的情况。具体可以到Percona官网进行查找。简单地说,修改IBUF_POOL_SIZE_PER_MAX_SIZE就可以对插入缓冲的大小进行控制。比如将IBUF_POOL_SIZE_PER_MAX_SIZE改为3,则最大只能使用1/3的缓冲池内存

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_InnoDB关键特性_04

②Change Buffer

  • InnoDB从1.0.x版本开始引入了Change Buffer,可将其视为Insert Buffer的升级。从这个版本开始,InnoDB可以对DML操作——insert、delete、update都进行缓冲,它们分别是:Insert Buffer、Delete Buffer、Purge Buffer
  • 和之前的Insert Buffer一样,Change Buffer适用的对象依然是非唯一的辅助索引
  • 对一条记录进行update操作可能分为以下两个过程:
  • 将记录标记为已删除
  • 真正将记录删除
  • 因此Delete Buffer对应update操作的第一个过程,即将记录标记为删除。Purge Buffer对应update操作的第二个过程,即将记录真正的删除
  • innodb_change_buffering参数:
  • 用来开启各种Buffer的选项,默认值为all
  • 可选的值有:inserts、deletes、purges、changes、all、none
  • inserts、deletes、purges就是前面讨论过的三种情况
  • changes表示启用inserts和deletes
  • all表示启用所有
  • none表示都启用

show variables like 'innodb_change_buffering'\G;

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_InnoDB关键特性_05

  • innodb_change_buffer_max_size参数:通过该参数来控制change buffer最大使用内存的数量
  • 查看Change Buffer的信息:
  • merged operations、discarded operation:表示合并与清理操作
  • 下面显示Change Buffer中每个操作的次数:insert表示Insert Buffer;delete mark表示Delete Buffer;delete表示Purge Buffer
  • discarded operation:表示当Change Buffer发生merge时,表已经被删除,此时就无需再将记录合并到辅助索引中了

show engine innodb status\G;

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_辅助索引_06

③Insert/Change Buffer的内部实现

  • Insert Buffer的数据结构是一棵B+树
  • 在MySQL 4.1之前每张表有一颗Insert Buffer B+树
  • 而现在的版本中,全局只有一颗Insert Buffer B+树,负责对所有的表的辅助索引进行Insert Buffer
  • 这棵B+树存放在共享表空间中,默认也就是ibdata1中。因此,视图通过独立表空间ibd文件恢复表中数据时,往往会导致CHECK TABLE失败。这是因为表的辅助索引中的数据可能还在Insert Buffer中,也就是共享表空间中,所以通过ibd文件进行恢复后,还需要进行REPAIR TABLE操作来重建表上所有的辅助索引
  • Insert Buffer是一棵B+树,非叶子节点存放的是查询的search key(键值),构造如下:
  • space:表示待插入记录所在表的表空间id(在InnoDB中,每个表有唯一的space id,可以通过该id查询得知是哪张表)。占用4字节
  • marker:用来兼容老版本的Insert Buffer。占用1字节
  • offset:表示页所在的偏移量。占用4字节

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_子节点_07

  • 当一个辅助索引要插入到页(space,offset)时,如果这个页不在缓冲池中,那么InnoDB首先根据上述规则构造一个search key,接下来查询Insert Buffer这棵B+树,然后再将这条记录插入到Insert Buffer B+树的叶子节点中。对于插入到Insert Buffer B+树叶子节点的记录(如下图所示),并不是直接将待插入的记录插入,而是需要根据如下的规则进行构造:
  • space、marker、page_no字段和之前非叶子节点的含义相同,一共占用9个字节
  • metadata字段占用4字节,其存储内容如下下图所示
  • IBUF_REC_OFFSET_COUNT:是保存两个字节的整数,用来排序每个记录进入Insert Buffer的顺序。因为从InnoDB 1.0.x开始支持Change Buffer,所以这个值同样记录进入Insert Buffer的顺序。通过这个顺序回放(replay)才能得到记录的正确值
  • 从Insert Buffer叶子节点的第5列开始,就是实际插入记录的各个字段了。因此较之原插入记录,Insert Buffer B+树的叶子节点记录需要额外13字节的开销

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_子节点_08

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_缓冲池_09

  • Insert Buffer Bitmap页
  • 因为启动Insert Buffer索引后,辅助索引页(space,page_no)中的记录可能被插入到Insert Buffer B+树中,所以为了保证每次Merge Insert Buffer页必须成功,还需要有一个特殊的页来标记每个辅助索引页(space,page_no)的可用空间。这个页的类型为Insert Buffer Bitmap
  • 每个Insert Buffer Bitmap页用来追踪16384个辅助索引页,也就是256个区(Extent)。每个Insert Buffer Bitmap页都在16384个页的第二个页中。关于Insert Buffer Bitmap页的作用会在后面介绍
  • 每个辅助索引页在Insert Buffer Bitmap页中占用4位(bit),由下表中的三个部分组成

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_辅助索引_10

④插入缓冲何时进行合并?

  • 通过上面我们知道Insert/Change Buffer是一棵B+树。若需要实现插入记录的辅助索引页不在缓冲池中,那么需要将辅助索引记录首先插入到这棵B+树中。但是Insert Buffer中的记录何时合并(merge)到真正的辅助索引中呢?这是下面要关注的重点
  • Merge Insert Buffer的操作可能发生在以下几种情况:
  • ①辅助索引页被读到缓冲池时
  • 当辅助索引页被读取到缓冲池中时,例如这在执行正常的select查询操作,这时需要检查Insert Buffer Bitmap页,然后确认该辅助索引页是否有记录存放于Insert Buffer B+树中。若有,则将Insert Buffer B+树该页的记录插入到该辅助索引页中。可以看到对该页多次的记录操作通过一次操作合并到了原有的辅助索引页中,因此性能会有大幅提高
  • ②Insert Buffer Bitmap页追踪到该辅助索引页已无可用空间时
  • Insert Buffer Bitmap页用来追踪每个辅助索引页的可用空间,并至少有1/32页的空间。若插入辅助索引记录时检测到插入记录后可用空间会小于1/32页,则会强制进行一个合并操作,即强制读取辅助索引页,将Insert Buffer B+树中该页的记录及待插入的记录插入到辅助索引页中
  • ③Mastert Thread
  • 在前面介绍过,Master Thread线程中每秒或每10秒会进行一次Merge Insert Buffer的操作,不同之处在于每次进行merge操作的页的数量不同
  • 在Master Thread中,执行merge操作的不止是一个页,而是根据srv_innodb_io_capacity的百分比来决定真正要合并多少个辅助索引页。但InnoDB又是根据怎么样的算法来得知需要合并的辅助索引页呢?
  • 在Insert Buffer B+树中,辅助索引页根据(space,offset)都已排序好,故可以根据(space,offset)的排列顺序进行页的选择。然而,对Insert Buffer页的选择,InnoDB并非采用这个方式,它随机地选择Insert Buffer B+树的一个页,读取该页中的space及之后所需要数量的页。该算法在复杂情况下应有更好的公平性。同时,若进行merge时,要进行merge的表已经被删除,此时可以直接丢弃已经被Insert/Change Buffer的数据记录

二、两次写(doublewrite)

  • 上面介绍的Insert/Change Buffer带给InnoDB的是性能上的提升,那么两次写(doublewrite)带给InnoDB的是数据页的可靠性

设计doublewrite的初衷

  • 当数据库宕机时,可能InnoDB正在写入某个页到表中,而这个页只写了一部分,比如16KB的页,只写了前4KB,之后就发生了宕机,这种情况被称为“部分写失效”。在InnoDB未使用doublewrite技术前,曾经出现过因为部分写失效而导致数据丢失的情况
  • 在doublewrite被设计出之前,如果发生写失效,可以通过重做日志进行恢复。这是一个办法,但是必须知道,重做日志中记录的是对页的物理操作,如偏移量800,写'aaaa'记录。如果这个页本身已经发生了损坏,再次对其进行重做是没有意义的
  • 什么是doublewrite:在应用重做日志前,用户需要一个页的副本,当写入失效发生时,先通过页的副本来还原该页,再进行重做,这就是doublewrite
  • doublewrite由两个部分组成:
  • 一部分是内存中的doublewrite buffer,大小为2MB
  • 另一部分是物理磁盘上共享表空间中连续的128页,即2个区(extent),大小同样为2MB

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_InnoDB关键特性_11

doublewrite工作原理:

  • 工作原理如下:
  • 在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer
  • 之后通过doublewrite buffer再分两次,每次1MB顺序地写入共享空间的物理磁盘上
  • 然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题
  • 在这个过程中,因为doublewrite页是连续的,因此这个过程是顺序写的,开销并不大。在完成doublewrite页的写入后,再将doublewrite buffer中的页写入各个表空间文件中,此时的写入则是离散的
  • 如果操作系统将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的doublewrite中找到该页的一个副本,将其复制到表空间文件,再应用重做日志
  • 下面显示了一个由doublewrite进行恢复的情况:

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_InnoDB关键特性_12

  • 可以通过以下命令观察到doublewrite运行的情况:
  • Innodb_dblwr_pages_written:是doublewrite一共写了多少页
  • Innodb_dblwr_writes:是实际的写入次数
show global status like 'innodb_dblwr%'\G;

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_辅助索引_13

MySQL 5.5.24版本之前的一个bug

  • “Innodb_buffer_pool_pages_flushed”变量表示当前从缓冲池刷新到磁盘页的数量

show global status like 'Innodb_buffer_pool_pages_flushed'\G;

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_缓冲池_14

  • 根据之前的介绍,用户应该了解到,在默认情况下所以页的刷新首先都需要放入到doublewrite中,因此该变量应该和“Innodb_dblwr_pages_written”一致。然而在MySQL 5.5.24版本之前,Innodb_buffer_pool_pages_flushed总是Innodb_dblwr_pages_written的2倍,此bug直到MySQL 5.5.24才修复
  • 因此用户若需要统计数据库在生产环境中写入的量,最安全的方法还是根据Innodb_dblwr_pages_written来进行统计

是否开启doublewrite功能(skip_innodb_doublewrite)

  • skip_innodb_doublewrite参数可以开启/禁止使用doublewrite功能,如果禁止之后可能会发生前面提到过的写失效问题
  • 如果用户有多个从服务器,需要提供较快的性能,禁止该参数也是一个办法。不过对于需要提供数据高可靠性的主服务器,任何时候用户都应该确保开启doublewrite功能

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_辅助索引_15

三、自适应哈希索引(AHI)

  • 哈希与B+树的复杂度对比:
  • 哈希(hash)是一种非常快的查找方法,在一般情况下这种查找的时间复杂度为O(1)
  • B+树的查找次数,取决于B+树的高度,在生产环境中,B+树的高度一般为3~4次,故需要3~4次的查询

AHI简介

  • InnoDB会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引(AHI)
  • AHI是通过缓冲池的B+树页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引
  • InnoDB会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引

AHI的使用要求

  • AHI有一个要求,即对这个页的连续访问模式必须是一样的。例如对于(a,b)这样的联合索引页,其访问模式可以是以下情况:
  • where a=XXX
  • where a=XXX and b=XXX
  • 访问模式一样指的是查询的条件一样,若交替进行上述两种查询,那么InnoDB存储引擎不会对该页构造AHI
  • 此外AHI还有如下的要求:
  • 以该模式访问了100次
  • 页通过该模式访问了N次,其中N=页中记录*1/16
  • 根据InnoDB存储引擎官方的文档显示,启用AHI后,读取和写入速度可以提高2倍,辅助索引的连接操作性能可以提高5倍。毫无疑问,AHI是非常好的优化模式,其设计设计思想是数据库自优化,即无需DBA对数据库进行人为调整
  • 通过下面的命令可以看到当前AHI的使用状况:
  • 下图显示了AHI的大小、使用情况、每秒使用AHI搜索的情况
  • 需要注意的是,哈希索引只能用来搜索等值的查询,如select * from table where index_col='xxx';。而对于其他查找类型,如范围查找,是不能使用哈希索引的,因此这里出现了non-hash searches/s的情况
  • 通过hash searches:non-hash searches可以大概了解使用哈希索引后的效率
show engine innodb status\G;
  • innodb_adaptive_hash_index参数:该参数可以用来控制AHI的开启/关闭,默认为开启状态
show variables like 'innodb_adaptive_hash_index'\G;

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_子节点_16

四、异步IO(AIO)

  • 为了提高磁盘操作性能,当前的数据库都采用异步IO(Asynchronous IO,AIO)的方式来处理磁盘操作。InnoDB也是如此

什么是异步IO

  • 先与同步IO做个比较:同步IO是指每进行一次IO操作,需要等待此次操作结束才能继续接下来的IO操作
  • 但是如果用户发出的是一条索引扫描的查询,那么这条SQL查询语句可能需要扫描多个索引页,也就是需要进行多次的IO操作。在每扫描一个页并等待其完成后再进行下一次的扫描,这是没有必要的。用户可以在发出一个IO请求后立即再发出另一个IO请求,当全部IO请求发送完毕后,等待所有IO操作的完成,这就是AIO

演示说明

  • AIO的另一个优势是可以进行IO Merge操作,也就是将多个IO合并为1个IO,这样可以提高IOPS的性能。例如用户需要访问页的(space,page_no)为:
  • 其中每个页的大小为16KB,那么同步IO需要进行3次IO操作。而AIO会判断这三个页是连续的(显然可以通过(space,page_no)得知),因此AIO底层会发送一个IO请求,从(8,6)开始,读取48KB的页

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_子节点_17

  • 例如,在Linux操作系统下通过iostat命令,可以观察rrqm/s和wrqm/s,例如:

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_缓冲池_18

MySQL中AIO的发展例程

  • 在InnoDB 1.1.x之前,AIO的实现通过InnoDB存储引擎中的代码来模拟实现
  • 从InnoDB 1.1.x开始(InnoDB Plugin不支持),提高了内核级别AIO的支持,称为Native AIO。因此在编译或运行该版本MySQL时,需要libaio的支持。若没有则会出现如下的提示:

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_InnoDB关键特性_19

  • 需要注意的是,Native AIO需要操作系统提供支持:
  • Windows系统和Linux系统都提供Native AIO
  • 而Mac OSX系统则未提供,因此在这些系统上,依旧只能使用原模拟的方式
  • 在选择MySQL数据库服务器的操作系统时,需要考虑这方面的因素
  • innodb_use_native_aio参数:该参数用来控制是否启用Native AIO,在Linux操作系统上,默认和为ON
  • MySQL官方的测试显示,启用Native AIO,恢复速度可以提高75%
show variables like 'innodb_use_native_aio'\G;

MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_InnoDB关键特性_20

  • 在InnoDB存储引擎中,read ahead方式的读取都是通过AIO完成,脏页的刷新,即磁盘的写入操作则全部由AIO完成

五、刷新邻接页

  • InnoDB还提供了Flush Neighbor Page(刷新邻接页)的特性
  • 工作原理:当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果是脏页,那么一起进行刷新
  • 这样做的好处显而易见,通过AIO可以将多个IO写入操作合并为一个IO操作,故该工作机制在传统机械磁盘下有着显著的优势
  • 但是需要考虑下面两个问题:
  • 是不是可能将不怎么脏的页进行了写入,而该页之后又会很快变为脏页
  • 固态硬盘有着较高的IOPS,是否还需要这个特性?
  • 为此,InnoDB存储引擎从1.2.x版本开始提供了参数innodb_flush_neighbors用来控制是否启动该特性。对于传统机械硬盘建议启动该特性,而对于固态硬盘有着超高的IOPS性能的磁盘,则建议将该参数设置为0