

Buffer Pool 的大小

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

Buffer Pool 里面的缓冲页 的描述信息,放在Buffer Pool 开头的位置,记录了 缓冲页 在 缓冲区的位置,所属的表空间、数据页编号。
Buffer Pool 会不会有内存碎片
Buffer Pool 的大小是你自己定的,划分完描述数据块和缓存页之后,就还剩一点点内存,放不下缓存页,那么就属于内存碎片了,让缓存页和描述数据块紧密的挨在一起,就可能减少内存浪费了,就能尽可能减少内存碎片的产生了。
数据库启动时,如何初始化Buffer Pool
数据库只要已启动就会按照设置的Buffer Pool 大小,在稍微加大点,去向OS 申请内存来作为 Buffer Pool 区域。
申请完毕后,就会按照缓冲页的大小和描述数据的大小,划分程一块块区域。
但是此时缓存页都是空的,需要执行CRUD 的时候,才会把数据页对应的页从磁盘文件里读取出来,放到缓存页,才会有数据。
如何知道哪些缓存页是空闲的 - free 链表
是一个双向链表结构,把Buffer Pool 里面的空白的缓存页的描述数据块 串起来,同时还有一个基础节点(不属于 Buffer Pool),指向双向链表的头尾节点,并且记录了连表中有多少个空闲的描述数据块,也就是多少个空闲的缓存页。
free链表里的 单个数据块节点数据结构 - 双向链表的单个节点
当block3 被使用了, 从 free 链表中移除
这里不太明白,block3 被使用了,block2 不应该指向 下一个空闲块么,为什么是 null 了呢?
如何将磁盘上的页读取到Buffer Pool 的缓存页中去?
- 先从free 链表中获取到一个缓存页的描述数据块
- 找到数据块中记录的缓存页的地址
- 把磁盘的数据页读取到对应的缓存页里去,同时把相关的描述数据写入到缓存数据页的描述数据块去
- 把该描述数据块从free 链表中去除

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

根据使用场景,执行CRUD 的时候,肯定要看对应的数据是否在Buffer Pool中,在的话直接修改 缓存区;不在的话,执行上面的从磁盘读取数据页的流程。
那么如何判断在不在呢?
Hash表数据结构 - 每次读取一个数据页到缓存区后,都会写入到Hash 表去
表空间号 + 数据页号 ,作为一个Key
缓存页的地址,作为 value
==查询 Hash 表 时间复杂度低,但是我要找的是行,他怎么和数据页对应起来。
脏数据页 到底为什么会脏

哪些缓存连是要刷回磁盘的呢?
MySQL 对 脏页 维持了一个 flush 链表 , 这个flush 链表本质也是通过 缓存页的描述数据块中的两个指针,让被修改过的缓存页的描述数据块,组成一个双向链表
描述数据块
block 2 更新
Buffer Pool 的内存也不够了,咋办?
假如 Buffer Pool 满了,free 链表为 空时,如何淘汰缓存页
- 把更新过的缓存页,刷到磁盘上去,然后缓存页就可以清空了。

