注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

网易杭研后台技术中心的博客

 
 
 
 
 

日志

 
 

MYSQL 5.5 和 5.6的IO控制简单分析  

来自杜皮   2013-03-12 20:30:36|  分类: 默认分类 |举报 |字号 订阅

  下载LOFTER 我的照片书  |

IO算法对于数据库的性能有着非常直接的影响,当数据库的页面(data,undo)不能完全存放于系统页缓存时,就会有读IO,而系统检查点,以及LRU链表的替换会发起写页面操作,而事务提交刷日志、数据段扩展等等也会有IO产生,各个IO相互影响相互制约。本文简单剖析MYSQL Innodb 是如何权衡各个IO的。由于5.1版本比较古老,并且5.5和刚发布的5.6对IO模块有较大改动,因此选取了5.5.22和5.6.7版本做简单分析。


MYSQL -5.5.22

INNODBbuffer中的页面写入文件中有两种操作Type

1.       BUF_FLUSH_LRU LRU链表尾部的页面刷出

2.       BUF_FLUSH_LIST FLUSH LIST 尾部的页面刷出

 

类型是由前台线程触发(注:INNODBLRUFLUSH页面后会将页面放到freeList中,下次由前台线程直接调用,不需要去LRU链表中找替换) ,调用buf_flush_free_margin->buf_flush_LRU->buf_flush_batch->buf_flush_LRU_list_batch 方法,此方法有一个参数是min_n代表期望至少要刷多少个页面(实际值未必能达到),这个值的计算方法在buf_flush_LRU_recommendation方法中,因此如何控制IO量就需要看懂这个方法:


首先明确几个参数:

         BUF_READ_AHEAD_AREA: 确定read-ahead算法读取几个页面,取64 和当前buffersize除以32后最近的2的幂数中的最小值

         BUF_LRU_FREE_SEARCH_LENLRU FLUSH中至少寻找几个页面,取两倍的BUF_READ_AHEAD_AREA5

         BUF_FLUSH_FREE_BLOCK_MARGIN:当buf_flush_free_margin被调用时尝试将这些数量的页面在freelistLRUlist中被替换。取BUF_READ_AHEAD_AREA5

         BUF_FLUSH_EXTRA_MARGINBUF_FLUSH_FREE_BLOCK_MARGIN之上额外的MARGIN。取BUF_FLUSH_FREE_BLOCK_MARGIN除以4100,再除以bufpool instance的个数


主要的流程为:

1.       得到bufpool freelist长度 n_replaceable

2.       得到LRU尾部的页面bPage

3.       循环: bPage不为NULL,并且n_replaceable 小于freeBlockMarginFlushExtraMargin之和,并且distant小于LRU FREE SEARCH LEN

a)         如果bPage已经准备好被替换,那么n_replaceable++

                                       i.              判断是否准备好被替换的条件是:

1.         bPage可以映射到文件

2.         bPageOldest_modification 中记录的lsn0

3.         bPageio_fix stateBUF_IO_NONE

4.         bPagebuf_fix_countpin)为0

b)         Distance++

c)         bPage指向LRU中前一个页面

4.       如果n_replaceable大于BUF_FLUSH_FREE_BLOCK_MARGIN,则返回0

5.       返回BUF_FLUSH_FREE_BLOCK_MARGIN加上BUF_FLUSH_EXTRA_MARGIN减去n_replaceable

 

