一、Buffer内存结构
MySQL 服务器启动的时候就向操作系统申请了一片连续的内存,默认128M,可通过从参数修改。
[server]
innodb_buffer_pool_size = 268435456
1.1 控制块
控制块包括该页所属的 表空间编号、页号、缓存页在 Buffer Pool 中的地址、链表节点信息、一些锁信息以及 LSN 信息。**控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边**
1.2 free链表管理(管理剩余空间)
1.2.1 free链表内存结构
free链表:所有空闲缓存页对应的控制块作为节点形成的链表。
链表的基节点:包含链表的头节点地址,尾节点地址,链表节点的数量;
1.2.2 使用free链表 空间申请过程
需要从磁盘中加载一个页到 Buffer Pool 中时,从 free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上(就是该页所在的表空间、页号之类的信 息),然后把该缓存页对应的 free链表 节点从链表中移除,表示该缓存页已经被使用了~
1.3 flush 链表(管理脏页)
1.4 Hash表(如何知道该页在buffer pool中)
表空间号 + 页号 作为 key , 缓存页 作为 value 创建一个哈希表;
需要访问某个页的数据 时,先从哈希表中根据 表空间号 + 页号查有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从 free链表 中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。
1.5 LRU链表(Buffer Pool 空间不够)
1.5.1 介绍
1、如果该页不在 Buffer Pool 中,在把该页从磁盘加载到 Buffer Pool 中的缓存页时,就把该缓存页对应的 控制块 作为节点塞到链表的头部。
2、如果该页已经缓存在 Buffer Pool 中,则直接把该页对应的 控制块 移动到 LRU链表 的头部。 只要用到某个缓存页,就把该缓存页调整到 LRU链表的头部,这样 LRU链表 尾部就是最近最少 使用的缓存页,所以当 Buffer Pool 中的空闲缓存页使用完时,淘汰链表尾部缓存页。
1.5.2 问题(预读和全表扫描降低缓存命中率,读取无用数据到buffer pool)
1、InnoDB会进行预读:
① 线性预读:顺序访问某个区的页面超过(innodb_read_ahead_threshold =56),异步读取一个区中的全部页面。
② 随机预读:buffer pool 缓存某个区的13个页面(无论顺序),触发异步读取本区全部页面(innodb_random_read_ahead 默认关闭)。
2、全表扫描
3 总结 降低Buffer Pool的情况
1、加载到 Buffer Pool 中的页不一定被用到。(预读)
2、如果非常多的使用频率偏低的页被同时加载到 Buffer Pool 时,可能会把那些使用频率非常高的页从 Buffer Pool 中淘汰掉。(全表扫描)
1.5.3 拆分LRU链表解决上述问题
1、一部分存储使用频率非常高的缓存页叫做 热数据 ,或者称 young区域 。
2、一部分存储使用频率不是很高的缓存页叫做 冷数据 ,或者称 old区域 。
**我们是按照某个比例将LRU链表分成两半的,不是某些节点固定是young区域的,某 些节点固定是old区域的,随着程序的运行,某个节点所属的区域也可能发生变化**
①预读场景解决:初次加载到Buffer Pool中的某个缓存页时,该缓存页对应 的控制块会被放到old区域的头部。
②针对全表扫描:old 区域的缓存页进行第一次访问时,就在它对应的控制块中 记录下来这个访问时间,如果最后一次访问时间与第一次访问的时间在某个时间间隔内(很明显在一次 全表扫描的过程中,多次访问一个页面中的时间不会超过 1s ),该页面就不会被 从old区域移动到young区域的头部,否则将它移动到young区域的头部。***如果一个old区页面第二次访问与第一次访问在时间间隔内(短时间内第二次访问)则不移动到young区***
1.5.4 LRU细节优化
被访问的缓存页位于 young 区域的 1/4 的后边,才被移动到 LRU链表头部,降低调整LRU频率。
1.6 脏页刷新
后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,两种路径:
1、BUF_FLUSH_LRU:从 LRU链表 的冷数据中刷新一部分页面到磁盘。
2、BUF_FLUSH_LIST: 从 flush链表 中刷新一部分页面到磁盘。
3、BUF_FLUSH_SINGLE_PAGE: 后台线程刷新脏页的进度比较慢,导致Buffer Pool没有内存,从LRU链表尾部释放未修改页面(刷新单个页面的方式)。
1.7 多实例Buffer Pool
通过参数innodb_buffer_pool_instances配置,默认为2;
当innodb_buffer_pool_size的值小于1G的时候设置多个实例是无效的,InnoDB会默认把 innodb_buffer_pool_instances 的值修改为1。
1.8 innodb_buffer_pool_chunk_size
1.8.1 chunk结构
一个 Buffer Pool 实例 其实是由若干个 chunk 组成的,一个 chunk 就代表一片连续的内存空间,里边儿包含了若干缓存页与其对应的控制块:
innodb_buffer_pool_chunk_size只能在服务器启动时指定,服务器运行过程中是不可以修改
1.8.2 buffer pool与chunk注意事项
1、innodb_buffer_pool_size 必须是 innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances 的倍数(这主要是想保证每一个 Buffer Pool 实例中包含的 chunk 数量相同)。
2、innodb_buffer_pool_size 的值必须是2G 的整数倍。假设指定innodb_buffer_pool_chunk_size 的值是 128M , innodb_buffer_pool_instances 的值是16 ,那么这两个值的乘积就是 2G。
二 总结
1. 磁盘太慢,用内存作为缓存很有必要。
2. Buffer Pool 本质是InnoDB向操作系统申请的一段连续内存空间,innodb_buffer_pool_size调整大小。
3. Buffer Pool 向操作系统申请的连续内存由控制块和缓存页组成,每个控制块和缓存页都是一一对应的,在填充足够多的控制块和缓存页的组合后, Buffer Pool 剩余的空间可能产生不够填充一组控制块和缓存页,这部分空间不能被使用,也被称为 碎片 。
4. InnoDB 使用了许多 链表 来管理 Buffer Pool 。
5. free链表 中每一个节点都代表一个空闲的缓存页,在将磁盘中的页加载到 Buffer Pool 时,会从 free链表 中寻找空闲的缓存页。
6. 哈希表: key 表空间号 + 页号,value 缓存页作为 快速定位某个页是否被加载到 Buffer Pool 。
7. flush链表:Buffer Pool 中被修改的页称为 脏页 ,脏页并不是立即刷新,而是被加入到 flush链表 中,待之后的某个时刻同步到磁盘上。
8. LRU链表 分为 young 和 old 两个区域,可以通过 innodb_old_blocks_pct 来调节 old 区域所占的比例。首次从磁盘上加载到 Buffer Pool 的页被放到 old 区域的头部,在innodb_old_blocks_time 间隔时间内访问该页不会把它移动到 young 区域头部。在 Buffer Pool 没有可用的空闲缓存页时,会首先淘汰掉 old 区域的一些页。
9. Buffer Pool多实例:我们可以通过指定 innodb_buffer_pool_instances 来控制 Buffer Pool 实例的个数,每个 Buffer Pool 实例中都有各自独立的链表,互不干扰。
10. 自 MySQL 5.7.5 版本之后,可以在服务器运行过程中调整 Buffer Pool 大小。每个 Buffer Pool 实例由若干个 chunk 组成,每个 chunk 的大小可以在服务器启动时通过启动参数调整。
11. 可以用下边的命令查看 Buffer Pool 的状态信息:SHOW ENGINE INNODB STATUS\G