12 | 为什么我的MySQL会“抖”一下?

167 阅读5分钟

现象

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

SQL语句为什么变“慢”了

基于WAL机制,InnoDB在处理更新语句的时候,只做了写日志这一个磁盘操作。这个日志叫做redo log(重做日志),在更新内存写完redo log后,就返回给客户端,本次更新成功。

类比:掌柜记账的账本是数据文件,记账用的粉板是日志文件(redo log),掌柜的记忆就是内存。

掌柜总要找时间把账本更新一下,这对应的就是把内存里的数据写入磁盘的过程,术语就是flush。在这个flush操作执行之前,孔乙己的赊账总额,其实就是跟掌柜手中账本里面的记录是不一致的,因为孔乙己今天的赊账金额还只是在粉板上,而账本里的记录还是老的,还没把今天的赊账算进去。

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

不论是脏页还是干净页,都在内存中,这个例子中,内存对应的就是掌柜的记忆。

接下来,我们用一个示意图来展示一下“孔乙己赊账”的整个操作过程。假设原来孔乙己欠账 10 文,这次又要赊 9 文。 image.png

平时执行很快的更新操作,其实就是在写内存和日志,而MySQL抖动那一下瞬间,可能就是在刷脏页(flush)。

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

第一种场景: InnoDB的redo log写满了,这时系统就会停止所有更新操作,把checkpoint往前推进,redo log留出空间可以继续写。

image.png

checkpoint 可不是随便往前修改一下位置就可以的。比如图 2 中,把 checkpoint 位置从CP 推进到 CP’,就需要将两个点之间的日志(浅绿色部分),对应的所有脏页都 flush到磁盘上。之后,图中从 write pos 到 CP’之间就是可以再写入的 redo log 的区域。

第二种场景: 系统内存不足。当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的是“脏页”,就要先将脏页写到磁盘。

难道不能直接把内存淘汰掉,下次需要请求的时候,从磁盘读入数据页,然后拿 redo log 出来应用不就行了?这里其实是从性能考虑的。如果刷脏页一定会写盘,就保证了每个数据页有两种状态:

  • 一种是内存里存在,内存里就肯定是正确的结果,直接返回;
  • 另一种是内存里没有数据,就可以肯定数据文件上是正确的结果,读入内存后返回。这样的效率最高。

第三种场景:MySQL 认为系统“空闲”的时候。

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

总结:

  • 第一种场景是“redo log写满了,要flush脏页”,出现该情况,整个系统无法再接受更新,所有更新被堵住,这种情况是InnoDB要尽量避免的。
  • 第二种场景是“内存不够用了,要先将脏页写到磁盘”,这种情况是常态。InnoDB用缓冲池(buffer pool)管理内存,缓冲池中的内存页有三种状态:
    • 第一种:还没有使用的
    • 第二种:使用了并且是干净页
    • 第三种:使用了并且是脏页

刷脏页虽是常态,但出现以下两种情况,会影响性能:

  1. 一个查询的要淘汰的脏页个数太多,会导致查询耗时明显变长。
  2. 日志写满了,更新全部堵住,导致写性能为0。

因此,InnoDB要有控制刷脏页比例的机制,来避免上述2种情况。

InnoDB刷脏页的控制策略

  • 需要正确告诉InnoDB所在主机的IO能力,这样InnoDB才能知道需要全力刷脏页时,可以刷多快。
  • 用到innodb_io_capacity这个参数,会告诉InnoDB你的磁盘能力,这个值建议设置为磁盘的IOPS。磁盘的IOPS可以通过fio这个工具测试。

InnoDB刷盘速度主要参考2个因素:

  1. 脏页比例
  2. redo log写盘速度

innodb_max_dirty_pages_pct脏页比例上限,默认是75%。 一旦一个查询请求,需要在执行过程中先flush掉一个脏页时,这个查询可能要比平时慢了。

MySQL有个机制,会导致查询更慢: 在准备刷一个脏页时,如果这个数据页旁边的脏页刚好也是脏页,会顺带着一起刷掉,同时这个逻辑可以蔓延,邻居的邻居还是脏页,也会被放到一起刷,由innodb_flush_neighbors参数控制:

  • innodb_flush_neighbors=0,只刷自己
  • innodb_flush_neighbors=1,会带着刷邻居的脏页

如何配置innodb_flush_neighbors?(结论:机械硬盘:设置为1,SSD:设置为0)

  • innodb_flush_neighbors=1好处:在机械硬盘时代,可以减少很多随机IO,机械硬盘时代的IOPS只有几百。
  • 如果使用的是SSD这种IOPS较高的设备,建议将innodb_flush_neighbors=0,这时候IOPS不是瓶颈,只刷自己可以更快的执行刷脏页的操作,减少SQL响应事件。

总结

  • 利用WAL技术,数据库将随机写转换成了顺序写,大大提升了数据库的性能。
  • 但是也带来了内存脏页的问题,脏页会被后台线程自动flush,也会由于数据页淘汰触发flush,刷脏页的过程会占用资源,可能会让你的更新和查询语句响应变长。