MySQL - InnoDB Buffer Pool

简介

MySQL InnoDB 缓冲池,里面缓存着大量数据(数据页),使 CPU 读取或写入数据时,MySQL 不会直接去修改磁盘的数据,因为这样做太慢了,MySQL 会先改内存,然后记录 redo log,等有空了再刷磁盘,如果内存里没有数据,就去磁盘 load,从而解决了因为磁盘性能慢导致的数据库性能差的问题。而这些数据存放的地方,就是 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 算法的简单机制。

Buffer Pool Instance 和 Buffer Pool几个链表

Buffer Pool Instance

Buffer Pool 实例,大小等于

innodb_buffer_pool_size / innodb_buffer_pool_instances

innodb_buffer_pool_instances 的大小可配置。

每个 Buffer Pool Instance 都有自己的锁,信号量,物理块,各个 instance 之间没有竞争关系,可以并发读取与写入。如果 innodb_buffer_pool_size 大小小于 1G,将只有一个 instance。

Buffer Pool Chunk

Buffer Pool Instance 由若干个 chunk 组成,一个 chunk 就是一片连续的空间,每个 chunk 的大小默认为 128MB,最小为 1MB,且这个值在 8.0 中是可以动态调整生效的。Buffer Chunk 是最底层的物理块,在启动阶段由操作系统申请,直到数据库关闭才释放。Buffer chunk 主要存储数据页和数据页控制体。如下图:


设计数据页控制体的主要目的是为了方便管理数据页,控制体中有指针指向数据页。InnoDB 为每一个数据页都创建了一些所谓的控制信息,数据页控制体和数据页是一一对应的。这些控制信息包括该页所属的表空间编号、页号、页在 Buffer Pool 中的地址等。每个数据页对应的控制信息占用的内存大小是相同的。

Free List

当最初启动 MySQL 服务器的时候,需要完成对 Buffer Pool 的初始化(分配 Buffer Pool 的内存空间),把它划分成若干对控制块和缓存页。此时并没有真实的磁盘页被缓存到 Buffer Pool 中(因为还没有用到),之后随着程序的运行,会不断的有磁盘上的页被缓存到 Buffer Pool 中。
因为刚刚完成初始化的 Buffer Pool 中所有的数据页都是空闲的,所以每一个数据页都会被加入到 Free List 中,假设该 Buffer Pool 中可容纳的数据页数量为 n,那增加了 Free List 的效果图就是这样的:

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

Lru List

既然 buffer pool 的目的是加速写和加速读,因此必须想办法提高内存数据页的缓存命中率。InnoDB 基于经典的 LRU 算法管理 buffer pool 中的数据页。一般情况下 list 头部存放的是热数据,就是所谓的 young page(最近经常访问的数据),list 尾部存放的就是 old page(最近不被访问的数据),
入缓冲池的页,优先进入老生代,页被访问,才进入新生代,以解决预读失效的问题。

Lru 有以下算法:

  • 3/8 的 list 信息是作为 old list,这些信息是被驱逐的对象;
  • list 的中点就是我们所谓的 old list 头部和 young list 尾部的连接点,相当于一个界限;
  • 新数据首先会插入到 old list 的头部;
  • 如果是 old list 的数据被访问到了,且在老生代停留时间超过配置阈值的(默认是1000ms),这个页信息才会被移动到 young list 的头部变成 young page,以解决批量数据访问,大量热数据淘汰的问题;
  • 在 InnoDB buffer pool 里面,不管是 young list 还是 old list 的数据,如果不会被访问到,最后都会被移动到 list 的尾部被淘汰。

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

Flush List

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

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

脏页最终肯定需要被刷回磁盘而变成干净页,但如果每次产生脏页后就立即同步到磁盘势必将严重影响程序的性能(毕竟磁盘慢的像乌龟一样)。所以每次修改缓存页后,我们并不着急立即把修改同步到磁盘上,而是在未来的某个时间点进行同步,由后台刷新线程依次刷新到磁盘,实现修改落地到磁盘。

由于不是立即同步刷新脏页,所以我们不得不再创建一个链表,将脏页保存起来。凡是在 LRU List 中被修改过的页都需要加入这个链表中,这个链表中的所有节点都是脏页,所以也叫 Flush List,一般被简写为 FLU List。

这里的脏页修改指的此页被加载进 Buffer Pool 后第一次被修改,只有第一次被修改时才需要加入 FLU List(代码中是根据 page 头部的 oldest_modification == 0 来判断是否是第一次修改),如果这个页被再次修改就不会再放到 FLU List 了,因为已经存在。需要注意的是,脏页数据实际还在 LRU List 中,而 FLU List 中的脏页记录只是通过指针指向 LRU List 中的脏页(即在 FLU List 上的页一定在 LRU List 上,反之不成立)。

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

虽然脏页既存在于 LRU List 中,也存在与 FLU List 中,但 LRU List 用来管理缓冲池中页的可用性,FLU List 用来管理将脏页刷新回磁盘,二者互不影响。

Buffer Pool预热

在 MySQL 重启后,由于 Buffer Pool 里面没有什么数据,因此这个时候业务上对数据库的操作,MySQL 就只能从磁盘中读取数据到内存,而这个过程可能需要很久才能恢复到 MySQL 重启前内存中保留的业务频繁使用的热数据。Buffer Pool 从无到重新缓存业务频繁使用热数据的过程称之为预热。在预热这个过程中,MySQL 数据库的性能不会特别好,并且 Buffer Pool 越大,预热过程越长。

