InnoDB存储引擎浅析

InnoDB存储引擎

image-20221010141141713

后台线程

  1. Master Thread

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

    我的理解是身兼数职。

  2. IO Thread

    在InnoDB存储引擎中大量使用了AIO(Async IO)来处理写IO请求,这样可以极大提高数据库的性能。而IO Thread的工作主要是负责这些IO请求的回调(call back)处理。

  3. Purge Thread

    事务被提交后,其所使用的undolog可能不再需要,因此需要PurgeThread来回收已经使用并分配的undo页。减轻Master Thread的压力

  4. Page Cleaner Thread

    其作用是将之前版本中脏页的刷新操作都放入到单独的线程中来完成。减轻Master Thread的压力

内存

内存结构图

image-20221010144248273

缓冲池

缓冲池 innodb_buffer_pool

缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、InnoDB存储的锁信息(lock info)、数据字典信息(data dictionary)等。不能简单地认为,缓冲池只是缓存索引页和数据页,它们只是占缓冲池很大的一部分而已。

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

1
2
3
4
5
-- 缓冲池的总大小 
SHOW VARIABLES LIKE'innodb_buffer_pool_size';
-- 缓冲池的总大小 单位是byte -- 134217728/1024/1024=128M
-- 缓冲池实例个数
SHOW VARIABLES LIKE'innodb_buffer_pool_instances';

image-20221010163151463

缓冲池状态信息

1
2
-- 查看缓冲池的状态
SELECT * FROM information_schema.INNODB_BUFFER_POOL_STATS;

image-20221010165055392

  • POOL_SIZE:表示当前缓冲池的页数为8191,8191*16kb/1024≈128M
  • FREE_BUFFERS:Free List空闲列表剩余页数
  • DATABASE_PAGES:LRU所管理的页数
  • OLD_DATABASE_PAGES:LRU列表OLD区数据页数
  • PAGES_MADE_YOUNG:当页从LRU列表的old部分加入到new部分时,称此时发生的操作为page made young
  • PAGES_NOT_MADE_YOUNG:因为innodb_old_blocks_time的变更而导致页没有从old部分移动到new部分的操作称为page not made young。
  • HIT_RATE:缓冲池命中率,通常该值不应该小于95%

FREE_BUFFERS+DATABASE_PAGES<=POOL_SIZE,因为缓冲池中的页还可能会被分配给自适应哈希索引、Lock信息、Insert Buffer等页,而这部分页不需要LRU算法进行维护,因此不存在于LRU列表中。

LRU List、Free List和Flush List

Innodb读取Page页

在每个Buffer Pool Instance中都会包含一个独立的Page_hash,其作用主要是为了避免对LRU List的全链表扫描,通过使用space_id和page_no就能快速找到已经被读入Buffer Pool的Page。

当InnoDB读取Page时,首先会从当前Buffer Pool Instance的page_hash查找,并分为三种情况来处理:

  1. 如果在page_hash找到,即Page在LRU List中,则会判断Page是在Old区还是Young区,如果是在Old区,在读取完Page后会把它添加到Young区的链表头部,发生的操作为page made young
  2. 如果在page_hash找到,并且Page在Young区,需要判断Page所在Young区的位置,只有Page处于Young区总长度大约1/4的位置之后,才会将其添加到Young区的链表头部
  3. 如果未能在page_hash找到,则需要去数据文件中读取Page,并将其添加到Old区的头部

当页从LRU列表的old部分加入到new部分时,称此时发生的操作为page made young

因为innodb_old_blocks_time的变更而导致页没有从old部分移动到new部分的操作称为page not made young。

LRU List

数据库中的缓冲池是通过LRU(Latest Recent Used,最近最少使用)算法来进行管理的。即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取到的页时,将首先释放LRU列表中尾端的页。

当某些操作需要访问的数据比较分散同时涉及的页数很多的时候,会导致大量的页被放入LRU缓存,如果直接使用传统的LRU算法,有可能导致热点数据被刷出,而这些页通常来说又仅在这次查询操作中需要,并不是活跃的热点数据,为了解决这个问题Innodb引入了midpoint概念

在InnoDB的存储引擎中,LRU列表中还加入了midpoint位置。新读取到的页,虽然是最新访问的页,但并不是直接放入到LRU列表的首部,而是放入到LRU列表的midpoint位置。这个算法在InnoDB存储引擎下称为midpoint insertion strategy。在默认配置下,该位置在LRU列表长度的5/8处。midpoint位置可由参数innodb_old_blocks_pct控制;

