image.png
image.png

Buffer Pool 的大小

image.png
磁盘 和 Buffer Pool 都是以 页为单位存储数据的,当从磁盘中读取数据 到 缓冲池Buffer Pool 中也是一页页读。默认数据页16KB , Buffer Pool 128MB , 读到缓冲池的 数据页,称为缓冲页。

image.png
Buffer Pool 里面的缓冲页 的描述信息,放在Buffer Pool 开头的位置,记录了 缓冲页 在 缓冲区的位置,所属的表空间、数据页编号。

Buffer Pool 会不会有内存碎片

Buffer Pool 的大小是你自己定的,划分完描述数据块和缓存页之后,就还剩一点点内存,放不下缓存页,那么就属于内存碎片了,让缓存页和描述数据块紧密的挨在一起,就可能减少内存浪费了,就能尽可能减少内存碎片的产生了。

数据库启动时,如何初始化Buffer Pool

数据库只要已启动就会按照设置的Buffer Pool 大小,在稍微加大点,去向OS 申请内存来作为 Buffer Pool 区域。
申请完毕后,就会按照缓冲页的大小和描述数据的大小,划分程一块块区域。
image.png
但是此时缓存页都是空的,需要执行CRUD 的时候,才会把数据页对应的页从磁盘文件里读取出来,放到缓存页,才会有数据。

如何知道哪些缓存页是空闲的 - free 链表

是一个双向链表结构,把Buffer Pool 里面的空白的缓存页的描述数据块 串起来,同时还有一个基础节点(不属于 Buffer Pool),指向双向链表的头尾节点,并且记录了连表中有多少个空闲的描述数据块,也就是多少个空闲的缓存页。
image.png
free链表里的 单个数据块节点数据结构 - 双向链表的单个节点
image.png
当block3 被使用了, 从 free 链表中移除
image.png
这里不太明白,block3 被使用了,block2 不应该指向 下一个空闲块么,为什么是 null 了呢?

如何将磁盘上的页读取到Buffer Pool 的缓存页中去?

  1. 先从free 链表中获取到一个缓存页的描述数据块
  2. 找到数据块中记录的缓存页的地址
  3. 把磁盘的数据页读取到对应的缓存页里去,同时把相关的描述数据写入到缓存数据页的描述数据块去
  4. 把该描述数据块从free 链表中去除

image.png

Hash表 - 查询数据页是否被缓存了

image.png
根据使用场景,执行CRUD 的时候,肯定要看对应的数据是否在Buffer Pool中,在的话直接修改 缓存区;不在的话,执行上面的从磁盘读取数据页的流程。

那么如何判断在不在呢?

Hash表数据结构 - 每次读取一个数据页到缓存区后,都会写入到Hash 表去
表空间号 + 数据页号 ,作为一个Key
缓存页的地址,作为 value
==查询 Hash 表 时间复杂度低,但是我要找的是行,他怎么和数据页对应起来。

脏数据页 到底为什么会脏

image.png
哪些缓存连是要刷回磁盘的呢?
MySQL 对 脏页 维持了一个 flush 链表 , 这个flush 链表本质也是通过 缓存页的描述数据块中的两个指针,让被修改过的缓存页的描述数据块,组成一个双向链表
image.png
描述数据块
image.png
block 2 更新
image.png

Buffer Pool 的内存也不够了,咋办?

假如 Buffer Pool 满了,free 链表为 空时,如何淘汰缓存页

  • 把更新过的缓存页,刷到磁盘上去,然后缓存页就可以清空了。image.png

那么应该把那个缓存页的数据刷入磁盘呢?
看缓存页的命中率,刷命中率低的进去

哪些是经常使用,哪些是不常使用的呢?
引入 LRU 链表判断哪些缓存页不常用,Least Recently Used ,最近最少使用

从磁盘加载一个数据页到缓存页的时候,就把缓存页的描述数据块放到LRU 链表头部去了,只要有数据的缓存页都在LRU里,而且最近被加载数据的缓存页会 放到LRU链表头部,再者尾部的缓存页 只要被查询或者修改,就会挪到LRU 链表头部去,也就是被访问的缓存页,一定在LRU头部。
当缓存页没有空闲的时候,就找个最近最少被访问的缓存页去刷磁盘,这样直接再LRU 链表的尾部找缓存页即可。

有点奇怪,LRU 是这样的吗?

这样的LRU有问题吗?

