Mysql_Buffer_Pool

Buffer Pool

简介:Innodb维护了一个缓存区域叫做Buffer Pool,用来缓存数据和索引在内存中。Buffer Pool可以用来加速数据的读写,如果Buffer Pool越大,那么Mysql就越像一个内存数据库,所以了解Buffer Pool的配置可以提高Buffer Pool的性能。

buffer pool的配置

innodb_buffer_pool_size:缓存区域的大小。
innodb_buffer_pool_chunk_size:当增加或减少innodb_buffer_pool_size时,操作以块(chunk)形式执行。块大小由innodb_buffer_pool_chunk_size配置选项定义,默认值128M。
innodb_buffer_pool_instances:当buffer pool比较大的时候(超过1G),innodb会把buffer pool划分成几个instances,这样可以提高读写操作的并发,减少竞争。读写page都使用hash函数分配给一个instances。
当增加或者减少buffer pool大小的时候,实际上是操作的chunk。buffer pool的大小必须是innodb_buffer_pool_chunk_sizeinnodb_buffer_pool_instances,如果配置的innodb_buffer_pool_size不是innodb_buffer_pool_chunk_sizeinnodb_buffer_pool_instances的倍数,buffer pool的大小会自动调整为innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances的倍数,自动调整的值不少于指定的值。
如果指定的buffer大小是9G,instances的个数是16,chunk默认的大小是128M,那么buffer会自动调整为10G。具体的配置可以参考mysql官网的介绍mysql reference

LRU算法

为了管理这些数据,innodb使用了一些链表。
lru链表:用来存储内存中的缓存数据。
free链表:用来存放所有的空闲页,每次需要数据页存储数据时,就首先检测free中有没有空闲的页来分配。
flush链表:在内存中被修改但还没有刷新到磁盘的数据页列表,就是所谓的脏页列表,内存中的数据跟对应的磁盘上的数据不一致,属于该列表的页面同样存在于lru列表中,但反之未必。

防止缓存污染

思考一个问题,如果msyql做一次全表扫描,那么全表扫面的数据就会放到buffer pool中,而且全表扫描的数据大部分都是用不到的,那么之前的热点数据也就被冲掉了。所以innodb有一些策略来防止缓存污染。
在Buffer Pool中,存储数据的最小单位是页,默认是16K,使用LRU算法的变体来进行页数据的淘汰和置换。Buffer Pool把LRU链表分为两个部分,一个部分叫做头部链表用来存储热点数据,一个部分叫做尾部链表,用来存储即将淘汰的数据。头部链表和尾部链表有一个分界点,默认是3/8,就是有3/8的空间用来存储old页面。在innodb中有个参数是innodb_old_blocks_pct,默认是37大概就是3/8,通过配置这个参数可以选择头部链表和尾部链表占用的空间比例,innodb_old_blocks_pct的可配置的范围是从5-95,值越大说明尾部链表占用的空间越大,也就越接近LRU算法。当mysql从磁盘往缓存区存数据的时候,都会先把数据存储在尾部链表,这样一来,即使有全表扫描,那么全表扫描的数据也只能进入尾部链表中,不会影响头部链表的数据。
在innodb中还有一个参数也是用来防止缓存污染的,就是innodb_old_blocks_time。这个参数的默认值是1000ms,意思是,在把数据读入尾部链表的1000ms之内,再次访问相同的数据,这个数据页不会进入到头部链表。这个值越大,那么数据进入头部链表的机会就越少,那么数据被淘汰的概率就越大。

预读操作

预读是mysql提高性能的一个重要的特性。预读是指,在获取一个页面的数据时,在不久的时间里面也会用到存储数据页面的后面的页面(page)或者块(extend)。在mysql中预读有两种。

Linear线性预读

