mysql 的Buffer Pool结构

284 阅读9分钟

数据页与缓存页

MySQL对数据抽象为一个个的数据页,每个数据页包含若干行数据,默认大小16KB

更新数据时,会把那行数据所在的数据页load到BP中,即BP中存放的是一个个的数据页,称为缓存页

BP中的缓存页与磁盘中的数据页大小是一一对应的16KB

缓存页描述信息

BP中每个缓存页都会对应一个描述信息,在BP中会多使用一块空间来保存,称为描述数据块

描述信息/数据(也叫控制信息/数据),包含数据页所属表空间、数据页编号,缓存页在BP中的地址等信息

BP中缓存页描述数据放最前面,各个缓存页放后面

描述信息大小

描述信息相当于缓存页大小的5%,大概800byte

Buffer Pool结构.png

Buffer Pool结构

BP默认大小128MB

三种链表

free链表:空缓存页描述数据块

lru链表:非空缓存页描述数据块

flush链表:脏页描述数据块

每个链表都有一个40byte的基础节点,不在BP内,记录头节点和尾节点地址,以及链表当前节点数量

描述数据块结构
DataDescBlock {
    // flush前驱指针
    // flush后继指针
    
    // lru前驱指针
    // lru后继指针
​
    // free前驱指针
    // free后继指针
​
}

空缓存页

DB把所有空闲缓存页对应的描述数据块组织为一个free双向链表

所以当DB启动时,所有描述数据块都在free链表中

非空缓存页

DB会维护一个哈希表,用表空间号+数据页号作为key,非空缓存页的地址作为value

这里哈希表的value是否对应lru链表的缓存页节点?

当要使用一个数据页时,通过表空间号和数据页号作为key去哈希表查询,如果没有就读取数据页,如果有则说明数据页已经被缓存了。

非空缓存页的描述数据块会被组织为lru链表(lru你懂的),当缓存页不足以load数据页,就会将lru尾节点对应的缓存页刷进磁盘(非脏页直接释放,脏页先刷盘再释放),腾出一个空缓存页,因为尾节点的缓存页即是缓存命中率最低的

优秀评论
free链表、lru链表和flush链表走的逻辑就像一个环一样,free链表->lru链表->flush链表->free链表,就像案例讲的拿一个update语句的执行来梳理整个机制。 将数据页数据写到缓存页中:先从数据页缓存的哈希表中,通过表空间号(database+table得)和数据页号(一致性算法得)定位该数据页对应的缓存页 (1)找到就说明该数据页已经在缓冲池中了在lru链表中了,直接更新lru链表中的节点,同时将被修改的缓存页加入到flush链表中,lru链表中的数据在free链表不够时,从lru末尾节点开始取节点,判断,如果节点同时也在flush链表中,由IO线程刷入到磁盘中并释放掉,重新归位到free链表里,完整整个流程;如果节点不在flush链表,那么它只是简简单单在缓冲池中的一个和磁盘中数据页数据相同的一个缓存页,直接释放即可; (2)如果没有找到,那就从这里开始,先从free链表中拿到一个描述信息,定位到一个空闲的缓存页,将数据页中的信息写入到缓存页中,回过头来将一些元数据相关信息写回到描述信息中,然后从free链表中调整指针脱离free链表,加入到lru链表,同时也将信息缓存到数据页缓存哈希表中,以供下次快捷取数;lru链表当然像之前说的一样,修改了就加入到flush链表,lru链表在free链表不够时,从末尾节点开始,根据是否在flush链表决定是否由io线程刷回到磁盘释放还是直接释放,并回归到free链表;

脏页

当进行增删改操作后,对应缓存页就变成脏页(脏数据),等待后台IO线程刷回磁盘文件

但需要区分哪些缓存页是脏页,而不可能把所有缓存页都刷回磁盘

所以类似free链表,脏页的描述数据块会被组织为一个flush链表,flush链表中对应的缓存页都需要被刷回磁盘

chunk

BP由多个chunk组成,大小由 innodb_buffer_pool_chunk_size控制,默认128MB

每个chunk包含了描述数据块和缓存页,根据描述数据块结构特点,free、flush、lru链表公共可见

可在运行时通过申请一块连续内存组织为chunk,然后分配给BP来动态调整BP大小

BP的chunk结构.png

缓存页生命周期

空闲缓存页在free链表 -> load完数据后加入lru链表 -> 被修改后加入flush链表 -> 释放内存(可能伴随刷盘)回归free链表

