InnoDB存储引擎

Posted by Vincent on February 23, 2021

架构

Image

InnoDB的多线程模型

  1. Master Thread : 非常核心的后台线程,主要负责
    1. 将缓冲池中的数据异步刷新到磁盘
      1. 每一秒将redo log buffer刷新到redo log文件
    2. 脏页的刷新、合并插入缓冲(INSERT BUFFER)、UNDO页的回收等
  2. IO Thread在InnoDB存储引擎中大量使用了AIO(Async IO)来处理写IO请求,这样可以极大提高数据库的性能。
    1. IO Thread的工作主要是负责这些IO请求的回调(call back)处理。
  3. Purge Thread : 事务被提交后,其所使用的undo log可能不再需要,因此需要PurgeThread来回收已经使用并分配的undo页。
  4. Page Cleaner Thread
    1. 作用 : 将之前版本中脏页的刷新操作都放入到单独的线程中来完成
    2. 目的 : 为了减轻原Master Thread的工作及对于用户查询线程的阻塞,进一步提高InnoDB存储引擎的性能。

流程

  1. 读取
    1. 首先将从磁盘读到的页存放在缓冲池中,这个过程称为将页“FIX”在缓冲池中。
      1. 每个根据哈希值平均分配到不同缓冲池实例
    2. 下一次再读相同的页时,首先判断该页是否在缓冲池中。
      1. 若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。
      2. 否则,读取磁盘上的页。
  2. 修改
    1. 写入redo log buffer
    2. 当事务提交时 (会触发将redo log buffer写入redo log的动作),写redo log, 写binlog
    3. 修改在缓冲池中的页
    4. 将脏页复制到double write buffer
    5. 顺序写写入double write file
    6. 然后再以一定的频率刷新到磁盘上。
      1. 页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为Checkpoint的机制刷新回磁盘。同样,这也是为了提高数据库的整体性能。

内存

  1. 缓冲池 (innodb_buffer_pool)
  2. LRU List & Free List & Flush List
  3. 重做日志缓冲池 (redo log buffer)
  4. 额外内存池 (innodb_additional_mem_pool_size)

缓冲池 : 一块内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响

  1. 作用 : 由于CPU速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。
  2. 内容 : Image
  3. 缓冲池数量 : innodb_buffer_pool_instances
  4. 算法
    1. 缓冲池是通过LRU(Latest Recent Used,最近最少使用)算法来进行管理的。即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取到的页时,将首先释放LRU列表中尾端的页。
    2. midpoint insertion strategy : 新读取到的页, 不是放到首部, 而是放在中间

redo log buffer

  1. InnoDB存储引擎首先将redo log信息先放入到redo log buffer,然后按一定频率将其刷新到重做日志文件。
  2. 重做日志缓冲一般不需要设置得很大,因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。
    1. innodb_log_buffer_size, 默认为8MB
  3. redo log buffer刷新到外部磁盘的redo log的情况
    1. Master Thread每一秒
    2. 每个事务commit
    3. 当redo log buffer剩余空间小于1/2时

额外内存池

  1. 在InnoDB存储引擎中,对内存的管理是通过一种称为内存堆 (heap) 的方式进行的
  2. 在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请
    1. e.g. 分配了缓冲池 (innodb_buffer_pool),但是每个缓冲池中的帧缓冲 (frame buffer) 还有对应的缓冲控制对象 (buffer control block) ,这些对象记录了一些诸如LRU、锁、等待等信息,而这个对象的内存需要从额外内存池中申请

脏页 : 当 内存数据页(缓冲池的页)磁盘上的页 新的时候,我们称这个内存页为脏页

刷脏页的时机

  1. redo log写满时,没有空间了,此时需要将checkpoint向前推进,推进的这部分日志对应的脏页刷入到磁盘,此时所有的更新全部阻塞,此时写的性能变为0,必须待刷一部分脏页后才能更新。
  2. 系统内存不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘。
  3. MySQL认为空闲的时间,这种没有性能问题。
  4. mysql正常关闭之前,会把所有脏页刷入磁盘,不存在性能问题。

Write Ahead Log (WAL)

  1. 现存问题 : 如果在从缓冲池将页的新版本刷新到磁盘时发生了宕机,那么数据就不能恢复了。
  2. 作用 : 避免发生数据丢失
  3. 内容 : 当事务提交时 (会触发将redo log buffer写入redo log的动作),先写redo log,再修改页。
    1. 当由于发生宕机而导致数据丢失时,通过redo log来完成数据的恢复

