跳至主要內容

了解 Buffer Pool

AruNi_Lu数据库MySQL约 2750 字大约 9 分钟

本文内容

1. 为什么需要 Buffer Pool?

我们都知道,MySQL 中的数据都是放在 磁盘 上的,凡是跟磁盘打交道,基本上都需要在内存中建立 缓存,因为磁盘的读写速度非常慢。

例如,Linux 在内存和磁盘之间就建立了一个缓冲区 Buffers,主要有两个 好处

  • 写操作时,如果 Buffers 中有该页,则直接在 Buffers 中写,如果没有则将该页先读入 Buffers,再在 Buffers 中写,后续再统一写回磁盘。也就是 将小量多次的写磁盘操作转换成了大量少次,大大提升了写的效率;
  • 读操作时,因为 Buffers 中就是最新的数据,所以可以直接从 Buffers 中读取,而不用到低速的磁盘上读,也大大提升了读的效率;

所以,MySQL 也建立了一个 缓冲池,由于 MySQL 的操作单位是页(一页 16K),所以如果需要访问某个页中的数据,就需要把 完整的页加载到内存中

就算只访问页中的一条记录,也需要加载整个页到内存中。

对这个页访问完毕后,也 不会立即将该页刷回磁盘,这样当后续再访问此页中的数据时,就可以直接在内存中操作了,从而节省磁盘的 I/O 开销。

MySQL 有专门的算法应对内存不足时如何淘汰页,后续文章会讲到。

2. 什么是 Buffer Pool?

为了缓冲磁盘中的页,InnoDB 在 MySQL 实例启动时就会向操作系统申请一片连续的内存,这片内存就叫 Buffer Pool(缓冲池)。

所以,Buffer Pool 是在 InnoDB 存储引擎层实现的。

这片内存空间的大小可以由 innodb_buffer_pool_size(单位字节)配置,默认大小为 128MB。一般建议设置成可用物理内存的 60%~80%。

3. Buffer Pool 缓存什么?

前面说到,MySQL 是以页为单位来进行数据存储的,所以 Buffer Pool 也是按页进行划分,我们把这些页称为 缓冲页。那么这些缓冲页都存放些什么内容呢?

Buffer Pool 不只缓存了 **数据页 **和 索引页,还包括了 插入缓冲页、undo 页、自适应哈希索引和一些锁信息 等。

需要注意的是,redo log 有自己的缓冲区,叫 redo log buffer。同时,还会有额外的内存池供其他操作使用。

所以,MySQL 的大致内存结构如下图:

image-20230305095743564

4. 如何管理缓冲页?

Buffer Pool 中的缓冲页这么多,怎么管理是个大问题,如何方便的获取我想要的页呢?

这就要说到 Buffer Pool 的内部组成了,InnoDB 为每个缓冲页都创建了一些 控制信息。这些控制信息包括 该页所属的表空间编号、页号、缓冲页在 Buffer Pool 中的地址 等,控制信息被放在一个叫 控制块 的内存中。

控制块和缓冲页是一一对应的,它们都被放在 Buffer Pool 中,控制块在前,整个内存空间结构图如下所示:

image-20230305101239267

可以看见,控制块和缓冲页之间还有一小块 内存碎片。这是因为在分配了足够多的控制块和缓冲页后,剩余的一小块空间不够一对控制块和缓冲页了,那么这块内存空间自然也就用不到了,也就是碎片空间。当然,如果把 Buffer Pool 大小设置得刚刚好,也有可能不会产生内存碎片。

注:控制块大约占缓冲页大小的 5%,innodb_buffer_pool_size 并不包含这些缓冲块的空间。所以 这片连续的内存空间大小会比 innodb_buffer_pool_size 大 5% 左右

下面就来看看,几个常见的页是如何被具体管理的。

4.1 如何管理空闲页?

Buffer Pool 是一片连续的内存空间,然后把它划分成若干对控制块和缓冲页。但是,刚开始并没有将磁盘中的页缓存到 Buffer Pool 中(还没使用到)。随着 MySQL 的运行,才会不断地将磁盘中的页被缓存到 Buffer Pool 中。

那么在要放入 Buffer Pool 时,如何放到未被使用过的缓冲页上呢?总不可能通过遍历的方式寻找空闲的缓冲页吧。

所以,我们需要在一个地方记录哪些缓冲页是可用的,这时候控制块就派上大用场了。我们可以 把所有空闲的缓冲页对应的控制块作为节点,放入一个链表中,这个链表称为 free 链表(空闲链表)。

img

为了方便管理 free 链表,还定义了一个头节点,包含链表的头节点地址、尾节点地址以及链表节点的数量。

有了 free 链表,我们每次将磁盘页载入缓冲页时,就可以通过这个链表找到空闲缓冲页的控制块,然后再通过控制块找到这个空闲缓冲页,最后将该空闲缓冲页对应的控制块从 free 链表中移除即可。