实际运行过程中,可能产生巨大的隐患

  • MySQL的预读机制

    1. 即从磁盘上加载一个数据页的时候,可能会连着把这个数据页相邻的其他数据页 也都加载到缓存里面去。<br />问题?<br />实际上被访问的只有是原来的那个缓存页,其连带着加载进去的缓存页, 没有被访问,但其却在LRU 链表的头部,其实LRU链表的尾部缓存页是被频繁 访问的。如果此时要淘汰缓存页,却把尾部频繁访问的淘汰了,保留了通过预读 机制加载进来,并没有被访问的缓存页。 --- 不合理<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/21550800/1648360149638-40026872-e509-4398-b13d-014c6c6a977d.png#clientId=ud7afd3ef-5167-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=611&id=u473af30c&margin=%5Bobject%20Object%5D&name=image.png&originHeight=611&originWidth=1043&originalType=binary&ratio=1&rotation=0&showTitle=false&size=163055&status=done&style=none&taskId=u95d7885f-f87a-4d54-94e0-3b64f90e4ae&title=&width=1043)<br />怎么触发预读机制?
    • 参数 innodb_read_ahead_threshold,默认值是56,意思是顺序的访问了一个区里面的数据页的个数 大于阈值56,就会触发预读机制,把下一个相邻区中所有数据页都加载到缓存里去。

**默认情况下,是这个条件下触发的预读机制,占据了LRU链表的头部, 造成把经常访问的缓存页,刷回磁盘的隐患。*

  • 如果Buffer Pool 里缓存了一个区里的13个连续的数据页,而且这些数据页被频繁的访问,就会触发预读机制,把这个区里的其他数据页都加载到缓存里去,通过 innodb_random_read_ahead 控制,默认是OFF
  • 全表扫描

    另一个导致频繁被访问的缓存页被淘汰的场景<br />select *  from user 不加  where 条件<br />导致一下子把这个表里的所有数据页,都从磁盘加载到Buffer Pool 里去,占据           LRU 链表的头部,如果全表扫描后,后面几乎没用到这个表里的数据,那么此时        链表尾部都是频繁被访问的缓存页。当要淘汰调一些缓存页的时候,反而把常访        问的淘汰掉了,留下来全表扫描后加载进来,却不常用的缓存页。
    

基于冷热数据分离的思想设计LRU链表

为了解决上述的LRU链表机制,被预读影响的问题。

产生的上述问题,因为所有的缓存页都混在一个链表里面,才导致的,真正的LRU链表,会被拆分成两个部分,一部分是热数据,一部分是冷数据
冷热数据比例,由innodb_old_blocks_pct 参数控制的,默认是37,即冷数据占37%
实际上
image.png

冷热数据如何使用?

数据页第一次被加载缓存区时,会被放在冷数据区的链表的头部。
image.png

什么时候会被放在热数据区域?

只要对冷数据区的缓存页进行一次访问,就立马把这个缓存页放到热数据区的头部其实是不合理的。因为加载到冷数据区的缓存页马上(1ms以内)就被访问,然后就被加载在热数据区,如果此后都不在访问了呢?
实际上MySQL的机制
设计了innodb_old_blocks_time 参数,默认值是1000,即1000ms。也就是说,一个数据页加载到缓存页之后,在1s之后,该数据页被访问,才会被挪动到热数据区的链表头部去。
思考:在节点上记录被访问的次数,用定时线程去扫描冷数据区,超过阈值的加入到热数据区。缺点:单个节点需要更大的空间,导致Buffer Pool 的能存的缓存页就变少了;需要浪费更多的资源,要定时线程需扫描。好处:更准确,加载到热数据区的缓存页是热数据的概率高多了。

冷数据区放的都是什么样的缓存页?

预读机制加载进来的缓存页? 全表扫描的数据页?第一次加载进来的数据页?

  1. 预读加载进来的缓存页数据,1S后没人访问的缓存页
  2. 全表扫描或者一些大的查询语句,加载一堆数据到缓存页,都是1S内访问下,后续就不再访问了

热数据的缓存预加载机制

每天统计出来哪些商品被访问的次数多,晚上启动一个定时任务,把热门数据,预加载到Redis 中。

LRU链表的热数据区是如何进行优化的?

减少热数据区的LRU链表节点的移动

