InnoDB 的 Buffer Pool 分析
MySQL版本: 8.0
用户使用数据库进行交互数据的,为了弥补磁盘与CPU的速度差距,一般都会采用Cache的方法. 在MySQL中,Buffer Pool就是用来在内存中缓存数据Page的,而换入换出的算法采用的LRU算法.
Buffer Pool的结构

Buffer Pool分为多个Instance,具体的数量由设定的size决定,每个Instance包含多个Chunk, 而Chunk又由多个Page组成. 将Buffer Pool分为多个instance的主要目的是为了减轻在高并发下锁争抢的压力.
Buffer Pool相关参数
- innodb_buffer_pool_size: Buffer Pool总的大小.
- innodb_buffer_pool_instances: Buffer Pool中instance的数量
- innodb_buffer_pool_chunk_size: Buffer Pool中chunk的大小, 默认为128M
Buffer Pool中chunk的大小可以自定义设置, 所以整个Buffer Pool的内存大小分配公式为 n innodb_buffer_pool_chunk_size innodb_buffer_pool_instances, n 为chunk的数量. n 由Buffer Pool初始化阶段计算得出.
重要数据结构
每个Instnace即buf_pool_t主要包含以下结构 :
LRU: 淘汰算法LRU维护的链表flush_list: 被修改过的Page链表:free: 可用的Page链表page_hash: Hash表, 避免查询的时候全量扫描LRU,可以通过space id和page no获取对应的Page, 用于Buffer Pool的Page定位.withdraw: 用于缩小Buffer Pool的大小的过程中回收Page的链表.
Block控制页结构:
1 | struct buf_block_t { |
Buffer Pool中基本数据单元是buf_block_t,其中包括数据Page的元信息和数据等信息.
Buffer Pool的初始化
-
buf_pool_init(): Buffer Pool初始化入口函数 -
buf_pool_create(): 初始化每个instance: 初始化链表, 创建相关mutex, 创建Chunk等. -
buf_chunk_init():- Chunk的初始化, 使用
os_mem_alloc_large()分配内存 - 初始化buf_block_t, 每一个Block包含
block->frame数据页和block->page页信息管理 - 将每一个Block的Page结构即
block->page加入buf_pool->free链表.
- Chunk的初始化, 使用
-
LRU_list分为 young 和 old 两部分, young 部分存储经常被使用的热点page,新读入的Page默认被加在 old 部分,只有满足一定条件后,才被移到 young 上,主要是为了预读的数据页和全表扫描污染Buffer Pool。LRU_old作为一个游标来作为滑动的指针。初始化阶段最后会设定LRU的比率参数:buf_LRU_old_ratio_update(100 * 3 / 8, FALSE). (LRU_old可以由buf_LRU_old_ratio_update()来更新)

对于支持大页的Linux环境,InnoDB使用shmget()申请共享内存, 而一般的Linux环境下使用mmap()申请指定大小的匿名页内存块.
Buffer Pool的操作
当我们需要一个Page的时候会通过Buffer Pool申请, 具体的逻辑:
申请数据页Block
buf_page_create()
-
通过
buf_pool的free_list中查找空闲的block(buf_LRU_get_free_block()). -
假如
free_list中为空,即没有空闲的Block, 则需要去LRU_list中寻找. -
LRU_list搜索策略:-
第一次搜索:
- 假如
buf_pool->try_LRU_scan被设置了true, 则通过lru_scan_itr从尾部往前搜索100(BUF_LRU_SEARCH_SCAN_THRESHOLD)个, 假如找到可以换出的Page,放入free_list. - 假如没有找到可以换出的Page, 则需要从
LRU_list尾部选择一个脏页进行刷盘, 即buf_flush_page(), 将其从page_hash和LRU中移除,然后放入free_list.
- 假如
-
第二次搜索:
- 需要搜索整个
LRU_list, 其余的策略与第一次搜索一致.
- 需要搜索整个
-
第三次搜索及以后:
- 仍然搜索整个
LRU_list但每次sleep 10ms
- 仍然搜索整个
-
当迭代次数超过20次,会打印一条频繁获取不到空闲Page的log.
-
-
判断Buffer Pool是否存在该Block, 假如存在直接返回.
-
否则需要通过
buf_page_init()初始化Block数据结构,将查找到的空闲的Block的信息替换为需要申请的这个Block, 插入page_hash. -
将该Block加入
LRU_list的LRU_young部分buf_page_make_young_if_needed(). 这里引入了时间的限制, 即假如第二次的访问时间必须超过buf_LRU_old_threshold_ms才会将其移动到 young 部分.
Buffer Pool与mini transaction
mini transaction的start(), commit()操作过程其实就是与Buffer Pool进行交互,完成数据页的获取,读写和修改。所以我们可以通过mini transaction的操作过程来理解Buffer Pool的工作原理。
mini transaction获取数据页
当 mini transaction 需要获取数据页时,首先会通过buf_page_get_gen去Buffer Pool中获取:
buf_pool_get(page_id)通过page id获取所对应的Buffer Pool的Instance. Instance与Page的对应关系很简单:page_number >> 6然后求模:% srv_buf_pool_instances.buf_page_hash_get_low(buf_pool, page_id)通过前面提及的page_hash能快速的找到该Page.- 假如Page在
LRU链表中处于 old 的部分,需要将其加至 young 部分. - 根据参数
rw_latch对Page加不同的锁.
Buffer Pool读取物理文件
假如查找的Page不存在于Buffer Pool中,会从文件中读文件至Buffer Pool中(buf_read_page_low()):
buf_page_init_for_read()调用buf_page_init()在Buffer Pool中初始化一个Page, 将该Block加至 old 部分.- 调用
fil_io()读取文件
mini transaction提交脏页
当mini transaction完成Commit的时候,假如该Block是第一次进行修改,会Block插入到Buffer Pool的flush_list,以后的修改在无需重复插入flush_list:
1 |
|
- 更新Page的
oldest_modification, 即mtr修改这个Block的起始lsn - 添加到
buf_pool->flush_list链表
Buffer Pool的刷脏线程
buf/buf0flu.cc:buf_flush_page_coordinator_thread()
Buffer Pool提供多个Flush线程进行Flush操作,前面我们也提及到假如目前需要一个空闲的Page, 用户线程会进行手工Flush: 从flush_list尾部选择一个Page进行Flush操作. 而我们下面分别介绍flush_list和LRU_list的刷脏线程buf_flush_page_coordinator_thread:
buf_flush_page_coordinator_thread是由用户创建的后台Flush协调线程.- 刷盘主线程会新建N个
buf_flush_page_cleaner_thread线程, 即普通刷脏线程. 创建的普通刷脏线程会 wait 在page_cleaner->is_requested等待事件. - 进入主逻辑的
while循环(while (srv_shutdown_state == SRV_SHUTDOWN_NONE)): - 判断是否需要sleep:
1 | if (srv_check_activity(last_activity) || buf_get_n_pending_read_ios() || |
-
假如目前数据库有活跃的操作或者Buffer Pool的读任务, 则需要睡眠1s.
-
假如目前的时间超过了上一次Flush设置的下一次刷盘时间
(ut_time_ms() > next_loop_time), 则设置OS_SYNC_TIME_EXCEEDED状态. -
否则不进入sleep
-
假如存在活跃的操作,则进入活跃刷新分支
if (srv_check_activity(last_activity)):-
通过
page_cleaner_flush_pages_recommendation()对每个Buffer Pool的Instance计算刷新脏页数量的建议. -
Flush协调线程会调用
os_event_set(page_cleaner->is_requested)唤醒等待中的任务线程. 被唤醒的任务线程会调用pc_flush_slot()来做刷盘操作, 而Flush协调线程自身也会调用pc_flush_slot(). -
pc_flush_slot()根据类型BUF_FLUSH_LRU或者BUF_FLUSH_LIST来选择对应的刷盘函数:BUF_FLUSH_LRU:buf_flush_LRU_list_batch()BUF_FLUSH_LIST:buf_do_flush_list_batch()遍历buf_pool->flush_list, 假如设置了srv_flush_neighbors=1即检查该Page的相邻的页是否允许Flush, 之后通过buf_flush_ready_for_flush()选择符合Flush规则的Page进行刷盘buf_flush_page(). 使用fil_io把Page写入文件.
-
-
后台刷新的协调线程会作为刷新调度协调的角色,它会确保每个 Buffer Pool 都已经开始执行刷新。如果哪个 Buffer Pool的刷新请求还没有被处理,则由刷新协调线程亲自刷新,且直到所有的 Buffer Pool 都已开始/进行了刷新.
LRU淘汰规则
buf_flush_ready_for_replace():
1 | return (bpage->oldest_modification == 0 && |
oldest_modification == 0: 表示这个Block没有被修改.bpage->buf_fix_count == 0:buf_fix_count == 1表示有线程正在读该Page.buf_page_get_io_fix(bpage) == BUF_IO_NONE: 表示该页目前没有任何IO操作.
Flush规则
buf_flush_ready_for_flush():
1 | if (bpage->oldest_modification == 0 |
oldest_modification == 0: 表示这个Block没有被修改,即无须Flushbuf_page_get_io_fix(bpage) != BUF_IO_NONE: 表示目前该Page存在操作,不允许进行Flush操作.
Buffer Pool的自适应刷脏算法
涉及刷脏的变量
介绍刷脏算法之前,我们先来介绍几个关于刷脏的变量:
-
innodb_max_dirty_pages_pct
设定Buffer Pool中的脏页比, 在MySQL8.0.3的版本中,默认值是90%
-
innodb_max_dirty_pages_pct_lwm
用来指定”低水位”值,其表示使用预刷脏来控制脏页比例的百分比,防止脏页的百分比达到
innodb_max_dirty_pages_pct的值,innodb_max_dirty_pages_pct_lwm默认0,禁用预刷脏行为。 -
innodb_io_capacity
设置InnoDB的后台线程允许每秒做多少次I/0
-
innodb_io_capacity_max
如果刷新活动落后,InnoDB可以比innodb_io_capacity施加的限制更积极地刷新。innodb_io_capacity_max定义了InnoDB后台任务在这种情况下每秒执行I/O操作的上限。
刷脏算法
-
计算当前刷脏的平均速度
avg_page_rate:1
2
3
4
5
6
avg_page_rate = static_cast<ulint>(
((static_cast<double>(sum_pages) / time_elapsed) + avg_page_rate) / 2); -
计算Redo Log产生的平均速度
lsn_avg_rate1
2
3
4
5
6
7
8
9
lsn_rate = static_cast<lsn_t>(static_cast<double>(cur_lsn - prev_lsn) /
time_elapsed);
lsn_avg_rate = (lsn_avg_rate + lsn_rate) / 2; -
根据
lsn_avg_rate计算刷脏的target_lsn -
遍历Buffer Pool的每一个Instance中的
flush_list, 将每一个Block的oldest_modifiaction与target_lsn比较. -
对每一个小于
target_lsn的Block进行计数,直到该Block大于target_lsn即break跳出1
2
3
4
5
6
n_pages = (PCT_IO(pct_total) + avg_page_rate + pages_for_lsn) / 3;
if (n_pages > srv_max_io_capacity) {
n_pages = srv_max_io_capacity;
} -
生成每一个Instance的刷脏数量建议:
1
2
3
4
5
6
7
8for (ulint i = 0; i < srv_buf_pool_instances; i++) {
page_cleaner->slots[i].n_pages_requested =
pct_for_lsn > 30 ? page_cleaner->slots[i].n_pages_requested * n_pages / sum_pages_for_lsn + 1
: n_pages / srv_buf_pool_instances;
}
手动触发Flush
set global innodb_buf_flush_list_now = 1手动强制进行flush_list的刷脏
innodb_lru_scan_depth
当free_list小于innodb_lru_scan_depth值时也会触发脏页刷新机制, 该值默认为1024
Buffer Pool的resize过程
Buffer Pool提供了专门的一个线程buf_resize_thread来完成resize过程, 具体的操作函数是buf_pool_resize(),resize流程如下:
- 假如开启了AHI, 需要关闭AHI.
- 假如是缩小Buffer Pool的大小, 需要设置每个Buffer Pool Instance的
withdraw_target, 即设置回收的Page数目. buf_pool_withdraw_blocks()进行回收Page操作:- 首先从
buf_pool->free开始回收, 将Page从free_list中释放, 插入withdraw_list. - 假如从
free_list中回收的Page数目没有达到要求, 则需要继续从lru_list中回收. 将脏页刷盘, 然后插入withdraw_list.
- 首先从
- 停止加载Buffer Pool
- 假如回收的Page数目小于设定的
withdraw_target, 需要等待2, 4, 8, 最大10s的时间重复执行buf_pool_withdraw_blocks() - 加锁Buffer Pool.
- 根据缩小或扩大的需求对Buffer Pool Instance的chunk数量进行增删操作.
- 更新每个Buffer Pool Instance的大小
buf_pool->curr_size和chunk数量buf_pool->n_chunks_new, Page数目等. - 假如Buffer Pool的大小扩大了2倍或者缩小了2倍, 则需要新建新的
page_hash和zip_hash. - 释放Buffer Pool的
buf_pool_mutex和旧的page_hash. - 重新开启AHI.
监控Buffer Pool的resize过程
用户可以通过Innodb_buffer_pool_resize_status查看Buffer Pool的resize过程中的状态:
1 | mysql> SHOW STATUS WHERE Variable_name='InnoDB_buffer_pool_resize_status'; |
Buffer Pool与Redo log buffer的区别
- Buffer Pool缓存的是数据页
- Redo log buffer缓存的是Redo log
总结
InnoDB根据Redo Log的生成速度和当前的刷脏速度,使用一种自适应的算法来估计下一次的刷新速度,从而保持整个数据库的性能平缓, 不会突然因为脏页的增多从而影响数据库的吞吐, 通过刷脏的算法我们可以看到InnoDB设计的巧妙,不是简单的通过限定某一个脏页比来决定是否刷脏.
Q&A
- 当 Buffer Pool 的脏页比率超过了限制,触发自动刷脏机制,如何处理 redo Log 的限制?
在正确的逻辑下,Redo Log必须先于Ditry Pages落盘, 否则在发生crash的情况下,会造成数据不一致. 为了保证这个条件,Buffer Pool的刷脏机制会在刷盘前将Page的
newest_modification与log_sys->flushed_to_disk_lsn比较,假如大于,则需要主动触发Redo Log的落盘:
1
2
3
4
5
6
7
8 const lsn_t flush_to_lsn = bpage->newest_modification;
if (log_sys->flushed_to_disk_lsn.load() < flush_to_lsn) {
Wait_stats wait_stats;
wait_stats = log_write_up_to(*log_sys, flush_to_lsn, true);
MONITOR_INC_WAIT_STATS_EX(MONITOR_ON_LOG_, _PAGE_WRITTEN, wait_stats);
}
2020 Leviathan