MySQL--InnoDB Buffer Pool详解

语言: CN / TW / HK

1 引言

在学习 MySQL 数据库相关知识的时候,你一定有这些疑惑:

  1. 如果每次都是直接操作磁盘的话,数据库的读写性能到底如何保证?
  2. MySQL 事务提交成功,被修改的数据是否就已经落到了磁盘上?

要搞清楚这两个问题,就必须理解 InnoDB Buffer Pool。

2 简介

MySQL InnoDB Buffer Pool,MySQL InnoDB 缓冲池。里面缓存着大量数据(数据页),使 CPU 读取或写入数据时,不直接和低速的磁盘打交道,直接和缓冲区进行交互,从而解决了因为磁盘性能慢导致的数据库性能差的问题。

3 详述

3.1 数据页

InnoDB 中,数据管理的最小单位为页,默认是 16KB。

3.2 Buffer Pool的工作机制

如上所述,buffer pool 最主要的功能便是加速读和加速写。

加速读就是当需要访问一个数据页的时候,如果这个页已经在缓存池中,那么就不再需要访问磁盘,直接从缓冲池中就能获取这个页面的内容。

加速写就是当需要修改一个数据页的时候,先将这个页在缓冲池中进行修改,记下相关的 redo log,这个页的修改就算已经完成了。至于这个被修改的页什么时候真正刷新到磁盘,这个是 buffer pool 后台刷新线程来完成的。

在实现上面两个功能的同时,需要考虑客观条件的限制,因为机器的内存大小是有限的。通常情况下,当数据库的数据量比较大的时候,缓存池并不能缓存所有的数据页,所以也就可能会出现,当需要访问的某个页时,该页却不在缓存池中的情况,这个时候就需要从磁盘中将这个页读出来,加载到缓存池,然后再去访问。这样就涉及到随机的物理 IO,也就增加了操作页所消耗的时间。

这样的情况是一个 bad case,是需要尽量避免的——因此需要想办法来提高缓存的命中率。innodb buffer pool 采用经典的 LRU 算法来进行页面淘汰,以提高缓存命中率。与传统的 LRU 算法相比,buffer pool 中的 LRU 列表其中间位置被打了一个 old 标识,可以简单的理解为将 LRU 列表分为两个部分,这个标记到 LRU 列表头部的数据页称为 young 数据页池,这个标志到 LRU 列表尾部的数据页称之为 old 数据页池。当一个页从磁盘上加载到缓存池的时候,会将它放在 old 标识之后的第一个位置,也就是说放在了 old 池子中(“中点插入策略”)。这个机制保证了在做大表的一次性全表扫描时,即使有大量新进来的数据页,也会被存放在 old 池子中,当 old 池子的大小不够缓存新进来页面的时候,也只是在 old 池子中进行循环冲洗,这样就不会冲洗 young 池子中的热点页,从而保护了热点页。这就是 buffer pool LRU 算法的简单机制。

3.3 基础知识

简单介绍了 buffer pool 的工作机制,再来看看 Buffer Pool Instance 的概念以及 buffer pool 里面最重要的几个链表。链表中的节点是数据页的控制体(控制体中的指针指向真正的数据页)。

3.3.1 Buffer Pool Instance

Buffer Pool 实例,大小等于 innodb_buffer_pool_size/innodb_buffer_pool_instances,每个 Buffer Pool Instance 都有自己的锁,信号量,物理块。即各个 instance 之间没有竞争关系,可以并发读取与写入。

3.3.2 InnoDB LRU List

InnoDB 基于 LRU 算法管理 buffer pool 中的数据页。一般情况下 list 头部存放的是热数据,就是所谓的 young page(最近经常访问的数据),list 尾部存放的就是 old page(最近不被访问的数据)。

LRU 有以下标准算法:

  1. 3/8 的 list 信息是作为 old list,这些信息是被驱逐的对象;
  2. list 的中点就是我们所谓的 old list 头部和 young list 尾部的连接点,相当于一个界限;
  3. 新数据首先会插入到 old list 的头部;
  4. 如果是 old list 的数据被访问到了,这个页信息就会被移动到 young list 的头部变成 young page;
  5. 在 InnoDB buffer pool 里面,不管是 young list 还是 old list 的数据,如果不会被访问到,最后都会被移动到 list 的尾部作为牺牲者。

一般情况下,页信息会被查询语句立马查询到而被移动到 young list,这就意味着他们会在 buffer pool 里面保留很长一段时间。表扫描(包括 mysqldump 或者没有 where 条件的 select 等操作)将会刷入大量的数据进入 buffer pool,同时也会将更多 buffer pool 当中的信息刷出去,即使这个操作可能只会使用到一次而已。

为了进一步提高读写性能,避免扫描 LRU List,实际上每个 Buffer Pool Instance 都有一个 page hash,通过它,使用 space_id 和 page_no 就能快速找到已经被读入内存的数据页,而不用线性遍历 LRU List 去查找。关于 page hash 的数据结构见总结模块中 InnoDB Buffer Pool 的架构图。

3.3.3 Flush List

在了解 Flush List 之前,首先需要了解脏页的概念。

