浅谈mysql InnoDB 缓冲池 bufferpool

浅谈mysql InnoDB 缓冲池 bufferpool

Scroll Down

浅谈mysql InnoDB 缓冲池 bufferpool

缓冲池解决了什么问题

        和所有的缓存的原理一样,mysql的缓冲池解决的依然是对热点数据的缓存,使得访问更快,从而使效率提升。在mysql中,我们知道,任意获取数据的方式均是以页(page)的方式获取的,mysql把经常访问到的数据页放到缓冲池(buffer pool中)    

        那么,如何判断一个数据是经常访问到的呢?一般通用的方式是通过LRU算法,最大限度地利用缓存。

mysql的缓冲池

传统的LRU是如何进行缓冲页管理?

最常见的玩法是,把入缓冲池的页放到LRU的头部,作为最近访问的元素,从而最晚被淘汰。这里又分两种情况:

(1)页已经在缓冲池里,那就只做“移至”LRU头部的动作,而没有页被淘汰;

(2)页不在缓冲池里,除了做“放入”LRU头部的动作,还要做“淘汰”LRU尾部页的动作;

image.png

如上图,假如管理缓冲池的LRU长度为10,缓冲了页号为1,3,5…,40,7的页。

假如,接下来要访问的数据在页号为4的页中:

image.png

(1)页号为4的页,本来就在缓冲池里;

(2)把页号为4的页,放到LRU的头部即可,没有页被淘汰;

画外音:为了减少数据移动,LRU一般用链表实现。

假如,再接下来要访问的数据在页号为50的页中:

image.png

(1)页号为50的页,原来不在缓冲池里;

(2)把页号为50的页,放到LRU头部,同时淘汰尾部页号为7的页;

mysql缓冲池特点

    传统的基于LRU算法的缓冲池对于mysql讲依旧存在一些缺陷和不足,不足以实现mysql的一些特定场景下的功能。mysql对于LRU进行了改造以实现以下功能

  1. 预读功能

    磁盘读写,并不是按需读取,而是按页读取,一次至少读一页数据(一般是4K),如果未来要读取的数据就在页中,就能够省去后续的磁盘IO,提高效率。数据访问,通常都遵循“集中读写”的原则,使用一些数据,大概率会使用附近的数据,这就是所谓的“局部性原理”,它表明提前加载是有效的,确实能够减少磁盘IO。所以,mysql会把一些可能要访问的数据提前加入缓冲池

  2. 防止缓冲池污染

    如果mysql执行了一条全表扫描的预计,那么会意味着所有的数据都会被新加入到LRU缓存里面,会导致LRU中所有的元素全部被刷新一遍,就没有起到LRU的作用。

总结一下上边说的可能降低缓冲池效率的两种情况:

  • 加载到缓冲池中的页不一定被用到。

  • 如果非常多的使用频率偏低的页被同时加载到缓冲池时,可能会把那些使用频率非常高的页从Buffer Pool中淘汰掉。

mysql缓冲池的实现与优化

在mysql中,缓冲池的本质是InnoDB的本质是向操作系统申请一块连续的内存空间,整体结构如下图所示

image.png

mysql缓冲池的设计中,为每一个缓存页均创建了一个控制块,这些控制块放置于内存的最开始的部分。包含了该页所属的表空间编号、页号、缓存页的地址等等。在对链表的操作均是对控制块进行的

对于这一块连续的空间,mysql可以通过三个链表来控制

对这片内存块的操作主要又几个链表实现

  1. free链表

            free链表主要是用于将所有还未被使用的所有控制块串起来的一个链表,当需要从磁盘中读取一个页时,就会就从free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的free链表节点从链表中移除,表示该缓存页已经被使用。

            可以知道,在数据库刚启动的时候,所有的缓存页均为空闲的,所以每一个缓存页的控制块均会被加入到free链表中。如下图所示

