MySQL是目前互联网的主流数据库,而InnDB更是核心应用表的首选存储引擎。本篇重在介绍InnnoDB存储引擎的体系架构及特性。
如下图所示,InnoDB有多个内存块,主要负责如下工作:
1.1 后台线程
InnoDB的后台线程有7个:4个IO thread(insert buff thread、log thread、read thread、write thread)、1个master thread,1个锁监控线程,1个错误监控线程。
1.2 内存
InnoDB内存由以下三部分组成:
缓冲池的工作方式:
• 读:将数据库文件按页(每页16K)读取到缓冲池,然后按照LRU算法来保留在缓冲池中的缓存数据。
• 写:每次写数据库文件,都是先修改在缓冲池中的页(发生修改后,该页即为脏页),然后再按照一定的频率将缓冲池的脏页刷新到文件。
可以通过 SHOW ENGINE INNODB STATUS命令来查看缓冲池的具体使用情况,在结果里的 BUFFER POOL AND MEMORY里看到缓冲池使用情况:buffer pool size表明一共有多少个缓冲帧(buffer frame),每个缓冲帧16K,Database pages表示已经使用的缓冲帧,Modified db pages表示脏页的数量。
缓冲池中缓存的数据页类型包括:索引页、数据页、undo页,插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、InnoDB存储的锁信息、数据字典信息等。
重做日志缓冲池将重做日志信息先放入这个缓冲区,然后按照一定频率将其刷新到重做日志文件。默认每秒刷新一次,因此该缓冲区的内存值一般不用设置太大。
额外的内存池,主要是为了在对一些数据结构本身分配内存时,需要从该区域申请。
2.1 master thread的工作原理
我们现在已经知道InnoDB存储引擎的主要工作都是在一个单独的后台线程master thread中完成的。通过阅读源码,可知master thread线程优先级最高,其中包括了四个循环:主循环(loop)、后台循环(background loop)、刷新循环(flush thread)、暂停循环(suspend thread)。
loop称为主循环,因为大部分的操作都在该循环中完成的,其中又有两大部分的操作:每秒钟的操作和每十秒钟的操作。
每秒一次的操作包括:
• 重做日志缓冲刷新到磁盘redo log文件
,即使该事务还没提交。这也解释了为什么再大的事务commit的时间也是很快的。
• InnoDB引擎会判断当前一秒的IO次数,如果小于5次,则认为当前IO压力很小,就会执行合并插入缓冲的操作
。
• InnoDB会判断当前缓冲池中脏页的比例是否超过了配置文件的参数(默认90,代表90%),如果超过这个阈值,则会将至多100个脏页写入磁盘
。
• 如果当前用户没有活动,则切换到background loop
。
每十秒一次的操作包括:
• InnoDB会判断过去10秒之内的磁盘IO操作是否小于200次,如果是则认为当前有足够的磁盘IO能力,因此将100个脏页刷新到磁盘
。
• 合并至多5个插入缓冲
。
• 将日志缓冲刷新到磁盘
。
• 执行full purge操作,删除无用的undo页
。事实上在对MySQL表执行update、delete操作时,原先的行被标记为删除,但是由于一致性读的关系,需要保留这些行版本的信息。在full purge的过程中,InnoDB会判断当前事务系统已被删除的行是否可以删除(比如有时候可能还有查询操作需要读取之前的版本的undo信息),如果可以,InnoDB会立即将其删除。
• 刷新100个(脏页的比例大于70%)或10个(脏页的比例小于70%)脏页到磁盘
。
• 产生一个检查点(checkpoint)
。将最老日志序列号(oldest LSN)的页写入磁盘。
2.2 master thread的潜在问题
• InnoDB最多只会刷新100个脏页到磁盘,合并20个插入缓冲,实际上每秒可能会产生大于100个脏页或产生大于20个插入缓冲,当固态硬盘出现时,这个规则很大程度上限制了磁盘IO的性能。并且在发生服务器宕机时由于很多数据还没刷回磁盘,可能恢复需要较长时间。
• InnoDB默认脏页占缓冲池超过90%的容量时就触发脏页刷新到磁盘的操作,但是这个比例也太大了,很容易造成脏页积压,给数据库服务器带来压力。
3.1 插入缓冲:insert buffer
插入缓冲
是InnoDB搜索引擎最令人激动的关键特性。首先我们知道,MySQL有聚集索引和非聚集索引,插入聚集索引一般是顺序的,不需要磁盘的随机读取,此时插入速度很快就能执行完成。但是对于非聚集且不是唯一的索引,由于B+树的特性,叶子节点的插入就不再是顺序得了,此时就需要离散的访问非聚集索引页,插入性能在这里就下降了。所以引入了插入缓冲。
插入缓冲:对非聚集索引的插入或更新操作,不是每一次直接插入索引页中,而是先判断插入的非聚集索引页是否在缓冲池中。如果在,则直接插入,如果不在,则先放入一个插入缓冲区,然后再以一定的频率执行插入缓冲和非聚集索引页子节点的合并操作,这时通常能将多个插入合并到一个操作中,这就大大提高了对非聚集索引的插入和修改的性能。
但是插入缓冲也是有限制条件的:
• 索引必须是辅助索引。
• 索引不能是唯一的。
3.2 两次写:double write
如果说插入缓冲带给InnoDB存储引擎的是性能,两次写带来的则是数据的可靠性。当数据库宕机时,如果此时正在写一个页面,而这个页面只写了一部分(比如16K的页面只写了6K),此时就会发生部分写失效(partial page write)进而导致数据丢失。
也许你会想,不慌,我们有redo log,保证数据可靠性。想法很好,但是必须清楚的是,redo log记录的是对页的物理操作,如果这个页本身已损坏,redo log日志中记录的数据必然也是损坏的,此时再去重做已然没有意义。
所以聪明的开发者设计了一种页的副本,当写入失效发生时,先通过页的副本来还原该页,再通过
redo log
进行重做,这就是double write
。
如上图所示,InnoDB会将脏页拷贝的double buffer,之后double buffer再分两次,每次写入1MB到共享表空间的物理磁盘上(此时是顺序写),然后再将double buffer中的页写入到各个表空间的数据文件中(此时是离散写),这就是两次写的过程。
那么在磁盘写入的过程崩溃了怎么办呢?可以看到,共享表空间有该页的一个副本,将其拷贝到表空间文件,再通过redo log即可恢复数据。
3.3 自适应哈希索引:adaptive hash index
InnoDB存储引擎会监控表上索引的查找,如果观察到建立哈希索引可以带来速度的提升则建立哈希索引,所以称之为自适应的。自适应哈希索引通过缓冲池的B+树构造,速度很快。而且不需要将整张表都建哈希索引,只是某些页建立。