
MySQL · 性能优化· InnoDB buffer pool flush 策略漫谈
背景
我们知道 InnoDB 使用 buffer pool 来缓存从磁盘读取到内存的数据页。buffer pool 通常由数
个内存块加上一组控制结构体对象组成。内存块的个数取决于 buffer pool instance 的个数,
不过在 5.7 版本中开始默认以 128M(可配置)的 chunk 单位分配内存块,这样做的目的是为了支
持 buffer pool 的在线动态调整大小。
Buffer pool 的每个内存块通过 mmap 的方式分配内存,因此你会发现,在实例启动时虚存很高,
而物理内存很低。这些大片的内存块又按照 16KB 划分为多个 frame,用于存储数据页。
虽然大多数情况下 buffer pool 是以 16KB 来存储数据页,但有一种例外:使用压缩表时,需要在
内存中同时存储压缩页和解压页,对于压缩页,使用 Binary buddy allocator 算法来分配内存
空间。例如我们读入一个 8KB 的压缩页,就从 buffer pool 中取一个 16KB 的 block,取其中 8KB,
剩下的 8KB 放到空闲链表上;如果紧跟着另外一个 4KB 的压缩页读入内存,就可以从这 8KB 中分裂
4KB,同时将剩下的 4KB 放到空闲链表上。
为了管理 buffer pool,每个 buffer pool instance 使用如下几个链表来管理:
LRU 链表包含所有读入内存的数据页;
Flush_list 包含被修改过的脏页;
unzip_LRU 包含所有解压页;
Free list 上存放当前空闲的 block。
另外为了避免查询数据页时扫描 LRU,还为每个 buffer pool instance 维护了一个 page hash,
通过 space id 和 page no 可以直接找到对应的 page。
一般情况下,当我们需要读入一个 Page 时,首先根据 space id 和 page no 找到对应的 buffer
pool instance。然后查询 page hash,如果 page hash 中没有,则表示需要从磁盘读取。在
读盘前首先我们需要为即将读入内存的数据页分配一个空闲的 block。当 free list 上存在空闲的
block 时,可以直接从 free list 上摘取;如果没有,就需要从 unzip_lru 或者 lru 上驱逐 page。
这里需要遵循一定的原则(参考函数 buf_LRU_scan_and_free_block , 5.7.5):
1. 首先尝试从 unzip_lru 上驱逐解压页;
2. 如果没有,再尝试从 Lru 链表上驱逐 Page;
3. 如果还是无法从 Lru 上获取到空闲 block,用户线程就会参与刷脏,尝试做一次 SINGLE
PAGE FLUSH,单独从 Lru 上刷掉一个脏页,然后再重试。
Buffer pool 中的 page 被修改后,不是立刻写入磁盘,而是由后台线程定时写入,和大多数数据
库系统一样,脏页的写盘遵循日志先行 WAL 原则,因此在每个 block 上都记录了一个最近被修改时
的 Lsn,写数据页时需要确保当前写入日志文件的 redo 不低于这个 Lsn。
然而基于 WAL 原则的刷脏策略可能带来一个问题:当数据库的写入负载过高时,产生 redo log 的
速度极快,redo log 可能很快到达同步 checkpoint 点。这时候需要进行刷脏来推进 Lsn。由于
这种行为是由用户线程在检查到 redo log 空间不够时触发,大量用户线程将可能陷入到这段低效
的逻辑中,产生一个明显的性能拐点。
评论