平时的工作中,不知道你有没有遇到过这样的场景,一条 SQL 语句,正常执行的时候特别快,但是有时也不知道怎么回事,它就会变得特别慢,并且这样的场景很难复现,它不只随机,而且持续时间还很短。
为什么我们执行sql语句偶尔会很慢
InnoDB在处理更新语句的时候,只做了写日志这一个磁盘操作。这个日志叫做redo log(重做日志),在更新完内存写完redo log 后,就返回给客户端,本次更新成功。
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
把内存中的数据写入到磁盘的过程叫做flush。
比如一张user表,原来id=1这条数据的age=20,现在要给这条数据age加1:
操作过程如下图:
所以,平时我们的更新操作很快,但偶尔会出现特别慢,可能就是在刷脏页(flush)。
引发 flush 过程的情况
引发 flush 过程的情况:
InnoDB 的 redo log 文件写满了。这时候系统会停止所有的更新操作,把checkpoint往前推进,redo log 留出的空间可以继续写。图中的绿色区域就是flush之后腾出来的空间,可以再次写入日志。
系统内存不足。当需要新的内存页而内存又不够用的时候,就需要淘汰一些数据页,空出内存给别的数据页使用,如果淘汰的是“脏页”,就要先将脏页写到磁盘。此时不需要动 redo log,不过执行redo log的时候会跳过已经刷盘的页。那么为什么不直接把内存淘汰掉,下次需要请求的时候从磁盘读入数据页,然后拿redo log 出来应用。这里其实是从性能考虑的。如果刷脏页一定会写盘,就保证了每个数据页有两种状态:
一种是内存里存在,内存里就肯定是正确的结果,可以直接返回;
另一种是内存里没有数据,就可以肯定数据文件上的是正确的结果,读入内存后返回。这样效率最高。
MySQL认为系统“空闲”的时候,主动flush。
MySQL正常关闭的时候。
在这四种情况中,后两种对系统性能没有多大影响,主要是前两种对系统有影响。
第一种是“redo log 写满了,要 flush 脏页”,这种情况是 InnoDB 要尽量避免的。因为出现这种情况的时候,整个系统就不能再接受更新了,所有的更新都必须堵住。如果你从监控上看,这时候更新数会跌为 0。
第二种是“内存不够用了,要先将脏页写到磁盘”,这种情况其实是常态。InnoDB 用缓冲池(buffer pool)管理内存,缓冲池中的内存页有三种状态:
第一种是,还没有使用的;
第二种是,使用了并且是干净页;
第三种是,使用了并且是脏页。
当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉(LRU算法):如果要淘汰的是一个干净页,就直接释放出来复用;但如果是脏页呢,就必须将脏页先刷到磁盘,变成干净页后才能复用。
所以,刷脏页虽然是常态,但是出现以下这两种情况,都是会明显影响性能的:
一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长;
日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的。
所以,InnoDB 需要有控制脏页比例的机制,来尽量避免上面的这两种情况。
InnoDB 刷脏页的控制策略
innodb_io_capacity参数:这个参数会告诉InnoDB所在主机的IO能力,这样InnoDB才能知道需要全力刷脏页的时候,可以刷多块。这个值建议设置成磁盘的IOPS。磁盘的IOPS可以通过fio工具测试。
参考语句:
fio -filename=/tmp/test_randread -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=2G -numjobs=10 -runtime=60 -group_reporting -name=mytest复制
但是,毕竟磁盘的能力不能只用来刷脏页,还要服务用户的其他需求,所以,InnoDB需要控制引擎按照 innodb_io_capacity 参数的百分比刷脏页。
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) 算法比较复杂,你只要知道 N 越大,算出来的值越大就好了。
然后,根据上述算得的 F1(M) 和 F2(N) 两个值,取其中较大的值记为 R,之后引擎就可以按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度。
脏页比例是通过 Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total 得到的(MySQL的information_schema库):
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;复制
如果查询过程中提示 The 'INFORMATION_SCHEMA.GLOBAL_STATUS' feature is disabled; see the documentation for 'show_compatibility_56' :
那是因为从MySQL5.7.6开始information_schema.global_status已经开始被舍弃,为了兼容性,此时需要打开 show_compatibility_56。
查看show_compatibility_56状态:
show VARIABLES like 'show_compatibility_56';复制
修改show_compatibility_56状态为ON:
set GLOBAL show_compatibility_56 = 'on';复制
然后执行上面的语句即可。
平时要多关注脏页比例,不要让它经常接近 75%。
MySQL在准备刷一个脏页的时候,如果这个数据页旁边的数据页刚好是脏页,就会把这个“邻居”也带着一起刷掉;而且这个把“邻居”拖下水的逻辑还可以继续蔓延,也就是对于每个邻居数据页,如果跟它相邻的数据页也还是脏页的话,也会被放到一起刷。
我们可以设置 innodb_flush_neighbors 参数为0,表示不找邻居,自己刷自己,如果设置为1,表示连带邻居一起刷。
结束!