为了减短这个预热过程,在 MySQL 关闭前,把 Buffer Pool 中的页信息保存到磁盘,等到 MySQL 启动时,再根据之前保存的信息把磁盘中的数据加载到 Buffer Pool 即可。

总结

这三个重要链表(Free List, LRU List, FLU List)的关系可以用下图表示:


Free List 跟 LRU List 的关系是相互流通的,页在这两个链表间来回置换。而 FLUSH List 中记录了脏页数据,即通过指针指向了 LRU List,所以图中 FLU List 被 LRU List 包裹。
三个链表的元素都是控制块指针,实际指向内存page。

数据页访问机制

下面梳理一下数据页的访问流程。

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

但存在空闲内存页被使用完的情况,不保证一定有空闲的内存页。假如 Free List 为空,则需要想办法尽快产生空闲的内存页。

首先去 LRU List 中找可以替换的内存页(干净页),查找方向是从链表的尾部开始找,只要找到可以替换的页,就将其从 LRU List 中移除,加入空闲列表,然后再去空闲列表中找空闲的内存页。第一次查找最多只扫描 100 个页,循环进行到第二次时,查找深度就是整个 LRU List。

如果在 LRU List 中没有找到可以替换的页,则进行单页刷新(从 FLU List 中取),将脏页刷新到磁盘之后,再将其加入到空闲列表。这便是 InnoDB 中的 LRU 页面淘汰机制。为什么只做单页刷新呢?因为它的目的是为了尽快获取空闲内存页,进行脏页刷新是不得已而为之,所以只会进行一个页的刷新。

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

在执行一条 SQL 语句的时候,如果恰好需要进行单页刷脏,这条 SQL 语句的执行便会比预期更加耗时,有时候看起来就像是数据库“抖了一下”,这也是一个 bad case,需要尽量避免。

通过数据页访问机制,可以知道当无空闲页时产生空闲页就成为了一个必须要做的事情。如果需要通过刷新脏页来产生空闲页,查找空闲页的时间就会延长。因此,innodb buffer pool 中存在大量可以替换的页,或者 Free List 中一直存在着空闲内存页,对快速获取空闲内存页就起到了决定性的作用。

重要参数配置

参数:innodb_buffer_pool_size
介绍:配置缓冲池的大小,在内存允许的情况下,DBA往往会建议调大这个参数,越多数据和索引放到内存里,数据库的性能会越好。

参数:innodb_old_blocks_pct
介绍:老生代占整个LRU链长度的比例,默认是37,即整个LRU中新生代与老生代长度比例是63:37。
画外音:如果把这个参数设为100,就退化为普通LRU了。

参数:innodb_old_blocks_time
介绍:老生代停留时间窗口,单位是毫秒,默认是1000,即同时满足“被访问”与“在老生代停留时间超过1秒”两个条件,才会被插入到新生代头部。

针对数据写的优化 - change buffer写缓冲

当需要更新一个数据页时,如果数据页在内存中就直接更新。
而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了,减少一次IO。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作,例如将数据合并(merge)恢复到缓冲池中。
写缓冲的目的是降低写操作的磁盘IO,提升数据库性能。

加入写缓冲优化后,修改不在内存中的数据页流程优化为:

  1. 在写缓冲中记录这个操作,一次内存操作;
  2. 写入redo log,一次磁盘顺序写操作。
    其性能与这个索引页在缓冲池中,相近。
为什么写缓冲优化,仅适用于非唯一普通索引页

对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束,InnoDB必须进行唯一性检查。也就是说,索引页即使不在缓冲池,磁盘上的页读取无法避免,否则怎么校验是否唯一,此时就应该直接把相应的页放入缓冲池再进行修改,而不应该再整写缓冲。 当数据不在内存页,就至少会产生一次IO。

除了数据页被访问,还有哪些场景会触发刷写缓冲中的数据呢.
还有这么几种情况,会刷写缓冲中的数据:

  • 有一个后台线程,会认为数据库空闲时;
  • 数据库缓冲池不够用时;
  • 数据库正常关闭时;
  • redo log写满时;

几乎不会出现redo log写满,此时整个数据库处于无法写入的不可用状态。

什么业务场景,适合开启InnoDB的写缓冲机制?

先说什么时候不适合,如上文分析,当:

  • 数据库都是唯一索引;
  • 或者,写入一个数据后,会立刻读取它;

这两类场景,在写操作进行时(进行后),本来就要进行进行页读取,本来相应页面就要入缓冲池,此时写缓存反倒成了负担,增加了复杂度。

什么时候适合使用写缓冲,如果:

  1. 数据库大部分是非唯一索引;
  2. 业务是写多读少,或者不是写后立刻读取;

可以使用写缓冲,将原本每次写入都需要进行磁盘IO的SQL,优化定期批量写磁盘。
例如,账单流水业务。

重要参数设置

参数:innodb_change_buffer_max_size
介绍:配置写缓冲的大小,占整个缓冲池的比例,默认值是25%,最大值是50%。
写多读少的业务,才需要调大这个值,读多写少的业务,25%其实也多了。

参数:innodb_change_buffering
介绍:配置哪些写操作启用写缓冲,可以设置成all/none/inserts/deletes等。

转载自

https://juejin.cn/post/6845166890470342670

你可能感兴趣的:(MySQL - InnoDB Buffer Pool)