InnoDB 体系架构

InnoDB 体系结构

前言

本篇文章主要介绍 InnoDB 存储引擎的整体架构,着重讲述后台线程是如何工作的,内存池中的缓冲池的特性和存储的内容、以及日志缓冲池仅简单介绍,这些内容会在之后的日志和索引篇章详细讲解.此外,本篇文章基本完全参考《MySQL 技术内幕:InnoDB 存储引擎》,略去了细枝末节的内容,感兴趣的读者可以去详细阅读这本书

① MySQL 查询或者更新数据的整体逻辑

  • 首先 MySQL 进程会检查内存中是否存在需要查询或者更新的数据
    • 如果内存中存在对应需要查询或者更新的数据,那么就会将内存中对应的数据更新
      • 内存中被修改的数据此时就会被称为脏页,与之相对没有被修改的页面就是干净的页面
      • 这里也就是说修改的内容并不会立刻写入磁盘中,而是暂时保存在内存中,至于什么时候写入磁盘就是后台线程的任务了
    • 如果内存中没有对应的数据,那么就会触发缺页中断,操作系统就会开始处理中断
  • 然后 MySQL 就会有对应的后台线程去负责从磁盘中将数据所在的整个数据页全部加载进入内存中
    • 如果内存中还有空闲的空间,那么直接将数据页全部加载进入内存就可以了
    • 如果内存中没有空闲的空间,那么就会采用 LRU(最近最少使用算法)去替换内存中的数据页

如此设计的理由和操作系统的设计是类似的,现代计算体系结构肯定要求数据必须在内存中才能够使用,但是数据库中存储的数据通常是非常多的,不可能全部都加载到内存中,所以采用这种按需调用的方式,而读取数据的最小单位为页而不是行,则是避免频繁的磁盘读写造成的性能下降,因为如果每次读写磁盘仅读写一行,那么十行数据就必须读写十次,这个效率是非常低的

注:概述的执行逻辑没有考虑日志、缓冲区的等内容,只是想说明数据一定是先被加载内存中修改再择机写入磁盘中的

② InnoDB 存储引擎架构图

整个架构图是根据书中的内容总结得到的,做了细微的扩展和调整。后台线程中增加 purge thread、page cleaner thread 两个线程,这两个线程是辅助 master 线程完成工作的,并不是很重要。日志缓冲区中增加了 undo log buffer,在书中并没有明确指出存在这个缓冲区,但是个人认为应该存在对应缓冲区,binlog 个人认为应该是存放在业务层对应的内存中的,而不是引擎层

InnoDB 存储引擎架构图

内存结构

概述

内存池的主要功能

  • 维护和管理线程需要使用的数据结构等信息
  • 缓存从磁盘中加载的数据页、索引页等相关数据信息,从而避免频繁地磁盘读取
  • 设计插入缓存、二次写缓冲、日志缓冲等缓冲结构,从而提升向磁盘写入数据的效率以及确保数据库宕机之后的恢复效率

内存池的主要组成

  • 内存池主要有三部分组成:缓冲池、日志缓冲池、额外内存池三部分组成
  • 缓冲池内部存储的信息主要是:数据页、索引页、插入缓冲、两次写缓冲、自适应哈希、锁等相关信息
  • 日志缓冲内部存储的信息主要是:redo log、undo log(我自己的想法)

内存池的大小

  • 参数 innodb_buffer_pool_size 控制缓冲池的大小
  • 参数 innodb_log_buffer_size 控制日志缓冲池的大小
  • 参数 innodb_additional_mem_pool_size 控制额外内存池的大小(MySQL 5.7 版本之后被移除)
两个参数的默认大小

缓冲池

缓冲池存储内容:缓冲池缓存的主要内容就是从磁盘中加载进来的数据页和索引页,其余优化策略所占据的内存相对较小

  • 后台线程的主要工作内存就是缓冲池:每次都从内存中查询或者更新内存中的数据,之后择机将数据写入磁盘中
  • 读取数据的最小单位是页:每次查询对应的一行或者多行数据的时候,都是将这些行所在数据页全部加载进来

注:数据页和索引页将会在之后表结构中详细讲述,现在知道这两个东西分别用于存储数据和索引就行了

查看缓冲池使用情况:

-- 查看引擎的使用情况
show engine innodb status \G;
缓冲池使用情况

插入缓冲

两次写缓冲

参考博客:两次写缓冲

1、为什么需要两次写缓冲?

MySQL 中默认每张数据页都是 16KB 的大小,文件系统每张数据页大小通常是 4KB,而磁盘 I/O 的速率显然是不足以将数据库中的数据页瞬间写入到文件系统中。如果在磁盘 I/O 传输的过程中数据库或者文件系统出现故障而宕机,磁盘中就存在未写完的残缺数据页。

这种残缺的数据页没有办法继续使用,也没有办法使用重做日志进行恢复(重做日志记录的是物理页的修改,在物理页已经损坏的情况下,重做日志无法使用)

这种情况就称为部分写失效(partial page write),所以二次写缓冲就是用于解决部分写失效问题的,用于保障数据库的可靠性的技术

部分写失效

2、两次写缓冲是如何完成的?

先来分析问题的核心,本质在于 ① 数据页出现新旧内容交替的情况,导致重做日志无法对其生效 ② 脏页的数据也丢失了

既然这样,在重做日志无法生效的情况下,那么只有考虑将脏页的数据提前备份,只要发生宕机就直接从备份数据中恢复就好

  • 先调用 memcpy 函数将脏页拷贝到两次写缓冲中(double write buffer)
  • 然后将两次写缓冲中的内容先拷贝到共享表空间中(ibdata1)
  • 最后将两次写缓冲中的内容离散地同步写入到磁盘中去

再来看看两次写缓冲是否解决了部分写失效的问题:

  • 如果在将脏页拷贝到两次写缓冲中去的时数据库宕机,那么磁盘页没有被损坏,那么就可以应用重做日志恢复数据重写落盘
  • 如果在将缓冲区的数据写入共享表空间中时数据库宕机(?)
  • 如果在写入磁盘的时候数据库宕机,那么只需要检验出数据页确实发生损坏,那么就可以从共享表空间中取出脏页副本恢复即可
二次写缓冲

3、两次写缓冲是否带来了问题?

自适应哈希

日志缓冲区

日志缓冲区主要存储内容:重做日志和回滚日志记录的内容

  • 后台线程每次根据 SQL 语句记录日志的时候,都会先将日志记录在日志缓冲区中,择机将日志缓冲区的内容写入磁盘中
  • 日志缓冲区的值不需要设置太大:后台线程会定期将日志缓冲区中的内容写入磁盘中,所以不用担心溢出

额外内存池

不是很明白这个是干什么的

后台线程

概述

后台线程主要功能

  • 后台线程主要负责更新内存池中的缓存数据,保证内存池中的数据基本是最新的
  • 后台线程还要负责将内存池中的数据定期刷回到磁盘中去,并且确保在写入过程发生宕机时,能够从宕机的异常状态恢复到正常运行状态

后台线程种类

① 后台线程主要分为 4 类:

  • 主线程:负责协调所有功能的线程
  • 锁线程:负责和上锁和释放锁(全局锁、表锁、行锁)的相关线程(不准确)
  • 错误监控线程:负责异常处理的线程(不准确)
  • I/O 线程:插入缓冲线程、日志线程、读写磁盘的线程

② 后台线程的数量

  • InnoDB 默认版本中后台线程的数量配置为 7 个,InnoDB Plugin 版本(MySQL 5.1)后台线程数量为 13 个
    • 7 个线程:4 个 I/O 线程(插入缓冲、日志、读磁盘、写磁盘线程)、主线程、错误监控线程、锁线程
    • 13 个线程:读磁盘线程和写磁盘线程从 1 个变为 4 个,其余线程的数量没有变化
  • Linux 系统的后台线程数量是不可以改变的,Windows 系统下可以通过相应的变量控制后台线程数量
    • InnoDB 默认版本中采用 innodb_file_io_threads 进行控制
    • InnoDB Plugin 版本中采用 innodb_read_io_threadsinnodb_write_io_threads 两个变量控制
