git-zjx / git-zjx.github.io

blog

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

git-zjx opened this issue · comments

你的 SQL 语句为什么变“慢”了

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

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

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

  1. InnoDB 的 redo log 写满了
    这时候系统会停止所有更新操作,把 checkpoint 往前推进,redo log 留出空间可以继续写,checkpoint 往前推进时,会把对应的所有脏页都 flush 到磁盘上
  2. 系统内存不足
    当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的是“脏页”,就要先将脏页写到磁盘。
    刷脏页一定会写盘,就保证了每个数据页有两种状态:
    a. 一种是内存里存在,内存里就肯定是正确的结果,直接返回;
    b.另一种是内存里没有数据,就可以肯定数据文件上是正确的结果,读入内存后返回。这样的效率最高。
  3. MySQL 认为系统“空闲”的时候
  4. MySQL 正常关闭的情况
    MySQL 会把内存的脏页都 flush 到磁盘上,这样下次 MySQL 启动的时候,就可以直接从磁盘上读数据,启动速度会很快

四种场景对性能的影响

  1. “redo log 写满了,要 flush 脏页”
    这种情况是 InnoDB 要尽量避免的。因为出现这种情况的时候,整个系统就不能再接受更新了,所有的更新都必须堵住。如果你从监控上看,这时候更新数会跌为 0。
  2. “内存不够用了,要先将脏页写到磁盘”
    这种情况其实是常态。InnoDB 用缓冲池(buffer pool)管理内存,缓冲池中的内存页有三种状态:
    a. 第一种是,还没有使用的;
    b. 第二种是,使用了并且是干净页;
    c. 第三种是,使用了并且是脏页。
    InnoDB 的策略是尽量使用内存,因此对于一个长时间运行的库来说,未被使用的页面很少。而当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉:如果要淘汰的是一个干净页,就直接释放出来复用;但如果是脏页呢,就必须将脏页先刷到磁盘,变成干净页后才能复用。
    所以,刷脏页虽然是常态,但是出现以下这两种情况,都是会明显影响性能的:
    a. 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长;
    b. 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的。
    所以,InnoDB 需要有控制脏页比例的机制,来尽量避免上面的这两种情况。

InnoDB 刷脏页的控制策略

innodb_io_capacity 参数会告诉 InnoDB 你的磁盘能力,这样 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 

innodb_max_dirty_pages_pct 参数是脏页比例上限,默认值是 75%。InnoDB 会根据当前的脏页比例(假设为 M),算出一个范围在 0 到 100 之间的数字 F1(M) ,InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,我们假设为 N。InnoDB 会根据这个 N 算出一个范围在 0 到 100 之间的数字 F2(N),根据上述算得的 F1(M) 和 F2(N) 两个值,取其中较大的值记为 R,之后引擎就可以按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度。

cc44c1d080141aa50df6a91067475374

InnoDB 会在后台刷脏页,而刷脏页的过程是要将内存页写入磁盘。所以,无论是你的查询语句在需要内存的时候可能要求淘汰一个脏页,还是由于刷脏页的逻辑会占用 IO 资源并可能影响到了你的更新语句,都可能是造成你从业务端感知到 MySQL“抖”了一下的原因
要尽量避免这种情况,你就要合理地设置 innodb_io_capacity 的值,并且平时要多关注脏页比例,不要让它经常接近 75%
其中,脏页比例是通过 Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total 得到的,具体的命令参考下面的代码:

mysql> 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;

MySQL 在准备刷一个脏页的时候,如果这个数据页旁边的数据页刚好是脏页,就会把这个“邻居”也带着一起刷掉;而且这个把“邻居”拖下水的逻辑还可以继续蔓延,也就是对于每个邻居数据页,如果跟它相邻的数据页也还是脏页的话,也会被放到一起刷。

innodb_flush_neighbors 参数就是用来控制这个行为的,值为 1 的时候会有上述的“连坐”机制,值为 0 时表示不找邻居,自己刷自己的
找“邻居”这个优化在机械硬盘时代是很有意义的,可以减少很多随机 IO。机械硬盘的随机 IOPS 一般只有几百,相同的逻辑操作减少随机 IO 就意味着系统性能的大幅度提升。
而如果使用的是 SSD 这类 IOPS 比较高的设备的话,我就建议你把 innodb_flush_neighbors 的值设置成 0。因为这时候 IOPS 往往不是瓶颈,而“只刷自己”,就能更快地执行完必要的刷脏页操作,减少 SQL 语句响应时间。
在 MySQL 8.0 中,innodb_flush_neighbors 参数的默认值已经是 0 了。

当内存不够用了,要将脏页写到磁盘,会有一个数据页淘汰机制(最久不使用),假设淘汰的是脏页,则此时脏页所对应的redo log的位置是随机的,当有多个不同的脏页需要刷,则对应的redo log可能在不同的位置,这样就需要把redo log的多个不同位置刷掉,这样对于redo log的处理不是就会很麻烦吗?

其实由于淘汰的时候,刷脏页过程不用动redo log文件的。这个有个额外的保证,是redo log在“重放”的时候,如果一个数据页已经是刷过的,会识别出来并跳过。

redo log是怎么记录对应脏页是否已经flush了?如果断电了重启导致内存丢失,前面几章说通过redo log进行数据恢复那redo log又怎么去释放空间?

不用记,重启了就从checkpoint 的位置往后扫。 如果已经之前刷过盘的, 不会重复应用redo log

innodb是如何知道一个页是不是脏页的?

每个数据页头部有LSN,8字节,每次修改都会变大。对比这个LSN跟checkpoint 的LSN,比checkpoint小的一定是干净页

一个内存配置为 128GB、innodb_io_capacity 设置为 20000 的大规格实例,正常会建议你将 redo log 设置成 4 个 1GB 的文件。但如果你在配置的时候不慎将 redo log 设置成了 1 个 100M 的文件,会发生什么情况呢?又为什么会出现这样的情况呢?

每次事务提交都要写 redo log,如果设置太小,很快就会被写满,这个“环”将很快被写满,write pos 一直追着 CP,这时候系统不得不停止所有更新,去推进 checkpoint,看到的现象就是磁盘压力很小,但是数据库出现间歇性的性能下跌