InnoDB管理缓冲页算法
最容易想到的,就是LRU(Least recently used)。
画外音:memcache,OS都会用LRU来进行页置换管理,但MySQL的玩法并不一样。
传统的LRU是如何进行缓冲页管理?
最常见的玩法是,把入缓冲池的页放到LRU的头部,作为最近访问的元素,从而最晚被淘汰。这里又分两种情况:
(1)页已经在缓冲池里,那就只做“移至”LRU头部的动作,而没有页被淘汰;
(2)页不在缓冲池里,除了做“放入”LRU头部的动作,还要做“淘汰”LRU尾部页的动作;
如上图,假如管理缓冲池的LRU长度为10,缓冲了页号为1,3,5…,40,7的页。
假如,接下来要访问的数据在页号为4的页中:
(1)页号为4的页,本来就在缓冲池里;
(2)把页号为4的页,放到LRU的头部即可,没有页被淘汰;
画外音:为了减少数据移动,LRU一般用链表实现。
假如,再接下来要访问的数据在页号为50的页中:
(1)页号为50的页,原来不在缓冲池里;
(2)把页号为50的页,放到LRU头部,同时淘汰尾部页号为7的页;
什么是预读?
磁盘读写,并不是按需读取,而是按页读取,一次至少读一页数据(一般是16K) ,如果未来要读取的数据就在页中,就能够省去后续的磁盘IO,提高效率。
所谓预读机制,说的就是当你从磁盘上加载一个数据页的时候,他可能会连带着把这个数据页相邻的其他数据页,也加载到缓存bufferpool里去! 预读请求的所有页集中在一个范围内(区)。
举个例子,假设现在有两个空闲缓存页,然后在加载一个数据页的时候,连带着把他的一个相邻的数据页也加载到缓存里去了,正好每个数据页放入一个空闲缓存页!
触发MySQL的预读机制
InnoDB使用两种预读算法来提高I/O性能:线性预读(linear read-ahead)和随机预读(randomread-ahead)
为了区分这两种预读的方式,我们可以把线性预读放到以extent为单位,而随机预读放到以extent中的page为单位。
线性预读着眼于将下一个extent提前读取到buffer pool中。
随机预读着眼于将当前extent中的剩余的page提前读取到buffer pool中。
线性预读
(1) 有一个参数是innodbreadahead_threshold,他的默认值是56,意思就是如果顺序的访问了一个区里的多个数据页,访问的数据页的数量超过了这个阈值,此时就会触发预读机制,把下一个相邻区中的所有数据页都加载到缓存里去。
线性预读方式有一个很重要的变量控制是否将下一个extent预读到buffer pool中,通过使用配置参数innodbreadahead_threshold控制触发innodb执行预读操作的时间。
如果一个extent中的被顺序读取的page超过或者等于该参数变量时,Innodb将会异步的将下一个extent读取到buffer pool中。可以设置为0-64的任何值(因为一个extent中也就只有64页),默认值为56,值越高,访问模式检查越严格。
例如,如果将值设置为48,则InnoDB只有在顺序访问当前extent中的48个pages时才触发线性预读请求,将下一个extent读到内存中。如果值为8,InnoDB触发异步预读,即使程序段中只有8页被顺序访问。
随机预读
(2) 如果Buffer Pool里缓存了一个区里的N个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会直接触发预读机制,把这个区里的其他的数据页都加载到缓存里去。N的默认值是13。
这个机制是通过参数innodbrandomread_ahead来控制的,由于随机预读方式给innodb code带来了一些不必要的复杂性,同时在性能也存在不稳定性,在5.5中已经将这种预读方式废弃,默认是OFF。
所以默认情况下,主要是线性预读可能会触发预读机制,一下子把很多相邻区里的数据页加载到缓存里去,这些缓存页如果一下子都放在LRU链表的前面,而且他们其实并没什么人会访问的话,那就会如上图,导致本来就在缓存里的一些频繁被访问的缓存页在LRU链表的尾部。
哪些情况会触发MySQL预读机制
现在我们已经理解了预读机制一下子把相邻的数据页加载进缓存,放入LRU链表前面的隐患了,预读机制加载进来的缓存页可能根本不会有人访问,结果他却放在了LRU链表的前面,此时可能会把LRU尾部的那些被频繁访问的缓存页刷入磁盘中!
所以我们来看看,到底哪些情况下会触发MySQL的预读机制呢?
(1) 有一个参数是innodbreadahead_threshold,他的默认值是56,意思就是如果顺序的访问了一个区里的多个数据页,访问的数据页的数量超过了这个阈值,此时就会触发线性预读机制,把下一个相邻区中的所有数据页都加载到缓存里去。
(2) 如果Buffer Pool里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会直接触发随机预读机制,把这个区里的其他的数据页都加载到缓存里去
这个机制是通过参数innodbrandomread_ahead来控制的,他默认是OFF,也就是这个规则是关闭的
所以默认情况下,主要是第一个规则可能会触发预读机制,一下子把很多相邻区里的数据页加载到缓存里去,这些缓存页如果一下子都放在LRU链表的前面,而且他们其实并没什么人会访问的话,那就会如上图,导致本来就在缓存里的一些频繁被访问的缓存页在LRU链表的尾部。
这样的话,一旦要把一些缓存页淘汰掉,刷入磁盘,腾出来空闲缓存页,就会如上所述,把LRU链表尾部一些频繁被访问的缓存页给刷入磁盘和清空掉了!这是完全不合理的,并不应该这样!
预读为什么有效?
数据访问,通常都遵循“集中读写”的原则,使用一些数据,大概率会使用附近的数据,这就是所谓的“局部性原理”,它表明提前加载是有效的,确实能够减少磁盘IO。
什么是预读失效?
由于预读(Read-Ahead),提前把页放入了缓冲池,但最终MySQL并没有从页中读取数据,称为预读失效。
举个例子,假设现在有两个空闲缓存页,然后在加载一个数据页的时候,连带着把他的一个相邻的数据页也加载到缓存里去了,正好每个数据页放入一个空闲缓存页!
但是接下来呢,实际上只有第1个缓存页是被访问了,第2个通过预读机制加载的缓存页,其实并没有人访问,此时这两个缓存页可都在LRU链表的最前面。
前两个缓存页都是刚加载进来的,但是此时第二个缓存页是通过预读机制捎带着加载进来的,他也放到了链表的前面,但是他实际没人访问他。
除了第2个缓存页之外,第1、3、4个缓存页都是一直有人访问的缓存页。
这个时候,假如没有空闲缓存页了,那么此时要加载新的数据页了,是不是就要从LRU链表的尾部把所谓的“最近最少使用的第4个缓存页”给拿出来,刷入磁盘,然后腾出来一个空闲缓存页了?
这个时候,如果你把上图中第4个缓存页刷入磁盘然后清空,你觉得合理吗?
他可是之前一直频繁被人访问的啊!只不过在这一个瞬间,被新加载进来的两个缓存页给占据了LRU链表前面的位置,尤其是第二个缓存页,居然还是通过预读机制加载进来的,根本就不会有人访问!
如果你要是把LRU链表尾部的缓存页给刷入磁盘,这是绝对不合理的,最合理的反而是把上图中LRU链表的第二个通过预读机制加载进来的缓存页给刷入磁盘和清空,毕竟他几乎是没什么人会访问的!
参考:https://blog.csdn.net/qq_34436819/article/details/106815706
如何判断预读是否失效
通过两个状态值,评估预读算法的有效性
show global status like '%read_ahead%';
- 1.Innodbbufferpoolreadahead:通过预读(后台线程)读入innodb buffer pool中数据页数
- 2.Innodbbufferpoolreadahead_evicted:通过预读来的数据页没有被查询访问就被清理的pages,无效预读页数