I/O 线程

主线程

主线程主要分为四个循环过程:主循环、后台循环、刷新循环。负责最主要功能的就是主循环,其余循环的功能基本都是主循环的子集。其主要内容就是就是 ① 将内存中的日志刷新到磁盘中去 ② 将内存中脏页更新到磁盘中去 ③ 将从磁盘中读取的数据和插入缓冲中的数据合并 ④ 删除不再使用的回滚日志

主循环主要的工作

主循环

① 主循环负责的功能种类

  • 每秒都需要执行的操作:通常称为系统繁忙的时候
  • 每间隔 10秒 执行的操作:通常称为系统空闲的时候

注:这里的时间频率只是大致上的,每秒执行的操作以及线程休眠都会造成一定的延时


② 主循环每秒执行的操作

1、刷新日志:无论事务是否提交,每秒都会将日志缓冲区中记录的日志全部刷新到磁盘中

不过,日志缓冲区记录的日志落盘除了主线程每秒刷新之外,还有其他情况也是会刷新到磁盘的

  • binlog:采用 sync_binlog 参数控制落盘时机
    • 不强制要求落盘时机,由系统自行判断落盘时机
    • 每提交一个事务就立刻将日志刷新到磁盘中
    • 每提交 N 个事务后才将日志刷新到磁盘中
  • redo log:
    • 采用 innodb_flush_log_at_trx_commit 参数控制落盘时机
      • 延迟写:事务提交时,准备将缓冲区中的日志写入磁盘,但是需要等待主线程的每秒刷新
      • 实时写、实时刷:事务提交时,不等主线程刷新,而是直接采用同步阻塞的方式将日志刷新到磁盘
      • 实时写、延迟刷:事务提交时,不等待主线程刷新,采用异步的方式将日志刷新到磁盘
      • 注:实时写实时刷可以保证日志写入磁盘中而不会丢失,而实时写延迟刷则无法保证,但是前者效率低后者效率高
    • 重做日志缓冲区写入的内容超过一半后,会强制将部分重做日志写入磁盘,并相应推进检查点

刷新日志的细节分析以及留存的疑问

  • 不必等待事务提交再将全部的日志刷新到磁盘中,所以事务提交过程比较快
  • 每秒都将日志缓冲区中的内容刷新到磁盘,I/O 性能不会很差吗?(我的疑问 ①)
  • 既然每秒中都刷新日志到磁盘中,那么两阶段提交还有什么意义呢?(我的疑问 ②)

注:这里提到的各种类型日志及其刷盘时机会在之后的日志部分详细讲述,包括两种日志如何配合都会在之后讲述

2、合并插入缓冲:将插入缓冲区中存放的修改记录和从磁盘中加载进入的数据页进行合并

  • 合并插入缓冲的条件
    • 如果当前一秒内发生的 I/O 次数小于 5 次,那么就会考虑执行合并插入缓冲的操作
    • 如果当前一秒内发生 I/O 的频率非常高,那么就不会执行合并插入缓冲的操作

3、刷脏页:将缓冲区中存放的脏页(已经被修改的数据页)写入磁盘中

  • 向磁盘写入脏页的条件:根据实际的 buf_get_modified_ratio_pct 比例确定

    • 如果缓冲池中的脏页所占内存比例超过阈值,那么就会将 100 个脏页全部写入磁盘中
    • 如果缓冲池脏页所占的比例没有超过阈值,那么就不会刷新任何脏页

    注:每秒刷新的脏页数量是固定,不会根据情况变动

  • 阈值的设置:可以通过 innodb_max_dirty_pages_pct 这个参数进行设置

    • 刷脏页的阈值默认值为 90%,在引擎之后的版本将这个值修改为 75%
    • 默认值修改的原因在于如果内存特别大的情况下,就容易导致长时间脏页没有落盘,从而导致宕机之后需要更多时间恢复树
  • 其余向磁盘写入脏页的条件

    • 重做日志记录内容超过一半的时候,也会将脏页刷新到磁盘中
    • 线程空闲的时候,也会将脏页刷新到磁盘中
    • 数据库正常退出的时候,会强制将所有脏页刷新到磁盘中

