InnoDB 的 LRU 策略分析
准备
- MySQL 8.0.25
参数解释
innodb_old_blocks_pct: 在 Buffer Pool 的 LRU list 中 old 部分所占的比例.
innodb_old_blocks_time: 当一个 Page 距第一次被访问的时间大于等于 innodb_old_blocks_time 时,再次被访问的时候,会被移动到 LRU list 的头部.
LRU list
InnoDB 的 Buffer Pool 使用 LRU 算法管理数据 Page, 为了防止全表扫描或者范围查询造成对 LRU 链表的污染, InnoDB 将 LRU 分为两个部分: young / old :
young 区域代表经常访问的数据 Page.
old 区域代表不常访问的数据 Page.
上图显示了 Buffer Pool 的布局.
5/8 的 “young” 区域和 3/8 的 “old” 区域划分是参数 innodb_old_blocks_pct 的默认值 37 决定的,这个参数可以动态调整.
源码分析
LRU 初始化
InnoDB 在启动时针对 Buffer Pool 进行初始化, 完成 Buffer Pool 的初始化后使用 100 * 3 / 8 = 37 来调整 LRU list 的 young 和 old 的区域.
插入 LRU old
当我们需要从从 Buffer Pool 中读取一个 Page, 并且这个 Page 需要从文件中进行读取时buf_page_init_for_read()
, 我们会从 Buffer Pool 中申请一个 Free Page, 之后需要插入 LRU 的 old 的头部区域buf_LRU_add_block()
, 即 old->head:
1 | 新读取的 Page 插入位置 |
插入 LRU young
LRU 区分了 young 和 old 区域,所以需要适时的将 old 区域的 Page 根据需求移动至 young 区域, 操作过程也比较简单,直接从 LRU 的 old 区域摘除然后插入 young 区域即可buf_page_make_young()
:
以下是插入 LRU young 区域的时机:
btr_search_guess_on_hash():
buf_page_optimistic_get():
buf_page_get_known_nowait():
Buf_fetch
::single_page(): 对于通过 buf_page_get_gen()
且 mode 不是 Page_fetch::SCAN 和 Page_fetch::PEEK_IF_IN_POOL 这两种的都会将 Page 插入 LRU list 的 young 区域.
LRU evict
Buffer Pool 的容量是有限的,为了用户的写入读取能获取 Free Page, Buffer Pool 要不停的从 LRU list 置换 “old” Page: 策略是从 Buffer Pool 的 old list 的尾部扫描合适的 Page 换出.
1 | LRU evict 起始位置 |
以下是 LRU evict 数据 Page 的时机:
buf_page_io_complete(): 当从 LRU list 刷脏完成后,会将 Page 从 LRU list 中移除.
buf_flush_LRU_list_batch(): 扫描 LRU list 时,将满足条件的 Page
buf_flush_ready_for_replace()
换出.buf_flush_single_page_from_LRU(): 当用户需要获取空闲 Page 而 LRU List 暂时没有 Free Page 时, 会选择一个 Page 直接换出
buf_flush_ready_for_replace()
或者buf_flush_ready_for_flush()
刷入磁盘.
总结
当一个 Page 从 disk 读入 Buffer Pool 后, 先插入 old 区域起始位置, 后续的非 scan mode 的读则会调整插入 young 区域. 在 young 区域的 page 假如再次被读到,会通过buf_page_peek_if_young()
判断是否接近被 evict, 否则在 young 中是不会调整 page 的顺序的.