mysql InnoDB 缓冲池管理方式

      数据库系统(disk-based database)通常使用缓冲池(Buffer Pool)来弥补CPU速度和磁盘速度之间的鸿沟。简单说缓冲池就是一块内存区域,通过内存来缓存频繁访问的数据,从而减少传统机械磁盘速度较慢对于数据库性能的影响。缓冲池中缓存的数据页类型有:索引页,数据页,undo页,插入缓存(Insert Buffer),自适应哈希索引(Adaptive Hash Index)锁信息。

在了解缓冲池管理之前,先来了解一些概念。

Buffer Pool Instance:

大小等于innodb_buffer_pool_size/innodb_buffer_pool_instances,每个instance都有自己的锁,信号量,物理块(Buffer chunks)以及逻辑链表(下面的各种List),即各个instance之间没有竞争关系,可以并发读取与写入。所有instance的物理块(Buffer chunks)在数据库启动的时候被分配,直到数据库关闭内存才予以释放。当innodb_buffer_pool_size小于1GB时候,innodb_buffer_pool_instances被重置为1,主要是防止有太多小的instance从而导致性能问题。每个Buffer Pool Instance有一个page hash链表,通过它,使用space_id和page_no就能快速找到已经被读入内存的数据页,而不用线性遍历LRU List去查找。注意这个hash表不是InnoDB的自适应哈希,自适应哈希是为了减少Btree的扫描,而page hash是为了避免扫描LRU List。

数据页:

InnoDB中,数据管理的最小单位为页,默认是16KB,页中除了存储用户数据,还可以存储控制信息的数据。InnoDB IO子系统的读写最小单位也是页。如果对表进行了压缩,则对应的数据页称为压缩页,如果需要从压缩页中读取数据,则压缩页需要先解压,形成解压页,解压页为16KB。压缩页的大小是在建表的时候指定,目前支持16K,8K,4K,2K,1K。即使压缩页大小设为16K,在blob/varchar/text的类型中也有一定好处。假设指定的压缩页大小为4K,如果有个数据页无法被压缩到4K以下,则需要做B-tree分裂操作,这是一个比较耗时的操作。正常情况下,Buffer Pool中会把压缩和解压页都缓存起来,当Free List不够时,按照系统当前的实际负载来决定淘汰策略。如果系统瓶颈在IO上,则只驱逐解压页,压缩页依然在Buffer Pool中,否则解压页和压缩页都被驱逐。

页的读取: 

随机预读:

指判断某个区域内的页是否大多数已经被访问,且被访问的页是热点数据,满足条件则InnoDB认为该区域的页都可能需要被访问,提前进行读取操作。

这种预读发生在一个数据页成功读入Buffer Pool的时候(buf_read_ahead_random)。在一个Extent范围(1M,如果数据页大小为16KB,则为连续的64个数据页)内,如果热点数据页大于一定数量,就把整个Extent的其他所有数据页(依据page_no从低到高遍历读入)读入Buffer Pool。这里有两个问题,首先数量是多少,默认情况下,是13个数据页。接着,怎么样的页面算是热点数据页,阅读代码发现,只有在young list前1/4的数据页才算是热点数据页。读取数据时候,使用了异步IO,结合使用OS_AIO_SIMULATED_WAKE_LATER和os_aio_simulated_wake_handler_threads便于IO合并。随机预读可以通过参数innodb_random_read_ahead来控制开关。此外,buf_page_get_gen函数的mode参数不影响随机预读。

线性预读:

 读取一个页,若该页是某个区域的边界,且改区域的部分页已经被顺序的访问,则触发线性预读操作,顺序地读取之后或者之前的几个页。线性预读要求页在物里上也是连续的。

