文章目录
- 一、前言
- 二、缓冲池(Buffer Pool )
- 1. 缓冲池的概念
- 2. LRU List、Free List 和 Flush List
- 2.1 Free 链表
- 2.1.1 缓冲页的哈希处理
- 2.2 Flush 链表
- 2.3 LRU 链表
- 2.3.1 简单 LRU 链表
- 2.3.2 优化后的 LRU 列表
- 2.3.3 更进一步的优化
- 3. 脏页的刷新
- 4. 多个 Buffer Pool 实例
- 三、重做日志缓冲(redo log buffer)
- 四、参考内容
一、前言
最近在读《MySQL 是怎样运行的》、《MySQL技术内幕 InnoDB存储引擎 》,后续会随机将书中部分内容记录下来作为学习笔记,部分内容经过个人删改,因此可能存在错误,如想详细了解相关内容强烈推荐阅读相关书籍。
二、缓冲池(Buffer Pool )
1. 缓冲池的概念
InnoDB 存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可将其视为基于磁盘的数据库系统(Disk-base Database)。在数据库系统中,由于CPU速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。
缓冲池(Buffer Pool )简单来说就是一块连续的内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。在数据库中进行读取页的操作,首先将从磁盘读到的页存放在缓冲池中这个过程称为将页“FIX”在缓冲池中。下一次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。
对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。这里需要注意的是,页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为 CheckPoint 的机制刷新回磁盘。
默认情况下 缓冲池(Buffer Pool )大小只有128M,可以通过 innodb_buffer_pool_size 参数设置。缓冲池(Buffer Pool )中的页类型有:索引页、数据页、undo页、插入缓冲、自适应哈希、InnoDB 存储的锁信息、数据字典信息等,不能简单的认为缓存池只是缓存索引页和数据页,他们只是占了很大一部分而已。1.0.x 开始允许有多个缓冲池实例,每个页根据哈希值平均分配到不同的缓冲池实例,减少了DB的内部资源竞争。如下图:
InnoDB中为每一个缓冲页都创建了一些控制信息,包括该页所属的表空间属性、页号、缓冲页在 Buffer Pool 中的地址、链表信息等。每个缓冲页对应的控制信息占用的内存大小是相同的,我们把每个页对应的控制信息占用的一块内存称为一个控制块,控制块和缓冲页一一对,都存放在 Buffer Pool 中。如下图:
这里需要注意:
- 在 DEBUG 模式下,一个控制块占用缓冲页大约 5% 的大小,正常模式下会更小一些。
- 每个控制块都对应一个缓冲页,当剩余空间不足以分配一对控制块和缓冲页的大小时就会产生碎片,如上图。
2. LRU List、Free List 和 Flush List
2.1 Free 链表
在 MySQL 刚启动的时候,需要完成对 Buffer Pool 的初始化过程:首先向操作系统申请 Buffer Pool 的内存空间,然后将其划分为若干对控制块和缓冲页。但此时还没有真正的磁盘页被缓存到 Buffer Pool 中,随着程序的运行会不断有磁盘页被缓存到 Buffer Pool 中。那么就需要记录 Buffer Pool 中的那些缓冲页是可用的:将所有空闲的缓冲页对应的控制块作为一个节点放到一个链表中,称为 Free 链表(空闲链表)。刚完成初始化的 Buffer Pool 中,所有的缓冲页是空闲的。如下:
从上图可以看到:
- Free 链表存在一个基节点,包含链表的头节点、尾节点指针以及链表中节点数量等信息。需要注意这个基节点占用的内存并包含在 Buffer Pool 申请的一大片连续内存空间之内,而是一块单独申请的内存空间。
- 每当需要从磁盘加载一个页到 Buffer Pool 中时,就哦那个 Free 链表中取一个空闲缓冲页,并且把该缓冲页对应的控制块的信息填上(即该页所在的表空间、页号等信息),然后把该缓冲页对应的 Free 链接节点(也就是对应的控制块)从链表中移除,表示该缓冲页已经被使用。
2.1.1 缓冲页的哈希处理
当我们需要访问某页中的数据时,就会把该页从磁盘加载到 Buffer Pool 中,如果该页已经在 Buffer Pool 中则直接使用。那么如何定位一个页是否在 Buffer Pool 中?
InnoDB 实际上是通过表空间号 + 页好来定位一个页的,所以可以通过表空间号 + 页号 作为 key,用缓冲页控制块的地址作为 value 来创建一个 哈希表。在需要访问某个页的数据时,先从哈希表中根据表空间号 + 页号 看看是否有对应缓冲页,如果有则直接使用,否则从Free 链表中选择一个空闲的缓冲页,然后把磁盘中对应的页加载到该缓冲页的位置。
2.2 Flush 链表
如果我们修改了 Buffer Pool 中某个缓冲页的数据,此时会造成缓冲页与磁盘页的数据不一致,称为"脏页"(不能修改完一个页的数据就立即刷新到磁盘,不然会严重影响程序性能。所以每次修改完缓冲页后并不是立即刷新而是等到某个刷新时间点后才刷新)。那么InnoDB 就需要记录 Buffer Pool 中哪些页是脏页,以便后续将脏页刷新到磁盘中,因此就出现了一个 FLUSH 链表,凡是被修改过的缓冲页对应的控制块都会作为一个节点加入到这个链表。其结构与 Free 链表相同,这里不再赘述。
注意:某个缓冲页对应的控制块不可能即是 Free 链表的节点又是 Flush 链表的节点。因为如果一个缓冲页时空闲的就不可能是脏页,是脏页就不可能是空闲页。
2.3 LRU 链表
当 Buffer Pool 中不再有空闲的缓冲页时就需要淘汰一部分页。理想情况下是将热点数据所在的页保留,将不常访问的数据所在的页移除,InnoDB 借由 LRU 算法实现了一个链表。
2.3.1 简单 LRU 链表
如果使用传统的 LRU 链表,当需要访问某个页时可以按照如下方式处理链表:
- 如果该页不在 Buffer Pool 中,在把该页从磁盘加载到 Buffer Pool 中的缓冲页时,就把该缓冲页对应的控制块作为节点塞到 LRU 链表的头部。
- 如果该页已经被加载到 Buffer Pool 中则直接把该页对应的控制块移动到 LRU链表的头部。
即:只要使用到某个缓冲页就将该缓冲页加入到 LRU链表头部,这样 LRU 链表的尾部就是最近最少使用的页,当 Buffer Pool 中空闲缓冲页用完后直接淘汰尾部的页即可。
但这种使用存在问题:
-
加载到 Buffer Pool 中的页不一定被用到: InnoDB 存在预读情况,即 InnoDB 认为在执行当前请求时,可能会在后面读取某些页面,就预先将这些页面加载到 Buffer Pool中。这里的预读有分为两种情况:
- 线性预读:如果顺序访问某个区的页面超过 innodb_read_ahead_threshold (默认56)系统变量的值就会触发一次异步读取下一个区中全部的页面到 Buffer Pool中的请求。
- 随机预读:当某个区的13个连续的非常热的页(非常热的页:非常热区域的页,下面有介绍)都被加载到 Buffer Pool中,无论这些页面是否是顺序读取,都会触发一次异步读取本区中所有其他页面到 Buffer Pool 中的请求。随机预读可以通过 innodb_random_read_ahead 变量来控制是否开启,默认关闭。
-
如果有非常多使用频率偏低的页被同时加载到 Buffer Pool 中,则可能会把那些使用频率非常高的页从 Buffer Poll 中淘汰掉:在执行一些大数据量操作时(如全表扫描)会一次性读取很多页,如果Buffer Pool 容量不足则会将其他页面挤出 LRU 链表。此时全表扫描所加载的页可能并不是热点页,而真正的热点页已经被排挤出 LRU 链表。
2.3.2 优化后的 LRU 列表
因为上述问题,InnoDB 的 LRU 链表做了一定的优化处理:InnoDB 将 LRU链表划分为两部分,访问频率高的数据称为热数据,或者称为 young 区域,另一部分访问频率低的数据称为冷数据,或者称为 old 区域。默认情况 young区域占比是 3/8 (37%),这个比例可以通过 innodb_old_blocks_pct 调整。
经过上述调整后 LRU 链表便可以上面提到的问题做处理了:
- 针对预读的页面可能不进行后续访问的优化:当磁盘上某个页面在初次加载到 Buffer Pool 中的某个缓冲页时,该缓冲页的控制块会被放到 old 区域的头部,这样一来,预读到 Buffer Pool 却不进行后续访问的页面就会逐渐从 old 区域逐出,而不会影响 young 区域中的热点数据。
- 针对全表扫描时,短时间内大量使用频率低的页面的优化:在对某个处于 old 区域的缓冲页进行第一次访问时,就在它对应的控制块中记录下着访问时间,如果后续的访问时间与第一次的访问时间在某个时间间隔(通过 innodb_old_blocks_time 控制,默认是1s)内,那么该页面就不会从 old 区域移动到 young 区域的头部,否则将他移动到 young 的头部。对于一个页来说,当它从磁盘加载到 LRU 链表中的 old 区域的某个页时,如果第一次和最后一次访问该页面的时间间隔小于1s,则该页不会加入到 young 区域,而对于一次全表扫描,多次访问一个页面(即读取同一个页面中的多条记录)的时间不会超过1s。
2.3.3 更进一步的优化
对于 LRU 链表中的 young 区域的缓冲区来说,每次访问一个缓冲页就要将其移动到 LRU链表的头部开销着实有点大,因为在 young 中的都是热点数据,频繁对 LRU 链表操作并不是明智之举。因此还存在一些优化策略:如只有被访问的缓冲页位于 young 区域1/4 的后面才会被移动到 LRU 链表的头部,这样可以降低调整 LRU 比链表的频率,提高性能。即 在 young 区域 前 1/4 的区域称为 常热区域,在 young 区域 后 3/4 的区域称为 非常热区域。
3. 脏页的刷新
InnoDB 后台有专门的线程负责每隔一段时间就把脏页刷新到磁盘,这样可以保证不影响用户线程处理正常情况,刷新方式主要有如下两种:
-
从 LRU 建表的冷数据中刷新一部分页面到磁盘 :后台线程会定时从 LRU 链表尾部开始扫描一些页面,如果发现了脏页则将其刷新到磁盘。这种刷新页面的方式称为 BUF_FLUSH_LRU。(缓冲页对应的控制块中存储了该缓冲页是否是脏页的信息,所以扫描 LRU链表可以很轻松获取到某个缓冲页是否是脏页)
-
从 Flush 链表中刷新一部分页面到磁盘 :后台线程会定时从 Flush 链表尾部开始扫描一些页面,刷新的速率取决于当时系统是否繁忙,这种刷新方式称为 BUF_FLUSH_LIST。
-
有时候后台线程刷新脏页的进度比较慢,导致用户线程在准备加载一个磁盘页到 Buffer Pool 中时没有可用的缓冲页,这时就会尝试查看 LRU 链表尾部,看是否存在可以直接释放掉未修改缓冲页。如果没有则将LRU 链表尾部的一个脏页同步刷新到磁。这种将单个页面刷新到磁盘中的刷新方式称为 BUF_FLUSH_SINGLE_PAGE。在系统特别繁忙时,也可能出现用户线程从 Flush 链表中刷新脏页的情况。
4. 多个 Buffer Pool 实例
Buffer Pool 本质上是向 OS 申请的一块连续的内存。在多线程环境下,访问 Buffer Pool 中的各种链表都需要加锁,在 Buffer Pool 特别大且线程并发量特别高的情况下, 单一的 Buffer Pool 可能会影响到请求速度,因此InnoDB 提供了 innodb_buffer_pool_instances 参数可以修改 Buffer Pool 的数量,需要注意的是InnoDB 规定当 innodb_buffer_pool_size 小于 1G 时设置多个实例时无效的,InnoDB 会默认将 innodb_buffer_pool_instances 改为 1。
三、重做日志缓冲(redo log buffer)
InnoDB 的内存区域除了缓冲池外,还有重做日志缓冲(redo log buffer)。InnoDB 首先将重做日志信息先放入到这个缓冲区。然后按照一定频率将其刷新到重做日志文件。重做日志缓冲一般不需要设置很大,因为一般情况下会有后台线程(默认每秒一次)将其刷新到日志文件中,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。
通常情况下 8M 的重做日志缓冲池足以满足绝大部分场景,因为重做日志在以下情况会刷新到重做日志文件
- MySQL 主线程 每秒将重做日志缓冲刷新到重做日志文件。
- 每个事务提交时会将重做日志缓冲刷新到重做日志文件。
- 当重做日志缓冲池剩余空间小于一半时会将重做日志缓冲刷新到重做日志文件。
四、参考内容
书籍:《MySQL是怎样运行的——从根儿上理解MySQL》、《MySQL技术内幕 InnoDB存储引擎 》
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正