浅谈InnoDB存储引擎 — 体系架构和关键特性

270 阅读13分钟

1. 概述

InnoDB存储引擎从MySQL 5.5版本开始是默认的表存储引擎,该存储引擎是第一个完整支持ACID事务的MySQL存储引擎,其特点是行锁设计、支持MVCC、支持外键、提供一致性非锁定读,同时被设计用来最有效地利用以及使用内存和CPU

2. 体系架构

InnoDB存储引擎有多个内存块,可以认为这些内存块组成了一个大的内存池,负责如下工作:

  • 维护所有线程/进程需要访问的多个内部数据结构
  • 缓存磁盘上的数据,方便快速读取,同时在对磁盘文件的数据修改之前在这里缓存
  • 重做日志(redo log)缓冲

InnoDB 存储引擎体系架构

2.1 后台线程

后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据,此外将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下 InnoDB 能恢复到正常运行状态

InnoDB 存储引擎是多线程模型,其后台有多个不同的后台线程,负责处理不同的任务,大致如下:

  • Master Thread:核心后台线程,主要负责将缓冲池中的数据异步刷新到磁盘中,保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO页的回收等。

  • IO Thread:在InnoDB存储引擎中大量使用了AIO来处理写IO请求,这样可以极大提高数据库的性能,而IO Thread的工作主要是负责这些IO请求的回调处理。分别有write、read、insert buffer和log四种IO Thread,共有10个IO Thread,分配如下: IO Thread分配.png

  • Purge Thread:事务被提交后,其所使用的重做日志(undolog)可能不再需要,因此需要 PurgeThread 来回收已经使用并分配的 undo 页。在版本1.1之前purge操作仅在Master Thread中完成,从版本1.1开始在单独的线程中,减轻了Master Thread的工作,从而提高CPU的使用率以及提升存储引擎的性能。

  • Page Cleaner Thread:在InnoDB 1.2.x版本中引入,其作用是将之前版本中脏页的刷新操作都放入到单独的线程中来完成,同样是为了减轻原Master Thread的工作以及对于用户查询线程的阻塞,进一步提高 InnoDB 存储引擎的性能。

2.2 内存

2.2.1 缓冲池

InnoDB 存储引擎是基于磁盘存储的,其中的记录按页的方式进行管理,是基于磁盘的数据库系统。由于CPU速度与磁盘速度之间差距过大,基于磁盘的数据库系统通常使用 缓冲池技术 来提高数据库的整体性能。

缓冲池简单来说就是内存中的一块区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。对于读操作,首先将从磁盘读到的页存放在缓冲池中,下次再读相同的页时,判断该页是否在缓冲池中,若命中则直接读取,否则读取磁盘上的页;而对于写操作,首先修改缓冲池中的页,然后再以一定的频率刷新到磁盘上,这里不是每次页发生更新都触发,而是使用 Checkpoint 机制 刷新回磁盘,提高数据库的整体性能。缓冲池内存大小可以通过参数 innodb_buffer_pool_size 来设置。

缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲、自适应哈希索引、InnoDB存储的锁信息、数据字典信息等,其中索引页和数据页占缓冲池很大的一部分,如下图:

从InnoDB 1.0.x版本开始,允许有多个缓冲池实例,每个页根据哈希值平均分配到不同缓冲池实例中,这么做的好处是减少数据库内部的资源竞争,增加数据库的并发处理能力,可以通过参数 innodb_buffer_pool_instances 配置,该值默认为1。

2.2.2 如何管理缓冲池

InnoDB 存储引擎是如何管理缓冲池这块内存区域?通常来说,数据库中的缓冲池是通过 LRU 算法来进行管理的,即最频繁使用的页在 LRU 列表的前端,而最少使用的页在 LRU 列表的尾端。当缓冲池不能存放新读取到的页时,将首先释放 LRU 列表中尾端的页。

LRU List

在InnoDB存储引擎中同样使用 LRU 算法对缓冲池进行管理,稍有不同的是InnoDB存储引擎对传统的 LRU 算法做了一些优化。在InnoDB存储引擎中,LRU 列表中还加入了 midpoint 位置。新读取到的页,虽然是最新访问的页,但并不是直接放入到 LRU 列表的首部,而是放入到 LRU 列表的 midpoint 位置。这个算法在InnoDB存储引擎下称为 midpoint insertion strategy(中点插入策略)。默认配置下,该位置在 LRU 列表长度的 5/8 处。

