206_mysql_innodb_5_Innodb_Buffer Pool2
一 Buffer Pool内部组成
Buffer Pool中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是16KB。为了更好的管理这些在Buffer Pool中的缓存页,InnoDB为每一个缓存页都创建了一些所谓的控制信息,
控制信息包括该页所属的表空间编号、页号、缓存页在Buffer Pool中的地址、链表节点信息、一些锁信息以及LSN信息
控制信息占用的内存大小是相同的,控制信息占用的一块内存称为一个控制块,
控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到Buffer Pool的前边,缓存页被存放到Buffer Pool后边
备注:
1 碎片 分配时候剩余空间不够一个控制块和缓存页的大小
2 申请的内存比innodb_buffer_pool_size的值大5%左右
控制块大约占用缓存页大小的5%,在MySQL5.7.21这个版本中,每个控制块占用的大小是808字节。
innodb_buffer_pool_size并不包含这部分控制块占用的内存空间大小,也就是说InnoDB在为Buffer Pool向操作系统申请连续的内存空间时
二 free链表的管理
Pool中哪些缓存页是可用的,我们可以把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表也可以被称作free链表(或者说空闲链表)。
- 初始化的Buffer Pool中所有的缓存页都是空闲的每一个缓存页对应的控制块都会被加入到free链表中
- 为了管理好这个free链表,特意为这个链表定义了一个基节点,包含着链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。
- 链表的基节点占用的内存空间并不包含在为Buffer Pool申请的一大片连续内存空间之内,而是单独申请的一块内存空间
三 缓存页的哈希处理
当我们需要访问某个页中的数据时,就会把该页从磁盘加载到Buffer Pool中,
如果该页已经在Buffer Pool中的话直接使用就可以了。那么问题也就来了,我们怎么知道该页在不在Buffer Pool中呢?我们其实是根据表空间号 + 页号来定位一个页的,
- key:表空间号+页号
- value: 控制块的值
四 flush链表的管理
修改了Buffer Pool中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页(英文名:dirty page)。
如果不立即同步到磁盘的话,那之后再同步的时候我们怎么知道Buffer Pool中哪些页是脏页,哪些页从来没被修改过呢?
创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,所以也叫flush链表。
假设某个时间点Buffer Pool中的脏页数量为n,那么对应的flush链表就长这样:
五 LRU链表
5.1 背景
Buffer Pool大小有限(free链表有限) 涉及初衷是减少IO,提高缓存命中率 ,当Buffer Pool中不再有空闲的缓存页时,就需要淘汰掉部分最近很少使用的缓存页
这个链表是为了按照最近最少使用的原则去淘汰缓存页的,所以这个链表可以被称为LRU链表(LRU的英文全称:Least Recently Used)
如果该页不在Buffer Pool中,在把该页从磁盘加载到Buffer Pool中的缓存页时,就把该缓存页对应的控制块作为节点塞到链表的头部。
如果该页已经缓存在Buffer Pool中,则直接把该页对应的控制块移动到LRU链表的头部。
LRU链表尾部就是最近最少使用的缓存页 当Buffer Pool中的空闲缓存页使用完时,淘汰LRU链表的尾部找些缓存页
5.2 问题
情况一:Buffer Pool中的页不一定被用到
InnoDB提供了预读服务(read ahead)InnoDB认为执行当前的请求可能之后会读取某些页面,就预先把它们加载到Buffer Pool中。
预读又可以细分为下边两种:
1 线性预读
系统变量innodb_read_ahead_threshold,如果顺序访问了某个区(extent)的页面超过这个系统变量的值,就会触发一次异步读取下一个区中全部的页面到Buffer Pool的请求,
innodb_read_ahead_threshold系统变量的值默认是56是一个全局变量(SET GLOBAL命令来修改)
2 随机预读
如果Buffer Pool中已经缓存了某个区的13个连续的页面,不论这些页面是不是顺序读取的,都会触发一次异步读取本区中所有其的页面到Buffer Pool的请求。
innodb_random_read_ahead系统变量,它的默认值为OFF
情况一总结
预读本来是个好事儿,如果预读到Buffer Pool中的页成功的被使用到,那就可以极大的提高语句执行的效率。
用不到呢?这些预读的页都会放到LRU链表的头部,会导致处在LRU链表尾部的一些缓存页会很快的被淘汰掉,会大大降低缓存命中率
情况二:非常多的使用频率偏低的页被同时加载到Buffer Pool
一些需要扫描全表的查询语句(select * from xxx )
意味着将访问到该表所在的所有页都加载到Buffer Pool中而这种全表扫描的语句执行的频率也不高,每次执行都要把Buffer Pool中的缓存页换一次血,降低了缓存命中率
5.3 解决方案
InnoDB把这个LRU链表按照一定比例分成两截
- young区域 一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做热数据
- old区域 另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做冷数据
innodb按照某个比例将LRU链表分成两半的,不是某些节点固定是young区域的,某些节点固定是old区域的,随着程序的运行,某个节点所属的区域也可能发生变化。
- 通过查看系统变量innodb_old_blocks_pct的值来确定old区域在LRU链表中所占的比例
- 可以在启动时修改innodb_old_blocks_pct参数来控制old区域在LRU链表中所占的比例
old区域在LRU链表中所占的比例是37%,也就是说old区域大约占LRU链表的3/8。
show variables like "%innodb_old_blocks_pct"
优化情况1 预读的页面可能不进行后续访情况的优化
当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应的控制块会被放到old区域的头部。
这样针对预读到Buffer Pool却不进行后续访问的页面就会被逐渐从old区域逐出,而不会影响young区域中被使用比较频繁的缓存页。
优化情况2针对全表扫描时,短时间内访问大量使用频率非常低的页面情况的优化
全表扫描有一个特点,那就是它的执行频率非常低,在对某个处在old区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,
如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。
间隔时间是由系统变量innodb_old_blocks_time控制 show variables like "%innodb_old_blocks_time" # 单位是ms
更进一步优化LRU链表
对于young区域的缓存页来说,每次访问一个缓存页就要把它移动到LRU链表的头部(开销是太大),毕竟在young区域的缓存页都是热点数据,为了避免频繁变革链表头部信息
比如只有被访问的缓存页位于young区域的1/4的后边,才会被移动到LRU链表头部,这样可以降低调整LRU链表的频率,从而提升性能(也就是说如果某个缓存页对应的节点在young区域的1/4中,再次访问该缓存页时也不会将其移动到LRU链表头部)
5.4 刷新脏页到磁盘
后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。
主要有两种刷新路径:
5.4.1 BUF_FLUSH_LRU
从LRU链表的冷数据中刷新一部分页面到磁盘
后台线程会定时从LRU链表尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth来指定,如果从里边儿发现脏页,会把它们刷新到磁盘
show variables like "%innodb_lru_scan_depth
5.4.2 BUF_FLUSH_LIST
从flush链表中刷新一部分页面到磁盘,后台线程也会定时从flush链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙
5.4.3 BUF_FLUSH_SINGLE_PAGE
后台线程刷新脏页的进度比较慢,导致用户线程在准备加载一个磁盘页到Buffer Pool时没有可用的缓存页,
这时就会尝试看看LRU链表尾部有没有可以直接释放掉的未修改页面,如果没有的话会不得不将LRU链表尾部的一个脏页同步刷新到磁盘
5.4.4 用户用户线程批量的从flush链表中刷新脏页
有时候系统特别繁忙时,也可能出现用户线程批量的从flush链表中刷新脏页的情况,很显然在处理用户请求过程中去刷新脏页是一种严重降低处理速度的行为
六 多个Buffer Pool实例
在多线程环境下,访问Buffer Pool中的各种链表都需要加锁处理 大并发下单一的Buffer Pool可能会影响请求的处理速度。
所以在Buffer Pool特别大的时候,可以把它们拆分成若干个小的Buffer Pool,每个Buffer Pool都称为一个实例,独立的去申请内存空间,独立的管理各种链表
我们可以在服务器启动的时候通过设置innodb_buffer_pool_instances的值来修改Buffer Pool实例的个数
[server]
innodb_buffer_pool_instances =2
innodb_buffer_pool_size的值小于1G的时候设置多个实例是无效的
单个buffer pool size = innodb_buffer_pool_size / innodb_buffer_pool_instances
6.1 innodb_buffer_pool_chunk_size
在MySQL 5.7.5之前,Buffer Pool的大小只能在服务器启动时通过配置innodb_buffer_pool_size启动参数来调整大小,在服务器运行过程中是不允许调整该值的。
设计MySQL的在5.7.5以及之后的版本中支持了在服务器运行过程中调整Buffer Pool大小的功能,
每次当我们要重新调整Buffer Pool大小时,都需要重新向操作系统申请一块连续的内存空间,然后将旧的Buffer Pool中的内容复制到这一块新空间,这是极其耗时的
所以MySQL决定不再一次性为某个Buffer Pool实例向操作系统申请一大片连续的内存空间,而是以一个所谓的chunk为单位向操作系统申请空间。
也就是说一个Buffer Pool实例其实是由若干个chunk组成的,一个chunk就代表一片连续的内存空间,里边儿包含了若干缓存页与其对应的控制块
上图代表的Buffer Pool就是由2个实例组成的,每个实例中又包含2个chunk。
我们在服务器运行期间调整Buffer Pool的大小时就是以chunk为单位增加或者删除内存空间,而不需要重新向操作系统申请一片大的内存,然后进行缓存页的复制。
chunk的大小是我们在启动操作MySQL服务器时通过innodb_buffer_pool_chunk_size启动参数指定的,它的默认值是134217728,也就是128M。
- innodb_buffer_pool_chunk_size的值只能在服务器启动时指定,在服务器运行过程中是不可以修改的
- innodb_buffer_pool_chunk_size的值并不包含缓存页对应的控制块的内存空间大小,每个chunk的大小要比innodb_buffer_pool_chunk_size的值大一些,约5%
show variables like "%innodb_buffer_pool_chunk_size"
七 配置Buffer Pool时的注意事项
- innodb_buffer_pool_size必须是innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances的倍数(这主要是想保证每一个Buffer Pool实例中包含的chunk数量相同)
- innodb_buffer_pool_chunk_size的值是128M,innodb_buffer_pool_instances的值是16,两个值的乘积就是2G,innodb_buffer_pool_size的值必须是2G或者2G的整数倍
如果不是相关倍数 会被调整到相关倍数(会比设置的值稍微大一些)
如果在服务器启动时,innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances的值已经大于innodb_buffer_pool_size的值,
那么innodb_buffer_pool_chunk_size的值会被服务器自动设置为innodb_buffer_pool_size/innodb_buffer_pool_instances的值
Buffer Pool中存储的其它信息
Buffer Pool的缓存页除了用来缓存磁盘上的页面以外,还可以存储锁信息、自适应哈希索引等信息
查看Buffer Pool的状态信息
SHOW ENGINE INNODB STATUS语句来查看关于InnoDB存储引擎运行过程中的一些状态信息,其中就包括Buffer Pool的一些信息
Total memory allocated:代表Buffer Pool向操作系统申请的连续内存空间大小,包括全部控制块、缓存页、以及碎片的大小
Dictionary memory allocated:为数据字典信息分配的内存空间大小,注意这个内存空间和Buffer Pool没啥关系,不包括在Total memory allocated中
Buffer pool size:代表该Buffer Pool可以容纳多少缓存页,注意,单位是页
Free buffers:代表当前Buffer Pool还有多少空闲缓存页,也就是free链表中还有多少个节点
Database pages:代表LRU链表中的页的数量,包含young和old两个区域的节点数量
Old database pages:代表LRU链表old区域的节点数量
Modified db pages:代表脏页数量,也就是flush链表中节点的数量
Pending reads:正在等待从磁盘上加载到Buffer Pool中的页面数量当准备从磁盘中加载某个页面时,会先为这个页面在Buffer Pool中分配一个缓存页以及它对应的控制块,然后把这个控制块添加到LRU的old区域的头部,但是这个时候真正的磁盘页并没有被加载进来,Pending reads的值会跟着加1
Pending writes LRU:即将从LRU链表中刷新到磁盘中的页面数量。
Pending writes flush list:即将从flush链表中刷新到磁盘中的页面数量。
Pending writes single page:即将以单个页面的形式刷新到磁盘中的页面数量。
Pages made young:代表LRU链表中曾经从old区域移动到young区域头部的节点数量。
一个节点每次只有从old区域移动到young区域头部时才会将Pages made young的值加1,
如果该节点本来就在young区域,由于它符合在young区域1/4后边的要求,下一次访问这个页面时也会将它移动到young区域头部,并不会导致Pages made young的值加1
Page made not young:
在将innodb_old_blocks_time设置的值大于0时,首次访问或者后续访问某个处在old区域的节点时由于不符合时间间隔的限制而不能将其移动到young区域头部时,Page made not young的值会加1
对于处在young区域的节点,如果由于它在young区域的1/4处而导致它没有被移动到young区域头部,这样的访问并不会将Page made not young的值加1。
youngs/s:代表每秒从old区域被移动到young区域头部的节点数量。
non-youngs/s:代表每秒由于不满足时间限制而不能从old区域移动到young区域头部的节点数量。
Pages read、created、written:代表读取,创建,写入了多少页。后边跟着读取、创建、写入的速率。
Buffer pool hit rate:表示在过去某段时间,平均访问1000次页面,有多少次该页面已经被缓存到Buffer Pool了。
young-making rate:表示在过去某段时间,平均访问1000次页面,有多少次访问使页面移动到young区域的头部了。
这里统计的将页面移动到young区域的头部次数不仅仅包含从old区域移动到young区域头部的次数,还包括从young区域移动到young区域头部的次数(访问某个young区域的节点,只要该节点在young区域的1/4处往后,就会把它移动到young区域的头部)。
not (young-making rate):表示在过去某段时间,平均访问1000次页面,有多少次访问没有使页面移动到young区域的头部
这里统计的没有将页面移动到young区域的头部次数不仅仅包含因为设置了innodb_old_blocks_time系统变量而导致访问了old区域中的节点但没把它们移动到young区域的次数,还包含因为该节点在young区域的前1/4处而没有被移动到young区域头部的次数。
LRU len:代表LRU链表中节点的数量。
unzip_LRU:代表unzip_LRU链表中节点的数量(由于我们没有具体唠叨过这个链表,现在可以忽略它的值)。
I/O sum:最近50s读取磁盘页的总数
I/O cur:现在正在读取的磁盘页数量
I/O unzip sum:最近50s解压的页面数量
I/O unzip cur:正在解压的页面数量
- 点赞
- 收藏
- 关注作者
评论(0)