可以通过调整下列参数,尽可能地保护热点数据

1
2
3
4
5
6
7
8
9
10
-- InnodbLRU算法midpoint的位置
SHOW VARIABLES LIKE'innodb_old_blocks_pct';
-- midpoint位置的数据多久放入LRU热点数据位置 单位:毫秒
show variables like 'innodb_old_blocks_time';

-- 查询LRU页的使用情况
select * FROM information_schema.INNODB_BUFFER_PAGE_LRU;

-- 查询LRU页数
select COUNT(*) FROM information_schema.INNODB_BUFFER_PAGE_LRU;

image-20221011001228695

Free List

Free List中存放的都是未曾使用的空闲Page,InnoDB需要Page时从Free List中获取,如果Free List为空,即没有任何空闲Page,则会从LRU List和Flush List中通过淘汰旧Page和Flush脏Page来回收Page。在InnoDB初始化时,会将Buffer chunks中的所有Page加入到Free List中。

image-20221011001325067

Flush List

所有被修改过且还没来得及被flush到磁盘上的Page(脏页),都会被保存在这个链表中。所有保存在Flush List上的数据都会在LRU List中,但在LRU List中的数据不一定都在Flush List中。在Flush List上的每个Page都会保存其最早修改的lsn,即oldest_modification,虽然一个Page可能被修改多次,但只记录最早的修改。Flush List上的Page会按照其各自的oldest_modification进行降序排序,链表尾部保存oldest_modification最小的Page,在需要从Flush List中回收Page时,从尾部开始回收。

Innodb存储引擎会将脏页的控制块放入一个flush list的链表中,注意,这里链表中不是真实的缓存数据页,而是数据页对应的控制块。

bh1ocl8tjo

Insert Buffer

当进行插入操作时,表中只有一个自增主键(插入时不能指定主键值,指定值就不是顺序插入了),没有其他二级索引,此时,只需要顺序插入主键索引树,不会出现随机访问多个页的情况,速度很快

但,实际情况下,一个表中很可能还有其他二级索引,插入非聚簇索引一定会涉及随机访问多个页,同时插入也不是顺序的

Change Buffer

InnoDB从1.0.x版本开始引入了Change Buffer,可将其视为Insert Buffer的升级。从这个版本开始,InnoDB存储引擎可以对DML操作——INSERT、DELETE、UPDATE都进行缓冲,他们分别是:Insert Buffer、Delete Buffer、Purge buffer。

Insert/Change Buffer是一棵B+树

当然和之前Insert Buffer一样,Change Buffer适用的对象依然是非唯一的辅助索引。

1
2
3

-- changebuffer大小 innodb_change_buffer_max_size值默认为25,表示最多使用1/4的缓冲池内存空间。而需要注意的是,该参数的最大有效值为50
SHOW VARIABLES LIKE'innodb_change_buffer_max_size';

Double Write

双写主要作用就是防止刷新脏页时,脏页未完全写入磁盘出现系统故障,同时系统故障又导致了磁盘数据页的损坏,由于数据页的损坏系统重启后无法使用redolog进行恢复数据。

如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的doublewrite中找到该页的一个副本,将其复制到表空间文件,再应用重做日志。

在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题。

image-20221011221835132

1
2
3
4
# 双写
SHOW GLOBAL STATUS LIKE'innodb_dblwr%';
innodb_dblwr_pages_written 写入的页数
innodb_dblwr_writes 写入次数

自适应哈希

(Adaptive Hash Index,AHI)是通过缓冲池的B+树页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引。InnoDB存储引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引。

自适应hash要求比较苛刻,个人觉得用处不大;用redis不香吗

1
2
-- 查询是否开启自适应哈希
show GLOBAL VARIABLES like 'innodb_adaptive_hash_index';

启动、关闭与恢复

1
show GLOBAL VARIABLES LIKE 'innodb_fast_shutdown';

0表示在MySQL数据库关闭时,InnoDB需要完成所有的full purge和merge insert buffer,并且将所有的脏页刷新回磁盘。这需要一些时间,有时甚至需要几个小时来完成。如果在进行InnoDB升级时,必须将这个参数调为0,然后再关闭数据库。

1是参数innodb_fast_shutdown的默认值,表示不需要完成上述的full purge和merge insert buffer操作,但是在缓冲池中的一些数据脏页还是会刷新回磁盘。

