前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 31|回复: 0

[开发应用] MySQL InnoDB 刷脏与 checkpoint 实战:从写入高峰吞吐骤降到 redo 扩容与匀速刷脏

[复制链接]

[开发应用] MySQL InnoDB 刷脏与 checkpoint 实战:从写入高峰吞吐骤降到 redo 扩容与匀速刷脏

[复制链接]
dbaai

主题

0

回帖

419

积分

DBAAI

积分
419
发表于 12 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
一、具体的问题


某订单系统 MySQL 5.7(自建,NVMe SSD),日常 TPS 稳定在 8000 左右,但每天上午 10 点左右写入高峰,TPS 会毫无征兆地从 8000 掉到几百,持续十几秒到两三分钟不等,期间 Threads_running 冲到 200+,等恢复后又一切正常。DBA 第一反应是慢 SQL,但 binlog 里高峰期的语句平日也就几毫秒;再看监控,CPU、磁盘 util 都不高,IOPS 甚至比平时还低——机器明明闲着,写入却堵死了。

另一个症状在重启时暴露:某次主机故障重启,InnoDB 崩溃恢复花了 23 分钟,期间实例完全不可用。恢复时间和两个参数直接相关——redo log 总容量太小、脏页积压太多。

把这两个现象连起来看,根因是同一个:redo log 容量 96MB(5.7 默认 2×48MB),对一张每小时产生十几 GB redo 的订单主库来说,checkpoint 根本推进不动,写入高峰时 redo 快写满,InnoDB 被迫进入激进的同步刷脏模式,把宝贵的写 IOPS 全部拿去洗脏页,业务线程反而被饿死。

二、核心原理


1. redo log 是个环形管道。 InnoDB 每改一页,先把物理变更写进 redo log(顺序写),再慢慢把脏页刷回数据文件(随机写)。redo log 文件组容量固定、循环覆写——写到底就回头从头写,但复写的前提是:被覆盖的位置之前的脏页必须已经落盘。这个"最多还能写多少"就是 checkpoint age。

2. checkpoint age 撞线的两个阈值。 当 checkpoint age 超过日志总容量的约 75%,InnoDB 进入异步刷脏:page cleaner 全力洗脏页,业务还能写但开始抖;逼近 90% 左右,转入同步刷脏——业务线程被拉去参与刷脏,每个写事务都被拖着洗脏页,这就是 TPS 断崖的直接原因。日志容量越小,同样的写入速率下越容易撞线。

3. 脏页比例也会触发刷脏。 innodb_max_dirty_pages_pct(默认 75%)是软上限,超过后 page cleaner 加速;innodb_max_dirty_pages_pct_lwm(5.7 新增,默认 0)可以提前开低水位刷脏。IO 能力不足时,innodb_io_capacity(默认 200)直接决定 page cleaner 每秒最多洗多少页——机械盘时代 200 合理,SSD 上就是摆设。

4. 双写(doublewrite)不是这里的问题。 刷脏要经过 doublewrite buffer(防部分写失效),它带来的开销是固定的;刷脏风暴的根子在"单位时间要洗的页超过了 io_capacity 配额"。反过来,innodb_flush_neighbors(默认 1,会把相邻脏页顺带洗掉)在机械盘上能合并寻道,在 SSD/NVMe 上纯粹增加写入放大,应设为 0。

5. 自适应刷脏(adaptive flushing)是缓冲垫,不是替身。 5.6 起默认开启 innodb_adaptive_flushing:InnoDB 根据 redo 产生速度估计"再过多久日志会绕回",提前加大刷脏力度。innodb_adaptive_flushing_lwm(默认 10)表示 checkpoint age 超过日志容量 10% 就开始介入。它能摊平轻度高峰,但日志容量太小时,摊平机制本身就变成了高峰——毕竟单位时间里要洗的页总量没变,只是从"突击"变成"长期加班"。所以顺序永远是:先把管道扩够,再谈刷脏策略。

6. 容量怎么定。 经验公式:redo 总容量 ≥ 高峰期 1 小时的 redo 产生量(可从 Innodb_os_log_written 增量估算)。日志够大,checkpoint age 长期低位,刷脏就能匀速进行,而不是被迫在高峰期突击。8.0.30 起可用 innodb_redo_log_capacity 在线调整,5.7 只能改参数重启。

三、实例参考(动手步骤)


