暂无图片
暂无图片
2
暂无图片
暂无图片
暂无图片

MySQL各种“Buffer”之InnoDB Buffer Pool

GrowthDBA 2021-09-18
3674

首先放一个MySQL官档中提供的InnoDB体系架构图:(MySQL 5.7)

(https://dev.mysql.com/doc/refman/5.7/en/innodb-architecture.html)

从上图中可以看出,InnoDB分为了内存结构和磁盘结构两大部分,Buffer Pool是内存结构中最为重要且核心的组件,今天就来一起了解一下Buffer Pool的工作原理。可以看到,内存结构中不仅有Buffer Pool,还有Adaptive Hash Index、Log Buffer、Change Buffer等组件,后面会开辟单独的文章进行介绍。下面开始今天的内容!~

本文摘录自:

58沈剑-架构师之路-公众号文章《缓冲池(buffer pool),这次彻底懂了!!!》

八怪(高鹏)-《深入理解MySQL主从原理》

InnoDB Buffer Pool介绍

MySQL InnoDB Buffer Pool,从字面意思理解就是:MySQL InnoDB缓冲池,既然是缓冲池,那么它的作用就是缓存表数据与索引数据,把磁盘上的数据加载到缓冲池,避免每次访问都进行磁盘IO,起到加速访问的作用。(把“最热”的数据放到“最近”的地方,以“最大限度”的降低磁盘访问。也可以理解成InnoDB缓冲池中缓存着大量的数据,使CPU读取或者写入数据时,不直接和低速的磁盘打交道,直接和缓冲区进行交互,从而解决了因为磁盘性能慢导致的数据库性能差的问题,弥补了两者之间的速度差异)。
  • 再来看一张图:

(Google公司给出的各层级硬件执行速度)
从上图中可以看出,如果想让MySQL运行的快,就要尽量避免CPU和磁盘直接打交道,而是多让CPU和内存打交道,处在内存结构中的InnoDB Buffer Pool就起着这样的作用。

预备知识必知(预读与传统LRU算法)

应用系统分层架构,为了加速数据访问,会把最常访问的数据,放在缓存(Cache)里,避免每次都去访问数据库。操作系统,会有缓冲池(Buffer Pool)机制,避免每次访问磁盘,以加速数据的访问。MySQL作为一个存储系统,同样具有缓冲池(Buffer Pool)机制,以避免每次查询数据都进行磁盘IO。
  • 速度快,那直接把所有数据都放到缓冲池里不就行了?
凡事都具备两面性,抛开数据易失性不说,访问快速的反面是存储容量小:
1、缓存访问快,但容量小,数据库存储了200G数据,缓存容量可能只有64G;
2、内存访问快,但容量小,买一台笔记本磁盘有2T,内存可能只有16G;
因此,只能把“最热”的数据放到“最近”的地方,以“最大限度”的降低磁盘访问。

预读

相较于磁盘的容量和速度。内存虽快,但容量小、价格贵的缺点还存在。所以就要考虑如何管理与淘汰缓冲池,使得性能最大化(如何让有限的空间做更多的事情)。

在介绍具体细节之前,先介绍下“预读”的概念。

  • 什么是预读?

操作系统的磁盘读写,并不是按需读取,而是按页读取,一次至少读一页数据(一般是4K),如果未来要读取的数据就在页中,就能够省去后续的磁盘IO,提高效率。

  • 预读为什么有效?

数据访问,通常都遵循“集中读写”的原则,使用一些数据,大概率会使用附近的数据,这就是所谓的“局部性原理”,它表明提前加载是有效的,确实能够减少磁盘IO。

  • 按页(4K)读取,和InnoDB的缓冲池设计有啥关系?

1、磁盘访问按页读取能够提高性能,所以缓冲池一般也是按页缓存数据

2、预读机制启示了我们,能把一些“可能要访问”的页提前加入缓冲池,避免未来的磁盘IO操作

LRU(Least Rrecently Used)

知道了缓冲池是以页为单位缓存数据的,往里放有了,那么又是如何淘汰的呢?最容易想到的,就是LRU(Least Rrecently Used)算法——最近、最少使用原则

传统LRU(常见包含如Memcache、OS)的缓冲页管理方式:把入缓冲池的页放到LRU的头部,作为最近访问的元素,从而最晚被淘汰。这里又分两种情况:

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

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

e.g.
直接描述有点难理解,举个栗子🌰便知。

缓冲池的LRU长度为10,缓冲了页号为1,2,3 … 9,10的页。
  • 假如,接下来要访问的数据在页号为4的页中:

  • 情况一

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

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

提示:为了减少数据移动,LRU一般用链表实现

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

  • 情况二

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

2、把页号为50的页,放到LRU头部,同时淘汰尾部页号为10的页。

传统的LRU缓冲池算法十分直观,OS,memcache等很多软件都在用。

MySQL中的LRU

相较于传统LRU缓冲池算法不同,MySQL InnoDB Buffer Pool就显得比较“矫情”,不能直接使用传统缓冲页的管理方式,原因有二:
一、预读失效:由于预读(Read-Ahead),提前把页放入了缓冲池,但最终MySQL并没有从页中读取数据,称为预读失效
二、缓冲池污染:当某一个SQL语句,要批量扫描大量数据时,可能导致把缓冲池的所有页都替换出去,导致大量热数据被换出,MySQL性能急剧下降,这种情况叫缓冲池污染。

MySQL针对预读失效的LRU优化

  • 知道了预读失效的弊端,该如何对预读失效进行优化?

要优化预读失效,思路是:

1、让预读失败的页,停留在缓冲池LRU里的时间尽可能短;

2、让真正被读取的页,才挪到缓冲池LRU的头部;

核心思想:就是以保证真正被读取的热数据留在缓冲池里的时间尽可能长
  • MySQL的具体实现方法是:

1、将LRU分为两个部分:

新生代(New Sublist)

老生代(Old Sublist)

2、新老生代收尾相连,即:新生代的尾(Tail)连接着老生代的头(Head);

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

* 如果数据真正被读取(预读成功),才会加入到新生代的头部

* 如果数据没有被读取,则会比新生代里的“热数据页”更早被淘汰出缓冲池

e.g.
还是举个栗子🌰,整个缓冲池LRU List如下图:

a. 整个LRU长度是10;

b. 前70%是新生代;

c. 后30%是老生代;

d. 新老生代首尾相连;

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

  • 情况一

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

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

  • 情况二
    假如50这一页立刻被读取到,例如SQL访问了页内的行row数据:
    1、它会被立刻加入到新生代的头部
    2、新生代的页会被挤到老生代,此时并不会有页面被真正淘

改进版缓冲池LRU能够很好的解决“预读失败”的问题。但也不要因噎废食,因为害怕预读失败而取消预读策略,大部分情况下,局部性原理是成立的,预读是有效的。但是,新老生代改进版LRU仍然解决不了缓冲池污染的问题

MySQL针对缓冲池污染的LRU优化

例如,有一个数据量较大的学生表,当执行:
SELECT * FROM student WHERE student_name LIKE '%路%';
  • 虽然结果集可能只有少量数据,但这类like不能命中索引,必须全表扫描,就需要访问大量的页

1、把页加到缓冲池(插入老生代头部);

2、从页里读出相关的row(插入新生代头部);

3、row里的student_name字段和字符串'路'进行比较,如果符合条件,加入到结果集中;

4、直到扫描完所有页中的所有row。

如此一来,所有的数据页都会被加载到新生代的头部,但只会访问一次,真正的热数据被大量换出

  • 怎么这类扫码大量数据导致的缓冲池污染问题呢?

MySQL缓冲池加入了一个“老生代停留时间窗口”的机制:

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

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

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

↑假如批量数据扫描,有21,22,23,24,25等五个页面将要依次被访问。

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

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

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

InnoDB里涉及LRU的重要的参数

SHOW VARIABLES LIKE '%innodb_buffer_pool_size%';SHOW VARIABLES LIKE '%innodb_old_blocks_pct%';SHOW VARIABLES LIKE '%innodb_old_blocks_time%';

  • 参数innodb_buffer_pool_size

介绍:配置缓冲池的大小,在内存允许的情况下,DBA往往会建议调大这个参数,越多数据和索引放到内存里,数据库的性能会越好。(一般为物理内存的50%~80%)
  • 参数innodb_old_blocks_pct

介绍:老生代占整个LRU链长度的比例,默认是37,即整个LRU中新生代与老生代长度比例是63:37。(如果把这个参数设为100,就退化为普通LRU了
  • 参数innodb_old_blocks_time

介绍:老生代停留时间窗口,单位是毫秒,默认是1000,即同时满足“被访问”与“在老生代停留时间超过1秒”两个条件才会被插入到新生代头部

知识补充

三种Page

其实在InnoDB Buffer Pool中,不止仅有LRU一种链表,还有Flush链表Free链表。MySQL的处理单位是Page,大小为16KB。而在Buffer Pool中,Page又被分为三种:
1、Free Page(空闲页):此Page未被使用,位于Free链表。
2、Clean Page(干净页):此Page已被使用,但是页面未发生修改,位于LRU链表。
3、Dirty Page(脏页):此Page已被使用,页面已经被修改,其数据和磁盘上的数据已经不一致。当脏页上的数据写入磁盘后,内存数据和磁盘数据一致,那么该Page就变成干净页了。脏页同时存在于LRU链表和Flush链表。

LRU链表知识点补充

从上面我们知道,LRU分成两部分,一个是New Sublist新生代,也可以称之为Young链表;另一个是Old Sublist老生代,也可以称之为Old链表。新老生代首尾相连,连接处的位置叫做Midpoint(当被访问的数据页被加载到Buffer Pool的时候,数据页加载的位置就是Midpoint,即Old链表的首部,Young链表的尾部)。

MySQL中提供了Buffer Pool的一些监控指标,可以通过下面的命令进行查看:

SHOW ENGINE INNODB STATUS\G
Buffer Pool缓冲池指标位于关键字“BUFFER POOL AND MEMORY”。

监控指标中,有两个需要关注一下:
1、young/s:该指标表示的是每秒访问Old链表中的页面,使其移动到Young链表的次数。如果MySQL实例都是一些小事务,没有大表扫描,且该指标很小,就需要调大innodb_old_blocks_pct或者减小innodb_old_blocks_time,这样会使得Old List的长度更长,Old中Page移动到Old List尾部消耗的时间会更久,那么就提升了下一次访问到Old List里面的页面的可能性。如果该指标很大,可以调小innodb_old_blocks_pct、调大innodb_old_blocks_time,保护热数据
2、non-young/s:该指标表示的是每秒访问Old链表中的页面,没有移动到Young链表的次数,因为其不符合innodb_old_blocks_time如果该指标很大,一般情况下是MySQL存在大量的全表扫描。如果MySQL存在大量全表扫描,且这个指标又不大的时候,需要调大innodb_old_blocks_time,因为这个指标不大意味着全表扫描的页面被移动到Young List了,调大innodb_old_blocks_time时间会使得这些短时间频繁访问的页面保留在Old List里面。
小提示

1、如果一个数据页已经处于Young链表,当它再次被访问的时候,只有当其处于Young链表长度的1/4(大约值)之后,才会被移动到Young链表的头部。这样做的目的是减少对LRU链表的修改,因为LRU链表的目标是保证经常被访问的数据页不会被淘汰。

2、innodb_old_blocks_time控制的是Old链表头部页面的转移策略。该Page需要在Old链表停留超过innodb_old_blocks_time时间,之后再次被访问,才会移动到Young链表。这样操作是避免Young链表被那些只在innodb_old_blocks_time时间间隔内频繁访问,之后就不被访问的页面塞满,从而有效的保护Young链表。

3、在全表扫描或者全索引扫描的时候,InnoDB会将大量的页面写入LRU链表的Midppoint位置,并且在短时间内访问几次之后就不再访问了。设置innodb_old_blocks_time的时间窗口可以有效保护Young List,保证了真正频繁访问的页面不被淘汰。

4、当扫描的表很大,Buffer Pool都放不下时,可以将innodb_old_blocks_pct设置为较小的值,这样只读取一次的数据页就不会占据大部分的Buffer Pool。例如,设置innodb_old_blocks_pct=5,会将仅读取一次的数据页在Buffer Pool的占用限制为5%。

5、当经常扫描一些小表时,这些页面在Buffer Pool移动的开销较小,我们可以适当调大innodb_old_blocks_pct,例如,设置innodb_old_blocks_pct=50%。

6、每间隔1秒,Page Cleaner线程执行LRU List Flush的操作,来释放足够的Free Page、innodb_lru_sacn_depth变量控制每个Buffer Pool实例每次扫描LRU List的长度,来寻找对应的脏页,执行Flush操作。

Flush链表

1、Flush链表里面保存的都是脏页,也会存在于LRU链表。
2、Flush链表是按照oldest_modification排序,值大的在头部,值小的在尾部。
3、当有页面访问被修改的时候,使用mini-transaction,对应的Page会进入Flush链表。
4、如果当前页面已经是脏页,就不需要再次加入Flush List,否则是第一次修改,需要加入Flush链表。
5、当Page Cleaner线程执行flush操作的时候,从尾部开始scan,将一定脏页写入磁盘,推进checkpoint,减少recover的时间。

Free链表

1、Free链表存放的是空闲页面,初始化的时候申请一定数量的页面。
2、在执行SQL的过程中,每次成功load页面到内存后,会判断Free链表的页面是否够用。如果不够用的话,就flush LRU链表和Flush链表来释放空闲Page。如果够用,就从Free链表里面删除对应的页面,在LRU链表增加页面,保持总数不变。

LRU链表和Flush链表的区别

1、LRU链表flush,由用户线程触发(MySQL 5.6.2之前);而Flush链表flush由MySQL数据库InnoDB存储引擎后台srv_master线程处理。(在MySQL 5.6.2之后,都被迁移到Page Cleaner线程中)。

2、LRU链表flush,其目的是为了写出LRU链表尾部的脏页,释放足够的空闲页,当Buffer Pool满的时候,用户可以立即获得空闲页面,而不需要长时间等待;Flush链表flush,其目的是推进Checkpoint LSN,使得InnoDB系统崩溃之后能快速的恢复。

3、LRU链表flush,其写出的脏页,需要从LRU链表中删除,移动到Free链表;Flush链表flush,不需要移动Page在LRU链表中的位置。

4、LRU链表flush,每次flush的脏页数量较少,基本固定,只要释放一定的空闲空间即可;FLUSH链表flush,根据当前系统的更新繁忙程度,动态调整一次flush的脏页数量,量很大。

5、在Flush链表上的页面一定在LRU链表上,反之则不成立。

小结

今天理论的知识很多,下面简单做一下总结
1、缓冲池(Buffer Pool)是一种常见的降低磁盘访问的机制;
2、缓冲池通常以页(Page)为单位缓存数据,OS的Page大小一般为4KB,MySQL的Page大小一般为16KB;
3、Page可以分为Free Page(空闲页)、Clean Page(干净页)、Dirty Page(脏页);
4、缓冲池中含有3个链表:LRU链表(LRU List)、Free链表(Free List)、Flush链表(Flush List),以及LRU链表和Flush链表的区别;
5、缓冲池常见的管理算法是LRU,Memcache、OS、MySQL的InnoDB存储引擎都使用了这种最近、最少使用原则算法(Least Rrecently Used);
6、MySQL的InnoDB存储引擎对普通的LRU进行了优化:
  • 将缓冲池分为New Sublist(新生代/Young)和Old Sublist(老生代/Old),入缓冲池的Page,优先从Midpoint进入Old Sublist,Page被访问,才进入New Sublist,以解决预读(Read-Ahead)失效的问题。
  • Page被访问,且在Old Sublist停留时间超过配置innodb_old_blocks_time阀值时,才进入New Sublist,以解决批量数据访问,大量数据淘汰的问题。
今天主要讲解了MySQL InnoDB Buffer Pool的工作原理,通过一些例子来说明整个过程,偏理论的知识,大家理解记忆即可,面试中也可能会被问到。站在巨人的肩膀上,每天进步一点点!~

end

文章转载自 GrowthDBA,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论