checkpoint : 在某些关键的时间点将 缓冲池中的 脏页 刷回到 磁盘

  1. 前提
    1. 不可能每次有脏页就刷新回磁盘, 开销很大
    2. redo log 和 缓冲池 不可能无限大
      1. redo log缓冲池 无限大的时候, 不需要将 缓冲池 的内容刷新到 磁盘 . 因为当发生宕机的时候, 完全可以通过redo log来恢复
        1. 但是 缓冲池 不可能无限大, 而且内存比磁盘贵很多
        2. 但是 redo log 无限大的话, 恢复需要太久时间
  2. 作用 :
    1. 缩短数据库的恢复时间
    2. 缓冲池 不够用时,将脏页刷新到磁盘
    3. redo log 不可用时,刷新脏页
  3. 好处 :
    1. 缩短恢复时间 : 当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘, 只需对Checkpoint后的重做日志进行恢复
    2. 缓冲池 不够用的时候, 根据LRU算法会溢出最近最少使用的页, 如果是脏页 (也有可能是select, 就不是脏页了), 会强制执行check point, 刷新回磁盘.
    3. redo log : 如果redo log还被需要时, 就必须强制产生check point, 将缓冲池中的page至少刷新到当前redo log的位置
      1. 前提 : 当前事务数据库系统对重做日志的设计都是循环使用的,并不是让其无限增大的
      2. redo log 不再需要 : 即便是宕机, 数据库恢复也不需要恢复这部分redo log
  4. 种类
    1. Sharp Checkpoint (default) : 数据库关闭时 将 所有的脏页 都刷新回 磁盘
    2. Fuzzy Checkpoint : 只刷新一部分脏页
      1. 情况
        1. Master Thread Checkpoint : [异步] [非阻塞] 以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘
        2. FLUSH_LRU_LIST Checkpoint : [PageCleaner线程] [非阻塞] InnoDB存储引擎需要保证 LRU列表 中需要有差不多100个空闲页可供使用
        3. Async/Sync Flush Checkpoint : redo log不可用的情况,这时需要强制将一些页刷新回磁盘,而此时脏页是从脏页列表中选取的。
        4. Dirty Page too much Checkpoint : 脏页的数量超过某个百分比 (innodb_max_dirty_pages_pct)

插入缓冲 (Insert Buffer) : 提高对于非聚集索引插入性能

  1. Insert Buffer和数据页一样,不是buffer, 也是物理页的一个组成部分
  2. 当表只有聚集索引时, 会构造一颗聚集索引的B+树
    1. 当插入聚集索引 (主键) 是顺序自增的时候,不需要磁盘的随机读取, 速度非常快。
      1. 非顺序的情况 : 主键是UUID 或 指定了主键
  3. 当表中有非聚集索引 (普通索引) 时,
    1. 依旧会按照主键构造一颗聚集索引的B+树,
    2. 同时会构造一颗非聚集索引的B+树
      1. 对于非聚集索引叶子节点的插入不再是顺序的了,这时就需要离散地访问非聚集索引页 (也是为什么表中有索引, 插入会变慢的原因)
      2. 由于随机读取的存在而导致了插入操作性能下降
  4. 插入缓冲 :
    1. 对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中
      1. 若在,则直接插入
      2. 若不在,
        1. 则先放入到一个Insert Buffer对象中,好似欺骗数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另一个磁盘位置。
        2. 然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge (合并) 操作,
        3. 这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。
  5. 启动插入缓冲的条件
    1. 索引是辅助索引 (secondary index) : 因为聚集索引是主键, 也要确保唯一性
    2. 索引不是唯一(unique)的 : 因为如果是唯一的话, 要确保现存的没有重复, 需要随机读, 这样插入缓冲就没意义了
  6. change buffer :
    1. insert buffer的升级, 不仅对insert, 还对update, delete都有对应的update buffer, delete buffer, purge buffer
    2. 对象 : 非唯一的辅助索引

两次写 (Double Write) : 解决部分写失效问题, 确保可靠性

  1. 部分写失效 : 当更新内存页到磁盘中时, 发生宕机, 导致页只写了一部分.
  2. redo log也不能解决部分写失效的问题 : 因为redo log记录的是对页的物理操作
  3. 解决办法 :
  4. 对缓冲池的脏页进行刷新的流程
    1. 不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer
    2. 通过doublewrite buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上 (因为doublewrite页是连续的,因此这个过程是顺序写的,开销并不是很大)
    3. 马上调用fsync函数,同步磁盘中的double write文件,避免缓冲写带来的问题
    4. 将doublewrite buffer中的页写入各个表空间文件中
    5. 恢复流程
      1. 从共享表空间中的doublewrite中找到该页的一个副本,将其复制到表空间文件
      2. 再应用重做日志
    6. 架构 Image

自适应哈希索引 (Adaptive Hash Index) : 对频繁访问的页, 建立索引

  1. Innodb会监控对表上各索引页的查询, 如果观察到建立哈希索引可以带来速度提升, 则建立哈希索引
  2. 要求
    1. 查询语句是一样的
    2. 以该模式访问了100次
    3. 页通过该模式访问了N次, 其中N = 页中记录 / 16
    4. 只能是等于某个值的查询, 其他查询 (e.g. 范围查询) 是不能使用自适应哈希索引的
  3. 性能提升
    1. 读取 & 写入 : 提升2倍
    2. 辅助索引 : 连接性能提升5倍
  4. 设计思想 : 数据库的自优化 (self-tuning)

异步IO (Async IO)

  1. 同时发出多个IO请求
  2. IO merge : 当判断到多个页是连续的, 可以将多个IO操作合并为1个IO, 提高IO per second 性能

刷新邻接页 (Flush Neighbor Page)

  1. 当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。
  2. 通过AIO可以将多个IO写入操作合并为一个IO操作,故该工作机制在传统机械磁盘下有着显著的优势