这中预读只发生在一个边界的数据页(Extend中第一个数据页或者最后一个数据页)上(buf_read_ahead_linear)。在一个Extend范围内,如果大于一定数量(通过参数innodb_read_ahead_threshold控制,默认为56)的数据页是被顺序访问(通过判断数据页access time是否为升序或者逆序来确定)的,则把下一个Extend的所有数据页都读入Buffer Pool。读取的时候依然采用异步IO和IO合并策略。线性预读触发的条件比较苛刻,触发操作的是边界数据页同时要求其他数据页严格按照顺序访问,主要是为了解决全表扫描时的性能问题。线性预读可以通过参数innodb_read_ahead_threshold来控制开关。此外,当buf_page_get_gen函数的mode为BUF_PEEK_IF_IN_POOL时,不触发线性预读。 InnoDB中除了有预读功能,在刷脏页的时候,也能进行预写(buf_flush_try_neighbors)。当一个数据页需要被写入磁盘的时候,查找其前面或者后面邻居数据页是否也是脏页且可以被刷盘(没有被IOFix且在old list中),如果可以的话,一起刷入磁盘,减少磁盘寻道时间。预写功能可以通过innodb_flush_neighbors参数来控制。不过在现在的SSD磁盘下,这个功能可以关闭。

页的刷新

检查点(checkpoint)是脏页刷新到磁盘的时间点,其目的有:

缩短数据库恢复时间

缓冲池不够用时,将脏页写回磁盘释放空间

重做日志不够用时,将脏页写回磁盘

InnoDB存储引擎存在两种检查点,分别是Sharp Checkpoint和Fuzzy Checkpoint。Sharp Checkpoint发生在数据库关闭时将所有脏页都刷新回磁盘,但运行时一般使用Fuzzy Checkpoint只刷新一部分脏页。Fuzzy checkpoint发生的情况如下:

Master Thread中以每秒或者每10秒的速度从缓冲池的脏页链表中异步刷新一定比例的页

当checkpoint_age超过max_checkpoint_age_async,触发异步写checkpoint过程

flush_lru_list检查点,因为需要保证lru链表和free链表中有可以立即使用的页。

InnoDB会刷新LRU链表中的脏页(BUF_FLUSH_LRU)以及flush链表中的脏页(BUF_FLUSH_LIST)。开始页的刷新操作时,其他线程不得使用页,刷新操作持有页对象的s-latch,刷新完成后通过IO thread释放s-latch。

为了提高刷新性能,InnoDB还支持邻接页的刷新,通过函数buf_flush_try_neighbors实现。当刷新一个脏页时,如果该页所在某个范围的所有页都是脏页且不在LRU链表的热端,则一起进行刷新。这样做可以将多个I/O写入操作合并为一个I/O操作。

Buffer Chunks:

包括两部分:数据页和数据页对应的控制体,控制体中有指针指向数据页。Buffer Chunks是最低层的物理块,在启动阶段从操作系统申请,直到数据库关闭才释放。通过遍历chunks可以访问几乎所有的数据页,有两种状态的数据页除外:没有被解压的压缩页(BUF_BLOCK_ZIP_PAGE)以及被修改过且解压页已经被驱逐的压缩页(BUF_BLOCK_ZIP_DIRTY)。此外数据页里面不一定都存的是用户数据,开始是控制信息,比如行锁,自适应哈希等。

逻辑链表:

链表节点是数据页的控制体(控制体中有指针指向真正的数据页),链表中的所有节点都有同一的属性,引入其的目的是方便管理。下面其中链表都是逻辑链表。

Free List:

其上的节点都是未被使用的节点,如果需要从数据库中分配新的数据页,直接从上获取即可。InnoDB需要保证Free List有足够的节点,提供给用户线程用,否则需要从FLU List或者LRU List淘汰一定的节点。InnoDB初始化后,Buffer Chunks中的所有数据页都被加入到Free List,表示所有节点都可用。

LRU List:

