MySQL缓冲池详解
Buffer Pool
本文参考开源项目:小林coding在线文档;
01-缓冲池概述
在MySQL查询数据的时候,是通过存储引擎去磁盘做IO来获取数据库中的数据,这样每次查询一条数据都要去做一次或者多次磁盘的IO,无疑是非常慢的。而缓冲池就能非常好的解决这个问题。
当数据从磁盘中取出后,缓存内存中,下次查询同样的数据的时候,直接从内存中读取。为此,Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能
有了缓冲池后:
- 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
- 当修改数据时,首先是修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页,最后由后台线程将脏页写入到磁盘。
02-缓冲池存储的内容
2.1-数据页在缓冲池中的存储
InnoDB 会把存储的数据划分为若干个「页」,以页作为磁盘和内存交互的基本单位,一个页的默认大小为 16KB。因此,Buffer Pool 同样需要按「页」来划分。
在 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的16KB
的大小划分出一个个的页,Buffer Pool 中的页就叫做缓存页。此时这些缓存页都是空闲的,之后随着程序的运行,才会有磁盘上的页被缓存到 Buffer Pool 中。
Buffer Pool 除了缓存「索引页」和「数据页」,还包括了 undo 页,插入缓存、自适应哈希索引、锁信息等等。
接下来我们讨论一下数据在缓冲池中是如何存储以及处理的:
1、既然我们要在缓冲池里存储数据页,那么数据页是怎样存储的呢?
在MySQL启动的时候,会申请一段连续的内存空间,缓冲池里有着许多的缓存页,而每个缓存页有唯一对应一个控制块,实际的存储情况如下图所示:
2、为什么上图会有空白的地方?
上图中控制块和缓存页之间灰色部分称为碎片空间。每一个控制块都对应一个缓存页,那在分配足够多的控制块和缓存页后,可能剩余的那点儿空间不够一对控制块和缓存页的大小,自然就用不到喽,这个用不到的那点儿内存空间就被称为碎片了。
当然,如果你把 Buffer Pool 的大小设置的刚刚好的话,也可能不会产生碎片。
2.2-缓冲池数据页的管理
2.2.1-Free链表
当我们的MySQL运行了一段时间后,缓冲池中的页有空闲的也有被使用的,当读取缓冲池中没有的数据时,我们要从磁盘去读取,磁盘读取之后,需要存到缓冲池。
但是此时我们读取到的数据应该放到哪个页中呢?当然是得放在空闲页中,那么我们应该如何找到空闲页呢,在MySQL缓冲池中,MySQL建立了一个Free链表,用来管理空闲的缓存页,当我们从磁盘新读到的数据,Free链表如图所示。
2.2.2-Flush链表
当对MySQL的数据进行修改操作后,并不需要每次都将缓冲池中的页写入磁盘,因为这样效率是比较低的,当缓存页中的数据发生改变后,MySQL会将该页标识为脏页。
与空闲页相同,MySQL也有一个Flush链表,记录了缓冲池中所有的脏页,Flush链表中的元素都是脏页,这样将脏页写入磁盘中就不用再去遍历所有的缓存页查看是否是脏页了,直接将Flush链表中的所有对应的缓存页写入磁盘就行,Flush链表如下图所示:
2.2.4-LRU链表
MySQL为了提高缓冲池的命中率,对于一些频繁使用的数据需要将其留在缓冲池中,在MySQL中使用了LRU(最近最少使用算法),该算法会淘汰最近最少使用的页,在MySQL缓冲池中有一个LRU链表。
下图我们可以看到LRU链表有一个头指针和尾指针,一个简单的LRU算法的实现是这样的:
- 当访问的页在 Buffer Pool 里,就直接把该页对应的 LRU 链表节点移动到链表的头部
- 当访问的页不在Buffer Pool里时,需要先把页放在LRU链表的头部,还要将尾部的页淘汰掉
假设现在有一个LRU链表,长度为5,目前有1、2、3、4、5五个页位于LRU链表中,也就是说有五个位于缓冲池中,如图所示:
现在假设我们要访问页3,那么我们就会将页3移动到head的位置,如图下图所示:
现在假设我们要访问页8,但是页8位于磁盘中,所以此时我们要将页8加入到LRU链表中,因此我们会淘汰掉末尾的页5,如图所示:
2.2.5-预读失效
对于如上所示的LRU算法会有一个预读失效的问题,我们先来解释一下预读失效是什么。
在MySQL加载数据页到缓冲池中时,由于空间局部性【访问谋个数据后,接下来很可能会访问其相邻的数据】,加载磁盘中的页到内存时,会将相邻存储的页也加载到内存中,但是如果提前加载的数据接下来不会被访问,这个就叫做预读失效。
预读失效导致的问题:
如果使用上述的LRU算法,那么就会导致一个问题,由于预读失效,加载了不会被访问的页放在了缓冲池中,由于加载了新的页,所以会要淘汰缓冲池中存在的页,那么就会导致缓冲池中可能会被频繁访问的页被淘汰了出去,这样就会降低缓冲池命中率。
MySQL对于预读问题的解决方案是将LRU链表划分成young
区域和old
区域,young区在LRU链表的前半部分,而old区域在LRU链表的后半部分。如下图所示。
关于young区域和old区域在LRU链表中的占比也可以通过参数设置,可以通过innodb_old_blocks_pct
参数进行设置,默认值是37,代表在缓冲池中,young区域和old区域占比是63:37。
解决预读失效产生的问题,当发生预读的时候,MySQL不会将预读的数据放到young区域,而是放在old区域的head部分,只有当预读的数据被访问的时候,才会被放在young区域的head部分,
2.2.6-Buffer Pool污染
上述使用LRU链表分young区和old区虽然能够解决预读失效导致的命中率下降问题,但是还存在Buffer Pool污染问题,我们先来介绍一下什么是buffer pool污染。
当一个SQL语句扫描了大量的数据的时候,在缓冲池空间比较有限的情况下,可能会将缓冲池中所有的热点数据都替换出去,等这些热点数据被再次访问的时候,由于缓存没有命中,就会产生大量磁盘IO,导致MySQL性能急剧下降,这种情况就叫Buffer Pool污染。
关于Buffer Pool污染的问题,MySQL是这样处理的,进入young区域条件增加了一个停留在old区域的时间判断。具体的处理是这样的,当访问第一次old区域的某个缓存页时,在其对应的控制块中记录当前访问的时间:
- 如果后续访问时间与第一次访问的时间在某个时间段内,那么这个缓存页不会从old区移到young区的头部
- 如果后续访问时间与第一次访问的时间不在某个时间段内,那么就会将这个缓存页从old区域移动到young区的头部
这个时间间隔可以通过innodb_old_blocks_time
控制,默认的时间是1000ms。
如果在默认的情况下,只有同时满足一次以上的访问
以及在old区停留超过1s
两个条件,才会被从old区移动到young区的头部,这样就解决了Buffer Pool污染的问题。