那么应该把那个缓存页的数据刷入磁盘呢?
看缓存页的命中率,刷命中率低的进去
哪些是经常使用,哪些是不常使用的呢?
引入 LRU 链表判断哪些缓存页不常用,Least Recently Used ,最近最少使用
从磁盘加载一个数据页到缓存页的时候,就把缓存页的描述数据块放到LRU 链表头部去了,只要有数据的缓存页都在LRU里,而且最近被加载数据的缓存页会 放到LRU链表头部,再者尾部的缓存页 只要被查询或者修改,就会挪到LRU 链表头部去,也就是被访问的缓存页,一定在LRU头部。
当缓存页没有空闲的时候,就找个最近最少被访问的缓存页去刷磁盘,这样直接再LRU 链表的尾部找缓存页即可。
有点奇怪,LRU 是这样的吗?
这样的LRU有问题吗?
实际运行过程中,可能产生巨大的隐患
MySQL的预读机制
即从磁盘上加载一个数据页的时候,可能会连着把这个数据页相邻的其他数据页 也都加载到缓存里面去。<br />问题?<br />实际上被访问的只有是原来的那个缓存页,其连带着加载进去的缓存页, 并 没有被访问,但其却在LRU 链表的头部,其实LRU链表的尾部缓存页是被频繁 访问的。如果此时要淘汰缓存页,却把尾部频繁访问的淘汰了,保留了通过预读 机制加载进来,并没有被访问的缓存页。 --- 不合理<br /><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%
实际上
冷热数据如何使用?
什么时候会被放在热数据区域?
只要对冷数据区的缓存页进行一次访问,就立马把这个缓存页放到热数据区的头部其实是不合理的。因为加载到冷数据区的缓存页马上(1ms以内)就被访问,然后就被加载在热数据区,如果此后都不在访问了呢?
实际上MySQL的机制
设计了innodb_old_blocks_time 参数,默认值是1000,即1000ms。也就是说,一个数据页加载到缓存页之后,在1s之后,该数据页被访问,才会被挪动到热数据区的链表头部去。
思考:在节点上记录被访问的次数,用定时线程去扫描冷数据区,超过阈值的加入到热数据区。缺点:单个节点需要更大的空间,导致Buffer Pool 的能存的缓存页就变少了;需要浪费更多的资源,要定时线程需扫描。好处:更准确,加载到热数据区的缓存页是热数据的概率高多了。
冷数据区放的都是什么样的缓存页?
预读机制加载进来的缓存页? 全表扫描的数据页?第一次加载进来的数据页?
- 预读加载进来的缓存页数据,1S后没人访问的缓存页
- 全表扫描或者一些大的查询语句,加载一堆数据到缓存页,都是1S内访问下,后续就不再访问了
热数据的缓存预加载机制
每天统计出来哪些商品被访问的次数多,晚上启动一个定时任务,把热门数据,预加载到Redis 中。
LRU链表的热数据区是如何进行优化的?
减少热数据区的LRU链表节点的移动
上述图片的移动是
上述图片的移动是不太必要的,LRU 链表的热数据区的访问规则被优化了,只有在热数据区的后3/4 部分的缓存页被访问了,才会移动到链表头去;前1/4 的缓存页被访问,是不会移动到链表头部去的。
Buffer Pool 缓存页及几个链表的使用回顾
Buffer Pool 在使用的过程中,实际上会频繁的从磁盘上加载数据页到缓存页中去
同时 free 链表、flush 链表、lru链表都在配合起作用。
- 数据加载到缓存页
- free 链表就会移除这个缓存页
- 在LRU链表 的冷数据区的链表头放置这个缓存页
- 如果修改一个缓存页,那么flush链表就会记录这个脏页,同事LRU 链表还可能把该缓存页从冷数据区移动到热数据区的头部去
- 如果查询一个缓存页,会把缓存页在LRU链表中移动到热数据区域去,或者在热数据区域中也有可能移动到头部去。
什么时候把LRU尾部的部分缓存刷入磁盘
定时把LRU链表的尾部缓存页 刷入磁盘
并不是只有缓存页满的时候,才会将LRU冷数据区尾部的几个缓存页刷入磁盘,
而是有一个后台线程运行定时任务,把这个定时任务每隔一段时间就把LRU链表的冷数据区域的尾部的一些缓存页,刷入磁盘,清空这几个缓存页,加入到free链表去。
所以只要有这个后台线程在,当你的缓存页还没有用完的时候,该线程就会把冷数据区的一些缓存页刷到磁盘,清空一些缓存页,那么空闲缓存页的数量又增加了。
只要有缓存页刷入磁盘,就会加入free 链表,移出 flush 链表,移出 lru 链表。
把flush 链表中的一些的缓存页定时刷入磁盘
LRU 链表的热数据区存在很多频繁被修改的缓存页,什么时候刷入磁盘呢?
后台线程同时也会在 MySQL 不怎么繁忙的时候,找个时间把flush 链表中的脏缓存页都刷入磁盘,同时移出lru 链表,加入free 链表。
实在没有缓存页了
当free链表 为 null时,flush链表有很多被修改过的脏缓存页,lru 链表有很多缓存页也做了冷热数据分离。
如果此时从磁盘加载数据页到Buffer Pool 中,就会找LRU 链表的冷数据区尾部的一个缓存页刷入磁盘,腾出空闲缓存页,之后才能基于缓存数据来执行CRUD操作。
思考题:

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


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

那么这多个线程会同时去访问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之间互相独立,有自己独立的缓存页 和 各种链表。
那么多线程的并发访问能力就会成倍的提升,多个线程可以在不同的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 这些链表。

基于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大小

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:现在正在读取磁盘页的数量