image.png

  1. LRU链表

    mysql中维护的LRU链表为了解决上面所提到预读功能和缓冲池污染问题,对常规的LRU链表进行了定制。下面逐个描述mysql的LRU链表做的优化

    • 预读操作的优化

        • 要优化预读失效,其思路为,

          1. 需要实现预读,即预读的页需要被提前加载到缓冲池中

          2. 让预读失败的页,在缓冲区LRU停留的时间尽可能短

          3. 真正被读取的页,才会被挪到LRU的头部

      所以,mysql为了实现上述问题,通过一个中间点将传统的LRU分为了两部分。(midpoint insertion strategy) 暂且将被分成的两部分成为新生代(new sublist)和老生带(old sublist) 默认的中间点位置是在链表的八分之三处。可以通过参数innodb_old_blocks_pct 调整 默认为37。

      添加了该策略中后,新数据页插入的规则就变成了以下步骤

      新页(例如被预读的页)加入缓冲池时,只加入到老生代头部:

      • 如果数据真正被读取(预读成功),才会加入到新生代的头部
      • 如果数据没有被读取,则会比新生代里的“热数据页”更早被淘汰出缓冲池

      image.png

      举个例子,整个缓冲池LRU如上图:

      (1)整个LRU长度是10;

      (2)前70%是新生代;

      (3)后30%是老生代;

      (4)新老生代首尾相连;

      image.png
      假如有一个页号为50的新页被预读加入缓冲池:

      (1)50只会从老生代头部插入,老生代尾部(也是整体尾部)的页会被淘汰掉;

      (2)假设50这一页不会被真正读取,即预读失败,它将比新生代的数据更早淘汰出缓冲池;

    image.png

    假如50这一页立刻被读取到,例如SQL访问了页内的行row数据:

    (1)它会被立刻加入到新生代的头部;

    (2)新生代的页会被挤到老生代,此时并不会有页面被真正淘汰;

    改进版缓冲池LRU能够很好的解决“预读失败”的问题。

    • 对于缓冲池污染的优化

      上面讲到,如果mysql执行了一条全表扫描的预计,那么会意味着所有的数据都会被新加入到LRU缓存里面,会导致LRU中所有的元素全部被刷新一遍,然而这些数据又只会被访问一次,下次很难再被访问到。所以其他的热点数据就被清空了。就没有起到LRU的作用。为此,MySQL缓冲池加入了一个“老生代停留时间窗口”的机制来解决这个问题。上面知道,新页(例如被预读的页)加入缓冲池时,只加入到老生代头部。所以,老生代停留时间窗口主要的实现方式为:

      在对某个处早老生代区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从老生代移动到新生代,否则将它移动到新生代区域的头部

      (1)假设T=老生代停留时间窗口;

      (2)插入老生代头部的页,即使立刻被访问,并不会立刻放入新生代头部;

      (3)只有满足“被访问”并且“在老生代停留时间”大于T,才会被放入新生代头部;

    image.png

    继续举例,假如批量数据扫描,有51,52,53,54,55等五个页面将要依次被访问。

    image.png

    如果没有“老生代停留时间窗口”的策略,这些批量被访问的页面,会换出大量热数据。

    image.png
    加入“老生代停留时间窗口”策略后,短时间内被大量加载的页,并不会立刻插入新生代头部,而是优先淘汰那些,短期内仅仅访问了一次的页。

image.png
而只有在老生代呆的时间足够久,停留时间大于T,才会被插入新生代头部。

  1. Flush链表

     flush链表是将所有修改过的数据页串起来的一个链表,当一个数据页被修改后,我们称这个数据页为脏页。这个脏页会被添加到flush链表中。flush 链表按照配置的速度和规则定时刷新到磁盘上。在Flush链表上的节点一定在LRU链表上,反之则不成立

    一个数据页可能会在不同的时刻被修改多次,在数据页上记录了最老(也就是第一次)的一次修改的lsn,即oldest_modification。不同数据页有不同的oldest_modification,Flush 链表中的节点按照oldest_modification排序,链表尾是最小的,也就是最早被修改的数据页,当需要从Flush 链表中淘汰页面时候,从链表尾部开始淘汰。在加入Flush链表时,需要使用flush_list_mutex保护,所以能保证FLU List中节点的顺序。

图片参考资料
https://juejin.im/post/5d11a79ee51d4555e372a624
掘金小册 MySQL 是怎样运行的:从根儿上理解 MySQL