MySQL一会儿快,一会儿慢的,我该怎么办?

  一条SQL语句,正常执行的时候特别快,但是有时也不知道怎么回事,它就会变得特别慢,并且这样的场景很难复现,它不只随机,而且持续时间还很短。看上去,这就像是数据库“抖”了一下。

1 SQL语句为什么变慢了

  Mysql中的WAL机制,InnoDB在处理更新语句的时候,只做了写日志这一个磁盘操作。这个日志叫作redo log(重做日志)。

  做下类比的话,掌柜记账的账本是数据文件,记账用的粉板是日志文件(redo log),掌柜的记忆就是内存。
图1 “孔乙己赊账”更新和flush过程
  平时执行很快的更新操作,其实就是在写内存和日志,而MySQL偶尔慢一下的那个瞬间,可能就是在刷脏页(flush)。

  当内存数据页跟磁盘数据页内容不一致的时候,称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。

1.1 什么情况会引发数据库的flush过程?

  1. 第一种场景是,InnoDB的redo log写满了。这时候系统会停止所有更新操作,把checkpoint往前推进,redo log留出空间可以继续写。
    图2 redo log状态图
      checkpoint可不是随便往前修改一下位置就可以的。比如图2中,把checkpoint位置从CP推进到CP’,就需要将两个点之间的日志(浅绿色部分),对应的所有脏页都flush到磁盘上。之后,图中从write pos到CP’之间就是可以再写入的redo log的区域。

  2. 第二种场景是,系统内存不足。当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的是“脏页”,就要先将脏页写到磁盘。如果刷脏页一定会写盘,就保证了每个数据页有两种状态:

  • 内存里存在,内存里就肯定是正确的结果,直接返回;
  • 内存里没有数据,就可以肯定数据文件上是正确的结果,读入内存后返回。
  1. 第三种场景是,MySQL认为系统“空闲”的时候。即使是用的比较多的时候,也要见缝插针地找时间,只要有机会就刷一点“脏页”。

  2. 第四种场景是,MySQL正常关闭的情况。这时候,MySQL会把内存的脏页都flush到磁盘上,这样下次MySQL启动的时候,就可以直接从磁盘上读数据,启动速度会很快。

1.2 上面四种场景对性能的影响

  第三种情况是属于MySQL空闲时的操作,这时系统没什么压力,而第四种场景是数据库本来就要关闭了。这两种情况下,不会太关注性能问题。


  第一种是“redo log写满了,要flush脏页”,这种情况是InnoDB要尽量避免的。因为出现这种情况的时候,整个系统就不能再接受更新了,所有的更新都必须堵住。如果从监控上看,这时候更新数会跌为0。


  第二种是“内存不够用了,要先将脏页写到磁盘”,这种情况其实是常态。InnoDB用缓冲池(buffer pool)管理内存,缓冲池中的内存页有三种状态:

  • 第一种是,还没有使用的;
  • 第二种是,使用了并且是干净页;
  • 第三种是,使用了并且是脏页。

  InnoDB的策略是尽量使用内存,因此对于一个长时间运行的库来说,未被使用的页面很少。
  而当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉:如果要淘汰的是一个干净页,就直接释放出来复用;但如果是脏页,就必须将脏页先刷到磁盘,变成干净页后才能复用。
  所以,刷脏页虽然是常态,但是出现以下这两种情况,都是会明显影响性能的:

  1. 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长;
  2. 日志写满,更新全部堵住,写性能跌为0,这种情况对敏感业务来说,是不能接受的。
      所以,InnoDB需要有控制脏页比例的机制,来尽量避免上面的这两种情况。

2 InnoDB刷脏页的控制策略

  首先,要正确地告诉InnoDB所在主机的IO能力,这样InnoDB才能知道需要全力刷脏页的时候,可以刷多快。
  这就要用到innodb_io_capacity这个参数,表示InnoDB的磁盘能力。这个值建议设置成磁盘的IOPS。磁盘的IOPS可以通过fio这个工具来测试命令:

fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest 

2.1 设计策略控制刷脏页的速度,会参考哪些因素?

  这个问题可以这么想,如果刷太慢,会出现什么情况?
  首先是内存脏页太多,其次是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),N越大,算出来的值越大。

  然后,根据上述算得的F1(M)和F2(N)两个值,取其中较大的值记为R,之后引擎就可以按照innodb_io_capacity定义的能力乘以R%来控制刷脏页的速度。

  图中的F1、F2就是上面我们通过脏页比例和redo log写入速度算出来的两个值。
图3 InnoDB刷脏页速度策略
  InnoDB会在后台刷脏页,而刷脏页的过程是要将内存页写入磁盘。所以,无论是查询语句在需要内存的时候可能要求淘汰一个脏页,还是由于刷脏页的逻辑会占用IO资源并可能影响到了更新语句,都可能是造成从业务端感知到MySQL“抖”了一下的原因。

  要尽量避免这种情况,就要合理地设置innodb_io_capacity的值,并且平时要多关注脏页比例,不要让它经常接近75%

  其中,脏页比例是通过Innodb_buffer_pool_pages_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;

一个有趣的策略:
  一旦一个查询请求需要在执行过程中先flush掉一个脏页时,这个查询就可能要比平时慢了。而MySQL中的一个机制,可能让你的查询会更慢:在准备刷一个脏页的时候,如果这个数据页旁边的数据页刚好是脏页,就会把这个“邻居”也带着一起刷掉;而且这个把“邻居”拖下水的逻辑还可以继续蔓延,也就是对于每个邻居数据页,如果跟它相邻的数据页也还是脏页的话,也会被放到一起刷。

  在InnoDB中,innodb_flush_neighbors 参数就是用来控制这个行为的,值为1的时候会有上述的“连坐”机制,值为0时表示不找邻居,自己刷自己的。

  找“邻居”这个优化在机械硬盘时代是很有意义的,可以减少很多随机IO。机械硬盘的随机IOPS一般只有几百,相同的逻辑操作减少随机IO就意味着系统性能的大幅度提升。

  而如果使用的是SSD这类IOPS比较高的设备的话,就建议把innodb_flush_neighbors的值设置成0。因为这时候IOPS往往不是瓶颈,而“只刷自己”,就能更快地执行完必要的刷脏页操作,减少SQL语句响应时间。

  在MySQL 8.0中,innodb_flush_neighbors参数的默认值已经是0了。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值