Innodb

缓存

对于InnoDB存储引擎来说,我们存储的用户数据及索引(聚簇索引、普通索引)、各种的系统数据都是以页的形式存储在表空间中,而表空间是InnoDB对文件的抽象,这些数据实际都是存储到磁盘中的。

我们知道跟CPU的速度相比,磁盘的速度是很慢的,所以InnoDB在处理客户端的请求时(比如查看记录),需要将磁盘中的对应的页加载到内存中,将整个页加载到内存后,进行读写操作,完成操作后并不会立刻将这个页对应的内存空间释放掉,而是缓存起来,当下一次访问到这个页面时,也就可以直接访问内存中的这个页,减少了IO开销。

InnoDB的缓冲区

为了缓存磁盘的页,InnoDB在MySQL启动时,会向操作系统申请一块连续的内存空间作为缓冲区,默认情况下,缓冲区大小为128MB,可以在配置文件中指定缓冲区的大小(innodb_buffer_pool_size启动选项)

缓冲区的组成

缓冲区是一个连续的内存空间,这些内存被划分为多个(缓冲页大小为16KB),为了管理这些缓冲页,这些缓存页都对应这一个控制块(包含缓冲页信息的内存区域),控制块包括了缓冲页表空间、页号,页在缓冲区的地址、链表信息,控制块和缓冲页都存放在缓冲区中。

缓冲区中可能存在剩余空间不足以存放一对控制块和缓冲页,这时就产生了内存碎片。

系统变量innodb_buffer_pool_size的大小中并不包括控制块占用的内存空间。

free链表

在MySQL启动时,需要完成缓冲区的初始化工作,也就是需要向操作系统申请内存空间,并将缓冲区划分为许多对控制块与缓冲页,此时缓冲区是没有加载页面的。

InnoDB将空闲的缓冲页对应的控制块作为一个节点,组成了一个空闲链表

为了维护这个空闲链表,还需要一个基节点,它包含了链表的头、尾节点、链表中的控制块的数量等信息,基节点占用40字节空间。

缓存哈希处理

当要访问的页不在缓冲区时,就需要将磁盘上的这个页加载到缓冲区中。

那如何判断这个页是否在缓冲区中呢?使用哈希表!

对于页,是根据表空间、页号来定位的,可以使用表空间+页号作为哈希表的key,页对应的控制块的地址作为value,这样就可以快速访问缓冲区的页信息,如果不能在哈希表中找到这个页,那就从free链表取出一个空闲缓冲页(对应的控制块),将从磁盘取出的页放到这个位置。

flush链表

当我们修改了缓冲区内页的数据后,它就与磁盘上的不一致了,这个页叫做脏页,同样我们对页进行了修改后,并不会立刻将页写入到磁盘中,而是在未来的时间点上刷新到磁盘上。

如何判断这个页是否被修改呢?使用链表结构将对应的缓冲页(对应的控制块)组织起来。

LRU链表

缓冲区的内存大小是固定的,当缓冲区已经没有空闲的缓冲页后,就需要将缓冲区的缓冲页淘汰,让新的页加载到缓冲区中。

使用缓冲区的目的是为了减少磁盘IO,理想情况是我们访问的页在缓冲区中,这样也就不需要从磁盘加载页到内存中了,我们希望缓冲区的命中率越高越好

为了减少磁盘IO,我们在淘汰缓冲页时,也就希望淘汰最近最少使用的缓冲页,使用链表结构完成让常使用的缓冲页放在链表头,让少用的缓冲页放在链表尾,这样也就形成了LRU链表

不足

使用简单的LRU链表是不能达到减少磁盘IO的目的的,主要原因是:

  1. InnoDB的预读,InnoDB认为执行当前请求时,可能会在以后读取到一些页面,就预先读取页面到缓冲区中全表扫描中,需要访问的叶子节点对应的页面放非常多,这会将缓冲区的其他查询语句用到的页面淘汰出去,严重影响了其他查询语句对缓冲区的使用

    • 线性预读:如果顺序访问某个区的页面超过系统变量innodb_read_ahead_threshold的值(默认是56),就会触发一次异步读取下一个所有页面到缓冲区的请求
    • 随机预读:如果某个区的13个连续的页(要求这些页是LRU链表是young区的前1/4部分)被加载到缓冲区中,就会触发一次异步读取本区所有其他页面到缓冲区的请求(innodb_random_read_ahead默认值是OFF)

降低缓冲区命中率的原因有:

  1. 加载到缓冲区的页面没有被利用

  2. 较少使用的页面加载到缓冲区中,使得常用的页面被淘汰

划分区域的LUR链表

InnoDB在设计LRU链表时,将这个链表分为2部分

  • 存储使用频率高的缓冲页的部分被称为热数据,young区

  • 存储使用频率低的缓冲页的部分是冷数据,old区

随着程序的运行,某个节点所属的区域可能发生变化

innodb_old_blocks_pct系统变量指明了old区占用的比例,默认是37%

