【MySQL自身的性能优化】InnoDB 的 Buffer Pool

发布时间:2024年01月19日


有近俩个来月没有写博客了,也不知道自己在瞎忙活什么。之前我有写过一篇关于 MySQL 的内存池一篇博客 InnoDB体系架构之内存池(buffer pool),那是看楠哥视频后总结出来的。然后现是看了《MySQL 是怎样运行的》一书,也可能是基础相比之前要好了些,理解的要好点了吧,就再总结一篇,现进入正文。

一、引入缓存的重要性

无论是系统数据还是用于存储用户数据的索引(包括聚簇索引和二级索引),都是以页为基本单位存放在表空间中。所谓的表空间,只不过是 InnoDB 对一个或几个实际文件的抽象(ibd)。说到底数据是存放在磁盘上的,那磁盘的速度是非常慢的,而且在磁盘缓冲区和内存之间进行交互的时候也是需要霸占着CPU的,不管是在性能上还是在资源上都是一种不好的选择。所以 InnoDB 存储引擎在处理客户端的请求时,如果需要访问某个页的数据,就会把完整的页中的数据全部加载到内存中。也就是说,即使你是需要访问某索引页的一条记录,也需要先把整个页的数据加载到内存中(这一方面是和MySQL存储数据是在索引中,另一方面就是为了更好的缓存数据)。将整个页加载到内存中后就可以进行读写访问了,而且在读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省下磁盘 I/O 的开销了。

二、InnoDB 的 Buffer Pool

为缓存上面提到的磁盘中的页,在 MySQL 服务器启动时就向操作系统申请了一片连续的内存,这块内存的名字就叫 Buffer Pool(缓冲池)
查看 innodb_buffer_pool_size 可以查看到其申请的内存大小,我这分配的是 128mb;
在这里插入图片描述

1. Buffer Pool 内部组成

整个Buffer Pool 由控制块、缓冲页和碎片组成。

  • 控制块:它记录着缓冲页的描述信息,比如该页所属的表空间编号、页号、缓冲页在 Buffer Pool 中的地址等等。设置的 innodb_buffer_pool_size 的大小不包含这个
  • 缓冲页:buffer pool 中存放的【数据页】称之为【缓冲页】,和磁盘上的数据页是一一对应的,都是16KB,缓冲页的数据,是从磁盘上加载到 buffer pool 当中的一个完整页。
  • 碎片:若设置的Buffer Pool 放满了缓冲页,剩余的内存则称之为碎片。

在这里插入图片描述

2. free 链表管理空闲页

在最初启动 MySQL 服务器的时候,需要完成 Buffer Pool 的初始化工作。先向系统中申请 Buffer Pool 的内存空间,然后把它划分成若干对控制块和缓冲页。此时是没有磁盘页被存到 Buffer Pool 中的。
而有个 free 链表就是用来管理这些空闲页的,以控制块作为节点,进行连接组成的一个链表
在这里插入图片描述当有磁盘页需要被缓存到 Buffer Pool 中时,就可以进行下面操作

  1. 可以取出一个空闲的缓冲页;
  2. 然后将其描述信息填入控制块;
  3. 最后将 free 链表中的对应节点删除,表示已经不再空闲,而哪个磁盘页就放到对应的缓冲页的位置上。
  4. 然后将 表空间号 + 页号 作为 key,控制块地址作为 value,存入到一个哈希表中,方便后续读取或写对应的缓冲页。

若读某页的逻辑就大概如下:
在这里插入图片描述

3. flush 链表管理脏页

设计 Buffer Pool 除了可以提高读性能,也可提高写性能。当需要更新数据的时,若可以根据 表空间号 + 页号 可以找到对应的控制块地址,说明 Buffer Pool 存在对应的缓冲页,那么进行写操作可以选择直接对 缓冲页 进行写操作,那么它就和对应磁盘上的数据不一致了,这样的缓冲页也被称为脏页

这些脏页的控制块最后也会被拼接成一个链表——flush 链表
在这里插入图片描述和 Free 链表是一样的,只是区别是 Free 链表的结点是空闲缓冲页的控制块,而 Flush 链表的结点是脏页的控制块。
有了 Flush 链表之后,后台线程就可以通过遍历这个链表,然后将脏页写入到磁盘

在这里插入图片描述从上图的InnoDB存储引擎的体系架构中看出其后台线程所担任的角色:
负责刷新内存池中的数据,保证缓存池中的内存缓存是最近的数据。此外将已修改的数据文件刷新到磁盘文件中,同时保证在数据库中发生异常的情况下 InnoDB 能恢复到正常运行状态。(如:Master Thread:负责将缓存池中的数据异步刷新到磁盘;IO Thread:异步处理IO请求,提高数据库的性能

4. LRU 链表提高缓存命中

看见 LRU(Least Recently Used) 第一反应就是最近最少使用淘汰策略
这里使用它的目的就是因为内存的针对,Buffer Pool 的内存资源也是有限的,当无法缓存新的数据的时候,希望把一些不用的缓冲页给淘汰掉,空闲出缓冲页供新的数据页使用。
针对上面的需求,Buffer Pool 使用了 LRU 淘汰策略去实现。

简单的 LRU 算法的实现思路是这样:

  • 当访问的页在 Buffer Pool 里,就直接把该页对应的 LRU 链表节点移动到链表的头部;
  • 当访问的页不在 Buffer Pool 里,除了要把页放入到 LRU 链表的头部,还要淘汰 LRU 链表末尾的节点。

阐述了这三种链表,咱就知道 Buffer Pool 是怎样管理数据的了:
在这里插入图片描述解释图:

  • Free Page(空闲页),表示此页未被使用,位于 Free 链表;
  • Clean Page(干净页),表示此页已经被使用,但是页面未发生修改,位于 LRU 链表;
  • Dirty Page(脏页),表示此页【已被使用】且【已经被修改】,其数据和磁盘上的数据已经不一致了。当脏页上的数据写入磁盘后,内存数据和磁盘数据一致,那么该页就变成了干净页。脏页同时存在于 LRU 链表和 Flush 链表。

简单的 LRU 算法并没有被 MySQL 使用,因为简单的 LRU 算法无法避免下面这俩个问题:

  • 预读失效;
  • Buffer Pool 污染;

何为预读?
Innodb 提供了一个看起来比较贴心的服务——预读。所谓预读,就是 InnoDB 认为执行当前的请求时,可能会在后面读取某些页面,于是就预先把这些页面加载到 Buffer Pool 中。默认是线性预读,而不是随机预读。

咱就是说预读本来是个好事,但是如果预读的页用不到呢?插入到 LRU 链表的头部,然后满了的话把尾部的页都淘汰掉,从而大大降低 Buffer Pool 的命中率。

大部分情况下局部性原理还是可靠的。

那咱需要咋地解决预读问题呢?

想要避免这种预读效果带来的影响,最好就是让预读页停留在 Buffer Pool 里的时间尽可能的短,让真正被访问的页才移动到 LRU 链表的头部,从而保证真正被读取的热数据留在 Buffer Pool 里的时间尽可能长

MySQL 改进了 LRU 算法,将 LRU 划分了俩个区域:Old 区域 和 Young 区域

在这里插入图片描述Young区域和Old区域在 LRU 链表的占比是由 innodb_old_blocks_pct 参数设置的。

划分这俩个区域后,预读的页就只需要加入到 Old 区域的头部,当页被真正访问的时候,才将页插入 Young 区域的头部。如果预读的页一致没有被访问,就会从 Old 区域移除,这样就不会影响 Young 区域的热点数据了。

假设有一个长度为 10 的 LRU 链表,其中 young 区域占比 70 %,old 区域占比 30 %。
在这里插入图片描述

现在有个编号为 20 的页被预读了,这个页只会被插入到 old 区域头部,而 old 区域末尾的页(10号)会被淘汰掉。

在这里插入图片描述

如果 20 号页一直不会被访问,它也没有占用到 young 区域的位置,而且还会比 young 区域的数据更早被淘汰出去。

如果 20 号页被预读后,立刻被访问了,那么就会将它插入到 young 区域的头部,young 区域末尾的页(7号),会被挤到 old 区域,作为 old 区域的头部,这个过程并不会有页被淘汰。

在这里插入图片描述

虽然通过划分 old 区域 和 young 区域避免了预读失效带来的影响,但是还有个问题无法解决,那就是 Buffer Pool 污染的问题。

何为 Buffer Pool 污染?
当某个 SQL 语句扫描了大量的数据时,在 Buffer Pool 空间比较有限的情况下,可能会将 Buffer Pool 里的所有页都替换出去,导致大量数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未能命中,就会产生大量的磁盘 IO,MySQL 性能就会急剧下降,这个过程被称为 Buffer Pool 污染
只要咱说无用查询,这种LRU策略就会引起 Buffer Pool 污染。

比如数据量很大的表 t_user,执行了下面这个查询:

select * from t_user where name like "%xiaolin%";

即时这查询的结果就几条,但是这也会引起索引失效,这样就得进行全表扫描(就是说从聚簇索引的叶子节点记录、节点遍历),接下来会发生下面这些过程:

  • 从磁盘读到的页加入到 LRU 链表的 Old 区域头部;
  • 当从页里读取行记录时,也就是页被访问的时候,就要将该页放到 Young 区域头部;
  • 接下来拿行记录的 name 字段和字符串 xiaolin 进行模糊匹配,如果符合条件,就加入到结果集里;
  • 如此往复,直到扫描完表中的所有记录。

经过这一番折腾,原本 Young 区域的热点数据都会被替换掉。

举个例子,假设需要批量扫描:21,22,23,24,25 这五个页,这些页都会被逐一访问(读取页里的记录)。
在这里插入图片描述

在批量访问这些数据的时候,会被逐一插入到 young 区域头部。

在这里插入图片描述

可以看到,原本在 young 区域的热点数据 6 和 7 号页都被淘汰了,这就是 Buffer Pool 污染的问题。

那咱需要咋地解决 Buffer Pool 污染问题呢?

很多缓存页只是被访问了一次,但是却只因为被访问了一次而进入到 Young 区域,从而导致热点数据被替换了。现在我们只要提高 Young 区域的门槛,这样就可以有效地保证 Young 区域里的热点数据不会被替换掉。

MySQL 是这样做的,进入到 Young 区域条件增加了一个停留在 Old 区域的时间判断。

具体是这样做的,在对某个处在 Old 区域的缓存页进行第一次访问时,就在它对应的控制块中记录下来这个访问时间:

  • 如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该缓存页就不会被从 old 区域移动到 Young 区域的头部;
  • 如果后续的访问时间与第一次访问的时间不在某个时间间隔内,那么该缓存页移动到 young 区域的头部;

这个间隔时间是由 innodb_old_blocks_time 控制的,默认是 1000 ms。

就是说,只有同时满足【被访问】与【在 Old 区域停留时间超过 1 秒】俩个条件,才会被插入到 Young 区域头部,这样就解决了 Buffer Pool 污染问题。

另外,MySQL 针对 young 区域其实做了一个优化,为了防止 young 区域节点频繁移动到头部。young 区域前面 1/4 被访问不会移动到链表头部,只有后面的 3/4被访问了才会。

5. 脏页什么时候被刷入磁盘?

若修改数据时 Buffer Pool 有这个数据,那么自然会直接修改 BufferPool 控制块映射的缓存页,这样一来这个缓存页就被称为脏页,其控制块也会被放入到 Flush 链表中进行管理。

但是脏页终究还是要被刷入磁盘的,保证缓存和磁盘数据的一致,一般情况下为了不影响性能,都会在一定时机进行批量刷盘。

可能大伙担心,如果脏页还没有来得及刷入到磁盘内,MySQL 宕机了,不就丢失数据了吗?
这个不用担心,InnoDB 考虑到这一点,更新操作采用的是 Write Ahead Log 策略,就是说先写入日志,再写入磁盘,通过 redo log 日志让 MySQL 拥有了崩溃恢复的能力

下面几种情况会触发脏页的刷新:

  • 当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘;
  • Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
  • MySQL 认为空闲时,后台线程会定期将适量的脏页刷入到磁盘;
  • MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;

在开启慢 SQL 监控后,如果你发现 【偶尔】会出现用时稍长的 SQL ,这可能是因为脏页在刷新到磁盘时可能会给数据库带来性能开销,导致数据库操作抖动。

如果间断地出现这种现象,就需要调大 Buffer Pool 空间或 Redo Log 日志的大小。

三、总结

为尽可能地降低磁盘 IO,来提高数据库的读写性能,InnoDB 存储引擎设计了一个 缓冲池(Buffer Pool)

Buffer Pool 以页为单位缓冲数据,可以通过 innodb_buffer_pool_size 参数调整缓冲池的大小,默认是 128 M。

Buffer Pool 的内部组成有三种角色,控制块、缓冲页、碎片

InnoDB 通过三种链表来管理缓冲页:

  • Free List(空闲页链表),管理空闲页;
  • Flush List(脏页链表),管理脏页;
  • LRU List(淘汰链表),管理脏页+干净页,将最近且经常查询的数据缓存在其中,而不常查询的数据就淘汰出去。

InnoDB 对 LRU 做了一些优化,我们熟悉的 LRU 算法通常是将最近查询的数据放到 LRU 链表的头部,而 InnoDB 做了以下俩点优化(当然还有很多优化点,这里阐述的是重要的俩个):

  • 将 LRU 链表分为了 Young 和 Old 区域俩个部分加入缓冲池的页优先被放到 Old 区域,页被访问时,才进入到 Young 区域,目的是为了解决预读失效问题
  • 当 【页被访问】 和 【Old 区域停留时间超过 innodb_old_blocks_time 阈值(默认为1秒)】 时,才会将页插入到 Young 区域,否则还是插入到 Old 区域,目的是为了解决批量数据访问(如全表扫描),大量热数据淘汰的问题。

可以通过调整 innodb_old_blocks_pct 参数,设置 young 区域和 old 区域比例。

在开启了慢 SQL 监控后,如果你发现 【偶尔】 会出现一些用时稍长的 SQL,这是因为脏页在刷新到磁盘时导致数据库性能抖动。如果在很短的时间出现这种现象,就需要调大 Buffer Pool 空间或 redo log 日志的大小。

参考文献:

  1. 《MySQL是怎样运行的》第十七章 InnoDB 的 Buffer Pool。
  2. 小林的解开Buffer Pool 的面纱
文章来源:https://blog.csdn.net/qq_63691275/article/details/135599955
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。