MySQL innodb buffer pool学习笔记

218 阅读10分钟

参考:《MySQL是怎样运行的——从根上理解MySQL》好书,强烈推荐大家购买阅读

MySQL innodb buffer pool的出现,和计算机的多级缓存模式有关,计算机的缓存从CPU的高速缓存到内存到磁盘(只是粗略分级,不代表实际计算机情况),速度是依次变慢。数据库是将数据永久保留在磁盘中,磁盘的速度在CPU看来慢的像乌龟,为了减少访问磁盘的频率,innodb在内存申请一块区域,用于缓存磁盘中的数据,这样就可以通过访问内存来弥补CPU和磁盘之间的性能差距。

1、buffer pool基础架构
2、buffer pool如何管理——buffer pool中的链表
3、刷新脏页到磁盘
4、buffer pool性能优化

1、buffer pool基础架构
首先,buffer pool本质是一块内存区域,而且是连续的;buffer pool的内存区域的最小单元叫缓冲页(参考《从根上理解MySQL》),缓冲页的大小,则与innodb表空间使用的页面大小一致。
为什么会大小一致呢?个人认为和innodb数据页保持同样的尺寸,在buffer pool和innodb磁盘数据交互的时候,两者可以无缝对接,做到一一对应,降低了底层逻辑的复杂度;如果大小不一致,比如buffer pool 缓冲页size是innodb磁盘数据页两倍,那么当从磁盘读取数据的时候,要一次读取两个页才能填满一个缓存页,如果两个页在磁盘不连续,则比读取两个页的成本则会增加;当把页内容从内存刷新到磁盘的时候,这个时候又有两个数据页连续和不连续两种情况等等,这样一来,会发现仅仅是磁盘数据页和内存缓冲页大小不一致,在底层设计上就要增加大量额外的考量,这明显不是一笔划算的买卖。
MySQL默认数据页大小是16KB,缓冲页也是16KB。

[(none)] 15:53:31> show variables like 'innodb_page_size';--单位是字节
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.00 sec)

buffer pool的页面有三种可能:控制块、缓冲页、碎片。缓冲页就是用来缓存磁盘数据页的数据,控制块则是为了管理缓冲页而设计,存储了缓冲页的相关控制信息,控制块和缓冲页是一一对应的;当控制块和缓冲页把buffer pool差不过用光,剩下的内容又不足以生成一对控制块和缓冲页,这部分零散空间就沦为了碎片。
控制块的大小是不是和缓冲页一样大呢?答案是否定的,一来控制块存储的控制信息有限,用不了那么多的空间,二来如果控制块和缓冲页大小一致,那么buffer pool差不多有一半的空间全都用来做控制块了,这对宝贵的内存来说也太浪费了,也不利于innodb通过内存减少磁盘IO的初衷。根据《从根上理解MySQL》,控制块的大小大约是缓冲页的5%左右,也就800字节的样子,而且innodb_buffer_pool_size,是只针对缓冲页的,不包含控制块。

2、buffer pool中的链表
buffer pool中主要链表有三个,分别是free链表、flush链表、LRU链表。三个链表的节点实际是控制块,而不是真实的缓冲页。为什么链表节点是控制块而不是缓冲页本身呢?个人认为控制块本身就是用来管理缓冲页的,几个链表正是对缓冲页的管理,所以用控制块作为链表节点合情合理,其次链表本身在遍历、删除、添加节点的操作可能会与缓冲页本身的数据操作冲突,会影响性能。
free链表就是把所有空闲缓冲页对应的控制块作为节点放到链表中;
flush链表就是把所有脏缓冲页(被修改过的缓冲页)对应的控制块作为节点放到链表中;
buffer pool除了空闲页,就是脏页了,如果只剩脏页,没有空闲页了,日子还得继续过下去,这个时候只能再拉一个链表,决定哪些缓冲页被刷新到磁盘,由脏页转为空闲页,这个决定哪些缓冲页被刷盘的链表,是依据其特性命名的,叫LRU链。只要从磁盘中加载一个页面到buffer pool的一个缓冲页中,该缓冲页对应控制块就会作为一个节点加入到LRU链表中,注意,是只要加载,而不是修改,所以从范围上来看,flush链是lru链的一个子集,flush链的节点,一定在lru链上,lru链上没修过数据的节点,不会出现在flush链里。

2.1 链表基节点
这些链表,都是双向链表,都有一个基节点,里面记录了首节点,尾节点和链表节点数量,首节点可以知道从哪里开始遍历,尾节点可以知道到哪里链表结束,链表节点数量能不用遍历而迅速知道链表的长度。此外,链表基节点所占内存不大,且独立于buffer pool单独申请。

2.2 链表如何使用
free链表用于记录buffer pool中所有的空闲缓冲页,如果不创建一个链表,每次去寻找空闲页的时候都要进行遍历,性能很低。当有数据页要加载到内存buffer pool中,就会从free链表获取一个空闲缓冲页的控制块,将其对应缓冲页提供出来用于缓存从磁盘读取的数据页,再将控制块从free链表移除,表示该缓冲页已被使用。

flush链表用于记录buffer pool中所有脏缓冲页,如果不创建一个链表,每次去寻找脏页的时候都要进行遍历,性能很低。flush链表上的控制块对应的缓冲页都是做过数据修改,需要被刷新到磁盘的,当对应的缓冲页刷新到磁盘后,对应的控制块就会从flush链表移除,重新成为可用的缓冲页。

buffer pool申请出来后大小就是固定的,所以如果数据库足够繁忙,那么缓冲页被用满的情况是很常见的,在总资源有限的情况下,为了保证尽可能减少磁盘IO的目的,那么拆东墙补西墙成为了手段之一,即把一部分缓冲页数据刷新到磁盘,让脏页重新变为空闲页,给最新需要缓冲到内存的数据腾空间。这个链表叫LRU,意思是least recently uesd,直译是最少最近使用,意译一般作最近最少使用。对于LRU链表,innodb做了大量的优化,这里依据《从根上理解MySQL》简单介绍主要的优化点。
V1.0版LRU链表:最简单的LRU链表模型,当访问某个页时,如果该页已经在buffer pool中,则直接将对应控制块放到链表头;如果该页在磁盘中,将该页从磁盘加载到buffer pool,再将其控制块放到链表头。也就是用哪个缓冲页,就把它放到链表头部,不用的慢慢就到了尾部,需要空间的时候,直接淘汰尾部。
V2.0版LRU链表:innodb有俩特性:线性预读和随机预读,这俩都会触发innodb将一个区的所有块全部异步加载到buffer pool中,如果预读的块没有用到,反而是把lru尾部本来要用到的块给刷到磁盘了,这样反而加剧了磁盘IO的问题。其次则是全表扫描,一次会加载大量数据块到buffer pool,会将lru上大量的块刷往磁盘,造成与预读类似的问题。为了解决这俩问题,有了2.0版的lru。2.0版lru主要是将整个lru链分成冷热两部分,热链缓存的访问频率极高的页,冷链缓存的是访问频率较低的页。当磁盘块初次加载到buffer pool中时,该缓冲页对应控制块会去冷链的头部,不会去挤占热链的空间,这些预读的页,如果不再访问,就会逐渐移动到冷链末端直至刷回磁盘。对于全表扫描,按照预读的解决方案行不通,因为一个页里会存在一个表的多条数据,如果按照预读的解决方案,一个页的多条数据,每访问一条数据就放热链头部,那全表扫描的页会在短时间内占据热链头部大量位置。如何解决全表扫描占据热链头部的问题呢,其实就是如何利用全表扫描的特性,将全表扫描这种操作甄别出来。全表扫描会持续性的读取一个块里的所有记录,其任意连续两次读取该块的间隔都是很短的,并且第一次和最后一次读取总的时间间隔也是很短的。所以利用这个特性,在第一次将块读取到lru冷链上的时候,记录下时间,如果最后一次访问这个块的时间与第一次的时间间隔不超过设定的阈值,就不会将这个块从冷链移动到热链头部。

2.3 缓冲页的哈希处理
这里要解决的问题是:当要访问一个页的数据时,如果buffer pool中不存在对应页,首先要把对应数据页加载到buffer pool中,如果存在,则直接读取,那么,如何快速判断,一个页是否存在于buffer pool中呢?全表扫描式的遍历肯定不行,效率太低了。《从根上理解MySQL》是这么说的,“定位一个页,实际是根据表空间号+页号作为key,也就相当于表空间号+页号是一个key,缓冲页控制块就是对应的value。在常见的数据结构里,通过一个key来快速搜素一个value最合适的就是哈希表。所以可以用缓冲页控制块的地址作为value来创建一个哈希表。在需要访问某个页的数据时,先从哈希表中根据表空间号+页号看是否有对应的缓冲页,如果有,说明该页已经在buffer pool中了,直接用就行,如果没有,就从free链表中选一个空闲的缓冲页,然后把磁盘中对应的页加载到该缓冲页的位置。”

3、刷新脏页到磁盘的主要方式
脏页刷新到磁盘,是由专门线程负责,而不是由用户线程来兼职干这活,这样的话,用户线程就能腾出手来处理正常请求,效率不会受到刷脏页的影响。刷新脏页主要有两种方式:从LRU链表冷链中刷新部分页面到磁盘、从flush链表中刷新部分页面到磁盘。

4、buffer pool性能优化
4.1 多buffer pool
一个buffer pool能处理的东西是有限的,当内存资源足够的时候,可以拆分为多个buffer pool,提高并发度,进而提高处理效率。但是内存不够的时候,拆分多个实例,可能会有相反的效果,还是要根据实际情况而定。
4.2 innodb_buffer_pool_chunk_size
这个参数将buffer pool分为若干个chunk,当调整buffer pool的大小时,不再向OS申请一整个buffer pool大小的连续内存,而是每次申请一个chunk大小的连续内存,将原buffer pool里chunk的数据复制到新申请的chunk里,这样能避免一次性申请整个buffer pool大小连续内存及复制数据的耗时。