对LRU链表进行了区域划分后,也就可以针对上面降低缓冲区命中率的情况进行优化:

  • 针对预读的页面可能不会被后续访问:当磁盘上的某个页被加载到缓冲区时,该缓冲页会被加入到old区的头部,这样,对缓冲区的页面进行淘汰时,页就不会影响young区使用频繁的页面

  • 针对全表扫描,InnoDB中的系统变量innodb_old_blocks_time指明了时间间隔(默认1s),对某个处于old区域的页面进行第一次访问后,就在它对应的控制块记录这个访问时间,如果后续的访问时间与第一次访问的时间在这个时间间隔内,就不会将old区的这个页面移动到young区的头部。

系统变量innodb_old_blocks_time如果为0,那么每次访问一个页面,就会将页面移动到young头部。

进一步的优化

频繁的对链表节点进行移动操作,造成的开销过大,针对这个情况,优化的策略是,只有被访问的缓冲页位于young区的1/4的后面,才会将这个缓冲页移动到LRU链表的头部,这样可以提高性能

 

其他

针对LRU链表的优化措施有很多

只要磁盘加载一个页面到缓冲区中,该缓冲页对应的控制块就会被加入到LRU链表

flush链表的节点一定是LRU链表中的节点

为了管理缓冲区的缓冲页,InnoDB还引入了其他链表,包括用于管理解压页unzip LRU链表、管理压缩页的zip clean 链表zip free数组中每一个元素都代表一个链表`,它们组成伙伴系统来为压缩页提供内存空间等。

刷新脏页到磁盘

后台会有专门的线程负责每隔一段时间将脏页刷新到磁盘上,这样不会影响用户线程的请求,刷新方式有:

  1. 从LRU链表的old区域刷新一部分页面到磁盘:后台线程定时从LRU链表尾部扫描一定数量(innodb_lru_scan_depth)的页面,如果发现脏页,就把它们刷新到磁盘,刷新方式是buf_flush_lru

  2. 从flush链表中刷新一部分链表到磁盘:后台线程定时从flush链表中刷新一部分页面到磁盘,刷新的速度取决于当时系统是否繁忙,刷新方式是:buf_flush_list

控制块中记录了缓冲页是否被修改的信息

为了更高效地执行脏页的刷新,InnoDB还设计了许多系统变量来控制刷新的过程:

  • innodb_flush_neighbors

  • innodb_io_capacity_max

  • innodb_adaptive_flushing

  • innodb_max_dirty_page_ptc

特殊情况

后台线程刷新脏页速度比较慢时,可能出现用户线程准备加载页面到缓冲区,而缓冲区没有空闲的页面的情况,这时就尝试查看LRU链表的尾部,释放未修改的缓冲页,如果没有未修改的缓冲页,那就不得不将LRU链表的一个脏页写入磁盘这种方式是buf_flush_single_page

系统特别繁忙时,会出现用户线程从flush链表刷新脏页到磁盘的情况,在处理用户请求时区刷新脏页是一种严重降低处理速度的行为

多个缓冲区实例

缓冲区本质就是向操作系统申请一块连续的内存空间,在多线程的环境下,访问缓冲区需要加锁,如果缓冲区十分大,并发访问量也比较高,就会影响到缓冲区的处理速度。

针对这种情况,可以将比较大的缓冲区拆分成若干个小的缓冲区,缓冲区之间是独立的(独立申请内存区空间,独立地处理各种链表),这样也就可以提高并发性了。

服务器启动时可以修改系统变量innodb_buffer_pool_instances指明缓冲区实例的个数,

一个缓冲区实例占用的空间是innodb_buffer_pool_size÷innodb_buffer_pool_instances,缓冲区大小小于1G时,无法设置多个缓冲区实例

chunk

在MySQL 5.7.5及以后的版本中,支持在服务器运行的过程中,调整缓冲区的大小,但是每次调整缓冲区的大小时,需要重新向操作系统申请一块连续的内存空间,并将原缓冲区的内容复制到新缓冲区中,耗时久。

MySQL不再一次性向操作系统申请一大块内存空间,而是以一个chunk为单位申请内存空间,缓冲区由多个chunk组成,一块chunk代表一个连续的内存空间。

所以我们在服务器运行期间修改缓冲区大小时,就可以以chunk为单位,来增加、减少内存空间,而不需要向操作系统申请一块大的内存空间

chunk的大小可以通过系统变量innodb_buffer_pool_chunk_size修改(默认大小是128M),在服务器运行期间,不允许对这个系统变量进行修改,它代表的是InnoDB向操作系统申请内存空间的大小,如果修改了这个值,就需要将原chunk的控制块、缓冲页复制到新的内存空间中

系统变量innodb_buffer_pool_chunk_size不包含对应控制块占用内存

配置缓冲区注意事项

  • innodb_buffer_pool_size:需要是innodb_buffer_pool_instances×innodb_buffer_pool_chunk_size的倍数(保证缓冲区实例中的chunk数量相同)

  • 如果innodb_buffer_pool_size大于实例数×chunk大小,但不是整数倍,那服务器会自动向上调整到整数倍

  • 如果缓冲区大小小于实例数×chunk大小chunk大小会被调整为缓冲区大小÷实例数

  • 缓冲区还可以存储自适应哈希索引的信息

  • 查看缓冲区状态信息:show engine innodb status\G;

posted @ 2022-05-05 21:58  Nausicaa0505  阅读(162)  评论(0编辑  收藏  举报