程序员写bug导致MySQL线上事故,看我复盘!

看监控,总有一些时刻看起来随机持续时间又短,难以复现。

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出来应用? 这其实是从性能考虑。若刷脏页一定会写盘,就保证了每个数据页有两种状态:

  1. 内存里存在,内存里就肯定是正确结果,直接返回
  2. 内存里无数据,就可以肯定,数据文件上是正确结果,读入内存后返回。 这样的效率最高。

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,而刷脏页的过程由于会占用资源,可能会让你的更新和查询语句的响应时间长。

相关推荐
brzhang8 分钟前
别再梭哈 Curosr 了!这 AI 神器直接把需求、架构、任务一条龙全干了!
前端·后端·架构
安妮的心动录22 分钟前
安妮的2025 Q2 Review
后端·程序员
程序员爱钓鱼23 分钟前
Go语言数组排序(冒泡排序法)—— 用最直观的方式掌握排序算法
后端·google·go
Victor3561 小时前
MySQL(140)如何解决外键约束冲突?
后端
Victor3561 小时前
MySQL(139)如何处理MySQL字符编码问题?
后端
007php0073 小时前
服务器上PHP环境安装与更新版本和扩展(安装PHP、Nginx、Redis、Swoole和OPcache)
运维·服务器·后端·nginx·golang·测试用例·php
武子康5 小时前
Java-72 深入浅出 RPC Dubbo 上手 生产者模块详解
java·spring boot·分布式·后端·rpc·dubbo·nio
椰椰椰耶7 小时前
【Spring】拦截器详解
java·后端·spring
brzhang8 小时前
我操,终于有人把 AI 大佬们 PUA 程序员的套路给讲明白了!
前端·后端·架构