步骤 0:确认现状基线
  1. -- redo 容量与 checkpoint 位置(5.7 看 LOG 段)
  2. SHOW ENGINE INNODB STATUS\G
  3. -- 关注 LOG 段三行:
  4. --   Log sequence number          当前 LSN
  5. --   Log flushed up to            已刷盘 LSN
  6. --   Last checkpoint at           上次检查点 LSN
  7. -- checkpoint age ≈ Log sequence number - Last checkpoint at
  8. -- 每分钟 redo 产生量(隔 60 秒采两次)
  9. SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';
  10. -- 脏页水位
  11. SELECT variable_name, variable_value
  12. FROM performance_schema.global_status
  13. WHERE variable_name IN ('Innodb_buffer_pool_pages_dirty',
  14.                         'Innodb_buffer_pool_pages_total');
  15. -- 当前刷脏参数
  16. SHOW VARIABLES WHERE variable_name IN
  17. ('innodb_log_file_size','innodb_log_files_in_group',
  18. 'innodb_io_capacity','innodb_io_capacity_max',
  19. 'innodb_flush_neighbors','innodb_max_dirty_pages_pct',
  20. 'innodb_page_cleaners','innodb_buffer_pool_size');
复制代码

该案例实测:redo 总量 96MB,高峰期每分钟 redo 约 220MB,checkpoint age 一度冲到 91%;脏页占比 71%;innodb_io_capacity=200、flush_neighbors=1。三项全踩。

步骤 1:先在线补短板——IO 配额与刷脏策略(5.7 可热更)
  1. SET GLOBAL innodb_io_capacity = 2000;        -- NVMe 按 4K 随机写 IOPS 的 1/4~1/2 估
  2. SET GLOBAL innodb_io_capacity_max = 4000;    -- 撞线时的冲刺配额
  3. SET GLOBAL innodb_flush_neighbors = 0;       -- SSD 无需邻页合并
  4. SET GLOBAL innodb_max_dirty_pages_pct = 60;      -- 提前压水位
  5. SET GLOBAL innodb_max_dirty_pages_pct_lwm = 10;  -- 10% 即开低水位匀速刷脏
复制代码

改完观察十几分钟:TPS 抖动幅度明显收窄,但 10 点高峰仍会撞——因为 96MB 的日志容量是硬伤,热改不了。

步骤 2:扩 redo log(5.7 需停机窗口)
  1. # my.cnf(5.7)
  2. innodb_log_files_in_group = 4
  3. innodb_log_file_size      = 2G    -- 总容量 8G
复制代码

5.7 收缩日志需要"干净关库 → 删旧 ib_logfile* → 重启",扩容同样要重启;确认 innodb_fast_shutdown=1 正常关闭后再替换。8.0.30+ 则一行搞定且在线生效:
  1. SET GLOBAL innodb_redo_log_capacity = 8 * 1024 * 1024 * 1024; -- 8G
  2. SELECT variable_name, variable_value
  3. FROM performance_schema.global_status
  4. WHERE variable_name LIKE 'Innodb_redo_log%';
  5. -- Innodb_redo_log_capacity_resized 确认已生效
复制代码

步骤 3:从源头看清"谁在产 redo"

扩容只是续命,如果 redo 量本身异常偏高,要回到业务侧找源头:
  1. -- 用 performance_schema 找高峰期写入最多的表
  2. SELECT object_schema, object_name, count_write, sum_timer_write
  3. FROM performance_schema.table_io_waits_summary_by_table
  4. WHERE object_schema NOT IN ('mysql','performance_schema','sys')
  5. ORDER BY count_write DESC LIMIT 10;
  6. -- 大事务排查:一次改几十万行的一条 UPDATE,其 redo 是逐行累计的
  7. -- 业务侧拆批:每批 2000~5000 行,批间 sleep,把 redo 产生量在时间轴上摊平
复制代码

该案例顺带发现:凌晨批处理任务一条 UPDATE 扫 900 万行,凌晨本应平稳的 checkpoint age 也顶到 60%——拆批后凌晨峰值降到 22%。