上述图片的移动是image.png
上述图片的移动是不太必要的,LRU 链表的热数据区的访问规则被优化了,只有在热数据区的后3/4 部分的缓存页被访问了,才会移动到链表头去;前1/4 的缓存页被访问,是不会移动到链表头部去的。

Buffer Pool 缓存页及几个链表的使用回顾

Buffer Pool 在使用的过程中,实际上会频繁的从磁盘上加载数据页到缓存页中去
同时 free 链表、flush 链表、lru链表都在配合起作用。

  1. 数据加载到缓存页
  2. free 链表就会移除这个缓存页
  3. 在LRU链表 的冷数据区的链表头放置这个缓存页
  4. 如果修改一个缓存页,那么flush链表就会记录这个脏页,同事LRU 链表还可能把该缓存页从冷数据区移动到热数据区的头部去
  5. 如果查询一个缓存页,会把缓存页在LRU链表中移动到热数据区域去,或者在热数据区域中也有可能移动到头部去。

什么时候把LRU尾部的部分缓存刷入磁盘

定时把LRU链表的尾部缓存页 刷入磁盘

并不是只有缓存页满的时候,才会将LRU冷数据区尾部的几个缓存页刷入磁盘,
而是有一个后台线程运行定时任务,把这个定时任务每隔一段时间就把LRU链表的冷数据区域的尾部的一些缓存页,刷入磁盘,清空这几个缓存页,加入到free链表去
image.png
所以只要有这个后台线程在,当你的缓存页还没有用完的时候,该线程就会把冷数据区的一些缓存页刷到磁盘,清空一些缓存页,那么空闲缓存页的数量又增加了。
只要有缓存页刷入磁盘,就会加入free 链表,移出 flush 链表,移出 lru 链表。

把flush 链表中的一些的缓存页定时刷入磁盘

LRU 链表的热数据区存在很多频繁被修改的缓存页,什么时候刷入磁盘呢?
后台线程同时也会在 MySQL 不怎么繁忙的时候,找个时间把flush 链表中的脏缓存页都刷入磁盘,同时移出lru 链表,加入free 链表。

实在没有缓存页了

当free链表 为 null时,flush链表有很多被修改过的脏缓存页,lru 链表有很多缓存页也做了冷热数据分离。
如果此时从磁盘加载数据页到Buffer Pool 中,就会找LRU 链表的冷数据区尾部的一个缓存页刷入磁盘,腾出空闲缓存页,之后才能基于缓存数据来执行CRUD操作。

思考题:

image.png
我觉得不能刷一个吧,起码一批一批的刷。

image.png
image.png

Buffer Pool 在访问的时候需要加锁吗

实际生产环境的一些经验,对Buffer Pool进行配置上的优化,来提升访问性能
假设MySQL 同时接受到多个请求,自然会用多个线程来处理这多个需求,每个线程会负责处理一个请求

image.png
那么这多个线程会同时去访问Buffer Pool ,去操作缓存页,去操作 free 、flush 、lru 链表吗?
那么他们都在访问这个Buffer Pool,访问内存里面的共享数据,是不是有并发的问题呢?需不需要 加锁呢?

多个线程访问一个Buffer Pool,必然是要加锁的,然后让一个线程先完成一系列操作,比如加载数据页到缓存页,更新free 链表,更新lru 链表,然后释放锁,再接着下一个线程再执行一系列操作

多线程并发访问加锁,数据库的性能如何?

多个线程访问一个 Buffer Pool 必然会加锁,多线程串行执行,性能也不差。因为每个线程都是查询或者更新缓存页里面的数据,这个操作发生在内存中的,都是微妙级别,包括更新free、flush、lru这些链表,都是基于指针操作,性能极高。但是有时候有些请求需要进行磁盘IO,会耗时多一些。

MySQL生产优化经验:多个Buffer Pool优化并发能力

设置多个Buffer Pool优化并发能力,但如何保证数据一致性?
分配给Buffer Pool 的总内存是8G,那么每个Buffer Pool 的大小是2G,有4个Buffer Pool
每个Buffer Pool 负责管理一部分缓存页和描述数据块,Buffer Pool之间互相独立,有自己独立的缓存页 和 各种链表。
image.png
那么多线程的并发访问能力就会成倍的提升,多个线程可以在不同的buffer Pool中加锁和执行自己的操作,来并发执行。

问题:那不会造成数据不一致吗,还是会有类似Hash映射,某张表的操作固定打到某个Buffer Pool?
比如 buffer Pool1 -> name = 1; buffer Pool2 - > name = 2; 那么此时pool2 先刷盘,name = 1 会把name = 2 覆盖掉。

