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

MySQL数据库InnoDB存储引擎 Buffer Pool页面分配详解

MySQLOPS 数据库与运维自动化技术分享 2012-06-05 00:09:47 浏览 1,943 次
MySQL数据库InnoDB存储引擎  Buffer Pool页面分配(innodb_buffer_pool_size)

当用户需要读取一个在外存中的页面,或者是分配一个新的页面进行插入,就需要调用buf0flu.cc::buf_LRU_get_free_block函数进行页面的分配。一个完整的MySQL数据库InnoDB存储引擎页面分配流程需要包含以下几个步骤:

1.尝试从Buffer Pool的free list中分配新页面,大部分情况下,Buffer Pool的free list都是包含空闲页面的,因此直接分配即可。分配出的页面,需要从free list中删除。

2.若Buffer Pool的free list中不存在空闲页面,同时当前系统正在进行BUF_FLUSH_LRU类型的flush (buf_pool->init_flush[BUF_FLUSH_LRU] == TRUE),则等待flush完成,然后跳转到1重新分配。

3.遍历LRU链表,寻找其中的非脏页面,将这些页面从LRU链表中摘除,并插入free list (buf_LRU_scan_and_free_block ->buf_LRU_free_from_common_LRU_list)。从LRU链表的尾部开始向前遍历,第一次遍历srv_LRU_scan_depth = 1024个page,之后每次遍历整个LRU链表。

buf_LRU_free_from_common_LRU_list函数从LRU链表的尾部遍历,对于遇到的每一个page,调用函数buf_LRU_free_block尝试释放当前page。buf_LRU_free_block是一个极为复杂的函数,其简要流程如下:

a)         判断page是否被pin在内存中(buf_page_can_relocate()),若page被pin住,则说明不能释放此page,直接返回。

b)         判断page是否是脏页(bpage->oldest_modification),若page为脏页,则说明不能释放此page,直接返回。

c)         不满足以上条件,page可以释放。首先将page从LRU list与buf pool的page hash中删除(buf_LRU_block_remove_hashed_page)。

d)         判断当前page的类型,若为zip page,需要做特殊处理,此处暂时略过不表。

e)          page中的记录,若存储在btr_search_sys->hash_index中,则从此hash表中删除(btr_search_drop_page_hash_index())。在调用此函数时,需要释放buf pool的mutex(buf_pool_mutex_exit()),考虑到同时有多个线程可以同时调用此函数,并且将此page标识为可替换page。因此,在释放buf pool mutex之前,将page标识为pin在内存中,不可被替换(buf_page_set_sticky())。

备注:

此处的hash表是MySQL数据库InnoDB存储引擎提供的adaptive hash index,记录级别的hash index (存储记录项到索引页面的映射)。通过参数innodb_adaptive_hash_index,可以控制是否使用此hash index。若参数设置为true,那么在做scan查询时(btr_cur_search_to_nth_level),就会调用btr_search_guess_on_hash函数就行hash index的查询,若存在则可以直接定位到对应的叶页面,减少了search path的代价。

关于InnoDB adaptvie hash index,后续将会做更为详尽的分析,此处简单说明其功能,页面被替换出去时,需要将页面中的记录从hash index中删除。除此之外,adaptive hash index系统中只有一个,使用全局的btr_search_latch来保护,因此存在着并发性能问题

f)         在page中的所有记录,均从btr_search_sys hash表中删除之后,重新获取buf pool mutex。并将page上的pin标识删除。然后将page添加到buf pool的free链表中(buf_LRU_block_free_hashed_page -> buf_LRU_block_free_non_file_page)。

g)         至此,一个clean page的释放过程完成。

h)           同时,InnoDB存储引擎还做了一个统计,当page是clean,可以被选择为替换page,同时判断page是否被访问过(buf_page_is_accessed)。若page未被访问过,那么增加统计计数buf_pool->stat.n_ra_pages_evicted。表示当前page是一个read ahead page,但是却是无效的read ahead。

4.若步骤3,遍历LRU链表,成功释放一个clean page,那么再次扫描buf pool的free list,就会找到空闲page,加以利用。成功返回即可。

5.若步骤3未成功释放一个clean page,则继续进行下一个动作:从LRU链表中flush一个dirty page,然后释放此page (buf_flush_single_page_from_LRU())。注意,此时由于处于用户线程之中,因此只flush一个page即可,保证用户的响应时间。

a)      从LRU链表的尾部遍历,找寻第一个可以立即被flush的page (buf_flush_ready_for_flush)。可以立即被flush的page需要满足以下要求:脏页,在FLUSH链表中;未被pin在内存中;