步骤 4:复核与固化
  1. -- 高峰期再看 checkpoint age 占比(目标 < 容量的 40%)
  2. -- 再次采样脏页比例(目标长期 < 40%)
  3. SELECT variable_value/variable_total*100 AS dirty_pct
  4. FROM (SELECT
  5.   MAX(CASE WHEN variable_name='Innodb_buffer_pool_pages_dirty'
  6.       THEN variable_value END) AS variable_value,
  7.   MAX(CASE WHEN variable_name='Innodb_buffer_pool_pages_total'
  8.       THEN variable_value END) AS variable_total
  9.   FROM performance_schema.global_status
  10.   WHERE variable_name IN ('Innodb_buffer_pool_pages_dirty',
  11.                           'Innodb_buffer_pool_pages_total')) t;
复制代码

治理前后对比

指标治理前治理后
redo 总容量96MB(2×48M)8G(5.7: 4×2G)
checkpoint age 峰值占比91%(同步刷脏)33%(匀速刷脏)
高峰期 TPS8000 → 骤降 500稳定 8600 左右
P99 写入延迟850ms(尖刺)14ms
脏页比例(高峰)71%38%
崩溃恢复时间23 分钟1 分 40 秒
每 10 秒 InnoDB fsync 抖动频繁 >2000ms基本 <50ms


四、实操检查清单



  • [ ] 已用 SHOW ENGINE INNODB STATUS 的 LOG 段算出 checkpoint age,并明确它占日志容量的百分比
  • [ ] 已采样高峰期每分钟 redo 产生量,日志总容量 ≥ 高峰 1 小时 redo 量
  • [ ] innodb_io_capacity 已按存储介质的实际随机写 IOPS 重新评估,不再停留在默认 200
  • [ ] innodb_io_capacity_max 已设置且约为 io_capacity 的 1.5~2 倍
  • [ ] SSD/NVMe 上 innodb_flush_neighbors=0 已确认
  • [ ] innodb_max_dirty_pages_pct_lwm 已设置低水位(如 10),让刷脏匀速前移而非事后突击
  • [ ] 脏页比例长期监控,高峰期不超过 40%~50%
  • [ ] 5.7 改日志容量已排停机窗口,且按"干净关库→删旧日志→重启"流程执行
  • [ ] 8.0.30+ 优先用 innodb_redo_log_capacity 在线调整,变更后确认 Innodb_redo_log_capacity_resized
  • [ ] 重启演练过一次,崩溃恢复时间已纳入监控(恢复时间随日志容量变大而换来的前提是刷脏跟得上)
  • [ ] 未为了"提吞吐"关闭 doublewrite(部分写失效风险不可接受)


几个容易踩的坑



  • 只调 io_capacity 不扩日志。 配额提上去了,96MB 的管道照样在高峰撞线,抖动只是推迟几分钟。两者必须一起看。
  • io_capacity 盲调过大。 直接给 10000,page cleaner 疯狂洗页挤占业务 IOPS,读延迟反而恶化。按介质实际能力给,用压测验证。
  • 5.7 收缩日志图省事直接删文件。 非干净关闭状态删 ib_logfile* 会丢失未刷盘事务,实例起不来甚至数据不一致。收缩必须干净关库。
  • 把 binlog 的 fsync 和 redo 的 fsync 混为一谈。 sync_binlog/binlog_group_commit 管的是 binlog 侧,本篇抖动的根因在 redo 侧 checkpoint,排查时别顺着 binlog 走偏。
  • innodb_flush_method 没检查。 部分 OS 默认未开 O_DIRECT,双重缓存放大脏页管理压力;文件系统缓存导致的"看似内存够用实际 IOPS 高"与此有关。
  • 崩溃恢复时间与日志容量是权衡。 日志给到 8G,理论上最坏恢复时间变长——但前提是允许脏页积压到那么晚。配合低水位刷脏(lwm)后,实际恢复时间反而更短,不要因噎废食。
  • cloud RDS 不暴露 redo 参数时,优先调 io_capacity 类参数并压测确认写瓶颈;若仍撞线,考虑拆分大事务或错峰批量写,从源头减少 redo 产生量(大事务里一批 UPDATE 的 redo 是逐行累计的)。
  • 忽略 Innodb_log_waits 这个信号灯。 它记录的是"日志缓冲太小、写 redo 被迫等待"的次数,很多人排查时只盯 checkpoint,结果真凶是 innodb_log_buffer_size(默认 16MB)扛不住批量写。若该值持续增长,把日志缓冲扩到 64MB~128MB 再看,成本极低。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-11-2 17:58 , Processed in 0.022436 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表