背景
建议读者站在设计者的角度,思考如何设计InnoDB缓存
以[mysql系列3---mysql索引图解]中的聚簇索引为例, 查询主键为33的记录:
[1] 加载聚簇索引根节点所在的数据页;
[2] 使用33与目录(21, 35)依次进行比较,得到子节点信息(地址、表空间、页号);
[3] 加载子节点所在的数据页;
[4] 使用33与目录(30, 32, 33)依次进行比较,得到叶子节点信息(地址、表空间、页号);
[5] 将叶子节点中保存的数据返回;
此过程涉及3次IO操作,由于索引和数据存放于磁盘中,3次IO均为磁盘IO,速度较慢;再次执行相同查询,仍然要经历相同的3次磁盘IO。
另外,mysql规定磁盘与内存交换的单位为数据页(16K),即查询一条记录需加载记录所在的整个页面;且处于同一页面中的数据存在关联性,可能导致同一个页面被多次冗余加载。因此,可考虑引入缓存。
缓存的设计需要以数据库特性为前提,即需考虑如下几个问题:
[1] 缓存大小: 为保障数据库功能正常,需要限制缓存大小(不能无限扩大),可通过提供配置参数实现并提供合适的默认值。
[2] 缓存查询: 提供判断缓存是否已加载的能力,以及根据表空间和页序号从缓存中获取数据的能力。
[3] 缓存维护和淘汰策略: 缓存有大小限制,需要引入合适的淘汰机制,保证缓存的命中率。
[4] 缓存入库: 修改的数据需要在适当的时机入库。
本文将从这几个角度介绍InnoDB缓存。
1.Buffer pool
mysql服务器启动时,会根据innodb_buffer_pool_size 变量分配一块连续的内存空间用于缓存数据库数据,称为Buffer pool; 其中innodb_buffer_pool_size的默认值为134217728(128M):
mysql
mysql> SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| innodb_buffer_pool_size | 134217728 |
+-------------------------+-----------+
mysql进一步将Buffer pool划分为一个个数据页(16K),并为每个数据页创建一个对应的控制块:
数据页用于缓存数据库数据; 控制块包含对数据页的管理信息,包括: 缓存页在Buffer pool的地址、表空间ID和数据页ID、lsn信息、链表节点信息(用于形成双向链表),控制块占据的空间远小于数据页。
有了Buffer pool后,可以将查询的数据页缓存到缓存页中,并在控制块中记录数据的表空间ID和数据页ID等信息。
在此基础上,建立一个哈希表用于加快缓存查询, 将表空间ID和数据页ID的组合作为key, 缓存页地址作为value。再次访问相同的数据页时,可以先判断哈希表是否包含对应的(表空间ID数据页ID),即数据页是否在Buffer pool中;如果包含,则根据(表空间ID数据页ID)从Buffer pool内存中加载数据,否则从磁盘根据地址加载数据页至内存。
上述缓存过程存在一个问题,如何得到一个空闲的缓存页?mysql引入free链为其提供了一个解决方案。
free链
因控制块与缓存页一一对应,且控制块中包含了缓存页在Buffer Pool的地址信息;可将空闲缓存页对应的控制块保存在一起,形成一个链表:
mysql启动时,由于所有的缓存页都未被使用,free链包含所有的控制块。
当从磁盘加载数据页缓存到Buffer Pool时,从free链可快速得到一个空闲的缓存页,将数据页数据加载到缓存页后,从free链路中删除对应的控制块。
如缓存-1 被用于缓存数据后,free链的变化如下图所示:
flush链
数据页被加载到Buffer Pool后,mysql可对数据页进行读写;数据页被修改后就会与磁盘数据不一致,称为脏页。为保证内存与磁盘的一致性,需将脏页刷盘,而刷盘速度较慢;mysql选择对脏页进行标记,在后续的某个时间点批量同步到磁盘上。
标记的方式为将修改后的数据页对应的控制块添加到flush链路中。mysql启动时,没有数据页被修改,则flush链只有头节点,当修改缓存1时,变化如下:
2.lru链和淘汰策略
在第一章中介绍了Buffer Pool的结构以及free链和flush链,理解了如何加载和读取缓存页。由于Buffer Pool缓存大小是固定的,意味着需要不断地淘汰不常用的数据,以提高缓存的命中率,从而提高系统性能。本章需要考虑的问题是:确定适合的淘汰算法。
mysql特性
所谓合适是指需符合mysql的特性。
[1] 预加载
考虑到数据的相关性,从磁盘加载数据到缓存时,可进行预加载,即预先加载可能被读取的数据。mysql中有两种预加载:(1) 顺序访问某个区的页面超过阈值,触发读取下一个区的全部页面; (2) 如果某个区中连续多个页面被加载,页面数量超过阈值时,触发加载当前区的所有页面。
[2] 批量查询
mysql中可能存在大批量查询甚至全表查询情况,这些大批量的数据往往是低频使用的,易导致缓存大批量换血;且mysql的数据交换以页为单位,全表查询时,多条记录往往在同一个数据页中,会导致mysql误判数据页被频繁访问。
基于上述原因,mysql引入LRU链给出了一个解决方案。
LRU链
LRU链由两个部分组成,young区域和old区域,两个区域的节点具备流动性(类似Java的垃圾回收器)。
LRU链规则:
[1] 数据页第一次被加载到内存时,将控制块添加到Old区域头部;
[2] Old区域的页间隔innodb_old_blocks_time时间后再次被访问,则被认定为热点数据,移动到Yong区域头部;
[3] 位于Yong区域内的数据被访问时,移动到头节点; Yong前1/4的数据则不必移动,以减少修改频率。
LRU链涉及两个变量,可根据具体业务场景进行调优:
[1] innodb_old_blocks_pct控制OLD区域节点占据LRU整体的百分比,默认值为37:
mysql
mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37 |
+-----------------------+-------+
[2] innodb_old_blocks_time控制热点数据判定的时间间隔,单位毫秒:
mysql
mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_time';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000 |
+------------------------+-------+
2.3 刷新脏页
将脏页刷新到磁盘的核心是选择脏页;mysql可以从flush中选择一部分数据刷入磁盘,也可以从lru的尾部选择一些脏页刷入磁盘。
**注意:**用户因查询数据而加载数据页进Buffer Pool时,如果没有足够的空间缓存,会同步处理数据释放问题,导致用户查询出现卡顿。通过调节Buffer pool大小,old区比例等参数,能一定程度减少卡顿问题出现的概率。