buf_flush_LRU_list_batch流程:

         1.    循环:从LRU链表尾部开始搜寻可以flush的页面,只要bPage不为空,并且搜索长度小于传入的min_n值。

         2       调用buf_flush_page_and_try_neighbors->buf_flush_try_neighbors

                   a. 如果LRU链表过短,则除了LRU尾端的页面不刷

                   b. 刷页面的区间为BUF_READ_AHEAD_AREA bufpool当前size1/16的最小值

                   c. 循环:刷写范围内的所有脏页(包括neighbor

I.       如果此脏页不在old区要跳过

                                     II.     如果是neighbors页面,那么buf_fix_count(pin)必须为0才刷出(为了防止semaphore waits,这里和具体的io实现有关,semaphore waits等待之前要刷double writer

此处一个不妥的地方是,INNODBtry Neighbors 是会将一个范围内所有的脏页都认为是邻居页面,但是这些脏页未必是物理连续的,那也就和刷页面顺带把相邻页面一起刷出的初衷有出入,percona改进了此点,详情以及测试效果可以见

http://www.mysqlperformanceblog.com/2012/01/17/benchmarks-of-new-innodb_flush_neighbor_pages/

 

2类型是由master线程触发, 调用buf_flush_list 方法,该方法有两个参数,一个是min_n至少要刷出的脏页数量(实际可能达不到),另一个参数是lsn_limit? 至少将页面old modification小于这个值的脏页给刷出:


1.   Master thread 会在有前台活动时进入LOOP中循环10次调用:

a)         bufpool中超过75%的页面被修改时,会以100%IO能力刷一次FllushList(默认刷两百个页面)

b)      如果脏页在75%以下,并且启用Adaptive flushing(为了保证redolog不会在检查点时爆炸,更加平稳)刷的页面数量根据buf_Flush_get_desired_flush_rate 得到(log生成情况计算得到)

         a. 获取日志组的日志容量和所有bufpoolflushlist长度(脏页)总和

         b. 得到平均日志量:从buf_flush_stat_sum统计信息中获取每秒产生的redo加上当前系统lsn减去buf_flush_stat_cur统计中的redo

         c. 得到LRU平均刷脏页量:从buf_flush_stat_sum统计信息中获取每秒刷的脏页量加上系统非flushlist刷的脏页减去buf_flush_stat_cur统计中的刷脏页量

         d. 得到以日志量计算的刷脏页总和:脏页总和乘以平均日志量除以日志组容量

         e. 将刷脏页总和减去LRU刷脏页数,得到flushList要刷的脏页数

IO100%和这个计算出来的脏页量的较小值传递给buf_flush_list方法

       其中进入这10次循环尽量做到每次循环持续1秒。如果发现没有前台线程活动,则进入background循环

 10秒之后:

获取系统pendIObufpoolpending着的页面IO总和加上日志系统的pendingIO总和

              获取系统IObufpool的读写io总和与日志系统的io总和

         如果pendIO小于3%系统io能力,并且10次循环中的系统IO增量小于200%的系统IO认为磁盘还有剩余的IO能力,那么再以100%的系统IO调用一次buf_flush_list

之后master thread会做fuzzy检查点

       如果脏页超过bufpool70%,以100%系统IO调用一次buf_flush_list,否则以10%系统IO调用一次buf_flush_list

进入background循环后进入flush循环,当系统关闭 fast shutdown参数小于2时要以100%系统IO能力刷脏页

2.         log_preflush_pool_modified_pages会刷脏页

3.         recv_apply_hashed_log_recs会刷脏页

(这两个都是在恢复过程中刷脏页,不在主流程中,因此暂不讨论)

 

5.5.22中刷脏页的统计信息的获取,flush相关的统计信息由后台线程srv_error_monitor_thread进行收集,由于是保留了前20秒的信息,因此mysql是实现了一个流式队列,当得到下一秒的信息后将替换20秒前的统计信息。

实际的写脏页 在buf_flush_list的实现:需要先将doublewrite memory中的内容刷到硬盘中,然后调用buf_flush_write_block_low方法刷页面,其中的核心是fil_io方法,里面通过不同参数控制是读写,操作页面的类型(数据、日志、Ibuf等)将页面发往不同的异步IO队列等待IO完成。具体不细展开。

 

MYSQL 5.6.7RC

测试效果见:

http://dimitrik.free.fr/blog/archives/2012/04/mysql-performance-55-and-56labs-tpcclike.html#Comments

相对于5.5 版本来说,5.6 在adaptive flush的算法上有非常大的改变。相对于5.5的不同之处如下:


前台线程会刷脏页,调用buf_flush_single_page_from_LRU, LRU链表尾部刷一个页面

有一个单独的线程buf_flush_page_cleaner_thread在完成刷脏页的任务

该线程有一个while循环,一个循环基本是1秒钟,这是INNODB主要的IO控制流程

1.  当检测到系统idle或者没有pendingIO,无事可做时,会sleep一直到next_loop_time。(目的是为保证有前台活动时,脏页能控流刷出,而一旦前台没有活动时,脏页能迅速以100%IO 快速刷出)

    if (srv_check_activity(last_activity)

                       || buf_get_n_pending_read_ios()

                       || n_flushed == 0) {

                            page_cleaner_sleep_if_needed(next_loop_time);

                   }

2.  如果检测到系统有活动

a)         刷一次LRU链表尾部 调用page_cleaner_flush_LRU_tail()

b)         再刷一次flush 链表 调用page_cleaner_flush_pages_if_needed()

3.  否则

a)         100%系统IO能力刷一次flushList

当系统关闭时,持续以100%IO能力刷脏页

 

LRU流程:

整个流程被打散到一个个小的chunk中,防止一次锁定LRU链表过长时间。一次搜索的深度最长为1024个页面,一个chunk的大小设定为100

page_cleaner_flush_LRU_tail-> buf_flush_LRU-> buf_flush_batch->buf_do_LRU_batch

1.       先根据unzip LRU链表刷未压缩的页面

a)         buffree链表长度小于1024,并且未压缩的LRU链表长度大于整个LRU链表长度的1/10,才刷

2.       如果未够100个页面则再根据LRU链表刷一次,刷够100为止

a)         Buffree链表小于1024,并且LRU链表长度大于256才刷页面

 

FLUSHLIST流程:

page_cleaner_flush_pages_if_needed-> page_cleaner_do_flush_batch

 

 

Adaptive Flush控流算法相对复杂:

1.       首先每30个循环(一般是一秒一循环)计算一个日志产生平均速率和刷脏页平均速率。这里不再像5.5的流式统计算法,这里是以30个循环作一个batch,只有在跨batch时才会丢弃之前的信息。

2.       获取buf pool中最老的dirtied LSN,跟据当前lsn和这个最老的dirtied lsn相减得到age

3.       计算脏页比率

a)         先得到系统脏页占buffer的比率pct_for_dirty

                         i.              有一个记录最大脏页比率的low water mark 默认50%

                       ii.              如果low water mark0(没有设置),那么如果内存脏页率超过75 返回100%

                      iii.              如果当前内存脏页率超过low water mark,返回dirty_pct * 100 /75

b)         根据age计算lsn比率pct_for_lsn(非常复杂的公式,估计是实验调优)

                         i.              有一个记录adaptive flushlow water mark,默认10%

                       ii.              记录af_lwm = low water mark * log capacity / 100

                      iii.              如果age < aflwm 则返回0

                      iv.              获取max_async_age为当前max_modified_age_async值(此值用于判断,当超过此值,系统起预刷buff pool

                       v.              如果没有到这个值,并且没开启adaptive flish,也返回0

                      vi.              获取 lsn_age_factor = age * 100/ max_modified_age_async

                    vii.              返回(Max_io_capacity / io_capacity) *(lsn_age_factor * sqrt(lsn_age_factor))/7.5

4.       获取pct_total pct_for_dirtypct_for_lsn的较大值。

5.       根据pct_total 算出来的io量,和之前30秒算出来的avg_page_rate做一个平均值,再和系统的max_io_capacity算一个较小值赋值为n_page

6.       根据当前lsn和上一次计算的lsn还有lsn产生的平均速度计算一个age_factor

7.       根据算出来的要刷的脏页n_page age_factor乘以平均lsn产生速率传递给刷脏页的主函数page_cleaner_do_flush_batch

总结:

5.5 的脏页flush分布在不同的线程中(LRU Flush在前台线程,Dirty Flush 在后台线程),后台线程的Dirty Flush利用流式的IO信息统计来估算后台线程需要刷的页面数,由于需要实时统计LRU FLUSH的页面数,而它们又由其他线程完成,因此不可能很精确的控制当前时间需要Dirty Flush的页面数,而且多线程的并发写页面也会造成IO不稳定的现象。

5.6 将主要的刷脏页任务集中在一个后台线程,前台线程一次最多刷一个页面,因此一个线程能做到更加精确的IO控制。除此之外,改进的Adaptive Flush 引入了一个日志age的统计维度,能够更精确的计算需要刷写的脏页量

最后,说一下自己的感想,一个表现稳定的IO算法必然需要大量的实验然后根据实验的结果进行参数调整,MYSQL每个大版本对IO的改动都非常大。IO控制算法也都有改进。在代码中部分参数可能让人摸不着头脑,但这些必定都是大量实践的结果。仔细分析和耐心测试才是系统级软件控制IO控制性能的唯一方法。



  评论这张
 
阅读(1122)| 评论(1)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017