IT技术博客大学习 共学习 共进步

MySQL数据库InnoDB存储引擎 Buffer pool LRU List Flush策略详解

MySQLOPS 数据库与运维自动化技术分享 2012-06-20 00:03:19 累计浏览 4,926 次

buffer pool LRU list flush(innodb_buffer_pool_size)

每当一个新页面读取buf pool之后,MySQL数据库InnoDB存储引擎都会判断当前buf pool的free page是否足够,若不足,则尝试flush LRU链表。

在MySQL 5.6.2版本中,MySQL数据库InnoDB存储引擎引入了page_cleaner线程,将遍历LRU链表,flush dirty page,释放free page的重任,从用户线程中剥离出来。page cleaner线程1s运行一次。

本小节的以下部分,将分别对MySQL 5.6.2以前的LRU list flush策略,以及page cleaner线程的LRU list flush策略,做详细的分析。

before MySQL 5.6.2

在MySQL 5.6.2之前,用户线程在读入一个page (buf_read_page)、新建一个page(buf_page_create)、预读page(buf_read_ahead_linear) 等等操作时,都会在操作成功之后,调用buf_flush_free_margin函数,判断当前buf pool是否有足够的free pages,若free pages不足,则进行LRU list flush,释放出足够的free pages,保证系统的可用性。接下来我们可以看看buf_flush_free_margin函数究竟是如何处理的?

Buf0flu.c::buf_flush_free_margin()

// 判断当前buf pool中需要flush多少dirty pages,才能够预留出足够的可被替换的

// 页面(free pages or clean pages in LRU list tail)。可用pages由以下两部分组成:

// 1. buf pool free list中的所有page,都是可以立即使用的。

// 2. buf pool LRU list尾部(5+2*BUF_READ_AHEAD_AREA)所有的clean pages。

//  其中:BUF_READ_AHEAD_AREA为64,是一个linear read ahead读取的大小,1 extent

//

// 那么多少可用的pages才是足够的呢?InnoDB中的定义如下:

// BUF_FLUSH_FREE_BLOCK_MARGIN() + BUF_FLUSH_EXTRA_MARGIN()

// 1. BUF_FLUSH_FREE_BLOCK_MARGIN = 5 + BUF_READ_AHEAD_AREA = 5 + 64 = 69

// 2. BUF_FLUSH_EXTRA_MARGIN = ((BUF_FLUSH_FREE_BLOCK_MARGIN / 4 + 100) /

//                                                                      srv_buf_pool_instance)

//  extra_margin的取值,为127除以系统中buf pool的个数

// 因此,对于1个instance的InnoDB系统,可用的个数约为:69 + 127 = 196

//

n_to_flush = buf_flush_LRU_recommendation();

// 读取buf pool free list中的页面个数

n_replaceable = UT_LIST_GET_LEN(buf_pool->free);

// 从buf pool LRU list tail开始向前遍历BUF_LRU_FREE_SEARCH_LEN(5+2*64)个

// pages,若page可以被replace,则增加n_replaceable取值

// LRU中的page,可以replace的条件,可见前面申请新空闲页中的分析。简单

// 来说,page一定是clean的;而且page没有被其他线程pin住,就可以replace

if (buf_flush_ready_for_replace())

n_replaceable++;

// 计算出n_replaceable取值之后,InnoDB还做了一个小小的优化

// 由于此时的flush LRU list是在用户线程中直接进行的,会影响到用户的响应时间

// 因此做了一个判断:若n_replaceable大于BUF_FLUSH_FREE_BLOCK_MARGIN

// 那么就不需要开始一次LRU flush;否则,返回需要flush的LRU list中的pages

// 个数为n_to_flushed

if (n_replaceable >= BUF_FLUSH_FREE_BLOCK_MARGIN)

return 0;

n_to_flush = BUF_FLUSH_FREE_BLOCK_MARGIN + BUF_FLUSH_EXTRA_MARGIN

- n_replaceable;

retrun n_to_flush;

// 若函数buf_flush_LRU_recommendation返回值大于0,说明需要进行LRU list flush

// 则调用buf_flush_LRU函数,进行LRU list flush。

// 由于buf_flush_LRU在MySQL 5.6.2之前与之后的版本中并无太大区别,因此将在

// 后面集中给于分析。

if (n_to_flush > 0)

buf_flush_LRU();

由于buf_flush_free_margin函数是在用户线程中调用执行的,若需要flush LRU list,那么对于用户的响应时间有较大的影响。因此,在MySQL 5.6.2之后,InnoDB专门开辟了一个page cleaner线程,处理dirty page的flush动作(包括LRU list flush与flush list flush),降低page flush对于用户的影响。在接下来的一小节,我们来看看page cleaner线程是如何处理每次的LRU list flush的。

after MySQL 5.6.2

在MySQL 5.6.2之前,buf pool LRU list的flush,是在用户线程中完成的,会影响用户的响应时间,为了解决此问题,MySQL 5.6.2版本,MySQL数据库InnoDB存储引擎在后台新增了一个page cleaner线程,专门用于处理dirty page的flush (包括LRU list与flush list)。接下来我们来看看page cleaner线程是如何处理的?

srv0start.cc::innobase_start_or_create_for_mysql();

// 在InnoDB启动的主流程中,创建page cleaner线程

os_thread_create(buf_flush_page_cleaner_thread, NULL, NULL);

while

// 如果当前系统处于活跃状态,则page cleaner线程每次休眠1s

if (srv_check_activity(last_activity))

page_cleaner_sleep_if_needed();