b)      若找到一个可以立即flush的page,则调用函数buf_flush_page将当前page写到磁盘中,同时设置flush type为BUF_FLUSH_SINGLE_PAGE。buf_flush_page函数中,主要需要将page pin在内存中(buf_page_set_io_fix(BUF_IO_WRITE));设置buf pool对应flush类型的数量;最后调用buf_flush_write_block_low函数写出page。

c)     一个被写出的dirty page,在写出之前,需要做一些额外处理,包括:将此page的最新修改的lsn (newest_lsn)写到页头/页尾;重新计算页面的页头checksum (buf_calc_page_new_checksum)与页尾checksum (buf_calc_page_old_checksum)。

d)      注意 页头checksum的计算,需要读取整个页面中的所有数据(一次读取1 byte);页尾checksum的计算,只需要读取页面的前26 bytes即可。而且,需要先计算页头checksum,写入page,然后再计算页尾checksum。在MySQL 5.6.3之后,InnoDB提供了新的checksum计算方法(crc32),同时新增了系统参数innodb_checksum_algorithm来设置不同的checksum算法。

e)     前面的buf_flush_page函数负责将page写出到disk,但是并不保证page一定被写到磁盘之上。仍旧需要调用函数buf_flush_sync_datafiles将写fsync到磁盘之上。

f)     当写出完全flush到磁盘之后,需要重新在buf pool的LRU链表中定位page,因为在写page时,会释放buf pool mutex以及页面mutex,因此当写完成之后,不能再继续使用原有的page内存结构,可能已经被其他的线程释放。

g)     重新定位到page之后,再次调用buf_LRU_free_block函数,判断当前page是否可以被从LRU链表中摘除。

6.buf_flush_single_page_from_LRU函数返回,无论是否成功flush一个dirty page,均再次尝试一个新的分配free page的循环。当完成20次循环之后,若仍旧不能找到一个可用的page,打出warnning,要求管理员增加buf pool的大小。

7.    疑问:在buf_flush_single_page_from_LRU中,flush一个page时,何时将page从LRU链表与flush链表中摘除?

a)       MySQL数据库InnoDB存储引擎中,无论是日志文件/数据文件的读写操作,当完成之后,会调用buf_page_io_complete函数(异步读/写操作,由后台的io_handler_thread线程组处理;同步读/写操作,由发起读/写操作的线程本身处理)。针对于dirty page的写操作,此函数进而调用buf_flush_write_complete函数,将page从flush链表中摘除(buf_flush_remove);设置in_flush_list为false;同时将页面的oldest_modification设置为0,标识当前页面为clean page,位于LRU链表中的clean page,可以被上面提到的流程重用。

b)         如果当前dirty page是有FLUSH LRU完成写出的,那么同时将flush后的clean page移到LRU链表的最后(LRU_old尾部),如此一来,保证下次页面分配可以快速重用此page。

c)         最后,无论是那种写操作,写成功之后,增加buf pool的统计信息:buf_pool->stat.n_pages_written++

8.当有free page可以使用之后,就可以读取外存页面到内存buf pool之中。前面提到buf_page_io_complete处理写操作的流程,那么该函数又是如何处理读操作的呢?

a)         若读到的是一个压缩页面,则将页面解压(buf_zip_decompress)

b)         调用buf_page_is_corrupted函数,判断页面是否已损坏。该函数主要的流程,就是重新计算页面的new checksum与old checksum,然后对比计算出来的checksum与页面中保存的checksum是否一致。若两个checksum均一致,则页面未损坏,否则返回页面已损坏。

c)         调用ibuf_merge_or_delete_for_page方法,若页面存在insert buffer记录,则合并。

d)       最后,释放页面上的latch与mutex,并返回。

d)         最后,释放页面上的latch与mutex,并返回。

建议继续学习

  1. Buffer和cache的区别是什么? (阅读 7,841)
  2. Linux操作系统中内存buffer和cache的区别 (阅读 6,341)
  3. 快速预热Innodb Buffer Pool的方法 (阅读 4,983)
  4. MySQL数据库InnoDB存储引擎 Buffer pool LRU List Flush策略详解 (阅读 4,923)
  5. InnoDB之Dirty Page、Redo log (阅读 4,481)
  6. MySQL数据库InnoDB存储引擎 Insert Buffer实现机制详解 (阅读 4,382)
  7. 小心grep 的buffer (阅读 4,103)
  8. grep 命令的buffer选项 (阅读 3,963)
  9. HBase如何合理设置客户端Write Buffer (阅读 3,720)
  10. 浅谈数据库系统中的cache (阅读 3,103)