数据页装入缓存页流程

  • 启动DB,根据BP参数指定的大小,到OS申请分配内存作为BP
  • 按照缓存页和描述数据大小,在BP中划分出一个个的缓存页和对应的描述数据
  • 执行crud操作时,到哈希表检查数据页是否已缓存,已缓存则直接读取缓存页
  • 若数据页未缓存,则从free链表中获取一个描述数据块,找到对应的空闲缓存页
  • 将磁盘中的数据页load进空闲缓存页,将描述数据块从free链表中移除(删除节点前后指针)

MySQL数据加载与更新.png

表和行、表空间和数据页

表和行是逻辑概念

表空间和数据页是物理概念,在磁盘层面,表里的数据都放在表空间中,表空间是由磁盘上数据文件组成的,数据文件存放了表里的数据,这些数据是由一个个数据页组织起来的

Buffer Pool运行原理

简单LRU算法隐患

  • 预读机制

    当从磁盘上加载一个数据页时,会连带着把相邻的其他数据页也加载到BP中

    触发MySQL预读机制时,将大量不频繁访问的数据页放进lru链表头部,导致频繁访问的数据页被挤到lru尾部,当内存不足,这些频繁访问的节点就会被淘汰

    触发预读机制有两种情况:innodb_read_ahead_threshold、innodb_random_read_ahead

  • 全表扫描

    全表扫描时会把表中所有数据页加载进BP,导致和第一种情况类似的结果

冷热机制

为了解决频繁访问的缓存页被误杀问题,MySQL把lru链表分为冷热两段

通过 innodb_old_blocks_pct参数控制热区占比,默认37%

热区放经常访问的缓存页,冷区放不常访问的缓存页

数据页load进BP后,刚load进来的数据全都放冷区

MySQL有一个innodb_old_blocks_time参数,默认1000ms

表示加载完只是1s内访问了一下,或者1s后没访问的就放冷区

1s之后访问的缓存页就挪到热区头部

LRU链表冷热区.png

学会运用冷热分离的思想

内存淘汰

两个时机

  • 后台线程,定时把flush链表lru链表冷区尾部缓存页刷盘,腾出空闲缓存页,回归free链表(对应Buffer Pool数据落盘)
  • 当空闲缓存页不足,直接淘汰lru链表冷区尾节点

若lru链表尾节点同时也在flush链表中,则先将脏页刷盘,再释放内存

若不在flush链表中,直接释放内存

归纳CRUD操作BP各种链表的动态运行流程

热区优化

热区的缓存页会经常被访问,如果每访问一个缓存页就把他挪到头结点,这样频繁操作会影响性能

针对这个问题,有一套热区优化的规则:

若节点在链表前1/4位置,则不挪到头部

若节点在链表后3/4位置,则挪到头部

Buffer Pool性能问题

并发性能问题

每个线程都可能会操作free、flush、lru链表,所以多线程并发访问BP时必须加锁,串行执行

因为BP是基于内存操作,操作链表只是调整指针,所以性能不算差

但有时可能要从磁盘读取数据页到BP缓存页,有磁盘IO,故耗时也会多一点

可以设置多个BP,且一个数据页只会在一个BP的缓存页 如何做到的?,以此来提高数据库并发性能

双倍IO问题

当并发过高,crud操作过多时,会导致空缓存页消耗速度快、释放慢,进而导致空缓存页频繁不足

根据BP运行原理,当缓存页不足时,会淘汰lru链表冷区尾节点,若节点是脏页,还需将他先刷进磁盘,再读取数据页,此处就有两次磁盘IO,导致性能不佳

解决方案

因为BP会有后台线程定时淘汰lru链表尾节点(可能伴随刷盘操作),释放空缓存页,所以缓存页是一个边用边释放的过程 结合BP运行原理动态理解

故可以通过增大BP内存,提高空缓存页数量,降低缓存页不足的频率,从而降低双倍IO次数,提高性能

虽然空闲缓存页整体还是会慢慢减少,但等DB请求高峰过去,在后台线程的作用下,空缓存页又会慢慢增加

所以可以根据实际情况,适当调整BP大小BP数量来提高性能

Buffer Pool大小设置

公式:BP总大小 = chunk大小 * BP数量 * 整数倍(即chunk块总数量)

BP建议设置为机器内存50% - 60%

SHOW ENGINE INNODB STATUS 查看BP信息

为何不能直接更新磁盘数据?

直接更新会对磁盘进行随机读写操作,性能极低,完全扛不起任何并发

所以才需要在内存更新,事务机制,后台定时刷盘来处理