mysql> show variables like 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37    |
+-----------------------+-------+
1 row in set, 5 warnings (0.00 sec)

参数 innodb_old_blocks_pct 默认值为 37,表示新读取的页插入到 LRU 列表尾端的37%的位置。在 InnoDB 存储引擎中,把 midpoint 之后的列表称为 old 列表,之前的列表称为 new 列表,也就是 new 列表中的页都是最为活跃的热点数据。

这里有个问题:为什么不直接采用 LRU 算法,直接将读取的页放入到 LRU 列表的首部呢?

因为若直接将读取的页放入到 LRU 的首部,那么某些SQL操作可能会使缓冲池中的页被刷新出,从而影响缓冲池的效率。常见的这类操作为索引或数据的扫描操作,这类操作需要访问表中的许多页,甚至是全部的页,而这些页通常来说仅在这次查询操作中需要,并不是活跃的热点数据。如果页被放入 LRU 列表的首部,那么非常可能将所需要的热点数据页从 LRU 列表中国移除,而在下一次需要读取该页时,InnoDB存储引擎需要再次访问磁盘。

为了解决某些SQL操作导致缓冲池中的页被刷新出去,InnoDB存储引擎还引入了一个参数 innodb_old_blocks_time ,用来表示页读取到 mid 位置后需要等待多久才会被加入到 LRU 列表的热端。

Free List

LRU 列表用来管理已经读取的页,但当数据库刚启动时, LRU 列表是空的,即没有任何的页。这时页都存放在 Free 列表中。当需要从缓冲池中分页时,首先从 Free 列表中查找是否有可用的空闲页,若有则将该页从 Free 列表中删除,放入到 LRU 列表中。否则,根据 LRU 算法,淘汰 LRU 列表末尾的页,将该内存空间分配给新的页。

当页从LRU列表的 old 部分加入到 new 部分时,称此时发生的操作为 page made young,而因为 innodb_old_blocks_time 的设置而导致页没有从 old 部分移动到 new 部分的操作称为 page not made young。

Flush List

在 LRU 列表中的页被修改后,称该页为脏页,即缓冲池中的页和磁盘上的页的数据产生了不一致。这时数据库会通过 Checkpoint 机制将脏页刷新回磁盘,而 Flush 列表中的页即为脏页列表。需要注意的是,脏页既存在于 LRU 列表中,也存在于 Flush 列表中。LRU 列表用来管理缓冲池中的页的可用性,Flush 列表用来管理将页刷新回磁盘,二者互不影响。

2.2.3 重做日志缓冲

InnoDB存储引擎会将重做日志信息先放入到缓冲区中,然后按一定频率将其刷新到重做日志文件。重做日志缓冲一般不需要设置的很大,因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件,因此只需要保证每秒产生的事务量在这个缓冲大小之内即可。该值可由配置参数innodb_log_buffer_size 控制,默认为 8MB。

mysql> show variables like 'innodb_log_buffer_size';
+------------------------+---------+
| Variable_name          | Value   |
+------------------------+---------+
| innodb_log_buffer_size | 1048576 |
+------------------------+---------+
1 row in set, 5 warnings (0.37 sec)

通常情况下,8MB 的重做日志缓冲池足以满足绝大部分的应用,因为重做日志在下列三种情况会将重做日志缓冲中得到内容刷新到外部磁盘的重做日志文件中。

  • Master Thread 每一秒将重做日志缓冲刷新到重做日志文件;
  • 每个事务提交时会将重做日志缓冲刷新到重做日志文件;
  • 当重做日志缓冲池剩余空间小于 1/2 时,重做日志缓冲刷新到重做日志文件。

2.2.4 额外的内存池

在InnoDB存储引擎中,对内存的管理是通过一种称为内存堆的方式进行的。在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不足时,会从缓冲池中进行申请。例如,分配了缓冲池,但是每个缓冲池中的帧缓冲还有对应的缓冲控制对象,这些对象记录了一些诸如 LRU 、锁、等待等信息,而这个对象的内存需要从额外内存池中申请。因此,在申请了很大的 InnoDB 缓冲池时,也应考虑相应地增加这个值。

2.3 Checkpoint技术

