看监控,总有一些时刻看起来随机持续时间又短,难以复现。
WAL 机制:InnoDB在处理更新语句时,只做写日志这个磁盘操作(redo log),在更新内存写完redo log后,就返回给客户端,本次更新成功。
想象你是一个老板:
- 数据文件 - 用来记账的账本
- redo log - 记账用的借条
- 内存 - 你的记忆
你总要找时间把账本更新下:把内存里的数据写盘(flush)。flush前,撕葱的赊账总额,其实跟你手中账本的记录不一致。因为撕葱今天的赊账金额还只在借条上,而账本里的记录是老的,还没把今天的赊账算进去。
内存数据页
跟磁盘数据页
内容不一致,该内存页为"脏页"- 内存数据写盘后,
内存数据页
和磁盘数据页
内容一致了,称为"干净页"
无论脏页 or 干净页,都指内存数据页
。
1 撕葱赊账示意图
假设撕葱原来欠账10元,这次又要赊10元。
所以平时执行很快的更新操作,其实就是在写内存和日志,而MySQL抖擞那一下,可能就是在刷脏页(flush) 。
2 何时触发MySQL的flush?
想想大老板何时会把赊账记录落到账本:
2.1 redo log写满
借条写满了,这时再有更多的撕葱们来赊账,老板必须放下手活,将借条上的记录擦掉一些,腾空间以继续记账。当然,擦掉前也必须先将正确欠款条目记录到账本。即InnoDB的redo log写满了。
这时系统会停止所有更新操作,把checkpoint往前推进,redo log留出空间可以继续写。
redo log状态图:
checkpoint 不是随便往前修改一下位置的。如上图,把checkpoint位置从CP ~ CP'
,就需要将两个点之间的日志(浅绿色部分),对应的所有脏页都flush。之后,write pos ~ CP'
就可再写redo log。
2.2 系统内存不足
这天生意太好,要记的帐太多,老板发现自己快记不住了,赶紧找出账本把撕葱这笔账先加进去。 当需要新的内存页,而内存又不够用时,就要淘汰一些数据页,空出内存给别的数据页用。若淘汰的是"脏页",就要先将脏页写到磁盘。
难道不能直接把内存淘汰掉,下次需要请求时,从磁盘读入数据页,然后拿redo log出来应用? 这其实是从性能考虑。若刷脏页一定会写盘,就保证了每个数据页有两种状态:
- 内存里存在,内存里就肯定是正确结果,直接返回
- 内存里无数据,就可以肯定,数据文件上是正确结果,读入内存后返回。 这样的效率最高。
2.3 MySQL认为系统"空闲"时
贷款生意不忙时,自然就没啥忙的,我这老板也闲,不如更新下我的账本。 MySQL空闲机会总是少的,可能很快把日志写满,所以MySQL要合理安排时间,即使很忙,也要见缝插针,有机会就刷一点"脏页"。
2.4 MySQL正常关闭
年底我要关门回家过年了,清算账目。这时候我就要把所有账都记到账本,过完年重新开张时,就能对着账本明确账目情况。
这时MySQL会把内存的脏页都flush,下次启动时,直接从磁盘读数据,启动速度很快。
居然这么多场景都会触发flush,会对线上MySQL的稳定性有啥不良影响吗?
3 各场景性能开销
2.3 MySQL空闲时操作,系统本就无压力,2.4 MySQL本就要关闭了。所以这俩case无需关注性能。
3.1 redo log写满,要flush脏页
InnoDB要尽量避免。因为此时,整个系统无法再接受更新请求,所有更新都必须堵住。
从监控图上看,这时更新数会跌为0。
3.2 内存不够,要先将脏页flush
常态case。InnoDB用缓冲池(buffer pool)管理内存,缓冲池中的内存页有如下状态:
- 尚未使用
- 使用了 && 干净页
- 使用了 && 脏页
InnoDB的策略是尽量使用内存,因此对于一个长时间运行的库,未被使用的页其实很少。当要读入的数据页不在内存时,就必须到缓冲池中申请一个数据页。这时只能把 最久不使用 的数据页从内存淘汰,视淘汰的对象不同:
- 干净页 直接释放(反正磁盘数据页已同步完成),以复用
- 脏页 须将脏页先刷盘,成为干净页了后,才能复用
所以,刷脏页虽是常态,但若出现如下情况,都会明显影响性能:
- 一个查询要淘汰的脏页个数太多,导致查询的响应时间明显变长
- 日志写满,更新全部阻塞,写性能跌为0,这对敏感业务无法接受
所以,InnoDB要有控制脏页比例的机制,尽量避免上述这俩情况。
4 InnoDB刷脏页的控制策略
不要急,都会和你说的!
innodb_io_capacity
首先,正确告诉InnoDB所在主机的I/O能力,这样InnoDB才知道需要全力刷脏页时,可以刷多快。
innodb_io_capacity
参数,告诉InnoDB你的磁盘能力。推荐设置成磁盘的IOPS。磁盘的IOPS可通过fio工具测试。如下就是测试磁盘随机读写的命令:
ini
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_io_capacity 参数导致的性能问题很多。如有时一个库的性能,可能MySQL的写很慢,TPS很低,但数据库主机的I/O压力并不大。 主机磁盘用的SSD,但 innodb_io_capacity 设置300。于是,InnoDB认为这个系统的能力就这么差,所以刷脏页特别慢,甚至比脏页生成的速度还慢,就造成脏页累积,影响查询和更新性能。
虽然我们现在已定义"全力刷脏页"的行为,但平时总不能一直是全力刷,毕竟磁盘能力不能只用来刷脏页,还需要服务用户请求。 InnoDB怎么控制引擎按"全力"的百分比来刷脏页呢?若你来设计策略控制刷脏页的速度,会考虑哪些因素?若刷太慢,会怎样?
- 内存脏页太多
- redo log写满
InnoDB刷盘速度主要参考:
- 脏页比例
- redo log写盘速度
InnoDB会根据这两个因素,先单独算出两个数字。
参数innodb_max_dirty_pages_pct
是脏页比例上限,默认75%。InnoDB会根据当前脏页比例(假设为M),算出一个范围在0到100之间的数字,计算这个数字的伪代码类似这样:
javascript
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写入速度算出来的两个值。
- InnoDB刷脏页速度策略
InnoDB会在后台刷脏页,而刷脏页过程要将内存页落盘。所以,无论是你的查询语句在需要内存时可能要求淘汰一个脏页,还是由于刷脏页的逻辑会占用IO资源并可能影响到你的更新语句,都可能导致MySQL抖动。
要尽量避免这种情况,就要合理配置innodb_io_capacity
,日常也要多关注脏页比例,不要让它经常接近75% 。
其中,脏页比例是通过
Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total
得到的,具体命令参考如下代码:
sql
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;
再看一个策略。 一旦一个查询请求需要在执行过程中先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。
总结
WAL机制后续需要的刷脏页操作和执行时机。利用WAL技术,数据库将随机写转换成了顺序写,大大提升了数据库的性能。
但是,由此也带来了内存脏页的问题。脏页会被后台线程自动flush,也会由于数据页淘汰而触发flush,而刷脏页的过程由于会占用资源,可能会让你的更新和查询语句的响应时间长。