线性预读的单位是extend,一个extend中有64个page。线性预读的一个重要参数是innodb_read_ahead_threshold,是指在连续访问多少个页面之后,把下一个extend读入到buffer pool中,不过预读是一个异步的操作。当然这个参数不能超过64,因为一个extend最多只有64个页面。
例如,innodb_read_ahead_threshold = 56,就是指在连续访问了一个extend的56个页面之后把下一个extend读入到buffer pool中。在添加此参数之前,InnoDB仅计算当它在当前范围的最后一页中读取时是否为整个下一个范围发出异步预取请求。

Random随机预读

随机预读方式则是表示当同一个extent中的一些page在buffer pool中发现时,Innodb会将该extent中的剩余page一并读到buffer pool中。由于随机预读方式给innodb code带来了一些不必要的复杂性,同时在性能也存在不稳定性,在5.5中已经将这种预读方式废弃,默认是OFF。若要启用此功能,即将配置变量设置innodb_random_read_ahead为ON。

数据页访问流程

表空间标号

InnoDB存储引擎是使用表空间来存储页的,表空间又可以被分为系统表空间和独立表空间。为了方便管理,每个表空间都会有一个4字节的编号,值得注意的一点是,系统表空间的编号始终为0,InnoDB也会根据一定规则给其他独立表空间也编上号。
所以,当我们查看或修改某个页的数据的时候,实际上需要同时知道表空间的编号和该页的编号,也就是表空间号 + 页号的组合才能定位到某一个具体的页。同时,InnoDB每个页的编号也是占用4个字节,而在一个表空间内页的编号是不能重复的,4个字节是32个二进制位,也就是说:一个表空间最多拥有 2³² 个页,默认情况下一个页的大小为16KB,也就是说一个表空间最多存储 64TB 的数据。
总之,MySQL从磁盘加载数据到内存中最小单元是页,而定位数据页的方式就是通过“表空间号 + 页号”组合来的。同样在内存中定位数据页也是通过“表空间号 + 页号”来的。然后对于“表空间号”在表示上可能会看见有叫SPACE或TABLESPACE的;对于“页号”在表示上可能会看见有叫PAGE NO,PAGE ID,PAGE NUMBER的,反正只要知道都是一个意思就行了。

访问流程

  1. 当访问的页面在缓存池在命中的话,直接返回该页。为了避免扫描LRU,innodb为每个instances维护了一个page hash,通过space id和page no可以直接找到对应的page。一般情况下,当我饿你需要读入一个Page时,首先根据space id和page no找到对应的instances,然后再查询page hash,如果page hash中没有,则需要从磁盘中读取。
  2. 如果没有命中,则需要把页面从磁盘加载到缓存池中,因此需要在缓存池中找到一个空闲的内存块来缓存这个页面。
  3. 如果空闲内存被使用完,也就是free链表上没有内存块了。则需要在生产一个空闲的内存块。
  4. 首先去LRU列表中找可以替换的内存页面,查找的方向是从列表的尾部开始找,如果找到可以替换的页面,将其从LRU列表中摘除,加入空闲列表,然后再去空闲列表中找空闲的内存块。第一查找最多值扫描100个页面,循环进行到第二次时,会扫描整个LRU列表。
  5. 如果在LRU列表中没有找到可以替换的页,则进行单页刷新,将脏页刷新到磁盘之后,然后将释放的内存块加入到空闲列表。然后再去空闲列表中取。为什么只做单页刷新呢?因为这个函数的目的是获取空闲内存页,进行脏页刷新是不得已而为之,所以只会进行一个页面的刷新,目的是为了尽快的获取空闲内存块。

通过数据页访问机制,可以知道其中当无空闲页时产生空闲页就成为一个必须要做的事情了。如果需要刷新脏页来产生空闲页面或者需要扫描整个LRU列表来产生空闲页面的时候,查找空闲内存块的时间就会延长,这个是一个bad case,是我们希望尽量避免的。因此,innodb buffer pool中存在大量可以替换的页面,或者free列表中一直存在着空闲内存块,对快速获取到空闲内存块起决定性的作用。

缓存池刷新策略

InnoDB会在后台执行某些任务,包括从缓冲池刷新脏页(那些已更改但尚未写入数据库文件的页)。