buffer pool ,能在运行期间动态调整大小吗?

就上述将的原理是不行的,假设原来8G,现在改为16G,就是得像OS 重新申请一块16G内存,把原来的Buffer Pool 缓存页都迁移到新内存去,性能低极耗时。

如何基于chunk 机制把buffer pool 给拆小

MySQL 设计了一个chunk 机制,即buffer pool 是由很多个chunk 组成的,大小是innodb_buffer_pool_chunk_size 参数控制的,默认是128MB。

假如Buffer Pool 的总大小是8G ,有4个buffer Pool ,每个buffer Pool 是2G,那么此时一个buffer Pool 就是由2G / 128MB = 16 个chunk 。
每个chunk由一系列描述数据块和缓存页构成,每个buffer Pool 里面的多个chunk共享一套 free、flush、lru 这些链表。

image.png

基于chunk 机制如何支持运行期间,动态调整buffer pool 大小的。

8GB -> 16GB
只需要申请到一系列的128MB大小的chunk就可以了,然后把这些申请到的chunk内存分配给buffer pool就行了,并需要额外申请16GB 的内存,并把已有缓存页进行拷贝,可以动态扩展。
Buffer Pool 由多个buffer Pool 组成,每个buffer pool 是多个chunk组成的,运行期间支持动态调整即可。

生产环境给BufferPool设置多少内存

假设你的机器内存32GB,那么你的OS起码用掉几个GB内存,同时还有除了Buffer Pool之外的数据结构
通常是设置比较合理、健康的比例,buffer Pool 设置机器内存的50% ~ 60%
128 -> 80 , 32 -> 20

buffer pool 总大小 = (chunk 大小 * buffer pool 数量)的倍数

设置了Buffer pool 总大小,就得考虑多少个buffer pool 和chunk大小
image.png
image.png

SHOW ENGINE INNODB STATUS

查看innoDB的具体情况

Total memory allocated xxxx;
Dictionary memory allocated xxx
Buffer pool size xxxx
Free buffers xxx
Database pages xxx
Old database pages xxxx
Modified db pages xx
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young xxxx, not young xxx
xx youngs/s, xx non-youngs/s
Pages read xxxx, created xxx, written xxx
xx reads/s, xx creates/s, 1xx writes/s
Buffer pool hit rate xxx / 1000, young-making rate xxx / 1000 not xx / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: xxxx, unzip_LRU len: xxx
I/O sum[xxx]:cur[xx], unzip sum[16xx:cur[0]


(1)Total memory allocated,这就是说buffer pool最终的总大小是多少
(2)Buffer pool size,这就是说buffer pool一共能容纳多少个缓存页
(3)Free buffers,这就是说free链表中一共有多少个空闲的缓存页是可用的
(4)Database pages和Old database pages,就是说lru链表中一共有多少个缓存页,以及冷数据区域里的缓存页
数量
(5)Modified db pages,这就是flush链表中的缓存页数量
(6)Pending reads和Pending writes,等待从磁盘上加载进缓存页的数量,还有就是即将从lru链表中刷入磁盘的数
量、即将从flush链表中刷入磁盘的数量
(7)Pages made young和not young,这就是说已经lru冷数据区域里访问之后转移到热数据区域的缓存页的数
量,以及在lru冷数据区域里1s内被访问了没进入热数据区域的缓存页的数量
(8)youngs/s和not youngs/s,这就是说每秒从冷数据区域进入热数据区域的缓存页的数量,以及每秒在冷数据区
域里被访问了但是不能进入热数据区域的缓存页的数量
(9)Pages read xxxx, created xxx, written xxx,xx reads/s, xx creates/s, 1xx writes/s,这里就是说已经读取、
创建和写入了多少个缓存页,以及每秒钟读取、创建和写入的缓存页数量
(10)Buffer pool hit rate xxx / 1000,这就是说每1000次访问,有多少次是直接命中了buffer pool里的缓存的
(11)young-making rate xxx / 1000 not xx / 1000,每1000次访问,有多少次访问让缓存页从冷数据区域移动到
了热数据区域,以及没移动的缓存页数量
(12)LRU len:这就是lru链表里的缓存页的数量
(13)I/O sum:最近50s读取磁盘页的总数
(14)I/O cur:现在正在读取磁盘页的数量