Administrator
发布于 2022-12-05 / 52 阅读
0
0

InnoDB的Buffer Pool

缓存

数据是存储在磁盘上的,但是磁盘的速度慢的跟乌龟一样,读取的速度跟不上 CPU 处理速度。所以 InnoDB 存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其 缓存 起来,这样将来有请求再次访问该页面时,就可以省去磁盘 IO 的开销了。

Buffer Pool

InnoDB 为了缓存磁盘中的页,在 MySQL 服务器启动的时候就向操作系统申请了一片连续的内存,这片内存叫做 Buffer Pool (中文名是 缓冲池 )。

Buffer Pool内部组成

Buffer Pool 中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是 16KB 。为了更好的管理这些在Buffer Pool 中的缓存页,InnoDB为每一个缓存页都创建了一些所谓的 控制信息 ,这些控制信息包括该页所属的表空间编号、页号、缓存页在 Buffer Pool 中的地址、链表节点信息、一些锁信息以及 LSN 信息

每个缓存页对应的控制信息占用的内存大小是相同的,每个页对应的控制信息占用的一块内存称为一个控制块,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool的前边,缓存页被存放到 Buffer Pool 后边

image-1670210619383

碎片是因为每一个控制块都对应一个缓存页,那在分配足够多的控制块和缓存页后,可能剩余的那点儿空间不够一对控制块和缓存页的大小,这个不够用的那点儿内存空间就被称为 碎片 了。当然,如果你把 Buffer Pool 的大小设置的刚刚好的话,也可能不会产生碎片

free链表

随着程序的运行,会不断的有磁盘上的页被缓存到 Buffer Pool 中。然而从磁盘上读取一个页到 Buffer Pool 中的时候该放到哪个缓存页的位置呢?或者说怎么区分 Buffer Pool 中哪些缓存页是空闲的,哪些已经被使用了呢?

所以最好在某个地方记录一下Buffer Pool中哪些缓存页是可用的,这个时候缓存页对应的 控制块 就派上大用场了,我们可以把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表也可以被称作 free链表 (或者说空闲链表)。刚刚完成初始化的 Buffer Pool 中所有的缓存页都是空闲的,所以每一个缓存页对应的控制块都会被加入到 free链表 中,假设该 Buffer Pool 中可容纳的缓存页数量为 n ,那增加了 free链表 的效果图就是这样的:

image-1670210786353

每当需要从磁盘中加载一个页到 Buffer Pool 中时,就从 free链表 中取一个空闲的缓存页,并且把该缓存页对应的 控制块 的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的 free链表 节点从链表中移除,表示该缓存页已经被使用了

缓存页的哈希处理

当我们需要访问某个页中的数据时,就会把该页从磁盘加载到 Buffer Pool 中,如果该页已经在 Buffer Pool 中的话直接使用就可以了。那么问题是,我们怎么知道该页在不在 Buffer Pool 中呢?

可以用 表空间号 + 页号 作为 key , 缓存页 作为 value 创建一个哈希表,在需要访问某个页的数据时,先从哈希表中根据 表空间号 + 页号 看看有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从 free链表 中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。

flush链表

如果修改了 Buffer Pool 中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为 脏页 (英文名: dirty page )。

脏页在redo日志里有说到,以及怎么刷盘;几时刷盘

现在问题是,在刷盘进行同步的时候我们怎么知道 Buffer Pool 中哪些页是 脏页 ,哪些页从来没被修改过呢?

我们可以创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫 flush链表。链表的构造和 free链表 差不多,假设某个时间点 Buffer Pool 中的脏页数量为 n ,那么对应的 flush链表 就长这样:

image-1670211076649

LRU链表

缓存不够的问题

Buffer Pool 对应的内存大小毕竟是有限的,如果需要缓存的页占用的内存大小超过了 Buffer Pool 大小,也就是 free链表 中已经没有多余的空闲缓存页的时候,要把某些旧的缓存页从 Buffer Pool 中移除,然后再把新的页放进来。

然而在移除页面的时候,我们只能选择那些很少使用的页面进行移除。

LRU链表

为了知道哪些页面是很少使用的,可以创建一个链表,这个链表是为了 按照最近最少使用 的原则去淘汰缓存页
,所以这个链表可以被称为 LRU链表 (LRU的英文全称:Least Recently Used)。

当我们需要访问某个页时,可以这样处理 LRU链表 :

如果该页不在 Buffer Pool 中,在把该页从磁盘加载到 Buffer Pool 中的缓存页时,就把该缓存页对应的
控制块 作为节点塞到链表的头部。

如果该页已经缓存在 Buffer Pool 中,则直接把该页对应的 控制块 移动到 LRU链表 的头部。

只要我们使用到某个缓存页,就把该缓存页调整到 LRU链表 的头部,这样 LRU链表 尾部就是最近最少使用的缓存页

LRU链表优化

LRU链表会出现的问题:

1、InnoDB 支持预读 (英文名: read ahead )。所谓 预读 ,就是 InnoDB 认为执行当前的请求可能之后会读取某些页面,就预先把它们加载到 Buffer Pool 中。根据触发方式的不同, 预读 又可以细分为下边两种:

线性预读
 InnoDB 提供了一个系统变量 innodb_read_ahead_threshold
顺序访问了某个区( extent )的页面超过这个系统变量的值,就会触发一次 异步读取下一个区中全部的页面
到 Buffer Pool 的请求,异步 读取意味着从磁盘中加载这些被预读的页面并不会影响到当前工作线程的正常执行。