这个是InnoDB中最重要的链表。所有新读取进来的数据页都被放在上面。链表按照最近最少使用算法排序,最近最少使用的节点被放在链表末尾,如果Free List里面没有节点了,就会从中淘汰末尾的节点。LRU List还包含没有被解压的压缩页,这些压缩页刚从磁盘读取出来,还没来的及被解压。LRU List被分为两部分,默认前5/8为young list,存储经常被使用的热点page,后3/8为old list。新读入的page默认被加在old list头,只有满足一定条件后,才被移到young list上,主要是为了预读的数据页和全表扫描污染buffer pool。

FLU List:

这个链表中的所有节点都是脏页,也就是说这些数据页都被修改过,但是还没来得及被刷新到磁盘上。在FLU List上的页面一定在LRU List上,但是反之则不成立。一个数据页可能会在不同的时刻被修改多次,在数据页上记录了最老(也就是第一次)的一次修改的lsn,即oldest_modification。不同数据页有不同的oldest_modification,FLU List中的节点按照oldest_modification排序,链表尾是最小的,也就是最早被修改的数据页,当需要从FLU List中淘汰页面时候,从链表尾部开始淘汰。加入FLU List,需要使用flush_list_mutex保护,所以能保证FLU List中节点的顺序

 

InnoDB 是以什么算法来管理缓冲页的呢?

LRU(latest recently used) 最近最少使用。很多地方都会用到这个算法哟。

InnoDB会在内存中维护一个缓冲池,用于缓存数据和索引。缓冲池分为两个区域,一个是sublist of new blocks区域(经常被访问的数据-热数据),一个是sublist of old blocks 区域(不经常访问的数据)。当用户访问数据时,如果缓冲区里有相应的数据则直接返回,否则会从磁盘读数据到缓冲区的sublist of old blocks区域,然后再移动到sublist of new blocks 区域,并通过LRU最近最少使用算法来提出旧数据页。

新旧区域的比例默认是3/8; 控制参数为 InnoDB_old_blocks_pct 默认为37 取值范围是5~95 这个参数用来表示旧区域的大小。

这里有两个问题:

1 什么时候数据会被放到旧数据区域。

2 什么时候数据会被放到新数据区域。

当从缓冲池中读取数据的时候,有三种情况可能发生:

1. new blocks(young list)区域有该数据

如果一个数据页已经处于young list,当它再次被访问的时候,不会无条件的移动到young list头上,只有当其处于young list长度的1/4(大约值)之后,才会被移动到young list头部,这样做的目的是减少对LRU List的修改,否则每访问一个数据页就要修改链表一次,效率会很低,因为LRU List的根本目的是保证经常被访问的数据页不会被驱逐出去,因此只需要保证这些热点数据页在头部一个可控的范围内即可

2  old 区域中有该数据

从old blocks 区域返回数据,同时将该数据移动到new blocks 区域头部,但是移动到new blocks 区域有一个时间间隔,由参数innodb_old_blocks_time 设置。为了避免缓冲池污染的情况。

 缓冲池污染:

当某一个SQL语句,要批量扫描大量数据时,可能导致把缓冲池的所有页都替换出去,导致大量热数据被换出,MySQL性能急剧下降,这种情况叫缓冲池污染

3 缓冲池中没有该数据,需要从磁盘里读取;

从磁盘中读取的数据首先进入old区域,等待一段时间后,才会移动到new的区域头部。如果缓冲池满了,就要用LRU算法将old数据页踢出。那么这个一段时间是由参数innodb_old_blocks_time来控制的,这个参数默认是1000毫秒也就是1秒。

总结:

缓冲池(buffer pool)是一种常见的降低磁盘访问的机制;

缓冲池通常以页(page)为单位缓存数据

缓冲池的常见管理算法是LRU

InnoDB对普通LRU进行了优化,(新旧区域)

innodb_old_blocks_time 和 Innodb_old_blocks_pct 的大小,影响着LRU淘汰数据策略

参考资料:

http://mysql.taobao.org/monthly/2017/05/01/

https://mp.weixin.qq.com/s/nA6UHBh87U774vu4VvGhyw

https://blog.csdn.net/zuimei_forver/article/details/50402050

发表评论

邮箱地址不会被公开。 必填项已用*标注