2表示不完成full purge和merge insert buffer操作,也不将缓冲池中的数据脏页写回磁盘,而是将日志都写入日志文件。这样不会有任何事务的丢失,但是下次MySQL数据库启动时,会进行恢复操作(recovery)。

1
show GLOBAL VARIABLES LIKE 'innodb_force_recovery';

该参数值默认为0,代表当发生需要恢复时,进行所有的恢复操作;

1(SRV_FORCE_IGNORE_CORRUPT):忽略检查到的corrupt页。

2(SRV_FORCE_NO_BACKGROUND):阻止Master Thread线程的运行,如Master Thread线程需要进行full purge操作,而这会导致crash。

3(SRV_FORCE_NO_TRX_UNDO):不进行事务的回滚操作。

4(SRV_FORCE_NO_IBUF_MERGE):不进行插入缓冲的合并操作。

5(SRV_FORCE_NO_UNDO_LOG_SCAN):不查看撤销日志(Undo Log),InnoDB存储引擎会将未提交的事务视为已提交。

6(SRV_FORCE_NO_LOG_REDO):不进行前滚的操作。

需要注意的是,在设置了参数innodb_force_recovery大于0后,用户可以对表进行select、create和drop操作,但insert、update和delete这类DML操作是不允许的。

重做日志缓冲

重做日志缓冲(redo log buffer)

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

Redo log落盘时机:

  • Master Thread每一秒将重做日志缓冲刷新到重做日志文件;

  • 每个事务提交时会将重做日志缓冲刷新到重做日志文件;

  • 当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件。

额外的内存池

每个缓冲池(innodb_buffer_pool)中的帧缓冲(frame buffer)对应缓冲控制对象(buffer control block),这些对象记录了一些诸如LRU、锁、等待等信息,而这个对象的内存需要从额外内存池中申请。因此,在申请了很大的InnoDB缓冲池时,也应考虑相应地增加这个值。

CheckPoint

checkpoint指的是当前要擦除的位置,主要作用是刷新脏页

为了避免发生数据丢失的问题,当前事务数据库系统普遍都采用了Write Ahead Log策略,即当事务提交时,先写重做日志,再修改页。当由于发生宕机而导致数据丢失时,通过重做日志来完成数据的恢复。这也是事务ACID中D(Durability持久性)的要求。

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

  • Sharp Checkpoint

    Sharp Checkpoint发生在数据库关闭时将所有的脏页都刷新回磁盘,这是默认的工作方式,即参数innodb_fast_shutdown=1。

  • Fuzzy Checkpoint

    InnoDB存储引擎内部使用Fuzzy Checkpoint进行页的刷新,即只刷新一部分脏页,而不是刷新所有的脏页回磁盘。在InnoDB存储引擎中可能发生如下几种情况的Fuzzy Checkpoint

    • Master Thread Checkpoint

      Master Thread差不多以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘。这个过程是异步的,即此时InnoDB存储引擎可以进行其他的操作,用户查询线程不会阻塞。

    • FLUSH_LRU_LIST Checkpoint

      Page Cleaner线程会检查LRU列表中是否小于1024个空闲页,如果小于,则将LRU列表尾端的页移除。如果这些页中有脏页,那么需要进行Checkpoint,而此时脏页是从LRU列表中选取的。

      innodb_lru_scan_depth设置LRU空闲页的个数,默认1024

    • Dirty Page too much Checkpoint

      当缓冲池的脏页占据一定比例时,会强制CheckPoint

      1
      2
      # 脏页比例默认90%
      SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct';
    • Async/Sync Flush Checkpoint

      Async/Sync Flush Checkpoint指的是重做日志文件不可用的情况,这时Page Cleaner线程强制将一些页刷新回磁盘,而此时脏页是从脏页列表中选取的。

      redo log采用的是循环写模式,当到达一定的阈值就会触发此类checkpoint,导致用户线程暂时阻塞

对于InnoDB存储引擎而言,其是通过LSN(Log Sequence Number)来标记版本的。而LSN是8字节的数字,其单位是字节。每个页有LSN,重做日志中也有LSN,Checkpoint也有LSN。

参考:

  1. MySQL技术内幕:Innodb存储引擎第二版
  2. http://mysql.taobao.org/monthly/2020/02/

InnoDB存储引擎浅析
http://example.com/2023/03/09/DB/InnoDB存储引擎/
作者
UncleBryan
发布于
2023年3月9日
许可协议