随机预读
如果 Buffer Pool 中已经缓存了某个区的13个连续的页面,不论这些页面是不是顺序读取的,都会触发
一次 异步 读取本区中所有其的页面到 Buffer Pool 的请求。不过随机预读默认是不开启的
可以将innodb_random_read_ahead 系统变量设置为on,将其开启

预读本来是个好事,如果预读到 Buffer Pool 中的页成功的被使用到,那就可以极大的提高语句执行的效率。可是如果用不到,这些预读的页都会放到 LRU 链表的头部,但是如果此时 Buffer Pool 的容量不太大而且很多预读的页面都没有用到的话,这就会导致处在 LRU链表 尾部的一些缓存页会很快的被淘汰掉,会大大降低缓存命中率(假设我们一共访问了 n 次页,那么被访问的页已经在缓存中的次数除以 n 就是所谓的 缓存命中率)。

2、需要扫描全表的查询语句(比如没有建立合适的索引或者压根儿没有WHERE子句的查询)。

扫描全表意味着将访问到该表所在的所有页!假设这个表中记录非常多的话,那该表会占用特别多的 页 ,
当需要访问这些页时,会把它们统统都加载到 Buffer Pool 中。

总结就是:

加载到 Buffer Pool 中的页不一定被用到。

如果非常多的使用频率偏低的页被同时加载到 Buffer Pool 时,可能会把那些使用频率非常高的页从
Buffer Pool 中淘汰掉。

对此的解决:
InnoDB 把这个 LRU链表 按照一定比例分成两截,分别是:

一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做 热数据 ,或者称 young区域 。
另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做 冷数据 ,或者称 old区域 。

image-1670211989336

两个区域是按照某个比例将LRU链表分成两半的,不是某些节点固定是young区域的,
某些节点固定是old区域的,随着程序的运行,某个节点所属的区域也可能发生变化。

可以通过命令查看其比例
SHOW VARIABLES LIKE 'innodb_old_blocks_pct';
也可以对其进行修改

对以上两种情况的优化:

1、针对预读的页面可能不进行后续访情况的优化

InnoDB规定,当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应
的控制块会被放到old区域的头部。这样针对预读到 Buffer Pool 却不进行后续访问的页面就会被逐渐从
old 区域逐出,而不会影响 young 区域中被使用比较频繁的缓存页。

2、针对全表扫描时,短时间内访问大量使用频率非常低的页面情况的优化

在对某个处在 old 区域的缓存页进行第一次访问时就在它对应的控制块中
记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被
从old区域移动到young区域的头部,否则将它移动到young区域的头部。
这个间隔时间是由系统变量innodb_old_blocks_time 控制的
SHOW VARIABLES LIKE 'innodb_old_blocks_time';
默认1000ms

更进一步优化LRU链表

对于 young 区域的缓存页来说,我们每次访问一个缓存页就要把它移动到 LRU链表 的头部,这样开销太大,毕竟在 young 区域的缓存页都是热点数据,也就是可能被经常访问的,这样频繁的对 LRU链表 进行节点移动操作不太好。

对此的优化策略:

比如只有被访问的缓存页位于 young 区域的 1/4 的后边,才会被移动到 LRU链表 头部,这样就
可以降低调整 LRU链表 的频率,从而提升性能(也就是说如果某个缓存页对应的节点在 young 区域的 1/4 中,
再次访问该缓存页时也不会将其移动到 LRU 链表头部)。

刷新脏页到磁盘

  1. 从 LRU链表 的冷数据中刷新一部分页面到磁盘。
    后台线程会定时从 LRU链表 尾部开始扫描一些页面,扫描的页面数量可以通过系统变量
    innodb_lru_scan_depth 来指定,如果从里边儿发现脏页,会把它们刷新到磁盘。这种刷新页面的方式被称
    之为 BUF_FLUSH_LRU 。
  2. 从 flush链表 中刷新一部分页面到磁盘。
    后台线程也会定时从 flush链表 中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种
    刷新页面的方式被称之为 BUF_FLUSH_LIST 。

有时候后台线程刷新脏页的进度比较慢,导致用户线程在准备加载一个磁盘页到 Buffer Pool 时没有可用的缓存
页,这时就会尝试看看 LRU链表 尾部有没有可以直接释放掉的未修改页面,如果没有的话会不得不将 LRU链表 尾部的一个脏页同步刷新到磁盘(和磁盘交互是很慢的,这会降低处理用户请求的速度)。这种刷新单个页面到磁盘中的刷新方式被称之为 BUF_FLUSH_SINGLE_PAGE 。

多个Buffer Pool

可以通过设置 innodb_buffer_pool_instances 的值来修改 Buffer Pool 实例的个数。

当innodb_buffer_pool_size的值小于1G的时候设置多个实例是无效的,InnoDB会默认把innodb_buffer_pool_instances 的值修改为1。

innodb_buffer_pool_chunk_size

每次当我们要重新调整 BufferPool 大小时,都需要重新向操作系统申请一块连续的内存空间,然后将旧的 Buffer Pool 中的内容复制到这一块新空间,这是极其耗时的。MySQL 决定不再一次性为某个 Buffer Pool 实例向操作系统申请一大片连续的内存空间,而是以一个所谓的 chunk 为单位向操作系统申请空间。也就是说一个 Buffer Pool 实例其实是由若干个 chunk 组成的,一个 chunk 就代表一片连续的内存空间,里边儿包含了若干缓存页与其对应的控制块。

image-1670212798544

innodb_buffer_pool_chunk_size的值只能在服务器启动时指定,在服务器运行过程中是不可以修改的


评论