Innodb为什么不使用朴素LRU缓存?

写在前面

  1. 为什么Innodb buffer 需要用LRU缓存?
  2. 为什么不用朴素LRU策略淘汰内存页?
  3. 改进版的LRU缓存能解决什么问题,为什么能解决?

什么是buffer pool

我们知道,mysql(使用innodb存储引擎)的数据存在磁盘上,当需要查询,修改数据时,系统会将该数据所在的页加载到内存中一个叫buffer pool的区域再进行操作

  • 若是进行读取操作,使用完毕后不会立即将该页从内存中移除,而是将其缓存起来,这样当下次还需要使用就能避免一次磁盘IO
  • 若是对该页进行了修改,使其变成了脏页,也不会立即将脏页刷回磁盘,而是通过记录redo log和后台任务定时将脏页刷回磁盘,这样既保证了数据的持久性,也通过将多个随机IO合并成少数顺序IO提高IO性能

用LRU策略淘汰缓存页

buffer pool能使用的内存大小是有限的,我们可以通过以下命令来查看其大小

SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

+-----------------------+----------+

|Variable_name |Value |

+-----------------------+----------+

|innodb_buffer_pool_size|4294967296|

+-----------------------+----------+

查询结果显示,这台msyql的buffer pool大小为4GB

随着程序的运行,若需要缓存的页面超过buffer pool大小,则需要将某些旧页面移除,这样才能装下即将要使用的页面。那移除哪些页面呢?为了使缓存命中率增大,减少磁盘IO次数,一种策略为:淘汰最近最少未使用的页面,这个页面在过去的一段时间最少被用到,根据时间局部性原理,预计在未来一段时间内也最少被用到,因此淘汰这个页面是合理的,这就是朴素LRU(Least recently used)策略

这样当访问某个页面时:

  • 若该页面已经在buffer pool,则将其从链表取出,加入LRU链表的头部
  • 若该页面不在buffer pool中,则从磁盘加载该页面,也加入LRU链接的头部,当空间不够时,淘汰掉链表尾部的页(若为脏页需刷回磁盘)

这样当buffer pool中某个页面一直未被使用时,就会逐渐移动到链接尾部,最终被淘汰

image.png

朴素LRU策略的问题

以上缓存淘汰策略在正常情况下能很好工作,但若遇到以下两种情况会导致buffer pool淘汰掉不是“最近最少使用”的数据,和LRU策略的初衷南辕北辙,进而降低缓存命中率,影响整个程序性能

  • 预读:根据空间局部性原理,mysql若认为可能使用当前页面周围的页面,就会将这些周围的页面异步加载到buffer pool,具体分为线性预读和随机预读

    • 线性预读:若顺序读取某个区(一个区有64个页)中的页面数量超越一定的数量,则预先将下一个区的全部页面读入到内存,该数量由系统变量innodb_read_ahead_threshold控制,默认为56
  • 随机预读:当buffer pool已经有一个区中的一些页面时,会预先将该区中的其他页面也写入buffer pool。是否开启这个功能由系统变量innodb_random_read_ahead控制,默认不开启

若预读出来的页面很快被使用,则该功能能降低响应时间,提升程序性能。但如果后续没有使用,且由于新加入这些页面,而淘汰掉一些后面会用到的老页面,则会起到反效果

  • 全表扫描:即将某个表的所有页都加载到buffer pool,若该表页面较多,则会把原本buffer pool中的页面都淘汰掉。这样程序想用以前的页面时,需要从磁盘加载一次

一般来说全表扫描触发的频率很低,于是上面的做法等于是将短时间内不会再用的页面缓存起来,而将未来大概率用到的页面驱逐出内存,和高效率的做法完全相反,极大的影响效率

改进版LRU

于是mysql将该LRU链接划分为两部分:

  • 前面一部分存储访问频率较高的热数据,称为young区域
  • 后面一部分存储访问频率较低的冷数据,称为old区域

image.png 每一部分占多大比例由系统变量innodb_old_blocks_pct控制,默认为37,即冷数据区域占37%

+---------------------+-----+

|Variable_name |Value|

+---------------------+-----+

|innodb_old_blocks_pct|37 |

+---------------------+-----+

当访问某个页面时,若该页面不在内存,即第一次访问,则将其放到old区域头部

这样若预读出来的页面后续没有使用,则会逐步被淘汰,且不会影响到频繁访问的老页面

到现在为止,young区域还没有用到,那什么时候可以将old区域的页晋升到young区域呢?即满足什么条件下,系统认为某个页面是属于热页面

  • 我们先看第一种策略:第二次访问该页则晋升

    • 除了加载页面的访问以外,若后续再访问该页面,就将其晋升到young区域。该策略完美适用于预读场景,但不适用于全表扫描的场景,你想,全表扫描将页面加载到buffer pool后,紧接着马上访问该页面的所有记录,每访问一条记录,相当于访问一次该页。若每条记录较小,算100字节,一个16k的页面大概能装160条记录,总共需要被访问160次。也就是说不仅是第二次访问,可能短时间内对一个页面访问了很多次,但该页面在业务上来说并不属于热数据
  • 那咋办?mysql采用了第二种策略:访问间隔时长晋升

    • 全表扫描有个特点,对每个页的访问集中在很短的时间内,因此该策略为:

第一次访问该页面时记录一个初始访问时间,若后续对该页面的访问时的时间,减去初始访问时间大于某个值,则将该页面加入LRU链表的热数据区

这里的“某个值”,即时间间隔的阈值由系统变量innodb_old_blocks_time控制,默认为1s(通常在内存访问一个页面的所有记录要不了1s

+----------------------+-----+

|Variable_name |Value|

+----------------------+-----+

|innodb_old_blocks_time|1000 |

+----------------------+-----+

也就是说,从第一次从磁盘加载某个页面后的1秒后,还有请求会访问该页,不论该页是不是因为全表扫描而进入buffer pool,该页都是一个会被高频访问的页,是热数据。而由于全表扫描而进入buffer pool中的页大部分都不会再被访问,是冷数据,也就会慢慢被淘汰出内存

该策略对于全表扫描的场景,也能有效防止淘汰掉真正频繁使用的页

总结

  • 因为innodb buffer pool空间有限,但没有可用空间时,需要选择一部分页面淘汰掉,以便装入即将使用的页面
  • 朴素LRU缓存淘汰策略在大多数场景下能淘汰掉最近最少使用的页面,提高缓存命中率,但不适用于预读和全表扫描的场景
  • 通过将LRU链表划分为冷热两部分,第一次访问时将页放入冷数据区域,一定时间间隔后还有对该页的访问,则将该页放入热数据区域,使得在这两种场景下也能保证真正的热页面常驻内存提高了缓存命中率

猜你喜欢

转载自juejin.im/post/7034413071586197517