链表的头节点并不包含在 Buffer Pool 的内存中,而是在上面讲到的额外内存池中单独申请一块内存空间。

> 小插曲:如何判断访问的页是否在 Buffer Pool 中?

前文说过,当我们需要访问某个页中的数据时,如果该页已经在 Buffer Pool 中,则可以直接使用,不在才需要加载进来。

那么,怎么判断访问的页是否在 Buffer Pool 中呢?难道依次遍历 Buffer Pool 中的所有缓冲页?这样显然效率很低。

我们其实是根据 表空间 + 页号来定位一个页的,所以可以使用 哈希表 来记录数据页和缓冲页控制块的映射关系。

具体来说,key 就是表空间 + 页号,value 就是缓冲页控制块的地址。这样我们在访问页时,就可以通过这个 key 快速判断这个页在 Buffer Pool 中有没有对应的缓冲页。

4.2 如何管理脏页?

前文说过,建立缓冲区不仅可以提高读性能,也能提高写性能,而且在写操作结束后,并不是立马刷回磁盘。

所以如果修改了某个缓冲页中的数据,那么它就与磁盘上的页数据不一致了,这样的页就称为 脏页,后续会有后台线程将脏页刷回磁盘(后面会讲解)。

同样,为了快速判断哪些缓冲页是脏页,又创建了一个 flush 链表,与 free 链表类似。

img

有了 flush 链表后,后台线程就可以直接遍历 flush 链表,将脏页刷回磁盘了。

4.3 脏页何时被刷回磁盘?

修改数据时,Buffer Pool 中的 脏页并不是立刻刷回磁盘的,这样当后续操作还要使用该页时,就可以直接在 Buffer Pool 中操作,因为 Buffer Pool 中的数据就是最新的

那如果脏页还没来得及刷回磁盘,这时候 MySQL 实例宕机了,数据会不一致吗?

不会的,数据的一致性是由 redo log 保证的,InnoDB 采用 WAL 技术(Write Ahead Log),在更新数据时,会 先写日志,再写磁盘。通过 redo log,MySQL 就有了 crash-safe 的能力,也就是崩溃恢复能力。这个在日志相关文章中会详细讲解。

脏页的刷盘时机有下面几种情况:

  • 空闲时,会有 后台线程 定时将适量的脏页刷回磁盘;

  • Buffer Pool 空间不足时,会淘汰一部分缓冲页,如果是脏页,则需要先刷回磁盘再淘汰;

  • redo log 写满时,也会将脏页刷回磁盘;

    因为数据的一致性就是靠 redo log 保证的,而 redo log 是一个 环状 日志。所以它写满时,后续的新记录会覆盖旧记录,那当然要先将脏页刷回磁盘了,否则就可能出现数据不一致的情况。

  • MySQL 正常关闭之前,会将所有的脏页刷回磁盘。

可以发现,刷脏页是一个常态,而且只有空闲时才会使用后台线程,所以需要注意 刷脏页对数据库性能的影响

出现以下这两种情况,都是会明显影响性能的:

  • Buffer Pool 空间不足时,如果一个查询 要淘汰的脏页个数太多,就会导致 查询的响应时间明显变长
  • redo log 写满时更新会全部堵住,写性能跌为 0,这种情况对敏感业务来说是不能接受的。

为了避免上述情况,我们可以适当调大 Buffer Pool 空间或 redo log 的大小。

4.4 LRU 链表的管理

除了上面提到的两个链表外,其实还有一个 非常重要LRU 链表

因为 Buffer Pool 空间是有限的,所以 当 free 链表中的缓冲页使用完之后,就需要将一些旧的缓冲页淘汰掉。显然,不能乱淘汰,我们要 尽量保留一些访问频率高的缓冲页。因此,就出现了 LRU 链表。

当然,LRU 链表中也会有脏页,因为 LRU 是根据最近访问时间和访问频率来进行保留和淘汰的。

上面说的 Buffer Pool 空间不足时,会淘汰一部分缓冲页,如果是脏页,则需要先刷回磁盘再淘汰,就是从 LRU 中筛选。

关于 LRU 的讲解后续会有详细的文章,本文只是简单介绍一下 Buffer Pool。

5. 总结

InnoDB 设计了一个缓冲池 Buffer Pool,用来提高数据库的读写性能。

Buffer Pool 中划分出了一个个缓冲页,InnDB 使用链表来管理这些缓冲页:

  • free 链表(空闲页链表):管理还没有使用的空闲页;
  • flush 链表(脏页链表):管理有过数据修改的脏页;
  • LRU 链表:管理 脏页 + 干净页(只读的页),将最近且经常访问的数据进行保留,而淘汰不常访问的数据。

img

此外,还需要特别注意脏页的刷盘时机,以免出现性能问题。

6. 参考文章

上次编辑于: