【MySQL】BufferPool的内存结构与管理
引言
BufferPool是InnoDB存储引擎中一块连续的内存区域,用于缓存磁盘上的关键数据和索引,以减少随机IO。
BufferPool的组成部分可分为三个部分:核心缓存区域、管理元数据区域和辅助功能区域。
核心缓存区域:作用是将磁盘上的数据页、索引页加载到内存中,避免频繁磁盘 I/O。
管理元数据区域:作用是管理BufferPool的内存分配、页的状态管理,其中包含控制块、Free链表、Flush链表、LRU链表。
辅助功能区域:这部分是InnoDB针对写操作、查询操作等特定场景的专项优化,其中包含自适应哈希索引、ChangeBuffer、UndologBuffer。
内存结构
BufferPool是内存中的一片区域,在专用服务器上会将多达80%的物理内存分配给缓冲池。

如上图,为了通过分散锁竞争提升并发性能,通常缓冲池中包含多个Instances实例,Instances是真正的缓冲池的实例对象,内存操作都是在 Instances 中进行的。每个Instances中包含多个Chunk块,Chunk是在运行状态下动态调整缓冲池大小时的单位大小。每个块中包含若干个从磁盘加载到内存中的Page。

控制块是用于管理缓存页的数据结构,控制块与缓存页是一一对应的,它们都被存放在BufferPool中,每个控制块的大小通常为一个缓存页的5%左右。控制块中记录的缓存页元数据信息主要包括以下几个方面:缓存页所属表空间编号、缓存页编号、缓存页在BufferPool中的地址、链表节点信息、锁信息、LSN信息。
管理
InnoDB通过三个链表对BufferPool中的缓存页进行管理:Free链表、Flush链表和LRU链表。
1.当BufferPool初始化完成后,缓存也中只是被分配了内存空间,并没有从磁盘中加载数据,这些缓存页是Free页,通过Free链表进行管理。

当InnoDB需要从磁盘加载新的页到BufferPool时,如果Free链表不为空会首先从Free链表中获取一个空闲页,将磁盘数据读取到该缓存页,同时把相关元数据写入控制块,最后将该控制块移动到LRU链表。
一个缓存页被加载到BufferPool之后,在之后的查询中需要快速知道BufferPool中是否已经加载了这个页。InnoDB维护一个页哈希表,key是表空间号+页号,value是缓存页控制块的地址,通过“表空间号+页号”作为key查询页哈希表,如果没有就要从磁盘加载数据页,如果已经有了就说明已经在缓冲池中了。
2.当不停的把磁盘数据加载到空闲缓存页中,Free链表中的空闲缓存页会越来越少直到为空。因此避免后续每次只能直接从磁盘读取数据,InnoDB通过LRU链表实现缓存页的淘汰置换,其中包含了所有非空闲的Clean页和Dirty页。
当某个区中的页被频繁加载时,InnoDB会通过预读取机制提前加载这些页所在的区和下一个区。如果直接将这些区中的页塞到LRU链表的头部,并且大多预读页根本没被再次访问,那么这些无用的预读页就会挤掉那些真正缓存命中率高的页面,导致缓冲池的整体命中率降低。因此需要将初次访问与再次访问的缓存页区分开来,初次访问的页面不应该放在LRU链表的头部。LRU链表被分为年轻代(young sublist)和老年代(old sublist),初次访问的页面只会被塞到old区域的头部,只有当old区域中的页面再次被访问才可以塞到young区域头部。
当进行全表扫描等大范围扫描时,由于每访问页面中的一条记录就相当于访问该页面一次,此时即使这些页面先被放入old区域也很快就会被塞到young区域,但是实际上这些页面中的大部分后续根本不会再用到,结果就是真正命中率高的页面又被挤出去了。因此InnoDB规定只有在页面进入到old区域的时间大于阈值后才可以进入到young区域。
young区域中的缓冲页会被频繁访问,对它们的频繁访问也意味着要对LRU链表的节点进行频繁的移动,这无疑会很消耗性能。因此为了减少LRU节点的移动,只有被访问的缓冲页位于young区域1/4处之后,该页才可以被移动到LRU链表的头部。

3. Flush链表用于管理那些被修改过并且需要被刷新到磁盘上的Dirty页。Flush链表确保了脏页能够按照一定的顺序和优先级被刷新,从而保证了数据的持久性和一致性。Flush链表中的脏页在执行了刷盘操作后会将空间还给Free链表。根据业务负载、系统状态、紧急程度的不同,对脏页的处理分为普通处理、冷处理、强行处理三种策略。
普通处理直接从Flush链表中定期刷新一些脏页到磁盘中,是InnoDB最常规的脏页刷盘策略,由后台线程异步执行,几乎不影响前台业务的读写性能。
冷处理从LRU链表的尾部扫描一些页面,如果发现脏页就刷新到磁盘中。优先刷写访问频率低的“冷脏页”,避免刷写“热脏页”导致的性能损耗,其核心目标是最大化保留BufferPool中的热点缓存。
强行处理是在脏页即将溢出、事务提交阻塞时触发的刷盘策略。当缓冲池位置紧张时必须要挤掉一些缓冲页,此时会先查看LRU链表尾部是否有可以直接释放掉的Clean页,如果没有就强行刷新尾部的脏页到磁盘中。
4.预读取机制包含两种方式:
线性预读:如果顺序访问的某个区的页面超过这个某个预设的值,就会触发一次异步读取下一个区中全部的页面到BufferPool中的请求,这个预设值即innodb read ahead_threshold默认是56。
随机预读:如果某个区的13个连续的页面都被加载到了BufferPool中,无论这些页面是不是顺序读取的,都会触发一次异步读取本区中所有其他页面到BufferPool中的请求。InnoDB提供了innodb_random read ahead系统变量,它的默认值为OFF,也就意味着InnoDB并不会默认开启随机预读的功能。
浙公网安备 33010602011771号