InnoDB引擎在设计中使用了很多技术思想。下面我们主要介绍一些InnoDB的关键特性,帮助你去更好了解 InnoDB。
关键特性
- 1.预读
- (1)预读的两种算法
- (2)在InnoDB中相关配置
- 2.插入缓冲
- 2.1 Insert Buffer
- 2.2 Change Buffer
- 2.3 Insert Buffer的内部实现
- Merge Insert Buffer
- 3.二次写(double write)
- 4.自适应哈希索引
- 5.异步IO
- 6.刷新邻接页
1.预读
预读,我们从字面就可以知道是指:预先读取一些信息。为什么要预读?判断哪些数据可以提前读取到内存中,从而减少IO次数。我们知道硬盘、内存的IO速度差距极大,在内存充裕的条件下,可以极大减少IO时间。但是如果内存不足,预读的数据还没有被真正读取,就被淘汰,那就做了无用功。所以要正确认识预读
认识预读前,先了解 InnoDB数据的逻辑存储单元——表空间、段、区和页。
上图可以看到这样的层级关系:表空间 -> 段 -> 区(64个页)-> 页(默认16KB)
(1)预读的两种算法
预读请求是指 预取缓冲池中的多个页面的异步IO请求,以预测这些页面即将出现的需求。请求在一个区段中引入所有页面。InnoDB使用两种预读算法来提高IO性能:
- 线性预读(Line read-ahead):一种基于 按顺序访问的缓冲池中的页面来预测可能很快需要哪些页面的技术。通过配置参数innodb_read_ahead_threshold,触发异步读请求所需的顺序页访问次数,来控制InnoDB执行预读操作的时间。在此之前,InnoDB只会在读取当前extent的最后一页时,计算是否对整个下一个extent发出异步预取请求。
例如,如果将该值设置为48,则只有在当前区段中有48个页面被连续访问时,InnoDB才会触发线性预读请求。 - Random read-ahead(随机预读)是一种技术,可以根据缓冲池中已经存在的页面预测何时可能需要页面,而不管这些页面的读取顺序如何。如果在缓冲池中发现同一个区段的13个连续页面,InnoDB会异步发出一个请求来预取该区段的剩余页面。通过配置变量innodb_random_read_ahead来控制随机读的。
(2)在InnoDB中相关配置
对于预读参数有两个参数:
mysql> show variables like '%read_ahead%';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| innodb_random_read_ahead | OFF |
| innodb_read_ahead_threshold | 56 |
+-----------------------------+-------+
innodb_random_read_ahead:开启随机预读技术,优化InnoDB I/O。默认关闭
innodb_read_ahead_threshold:控制InnoDB用于将页面预取到缓冲池中的线性预读的灵敏度
2.插入缓冲
2.1 Insert Buffer
先谈谈我们为什么要引入 Insert Buffer ?
在InnoDB引擎中,无论我们是否自定义主键(如果我们不定义唯一索引,引擎会帮我们建立隐藏的主键列)。因为InnoDB引擎必须要使用主键来构建 聚集索引。一般的主键都是自增的,这样在插入时是顺序插入的。并且聚集索引页也是顺序的,不容易造成频繁的页分裂。
但是我们一般不仅仅使用聚集索引,通常我们会设置非聚集索引不唯一的索引。那么在进行插入时,数据还是按主键来进行顺序存放的。但是对于非聚集索引页叶子节点的插入就不是顺序的了。这时就需要离散地访问非聚集索引页。由于随机写,就会导致插入操作性能下降。这是B+树的特性决定了非聚集索引插入的离散性。
提醒:也不是所有非聚集索引都是写都是完全离散或者说无序的。在某些情况下,非聚集索引的插入依然有序或者比较有序。例如:记录创建时间字段作为 索引列时,插入顺序是根据插入时间。此时插入就是 顺序写了
那么我们如何解决非聚集索引插入时的随机写。InnoDB引擎就开创性的设计了Insert Buffer,对于非聚集索引的插入或者更新操作,不是每次直接插入索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若在,则直接插入。如果不在,则先放入一个 Insert Buffer 对象中,然后告诉 数据库已经将数据插入到叶子结点中(实际并没有)。选择放在 Insert Buffer中暂存,然后再以一定频率和情况进行 Insert Buffer 和 非聚集索引页子节点的 合并操作。这是通常可以使得多个插入 合并称一个操作。因为可能存在不少插入操作,其实操作的是同一个索引页中。这就大大提高了对于非聚集索引的插入性能。
但是并不是所有索引都可以使用 Insert Buffer,需要满足以下条件:
- 索引是非聚集索引
- 索引列不是唯一的
当满足以上两个条件时,InnoDB存储引擎会使用 Insert Buffer。第一个条件显而易见,聚集索引不需要使用Insert Buffer。那为什么 索引列值不能限定它是唯一的呢?因为在插入缓冲时,如果需要去查询索引页来判断插入的记录为唯一性。那么就会造成随机读索引页,也就失去设计 Insert Buffer 的必要了。
需要注意到:
- 当进行大量插入操作(涉及到非聚集索引)后,如果此时数据库发生宕机,这时势必有大量的 Insert Buffer 并没有合并到实际的非聚集索引中去。因此此时进行恢复,可能会需要很长时间。
- 在写密集的情况下,插入缓冲会占用过多缓存池内存(innodb_buffer_pool),默认最大可以占用到1/2的缓冲池内存。这对于其他的操作可能会产生一些影响。
2.2 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只适用于非唯一的非聚集索引。
2.3 Insert Buffer的内部实现
Insert Buffer 的数据结构是一颗B+树。在MySQL4.1之前,每张表都有一颗 Insert Buffer B+ 树。而现在的版本中,全局只有一颗 Insert Buffer B+树,负责对所有的表的非聚集索引进行 Insert Buffer。这颗树存放在共享表空间中,默认也就是 ibdata1中。因此,试图通过独立表空间ibd文件恢复表中数据时,往往会导致 CHECK TABLE失败。这是因为表的非聚集索引中的数据可能还在 Insert Buffer,也就是共享表空间中,所以通过 ibd 文件进行恢复后,还需要 REPAIR TABLE 操作来重建表上所有的非聚集索引。
Insert Buffer 是一颗B+树,因此其也是由叶子结点和非叶子结点组成。非叶子结点存放的是查询的 search key。
search key 占用9个字节,其中 space 表示待插入记录所在表的表空间id,在 InnoDB 存储引擎中,每个表有一个唯一的 space id,可以通过 space id 查询得知时哪张表。space 占用4字节。marker占用1字节,它是用来兼容老版本的 Insert Buffer。offset 表示页所在的偏移量,占用4字节。
当一个非聚集索引要插入到页(space,offset)时,如果这个页不在缓冲池中,那么 InnoDB 存储引擎首先根据上述规则构造一个 search key,接下来查询 Insert Buffer 这颗B+树,然后再将这条记录插入到 Insert Buffer B+树的叶子节点中。
对于插入到叶子结点的记录,并不是直接把待插入的记录插入,而是需要根据一定规则构造。结构如上图。
space、marker、offset字段和之前的非叶子节点中的含义一样,一共占用9个字节。第四个字段 metadata 占用4字节,由三部分组成:
- IBUF_REC_OFFSET_COUNT:
占用2个字节,用来排序每个记录进入 Insert Buffer 的顺序。因为 InnoDB开始支持 ChangeBuffer,所以这个值同样被引入到 Insert Buffer中。通过这个字段来标记操作顺序,保证通过顺序回放能得到记录的正确值。
从 - IBUF_REC_OFFSET_TYPE:操作类型
- IBUF_REC_OFFSET_FLAGS:标志位,当前只有 IBUF_REC_COMPACT。
从 Insert Buffer 叶子节点的第5列开始,就是实际插入记录的各个字段了。因此较原插入记录,Insert Buffer B+树的叶子节点记录额外需要 9+4 字节的开销。
因为启用 Insert Buffer后,原先插入非聚集索引页中的记录可能会被插入到 Insert Buffer B+树中。所以我们需要保证每次 Merge Insert Buffer页必须成功。由此需要引入 Insert Buffer Bitmap,它是一个特殊的页,用来标记每个非聚集索引页的可用空间。这个页的类型是 Insert Buffer Bitmap。
Insert Buffer Bitmap 中 每个非聚集索引页占用4位(bit):
- IBUF_BITMAP_FREE:占用2bit,枚举值:0-表示无可用剩余空间;1-表示剩余空间大于1/32页;2-表示剩余空间大于1/16页;3-表示剩余空间大于1/8页;
- IBUF_BITMAP_BUFFERED:占用1bit,为1时表示该非聚集索引页有记录被缓存在 Insert Buffer B+树中
- IBUF_BITMAP_IBUF:占用1bit,为1时表示该页为 Insert Buffer B+树的索引页
因此 每个 Insert Buffer Bitmap 页可以用来追踪16384个非聚集索引页,也就是256个区。每个 Insert Buffer Bitmap 页都在16384个页的第二个页中。
Merge Insert Buffer
前面说到的 Insert Buffer、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页,则会强制进行一个合并操作。将 Insert Buffer B+树中该页的记录及待插入的记录一起插入到 非聚集索引页中。 -
Master Thread
在 Master Thread 线程中每秒或每10秒回进行一次 Merge Insert Buffer 的操作,不同之处在于每次进行merge操作的页的数量不同。它是随机地选择 Insert Buffer B+树的一个页,读取该页中的space及之后所需要数量的页。
3.二次写(double write)
Insert Buffer 带给了 InnoDB存储引擎的是性能的提升,二次写带给 InnoDB 的是数据页的可靠性。
InnoDB中页的默认是16KB,而Linux中页的大小是4KB。所以当数据库宕机时,如果InnoDB正在写入某个页到表中,那么这个页可能只写了一部分。这种情况叫做 部分写失效。这种失效,不能通过重做日志解决。因为重做日志中记录的是对原有页的物理操作,它是基于原来的页的,如果页数据已经损坏,重做也是无意义的。
所以我们需要一个副本,当发生写入失效的时候,通过页的副本来还原该页,再进行重做。这就是 double write。
double write 有两部分组成,一部分是处于内存中的 double write buffer,大小为2MB,另外一个部分是处于物理磁盘上共享表空间中连续的2个区(128个页),大小也为2MB。对缓冲池中的脏页进行刷新,并不是直接写磁盘,而是会通过memcpy函数将脏页数据先复制到内存中的 double write buffer中,之后通过 double write buffer 分两次,每次1MB顺序地写入到 共享空间的物理磁盘上,然后马上调用 fsync函数,同步到磁盘。
因为写入到 磁盘中的doulewrite页是连续的,所以这个过程是顺序写,速度很快。在完成 double write 页的写入后,再从 内存中 double write buffer中将脏页写入到 各脏页所在的表空间文件中。因为脏页之间不是连续的,是各个表空间的脏页,所以此时写是离散写,速度很慢。
通过参数 skip_innodb_doublewrite 可以禁止使用 二次写功能,提升较快速度,但是可能会发生写失效问题。
有些文件系统本身提供了部分写失效的防范机制,如ZFS文件系统。那么此时则没有必要开启 二次写 功能。
4.自适应哈希索引
哈希(hash)是一种非常快的查找方法,在一般情况下这种查找的时间复杂度O(1),即一般仅需要一次查找就能定位数据。而B+树的查找次数,取决了B+树的高度,一般B+树的高度为3~4层,即3-4次IO。
InnoDB 存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index,AHI)。AHI是通过缓冲池的B+树页构建而来,因此建立速度很快,而且不需要对整张表构建哈希索引。InnoDB存储引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引
AHI 是有要求的,只有页的连续访问模式是一样的,才会对该页构建AHI。例如:对联合索引页(a,b),其访问模式可以是:
- where a = xxx
- where a = xxx and b = yyy
访问模式一样指的是查询的条件一样,若交替的执行上述另种查询,那么InnoDB不会对该页进行构建。除此之外还有如下要求: - 以该模式访问了至少100次
- 页通过该模式访问了至少N次,其中N=页中记录* 1/16
官方文档中说过,启动AHI后,读取和写入速度可以提高2倍,辅助索引的连接操作性能可以提高5倍。AHI是很好的数据库自优化的模式。我们可以自己通过命令 SHOW ENGINE INNODB STATUS
可以看到当前AHI的使用状况。
注意:哈希索引只适用于等值查询
5.异步IO
为了提高磁盘操作性能,目前数据库系统都采用异步IO(AIO)的方式来处理磁盘操作。InnoDB存储引擎也是如此。与AIO对应的是Sync IO,即每进行一次IO操作。需要等待此次操作结束后,才能继续接下来的操作。但是如果用户发出的是一条索引扫描的查询,那么这条SQL查询语句可能需要扫描多个索引页,也就是进行多次IO操作。在每扫完一个页并等待其完成后再进行下一次的扫描,这是没有必要的。用户可以在发出一个IO请求后 立即再发出其他IO请求,当全部IO请求发送完毕后,等待所有IO操作的完成,这就是AIO。
AIO的另外一个优势就是可以进行 IO Merge操作,也就是将多个IO合并成1个IO,这样可以提高 IOPS 的性能。例如用户需要访问页的(space,page_no)为:(8,6)、(8,7)、(8,8)。每个页的大小为16KB,那么同步IO需要进行3次IO操作。而AIO会判断到这三个页是连续的。因此AIO底层只会发送一个IO请求,从(8,6)开始读取48KB的页。
InnoDB1.1.x之前,AIO的实现是通过InnoDB存储引擎中的代码来模拟实现的。而从InnoDB1.1.x开始,提供了内核级别的AIO支持,称为Native AIO。
通过参数 innodb_use_native_aio
控制是否启用 Native AIO,在Linux操作系统下,默认为ON。官方测试,启用 Native IO,速度可以提升75%。
InnoDB中,read ahead 方式的读取是由AIO完成,脏页的写入等磁盘写入操作也是由AIO完成。
6.刷新邻接页
InnoDB 存储引擎提供 刷新邻接页(Flush Neighbor Page)的特性。其工作原理为:当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。这样做的好处就是,通过AIO可以将多个IO写入操作合并成一个IO操作,该工作机制在传统机械硬盘下有显著的优势。但是机制会引出下面2个问题:
- 是否会把不怎么脏的页 进行了写入,而该页之后又很快变成脏页?
- 固态硬盘有很高的 IOPS,那么是否还需要这个机制?
使用参数 innodb_flush_neighbors
控制是否开启
对于机械硬盘建议使用该特性