脏页:内存数据页和磁盘数据页内容不一致的时候,这个数据页被称为“脏页”。内存数据写入磁盘后,内存和磁盘的数据页内容就一致了,称为“干净页”。不论脏页还是干净页,都存在内存里。

这个链表中的所有节点都是脏页,在 FLU List 上的页面一定在 LRU List 上,反之不成立。一个数据页可能会在不同的时刻被修改多次,在数据页上记录了最早一次也就是第一次修改的 lsn,即 oldest_modification。不同数据页有不同的 oldest_modification,FLU List 中的节点按照 oldest_modification 排序,链表尾是最小的,也就是最早被修改的数据页。当需要从 FLU List 中淘汰页的时候,从链表尾部开始淘汰。加入 FLU List,需要使用 flush_list_mutex 保护,所以能保证 FLU List 中节点的顺序。

3.3.4 Free List

其上的节点都是未被使用的节点,如果需要从数据库中分配新的数据页,直接从上获取即可。InnoDB 需要保证 Free List 有足够的节点,提供给用户使用,否则需要从 FLU List 或者 LRU List 淘汰一定的节点。

3.3.5 总结

为了管理 buffer pool,每个 buffer pool instance 使用如下几个链表来管理内存数据页:

LRU List:缓存了所有读入内存的数据页。包含三类:

  1. 未修改的页面,可以从该链表中摘除,然后移到 Free List 中;
  2. 已修改还未刷新到磁盘的页面;
  3. 已修改且已经刷新到磁盘的页面,可并为第一类。

Free List:空闲内存页列表,需要装载(缓存)磁盘上数据页的时候,从此列表取内存块。

Flush List:在内存中被修改但还没有刷新到磁盘的数据页(脏页)链表,内存中的数据跟对应磁盘上的数据不一致,属于该链表的页同样存在于 LRU List 中,但反之未必。

3.4 数据页访问机制

下面来看一下一个数据页的访问流程。

当访问的页面在缓存池中命中,则直接从缓冲池中访问该页面。如果没有命中,则需要将这个 page 从磁盘上加载到缓存池,因此需要在缓存池中的 Free List 中找一个空闲的内存页来缓存这个从磁盘读入的 page。

但存在空闲内存页被使用完的情况,不保证一定有空闲的内存页。假如 Free List 为空,则需要想办法产生空闲的内存页。 首先是在 LRU List 中找可以替换的内存页,查找方向是从列表的尾部开始找,如果找到可以替换的 page,将其从 LRU List 中摘除,加入 Free List,然后再去 Free List 中找空闲的内存页。第一次查找最多只扫描 100 个 page,循环进行到第二次时,查找深度就是整个 LRU List。这就是 LRU List 的页面淘汰机制。

如果在 LRU List 中没有找到可以替换的页,则进行单页刷新,将脏页刷新到磁盘后,再将释放的内存页加入到 Free List,最后再去 Free List 取。为什么只做单页刷新呢?因为它的目的是获取空闲内存页,进行脏页刷新是不得已而为之,所以只会进行一个页的刷新,目的是为了尽快的获取空闲内存页。

因为 Free List 是一个公共的链表,所有的用户线程都可以使用,存在争用的情况。因此,自己产生的空闲内存页有可能会刚好被其它线程所使用,因此用户线程可能会重复执行上面的查找流程,直到找到空闲的内存页为止。

通过数据页访问机制,可以知道当无空闲页时产生空闲页就成为了一个必须要做的事情。

如果需要通过刷新脏页来产生空闲页或者需要扫描整个 LRU List 来产生空闲页,查找空闲页的时间就会延长,这是一个 bad case。

因此,innodb buffer pool 中存在大量可以替换的页,或者 Free List 中一直存在着空闲内存页,对快速获取空闲内存页就起到了决定性的作用。

而在 innodb buffer pool 的机制中,是采用何种方式来产生空闲内存页以及可以替换的内存页呢?这就是下面要讲的内容——脏页刷新策略。

3.5 脏页刷新策略

略。

3.6 Bad Case

以下两种情况,InnoDB 会强制刷脏页。

  1. InnoDB 的 redo log 写满了。这时候系统会停止所有更新操作去刷盘。这种情况应该尽量避免,因为出现这种情况的时候,整个系统就不能再接受更新,写性能跌为 0。这种对于敏感业务来说,是不能接受的;
  2. 内存不足。当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给新的数据页使用,此时如果淘汰的是“脏页”,就要将脏页写到磁盘。

InnoDB 的策略是尽量使用内存,因此对于一个长时间运行的库来说,未被使用的页很少。当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉;如果要淘汰的是一个干净页,就直接释放出来复用;如果是脏页,就必须先将脏页刷盘,变成干净页后才能复用。所以刷脏页是常态。

除了上述两种情况,一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长,这也会影响性能。

4 参考阅读

  1. [玩转MySQL之十]InnoDB Buffer Pool详解
  2. 为什么MySQL会抖一下 - 关于刷脏页磁盘
  3. InnoDB数据页结构
  4. MySQL Innodb 内存池实现简介
  5. MySQL · 引擎特性 · InnoDB Buffer Pool
分享到: