Page 的读取有一个统一的入口函数 ,该方法的主要入参为 page_id
,即获取指定的页,MySQL 8.0 中的主要流程如下:
读取 1 个 Page 时,首先会检查 page_hash
,如果 page_hash
中存在,则直接读取并设置 buf_fix_count
后返回;否则需要从文件中读取 Page,从文件中读取 Page 时首先需要申请 1 个 Block(具体申请过程在后面介绍),然后添加到 page_hash
和 LRU
列表中,最后进行数据的读取。对于 1 个新的 Page 的创建过程,入口函数为 buf_page_create
,基本流程如下:
/* buf_page_create */
|--> buf_page_create
| |--> buf_LRU_get_free_block // 申请 1 个 block
| |--> buf_page_hash_get_low // 检查 page_hash 中是否存在
| |
| |--> buf_page_init
| | |--> buf_block_init_low
| | |--> buf_page_init_low
| | |--> HASH_INSERT // 插入 page_hash
| |--> buf_block_buf_fix_inc // buf_fix_count 计数 +1
| |--> buf_LRU_add_block // 添加到 LRU
Block 申请
Block 申请的入口函数为 buf_LRU_get_free_block
,该方法会从 Buffer Pool 中申请 1 个 Block 供后续的 Page 读取使用。Block 申请的主要流程如下:
|--> buf_LRU_get_free_block // loop
| |--> buf_LRU_get_free_only // 从 free_list 分配
| |
| |--> buf_LRU_scan_and_free_block // 从 LRU 中回收
| | |--> buf_LRU_free_from_unzip_LRU_list
| | | |--> buf_LRU_free_page
| | |--> buf_LRU_free_from_common_LRU_list
| | | |--> buf_flush_ready_for_replace
| | | |--> buf_LRU_free_page
| |
| |--> os_event_set(buf_flush_event) // 唤醒刷脏线程
| |
| |--> buf_flush_single_page_from_LRU // 从 LRU 中刷脏
| | |--> buf_LRU_free_page
| | |--> buf_flush_page
Buffer Pool 中维护了三个列表:free_list
、LRU
、flush_list
。其中 free_list
列表是当前可供使用的 Block,LRU
列表中保存了当前所有已经使用的 Block,flush_list
列表中保存了所有脏页 Block。申请 1 个 Block 时:
- 首先判断当前
free_list
列表是否为空,若free_list
列表非空,则直接从free_list
列表中进行分配。若无法直接从free_list
列表分配,则会尝试从LRU
列表中进行回收。 LRU
是一个非严格的最近使用列表,从LRU
列表回收时会从列表尾部往前遍历(加入LRU
列表时从头部加入),如果找到可回收的 Page(遇到脏页会跳过),则会释放 Page 并将对应的 Block 重新放入free_list
列表中。LRU
列表的遍历过程并不是无限的,例如:在第一次遍历时,当检查的 Page 数目达到BUF_LRU_SEARCH_SCAN_THRESHOLD
时会退出遍历过程。- 如果无法从
LRU
列表中回收 Block,则会唤醒刷脏线程,刷脏线程的处理流程在下面会做介绍。
前面提到过 flush_list
列表中保存的是所有脏页 Block,脏页在 mtr 提交时会加入 中,基本过程如下:
注意:flush_list
是一个非严格有序的列表(可以看做按照 oldest_modification
有序),脏页插入列表后位置不再修改,再次修改时仅修改 newest_modification
。
加入 LRU
|--> buf_LRU_add_block
| |--> buf_LRU_add_block_low
| | |--> UT_LIST_ADD_FIRST // 插入 young 区域头部
| | |--> UT_LIST_INSERT_AFTER // 插入 old 区域头部
| | |
| | |--> buf_LRU_old_adjust_len // 调整 LRU
前面提到 LRU
是一个非严格的最近使用列表,InnoDB 将 LRU
列表划分为两个区域:young 区域和 old 区域。LRU
列表的示意图如下:
/** LRU 列表示意图
LRU_old
|
**********************young************************|********old*********
|==================================================|===================|
几个主要的常量:
BUF_LRU_OLD_TOLERANCE 20
BUF_LRU_NON_OLD_MIN_LEN 5
BUF_LRU_OLD_MIN_LEN 512
BUF_LRU_OLD_RATIO_DIV 1024
参数控制:
innodb_old_block_pct old 区域占比
*/
buf_LRU_old_adjust_len
方法会根据 innodb_old_block_pct
参数,维护 young 区域和 old 区域的长度,主要逻辑如下:
- 当
LRU
长度小于 ` BUF_LRU_OLD_MIN_LEN` 时,不划分区域。 - 不是每次操作
LRU
列表后都需要立即调整,BUF_LRU_OLD_TOLERANCE
可以看成是容忍范围。 - 当 old 区域变大时,LRU_old 指针向前移动;反之向后移动。
当 Block 被再次访问时,会触发 buf_page_make_young_if_needed
函数进行 Block 位置的调整,基本过程如下:
buf_page_make_young_if_needed
移动 Block 时需要考虑:
- 访问间隔需要大于
buf_LRU_old_threshold_ms
。 - 当 Block 在 young 区域前 1/4 时,不需要移动。
InnoDB 中 LRU
列表的设计虽然简单,但是也有许多优化在里面,感兴趣的同学可以仔细研究,本文仅是一个简单的介绍。
释放 Page
前面提到,当 free_list
列表为空时,会首先尝试从 LRU
列表中进行回收,Page 的释放入口函数为 buf_LRU_free_page
,该方法的主要处理流程如下:
|--> buf_LRU_free_page
| |--> buf_page_can_relocate // 检查 buf_fix_count 计数和 io_fix 状态
| |
| |--> buf_LRU_block_remove_hashed // 从 LRU 和 page_hash 中删除
| | |--> buf_LRU_remove_block
| | | |--> buf_LRU_old_adjust_len
| | |--> HASH_DELETE
| |
| |--> btr_search_drop_page_hash_index // 从 AHI 中删除
| |
| |--> buf_LRU_block_free_hashed_page // 放回 free_list
| | |--> buf_LRU_block_free_non_file_page
同步刷脏的入口函数为 buf_flush_page
,同步刷脏过程仅会刷 1 个 Page,保证能够获取到 1 个可用的 Block,主要处理流程如下:
|--> buf_flush_page // 刷单个 page
| |--> buf_page_set_io_fix // io_fix 设置为 BUF_IO_WRITE
| | |--> log_write_up_to // 写 redo
| | |
| | |--> fil_io
| | |--> buf_dblwr_write_single_page // 写数据页
| | |
| | |--> fil_flush
| | |--> buf_page_io_complete
| | | |--> buf_flush_write_complete
| | | | |--> buf_flush_remove
| | | | |--> buf_page_set_io_fix // io_fix 设置为 BUF_IO_NONE
| | | |
| | | |--> buf_LRU_free_page
InnoDB 通过严格的 WAL 机制保证数据的一致性,刷脏过程同样如此。首先需要保证对应的日志文件落盘,然后再写入数据页。最后将 Block 从 flush_list
列表中移除,此时 Page 变成可回收状态,再次调用 buf_LRU_free_page
进行回收。
同步刷脏的过程不仅在获取 Block 时会被调用,在表删除的时候同样会被调用,表删除时会根据 space_id
进行批量的刷脏,入口函数为 buf_LRU_flush_or_remove_pages
,处理流程如下:
具体的过程在此不再赘述,大家可以自己去阅读相应的代码。需要注意的是:如果单个 session 中使用了临时表,那么在 session 退出的时候,也会进入到上述的刷脏流程,当 LRU
列表很大时,session 退出的性能将会受到很大的影响。AliSQL 对此进行了优化,欢迎试用。
异步刷脏
除了同步刷脏之外,MySQL 中还引入单独的刷脏线程进行异步刷脏。刷脏线程按照功能划分包括两种:coordinator 线程和 cleaner 线程。coordinator 线程会计算最大的刷脏量,然后分配刷脏任务给 cleaner 线程,cleaner 线程进行实际的刷脏工作(coordinator 线程本身也会参与刷脏)。异步刷脏的入口函数为 buf_flush_page_cleaner_init
,基本流程如下:
|--> buf_flush_page_coordinator_thread
| |--> os_event_wait(buf_flush_event)
|
| /* loop */
| |--> page_cleaner_flush_pages_recommendation // 计算最大刷脏量
| |--> pc_request // 任务分发,slot 数目等于 bp_instance 数目
| | |--> os_event_set(page_cleaner->is_requested)
| |--> pc_flush_slot // 参与刷脏
| |--> pc_wait_finished
|--> buf_flush_page_cleaner_thread
| |--> os_event_wait(page_cleaner->is_requested)
| |--> pc_flush_slot // 1 个线程处理 1 个 bp_instance
| | |--> buf_flush_LRU_list // 从 LRU 中刷脏
| | | |--> buf_flush_do_batch(BUF_FLUSH_LRU)
| | |
| | |--> buf_flush_do_batch(BUF_FLUSH_LIST) // 从 flush_list 刷脏
| | | |--> buf_flush_batch
| | | | |--> buf_do_LRU_batch
| | | | | |--> buf_free_from_unzip_LRU_list_batch
| | | | | |--> buf_flush_LRU_list_batch
| | | | | | |--> buf_LRU_free_page
| | | | | | |--> buf_flush_page_and_try_neighbors
| | | | | | | |--> buf_flush_try_neighbors
| | | | | | | | |--> buf_flush_page
| | | | |
| | | | |--> buf_do_flush_list_batch
异步刷脏的具体过程可以参考,异步刷脏过程中有一个非常重要的点就是 page_cleaner_flush_pages_recommendation
计算最大刷脏量,相关的细节在此不再展开,后面有机会再单独整理一篇各种后台线程的更新逻辑。