有时候一条本应该执行很快的SQL语句不知道怎么回事执行的很慢,并且这样的场景很难复现,这样的问题不只是岁间,而且持续时间很短。看上去,好像就是数据库“抖”了一下。
你的SQL语句为什么变“慢”了
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
这儿有个过程,就是把脏页变成干净页,也就是刷脏页。可能MySQL的“抖”的那一下就是在刷脏页。
什么情况会刷脏页?
第一种场景,InnoDB的redo log写满了。这时候系统会停止所有更新操作,把checkpoint往前推进,redo log留出空间可以继续写。
把checkpoint位置从CP推进到CP' ,就需要将两个点之间的日志对应的脏页都flush倒磁盘上。之后,从write pos到CP' 之间就是可以再写入的redo log区域。
第二种场景,系统内存不足。当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的数据页是“脏页”,就要先将脏页写到磁盘。
第三种场景,MySQL认为系统“空闲”的时候,将脏页写入磁盘。
第四种场景,MySQL正常关闭的时候,会刷脏页。
以上四种情况对性能的影响。
第三种和第四种场景一般不太会关注性能,所以主要看前两种情况下的性能问题。
第一种情况,redo log写满了,要flush脏页,这种情况是InnoDB要尽量避免的。因为出现这种情况的时候,整个系统就不能再接收更新了,所有的更新都必须堵住。从监控上看,这时候更新数据会跌为0。
第二种情况,内存不够用,杨先将脏页写到磁盘上。这种情况其实是常态。InnoDB用缓冲池(buffer pool)管理内存,缓冲池中的内存页有三种状态:
1、还没有使用;
2、使用了并且是干净页;
3、使用了并且是脏页。
InnoDB的策略是尽量使用内存,因此对于一个长时间运行的库来说,未被使用的页面很少。
当要读入的数据没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能是把最久不用的数据页淘汰掉;如果淘汰的是一个干净页,直接释放了就可以,如果淘汰的是脏页,就必须将脏页先刷到磁盘,变成干净页后才能复用。
刷脏页虽然是常态,但是出现以下两种情况,都是会明显影响性能的:
1.一个查询要淘汰的脏页个数太多,会导致查询的相应时间明显变长;
2.日志写满,更新全部堵住,写性能跌为0,这种情况对于敏感的业务来说,是不能接受的。
所以InnoDB需要有控制脏页比例的机制,来尽量避免上面这两种情况。
InnoDB刷脏页的控制策略
首先要正确的告诉InnoDB所在主机的IO能力,这样InnoDB才能知道需要全力刷脏页的时候,可以刷多快。
这个参数:innodb_io_capacity 这个参数会告诉InnoDB你的磁盘的能力。
这个值建议设置成IOPS。磁盘的IOPS可以通过fio这个工具来测试。
设计策略控制刷脏页的速度需要考虑哪些因素?
如果刷太慢,会出现什么情况?首先是内存脏页太多,其实是redo log写满。
所以,InnoDB的刷脏页速度就是要考虑这两个因素:一个是脏页比例,一个是redo log写盘速度。
InnoDB会根据这两个因素先单独算出两个数字。
参数innodb_max_dirty_pages_pct是脏页比例上限,默认值是75%。InnoDB会根据当前的脏页比例(假设为M),算出一个范围在0到100之间的数字,计算这个数字的伪代码类似这样:
F1(M)
{
if M>=innodb_max_dirty_pages_pct then
return 100;
return 100*M/innodb_max_dirty_pages_pct;
}
InnoDB每次写入的日志都有一个序号,当前写入的需要跟checkpoint对应的序号之间的差值,我们假设为N。InnoDB会根据这个N算出一个范围在0到100之间的数字,这个计算公式可以记为F2(N)。F2(N)的算法比较复杂,但是要知道,算出来的值越大就越好。
然后取上面F1(M)和F2(N)两个值,取其中较大的值记为R,之后引擎就可以按照innodb_io_capacity定义的能力乘以R%来控制刷脏页的速度。
所以业务端感到MySQL“抖”了一下的原因就可能就是后台在刷脏页占用了IO资源而影响了更新语句,查询语句在需要内存的时候可能要淘汰一个脏页。
要尽量避免这种情况,就要合理的设置innodb_io_capacity,并且平时要多关注脏页比例,不要让它经常接近75%。
其中脏页比例是通过innodb_buffer_pool_dirty/innodb_buffer_pool_pages_total得到的,具体的命令参考下面的代码:
select variable_value into @a from global_status where variable_name='Innodb_buffer_pool_pages_dirty';
select variable_value into @b from global_status where variable_name='Innodb_buffer_pool_pages_total';
select @a/@b;
另一个有趣的策略。
刷脏页的时候,如果这个数据页旁边的数据页刚好也是脏页,就会把这个“邻居”也带着一起刷掉,而且邻居还还检查自己的邻居,会蔓延。这样会让查询更慢。
参数innodb_flush_neighbors用来控制这个行为,值为1的时候会有上述的“连坐”机制,值为0的时候,不找邻居,自己刷自己的。
找“邻居”这个优化在机械硬盘时代还是很有意义的,可以减少很多随机IO。机械硬盘的随机IOPS一般只有几百,相同的逻辑操作减少随机IO意味着系统性能的大幅度提升。
而如果是SSD这类IOPS比较高的设备的话,还是建议将这个行为关掉。因为IOPS往往不是瓶颈,而“只刷自己”,就能更快的执行完必要的脏页操作,减少SQL语句的响应时间。
在MySQL8.0中,改参数的默认值已经是0了。