你的SQL语句为什么变慢了?
通过WAL机制,知道了,InnoDB在处理更新语句的时候,只做了写日志这个磁盘操作。这个日志叫 redo log(重做日志)、也就是掌柜用来记账的粉板,在更新完内存写完redo log后,就返回给客户端,本次更新成功。
类比一下:掌柜记账的账本是数据文件,记账用的粉板是日志文件(redo log),掌柜的记忆就是内存。
掌柜总要找时间把账本更新一下,对应的就是把内存里的输入写入磁盘的过程,术语就是flush。在这个flush操作执行之前,孔乙己的赊账总额,其实和掌柜手中的账本里面的记录是不一致的,因为孔乙己今天的赊账金额还只在粉板上,而账本里的记录是老的,还没把今天的赊账算进去。
当内存数据页跟磁盘数据页内容不一致的时候,我们称内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”
平时执行很快的更新操作,其实就是在写内存和日志,而MySQL偶尔会“抖”一下的那个瞬间,可能就是在刷脏页(flush)。
想一想:掌柜在什么情况下会把粉板上的赊账记录改到账本上?
第一种场景,粉板写满了,记不下了。这时候如果再有人来赊账,掌柜就只能放下手里的活儿,将粉板上的记录擦掉一些,流出空位以便继续记账。当然擦掉之前,必须先将正确的账目记录到账本中才行。
对应的场景就是InnoDB的redo log写满了。这个时候系统会停止所有更新操作,把checkpoint往前推进,redo log留出空间可以继续写。
checkpoint可不是随便往前修改下一个位置就可以的,把checkpoint位置从cp推进到CP’,就需要将两个点之间的日志(浅绿色部分),对应的所有脏页都flush到磁盘上。之后,图中write pos到CP‘之间就是可以再写入redo log的区域。第二种场景,这一天生意太好了,要记住的事情太多,掌柜发现自己快记不住了,赶紧找出账本把孔乙己这笔帐先加进去。
这种场景,对应的就是系统内存不足,当需要新的内存页,而内存不够的时候,就要淘汰一些数据页,空出来内存给别的数据页使用。如果淘汰的是“脏页”,就要先将脏页写到磁盘。
这时候难道不能直接把内存淘汰掉,下次需要请求的时候,从磁盘读入数据页,然后拿redo log出来应用不就行了?这里其实是从性能考虑的,如果刷脏页一定会写盘,就保证每个数据页有两种状态:- 一种是内存里存在,内存里就肯定是正确的结果,直接返回。
- 另一种是内存没有数据,就可以肯定数据文件上是正确的结果,读入内存后返回。
第三种场景,生意不忙的时候,或者打烊之后,这时候柜台没事,掌柜闲着,不如更新账本。
这种场景,对应的就是MySQL认为系统“空闲”的时候,MySQL”这家酒店”的生意好起来可是会很快就能把粉板记满的,所以“掌柜”要合理地安排时间。第四种场景,年底了酒店关门,需要把账节结清一下。
这种场景,对应的MySQL正常关闭的情况。这时候,MySQL会把内存的脏页flush到磁盘上。
接下来分析一下四种场景对性能的影响:
主要关注第一种,和第二种。
第一种是“redo log写满了,要flush脏页“,这种情况是InnoDB要尽量避免的。因为出现这种情况的时候,整个系统就不能再接受更新了,搜有的更新都必须堵住。
第二种是”内存不够用了,要先将脏页写到磁盘“,这中情况其实是常态。**InnoDB用缓冲池(buffer pool)**管理内存,缓冲池中的内存页有三种状态:
- 第一种,还没使用的;
- 第二种,使用了并且是干净页;
- 第三种,使用了并且是脏页。
InnoDB的策略是尽量使用内存,因此对于一个长时间运行的库来说,未被使用的页面很少。
当要读入的数据页没有内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉:如果要淘汰的是一个干净页,就直接释放出来复用;如果是脏页,就必须将脏页先刷到磁盘,变成干净页后才能复用。
所以,刷脏页虽然是常态,但是出现以下两种情况,就会明显影响性能:
- 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长;
- 日志写满,更新全部堵住,写性能跌为0,这种情况对敏感业务来说,是不能接受的。
所以,InnoDB需要有控制脏页比例的机制,来尽量避免上面这个两种情况。
InnoDB刷脏页的控制策略
首先,要正确告诉InnoDB所在主机的IO能力,这样InnoDB才能知道需要全力刷脏页的时候,可以刷多快。
这就要用到innodb_io_capacity这个参数,它会告诉InnoDB你到磁盘能力。这个值设置称磁盘的IOPS。词筽安的IOPS可以通过fio这个工具来测试,下面的语句用来测试磁盘随机读写的命令:
1 |
|
其实,因为没能正确地设置innodb_io_capacity参数,而导致的性能问题也比比皆是。MySQL的写入速度很慢,TPS很低,但是数据库主机的IO压力并不大。经过排查,发现罪魁祸首就是这个参数的设置出现了问题。
主机磁盘用的SSD,但是innodb_io_capacity的值设置是300。于是,InnoDB认为系统能力就这么差,所以刷脏页刷的特别慢,甚至比脏页生成的速度还慢,这就造成脏页累积,影响了查询和更新性能。
虽然我们现在已经定义了“全力刷脏页”的行为,但平时总不能一直是全力刷吧?毕竟磁盘能力不能只用来刷脏页,还需要服务用户请求。所以接下来,我们讨论InnoDB怎么控制引擎按照“全力”的百分百来刷脏页。
如果设计策略控制刷脏页的速度,会参考哪些因素?
刷的慢会导致,内存脏页太多,其次是redo log写满。
所以,InnoDB的刷盘速度就要参考这个两个因素:一个是脏页比例,一个是redo log写盘速度。
InnoDB会根据这个两个因素先单独算出两个数字。
参数innodb_max_dirty_pages_pct是脏页比例上限,默认值是75%。InnoDB会根据当前的脏页比例(假设为M),算出一个范围在0到100之间的数字,计算这个数字的伪代码类似这样:
1 | F1(M) |
InnoDB每次写入的日志都有一个序号,当前写入的需要跟checkpoint对应的序号之间的差值,我们假设为N。InnoDB会根据这个N算出一个范围在0到100之间的数字,这个计算公式记为F2(N)。
根据上述算得的F1(M)和F2(N)这两个值,取其中比较大的值记为R,之后引擎就可以按照innodb_io_capacity定义的能力乘以R%来控制刷脏页的速度。
InnoDB会在后台刷脏页,而刷脏页的过程是将内存页写入磁盘。所以,无论是你的查询语句在需要内存的时候可能要求淘汰一个脏页,还是由于刷脏页的逻辑会占用IO资源并可能影响到了你的更新语句,都可能是造成你从业务端感知到MySQL“抖”了一下的原因。
要尽量避免这种情况,要合理地设置innodb_io-capacity的值,并且平时要多关注脏页比例,不要让它经常接近75%。
脏页比例是通过Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total得到的。
1 |
|
一旦一个查询请求需要在执行过程中先flush掉一个脏页时,这个查询就可能要比平时慢了。而MySQL中的一个机制,可能让你的查询会更慢:在准备刷一个脏页的时候,如果这个数据页旁边的数据页刚好是脏页,就会把这个“邻居”也带着一起刷掉;而且这个把“邻居”拖下水的逻辑还可以蔓延,也就是对每个邻居数据页,如果跟他相邻的数据页也还是脏页的话,也会被放到一起刷。
在InnoDB中,innodb_flush_neighbors参数就是用来控制这个行为的,值为1的时候会有上述事务“连坐”机制,值为0时表示不找邻居,自己刷自己的。
找“邻居”这个优化在机械硬盘时代是很有意义的,可以减少很多随机IO。机械硬盘的随机IOPS一般只有几百,相同的逻辑曹祖耦减少随机IO就意味着系统性能的大幅度提升。
而如果使用的SSD这类IOPS比较高的设备的话,建议把innodb_flush_neighbors的值设置成0.因为这时候IOPS往往不是瓶颈,而是“只刷自己”,就能更快地执行完必要的刷脏页操作,减少SQL语句响应时间。
一个内存配置为128GB、innodb_io_capacity设置成20000的大规格实例,建议将redo log设置成4个1GB文件。