前面讲到为了协调 CPU 速度与磁盘速度之间的差距,InnoDB 存储引擎使用缓冲池技术来提高数据库性能。而页的操作首先都是在缓冲池中完成的,因此这时候就会有如何保证缓冲池中的数据和磁盘中的数据一致性的问题。

在 InnoDB 存储引擎中并不是每次页发生变化就将页的版本刷新到磁盘,这样的做法对性能的开销过大。当前事务数据库系统普遍采用 Write Ahead Log 策略,即当事务提交时,先写重做日志,再修改页。当由于发生宕机而导致数据丢失时,通过重做日志来完成数据的恢复。

在这里做个思考,当缓冲池足够大到能缓存所有的数据库数据,且重做日志也可以无限地增大时,是不是可以不需要对缓冲池中更新的页刷新回磁盘中呢?答案是可以的,即使数据库服务器突然发生宕机,我们也可以通过重做日志来进行数据恢复。但实际上这种做法是不可取的,分析上面的假设有如下两个原因:

  • 现阶段 3TB 的数据库并不少见,但 3TB 的内存是十分少见的,由于此条件受限,缓冲池缓存整个数据库的数据的做法在数据量较大的情况下是难以实现的;
  • 重做日志的无限增大就算忽略存储空间的限制,假如数据库服务器发生宕机时需要恢复数据,这时候将需要非常长的时间来恢复数据,这样的时间成本同样也是不可取的。

因此 Checkpoint(检查点) 技术的目的是解决以下问题:

  • 缩短数据库的恢复时间:当数据库发生宕机时,数据库不需要重做所有日志,因为 Checkpoint 之前的页都已经刷新回磁盘,所以数据库只需对 Checkpoint 后的重做日志进行恢复,这样就缩短了数据恢复时间;
  • 缓冲池不够用时,将脏页刷新到磁盘:当缓冲池不够用时,根据 LRU 算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行 Checkpoint ,将脏页刷新回磁盘;
  • 重做日志不可用时,刷新脏页:重做日志的设计并不是无限地增大而是循环利用,重做日志中被循环利用的部分是指这些重做日志已经不再需要,数据库发生宕机时这部分数据不需要回复。若此时重做日志还需要使用,那么必须强制产生 Checkpoint ,将缓冲池中的页至少刷新到当前重做日志的位置。

也就是说,Checkpoint 所做的事其实就是将缓冲池中的脏页刷回磁盘。

在 InnoDB 存储引擎内部,有两种 Checkpoint ,分别为:

  • Sharp Checkpoint:发生在数据库关闭时将所有的脏页都刷新回磁盘,这是默认的工作方式,参数 innodb_fast_shutdown = 1 。若数据库运行时也使用 Sharp Checkpoint ,那么数据库的可用性会受到很大的影响;
  • Fuzzy Checkpoint:只刷新一部分脏页,而不是刷新所有的脏页回磁盘。在 InnoDB 存储引擎中可能发生如下几种情况的 Fuzzy Checkpoint:
    • Master Thread Checkpoint:主线程中发生的 Checkpoint ,差不多以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘。这个过程是异步的,不会阻塞用户查询线程;
    • FLUSH_LRU_LIST Checkpoint:发生在 LRU 列表移除尾端页的 Checkpoint,若移除的页中有脏页,此时需要进行 Checkpoint 。从 InnoDB1.2.x 版本开始这个检查放在了一个单独的 Page Cleaner 线程中进行,可以通过参数 innodb_lru_scan_depth 控制 LRU 列表中可用页的数量,该值默认为 1024 ;
    • Async/Sync Flush Checkpoint:发生在重做日志不可用时需要强制将一些页刷新回磁盘,保证重做日志循环使用的可用性。发生的时机通过一些参数计算比较得出,从 InnoDB1.2.x 版本开始这部分刷新操作同样在了一个单独的 Page Cleaner 线程中进行,不会阻塞用户查询线程;
    • Dirty Page too much Checkpoint:发生在脏页数量太多,导致 InnoDB 存储引擎强制进行 Checkpoint ,其目的总的来说还是为了保证缓冲池中有足够的可用的页。可由参数 innodb_max_dirty_pages_pct 控制,该值默认为 75 ,表示当缓冲池中脏页的数量占据 75% 时,强制进行 Checkpoint ,刷新一部分的脏页到磁盘。

3. InnoDB关键特性

InnoDB 存储引擎的关键特性包括:

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

参考: 《MySQL技术内幕 InnoDB存储引擎》第二版