MySQL「07」Buffer pool

238 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第07,点击查看活动详情

InnoDB 中,内存、磁盘之间交互的基本单位是页。 即使是我们只访问某个页中的一条记录,InnoDB 也会将该记录所属的页整个加载到内存中。 在读写操作之后,相关页并不会立即写回磁盘,而是缓存起来,以便将来访问时能够快速返回结果。

01-缓冲池结构

Buffer Pool(缓冲池)是一块连续的内存空间,默认情况下只有128MB(如果只缓存页,可以缓存8192个)。 空间大小可已通过参数 innodb_buffer_pool_size 进行调节(注:最小为5MB,再小便不会生效),一般建议设置成可用物理内存的60%~80%。 缓冲池中存储了两类数据:

  • 页,从磁盘中加载的页(可能被修改过,称为脏页)
  • 控制块,与某个页关联,存储的是该页相关的控制信息,例如表空间编号、页号、缓存页在缓冲池中的地址等;

注:innodb_buffer_pool_size 参数指定的空间大小不包括控制块

页与其关联控制块一一对应。 且页与控制块分别存储在缓冲池的两端,其分布如:|控制块|可用空间|页|

img_mysql-innodb-buffer-pool.png

注:如果 innodb_buffer_pool_size 设置的不是页大小的整数倍,可用空间中会存在一部分不能利用的碎片。 MySQL 会自动将其对齐到 innodb_buffer_pool_chunk_size 表示的整数倍,例如134217730 > 128MB(134217728),最终查询出的值为268435456(256MB)。 innodb_buffer_pool_chunk_size 是一个只读变量,默认为128MB,在会话中不可修改。

Buffer Pool 中有几个核心的数据结构,按照类型可分为:

  1. 链表
    • free 链表,表示空闲、可用空间。
    • flush 链表,表示脏页,需要刷新到磁盘的内容。
    • LRU 链表,用于淘汰旧页
  2. 哈希表
    • 表空间+页号 -> 控制块,用来判断某个页是否存在 buffer pool 中。

接下来,我们按照上述的顺序,逐个来介绍这些数据结构的一些相关内容。

01.1-free 链表的管理

所有空闲页对应的控制块组成的链表,称为空闲链表,表示可用的缓冲区。

01.-flush 链表的管理

当缓冲池中页面被修改后(与磁盘中对应页不一致),称为脏页,需要写回到磁盘上。 出于性能考虑,不能每次修改都从缓冲池刷新到磁盘。 所以,将所有的脏页对应的控制块串联起来形成链表,就是 flush 链表。

InnoDB 会适当地将 flush 链表中的内容刷新到磁盘中。

01.3-LRU 链表的管理

缓冲池是有大小限制的。 如果空闲列表为空,即无可用空间时,需要从缓冲池中移除一部分缓存的页。 移除规则就是 LRU,按照最近最少使用原则。 实现 LRU 有两种基本的策略:

  1. 简单 LRU 链表。只要使用到(初次加载、命中)缓冲池中的页,就将其置于 LRU 链表头部。
  2. 分区 LRU 链表。(InnoDB 实际使用的策略)。

情形一、由于 InnoDB 会预读页到缓冲区,所以按照简单 LRU 策略,如果缓冲区空间较小,预读进来的页会将之前的页从缓冲区中挤出,导致命中率下降。 情形二、全量扫描表的查询会导致缓冲池中的缓存“大换血”,导致命中率下降。 总结一下就是:

  • 加载到缓冲池中的页不一定会用到,预读失效
  • 如果非常多使用率偏低的也被同时加载到缓冲池中时,可能会把使用频率较高的缓存页淘汰出去,缓冲池污染

基于上述考虑,InnoDB 中对简单 LRU 链表进行了分区优化(按照比例,可以通过 innodb_old_blocks_pct 控制 old 区域所占比例)。

  • 一部分存储使用频率非常高的缓存页,称为热数据,或 young 区域;
  • 另一部分用来存储使用频率不是很高的缓存页,称为冷数据,或 old 区域。

01.4-如何判断某个页是否在缓冲池中

表空间 ID + 页号,可唯一定位一个页,将其作为哈希表的 key,控制块作为 value。 如果某个页在哈希表中,则返回; 否则,从空闲列表中取下一个可用页,将磁盘中的数据加载到该页中。

02-缓冲池的工作机制

缓冲池中的页分为三类:

  • 脏页,指被语句更新过,与磁盘中的页不一致,需要刷新到磁盘中。在 flush 链表中管理。
  • 干净页,与磁盘中的页内容一致。
  • 空闲页,尚未被使用的缓冲池空间。

img_mysql-pages-in-buffer-pool.png

02.1-flush 链表刷磁盘的时机

  1. redo log 满了(此时指的应该是磁盘上的 redo log 文件)。 如果 redo log block 中关联的脏页尚未被刷新到磁盘上时,此 block 是不能被覆盖的。 要继续写 redo log,就需要将相关的脏页刷新到磁盘中。
  2. 空闲链表中资源不足,此时需要淘汰缓冲区中的一些页。 此时,淘汰的页有可能是干净页,也有可能是脏页;若是后者,需要将内容刷新到磁盘。
  3. 后台线程定时刷新脏页到磁盘。
  4. 服务正常关闭时。

第3、第4中场景,对数据库性能影响不大,或者说这两种场景下是不关注性能的。

第1种场景下,redo log 满了,意味着不能再接受任何更新了。 若要继续更新,必须清理出可用的 redo log。 此时,从监控上看,更新数为0。

第2种场景下,是一种常规现象。

02.2-查看缓冲池的状态

可以通过如下语句查看缓冲池的状态信息。

show engine innodb status \G;

其中,\G 表示将输出结果反转显示(即纵向显示)。 部分输出如下:

*************************** 1. row ***************************
  Type: InnoDB
  Name:
Status:
=====================================
2022-12-02 10:54:31 0x1350 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 46 seconds
---
LOG
---
Log sequence number          64605976
Log buffer assigned up to    64605976
Log buffer completed up to   64605976
Log written up to            64605976
Log flushed up to            64605976
Added dirty pages up to      64605976
Pages flushed up to          64605976
Last checkpoint at           64605976
15 log i/o's done, 0.00 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 412090368
Dictionary memory allocated 386811
Buffer pool size   24576
Free buffers       23496
Database pages     1080
Old database pages 418
Modified db pages  0
Pending reads      0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 938, created 142, written 154
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 1080, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
----------------------------
END OF INNODB MONITOR OUTPUT
============================

1 row in set (0.00 sec)

Buffer pool size 表示当前缓冲池的大小,可以存储页(16KB)的个数。 Free buffers 表示空闲表数。

refs