当启用innodb_max_dirty_pages_pct_lwm(默认值0)参数时,表示启用了脏页面预刷新行为,以控制脏页面占比。也是为了防止脏页占有率超过innodb_max_dirty_pages_pct(默认值75%)的设定值。默认禁用“预刷新”行为。如果当脏页的占有率达到了innodb_max_dirty_pages_pct的设定值的时候,InnoDB就会强制刷新buffer pool pages。另外当free列表小于innodb_lru_scan_depth值时也会触发刷新机制,innodb_lru_scan_depth控制LRU列表中可用页的数量,该值默认为1024。

后台刷新的动作由后台刷新协调线程触发,该线程的所有工作内容均由buf_flush_page_cleaner_coordinator函数完成,我们后面简称它为协调函数。接下来,来看后台刷新协调函数的主体流程。

  1. 调用page_cleaner_flush_pages_recommendation建议函数,对每个缓冲池实例生成脏页刷新数量的建议。在执行刷新之前,会用建议函数生成每个buffer pool需要刷新多少个脏页的建议。

  2. 生成刷新建议之后,通过设置事件的方式,向刷新线程(Page Cleaner线程)发出刷新请求。后台刷新线程在收到请求刷新的事件后,会执行pc_flush_slot函数对某个缓存池进行刷新,刷新的过程首先是对lru列表进行刷新,执行的函数为buf_flush_LRU_list,完成LRU列表的刷新之后,就会根据建议函数生成的建议对脏页列表进行刷新,执行的函数为buf_flush_do_batch。

  3. 后台刷新的协调线程会作为刷新调度总负责人的角色,它会确保每个buffer pool都已经开始执行刷新。如果哪个buffer pool的刷新请求还没有被处理,则由刷新协调线程亲自刷新,且直到所有的buffer pool instance都已开始/进行了刷新,才退出这个while循环。

  4. 当所有的buffer pool instance的刷新请求都已经开始处理之后,协调函数(或协调线程)就等待所有buffer pool instance的刷新的完成,等待函数为pc_wait_finished。如果这次刷新的总耗时超过4000ms,下次循环之前,会在数据库的错误日志记录相关的超时信息。它期望每秒钟对buffer pool进行一次刷新调度。如果相邻两次刷新调度的间隔超过4000ms ,也就是4秒钟,MySQL的错误日志中会记录相关信息,意思就是“本来预计1000ms的循环花费了超过4000ms的时间。

前面我们反复讲到,每个buffer pool需要刷新多少页面是由建议函数生成的,它在做刷新建议的时候,具体考虑了哪些因素?现在我们来详细解析。

在讲这段内容之前,我们先来了解两个参数:innodb_io_capacity与innodb_io_capacity_max,这两个参数大部分朋友都不陌生,设置这个参数的目的,是告诉MySQL数据库,它所在服务器的磁盘的随机IO能力。MySQL数据库目前还没有去自己评估服务器磁盘IO能力的功能,所以磁盘io能力大小由这个参数提供,以便让数据库知道磁盘的实际IO能力。这个参数将直接影响建议刷新的页面的数量。

建议函数它会计算当前的脏页刷新平均速度(也就是一秒钟刷新了多少脏页)以及重做日志的生成平均速度。但这个函数并不是每次被调用时,都计算一次平均速度。它是多久计算一次的呢?这个是由数据库参数innodb_flushing_avg_loops来决定的,默认是30,当这个函数被调用了30次之后或者经过30秒之后,重新计算一次平均值。我们暂且简单理解为30秒钟。计算规则是当前的平均速度加上最近30秒钟期间的平均速度再除以2得出新的平均速度。两个平均值相加再平均,得出新的平均值。这样的平均值能明显的体现出最近30秒的速度的变化。

<参考>
http://www.ywnds.com/?p=9886
https://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool.html#innodb-buffer-pool-lru

Feidao wechat
关注我,一起打怪升级吧!