如果当前线程没有任何活动或者操作行为,就切换到后台循环


③ 主循环每 10 秒执行的操作

1、刷脏页:将缓冲区中存放的脏页(已经被修改的数据页)写入磁盘中

  • 刷新脏页的条件:
    • 如果过去的 10 秒内执行的 I/O 操作少于 200 次,那么就会将 100 个脏页全部写入磁盘中
    • 如果过去的 10 秒内执行 I/O 的频率过高,那么存储引擎会认为当前没有空闲将脏页写入磁盘

2、合并插入缓冲 + 刷新日志到磁盘 + 删除不再使用 undo 页

  • 执行条件:
    • 无论过去时间内 I/O 的频率如何,这里都是会执行合并插入缓冲和刷新日志的操作
    • 无论事务是否提交,都会将日志缓冲区中的内容刷新到磁盘中
    • 没有其他更早的事务使用当前事务的回滚日志时,就会删除回滚日志对应的 undo 页

注:undo log 的详细内容将在日志中讲述,这里只需要知道它是事务原子性的底层原理

3、继续刷脏页:将 100 个或者 10 个脏页全部写入磁盘中

  • 继续刷新脏页的条件
    • 如果脏页在缓冲池中的内存占比已经超过 70%,那么就会考虑刷新 100个 脏页进入磁盘中
    • 如果脏页在缓冲池中的内存占比没有超过 70%,那么仅会刷新 10 个脏页进入磁盘

注:不同于每秒操作,每隔 10 秒则必然会将部分脏页写入磁盘中


④ 主循环的伪代码

void master_thread(){
goto main_loop;
// 主循环
for(int i = 0;i < 10;i++){
// 线程存在休眠,所以每秒并不是精确的
thread.sleep(1);
// 将日志刷新到磁盘中
do log buffer flush to disk;
if(last_one_seconds < 5)
// 执行插入缓冲合并
do merge at most 5 insert buffer;
if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
// 将 100 个脏页刷新到磁盘
do buffer pool flush 100 dirty pages;
if(no user activity)
// 如果没有用户活动就切换到后台线程
goto background loop;
}
if(last_ten_seconds < 200)
do buffer pool flush 100 dirty pages;
do merge at most 5 insert buffer;
do log buffer flush to disk;
// 删除不再使用的 undo 页
do full purge;
if(buf_get_modified_ratio_pct > 70%)
do buffer pool flush 100 dirty pages;
else
do buffer pool flush 10 dirty pages;
}

后台循环

  • 删除不再使用 undo 页面
  • 合并插入缓冲(20个)
  • 如果线程当前需要执行新的操作或者活动,那么就会跳回到主循环中去执行
  • 如果当前线程依旧没有任何需要执行的操作,那么就会跳转到刷新循环中执行

刷新循环

  • 不停地刷新脏页到磁盘中去(100个),直到没有脏页可以刷新的时候,就跳转到暂停循环中

暂停循环

  • 如果线程进入暂停循环了,那么存储引擎就会将该线程挂起,不在执行任何操作
  • 如果线程需要执行新的操作,那么再由存储引擎将其唤醒,进入主循环中继续执行

注:如果 MySQL 启用了 InnoDB 引擎,但是没有任何表在定义的时候使用 InnoDB 引擎,那么主线程就始终处于暂停循环

参考资料:

《MySQL 技术内幕:InnoDB 存储引擎》

Author: Fuyusakaiori
Link: http://example.com/2022/01/19/database/mysql/structure/InnoDB 体系架构/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.