next_loop_time = ut_time_ms() + 1000;

// flush LRU list,在MySQL 5.6.6中,LRU list flush,只能由page cleaner线程发起

// 1. InnoDB新增了一个系统参数,innodb_lru_scan_depth,用于控制每次LRU lsit

//      flush需要扫描的LRU tail链表的长度,默认取值为1024

// 2. 于此同时,一个LRU list flush操作,被拆分为每次100个chunk大小的操作

//      保证能够及早的释放出free page,用户线程不需要做长时间的等待。

page_cleaner_flush_LRU_tail();

for (j = 0; j < srv_LRU_scan_depth; j += PAGE_CLEANER_LRU_BATCH_CHUNK_SIZE)

buf_flush_LRU(PAGE_CLEANER_LRU_BATCH_CHUNK_SIZE);

// 进行完LRU list的flush之后,再进行flush list的flush,将在以下的章节分析

page_cleaner_flush_pages_if_needed();

buf_flush_LRU

在MySQL 5.6.2前后的版本中,LRU list flush的不同之处在于是由用户线程发起,还是有后台page cleaner线程发起。但是,无论是用户线程,还是后台page cleaner线程,再决定需要进行LRU list flush之后,都会调用buf_flush_LRU函数进行真正的flush操作。

不同之处在于,MySQL 5.6.2之前,用户线程调用的buf_flush_free_margin函数,在判断是否真正需要进行LRU list flush时,将LRU list tail部分的clean pages也归为可以被replace的pages,不需要flush。而在page cleaner线程中,每隔1s,无论如何都会进行一次LRU list flush调用,无论LRU list tail中的page是否clean。这也可以理解,用户线程,需要尽量降低flush的概率,提高用户响应;而后台线程,尽量进行flush尝试,释放足够的free pages,保证用户线程不会堵塞。

接下来详细分析下buf_flush_LRU函数是如何实现的 (主要基于MySQL 5.6.6 labs版本的实现,对比了MySQL 5.5.16版本,有部分差别)?

  1. 初始化BUF_FLUSH_LRU类型的flush (buf_flush_start()).
  1. 开始进行BUF_FLUSH_LRU类型的flush (buf_flush_batch). 首先获得buf pool的mutex,在整个LRU list flush过程中,buf pool mutex一直持有。因此flush过程中,用户线程是无法分配新页面的,这也是为什么在前面,LRU list flush要被划分为一个个小chunk的原因所在。
  1. 首先尝试从buf pool的unzip LRU中释放free pages (MySQL 5.6.6版本新增)。若最终释放的空间不足,则继续调用buf_flush_LRU_list_batch函数,flush common LRU list。
  1. 从LRU list尾部向前遍历,对于读取到的page,判断page是否属于clean page并且未被pin住,若是,则直接调用buf_LRU_free_block函数释放当前page,从LRU链表中删除,插入free链表。
  1. 若page不是clean page,则调用buf_flush_page_and_try_neighbors函数flush当前page (与前面Buffer Pool页面分配章节不同,在那个章节,只flush一个page,调用的函数是buf_flush_page)。

后台LRU flush,在flush当前页面的同时,也会尝试对当前页面的neighbors进行flush (neighbors的定义为:当前page所属的read ahead area属于同一neighbors,64个pages,一个extent)。遍历当前页面所属的extent,尝试对其中的每一个page进行flush。在LRU list flush中,可以被flush的neighbor page必须满足两个条件:

l  neighbor page必须是old page (bpage->old);疑问:bpage->old何时被设置?

l  neighbor page必须未被pin在内存中,未被使用,当前LRU flush线程能够获取page上的latch,而不需要等待。

满足以上两个条件的neighbor page,就可以调用buf_flush_page函数进行真正的flush了。关于此函数的处理流程,可见本文上面的分析。

  1. 在完成LRU list flush之后(buf_do_LRU_batch函数返回),释放buf pool mutex,然后调用buf_dblwr_flush_buffered_writes函数(在MySQL 5.5.16版本中,这个函数是buf_flush_buffered_writes)将DoubleWrite buffer flush到磁盘上,然后将DoubleWrite buffer对应的dirty page写出到正确的位置(sync)。sync的过程中,会发出signal通知write thread,write thread收到写成功的消息之后,会调用buf_page_io_complete函数,完成dirty page flush的最后清理操作(本文前面已经提到,需要清理的包括从flush list中删除,清除page的oldest_modification标识等等)。
  1. 最后,与步骤1对应,将步骤1初始化的BUF_FLUSH_LRU标识清理(buf_flush_end),然后记录相应的统计信息(buf_flush_common)。完成这些操作之后,一次buf_flush_LRU操作结束。

建议继续学习

  1. Buffer和cache的区别是什么? (累计阅读 7,843)
  2. Linux操作系统中内存buffer和cache的区别 (累计阅读 6,343)
  3. 快速预热Innodb Buffer Pool的方法 (累计阅读 4,985)
  4. InnoDB之Dirty Page、Redo log (累计阅读 4,483)
  5. MySQL数据库InnoDB存储引擎 Insert Buffer实现机制详解 (累计阅读 4,386)
  6. 小心grep 的buffer (累计阅读 4,106)
  7. grep 命令的buffer选项 (累计阅读 3,965)
  8. ConcurrentHaspLRUHashMap实现初探 (累计阅读 3,764)
  9. HBase如何合理设置客户端Write Buffer (累计阅读 3,725)
  10. 缓存算法–LRU (累计阅读 3,465)