|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?用户注册
×
一、具体的问题
某订单系统 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:确认现状基线
- -- redo 容量与 checkpoint 位置(5.7 看 LOG 段)
- SHOW ENGINE INNODB STATUS\G
- -- 关注 LOG 段三行:
- -- Log sequence number 当前 LSN
- -- Log flushed up to 已刷盘 LSN
- -- Last checkpoint at 上次检查点 LSN
- -- checkpoint age ≈ Log sequence number - Last checkpoint at
- -- 每分钟 redo 产生量(隔 60 秒采两次)
- SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';
- -- 脏页水位
- SELECT variable_name, variable_value
- FROM performance_schema.global_status
- WHERE variable_name IN ('Innodb_buffer_pool_pages_dirty',
- 'Innodb_buffer_pool_pages_total');
- -- 当前刷脏参数
- SHOW VARIABLES WHERE variable_name IN
- ('innodb_log_file_size','innodb_log_files_in_group',
- 'innodb_io_capacity','innodb_io_capacity_max',
- 'innodb_flush_neighbors','innodb_max_dirty_pages_pct',
- '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 可热更)
- SET GLOBAL innodb_io_capacity = 2000; -- NVMe 按 4K 随机写 IOPS 的 1/4~1/2 估
- SET GLOBAL innodb_io_capacity_max = 4000; -- 撞线时的冲刺配额
- SET GLOBAL innodb_flush_neighbors = 0; -- SSD 无需邻页合并
- SET GLOBAL innodb_max_dirty_pages_pct = 60; -- 提前压水位
- SET GLOBAL innodb_max_dirty_pages_pct_lwm = 10; -- 10% 即开低水位匀速刷脏
复制代码
改完观察十几分钟:TPS 抖动幅度明显收窄,但 10 点高峰仍会撞——因为 96MB 的日志容量是硬伤,热改不了。
步骤 2:扩 redo log(5.7 需停机窗口)
- # my.cnf(5.7)
- innodb_log_files_in_group = 4
- innodb_log_file_size = 2G -- 总容量 8G
复制代码
5.7 收缩日志需要"干净关库 → 删旧 ib_logfile* → 重启",扩容同样要重启;确认 innodb_fast_shutdown=1 正常关闭后再替换。8.0.30+ 则一行搞定且在线生效:
- SET GLOBAL innodb_redo_log_capacity = 8 * 1024 * 1024 * 1024; -- 8G
- SELECT variable_name, variable_value
- FROM performance_schema.global_status
- WHERE variable_name LIKE 'Innodb_redo_log%';
- -- Innodb_redo_log_capacity_resized 确认已生效
复制代码
步骤 3:从源头看清"谁在产 redo"
扩容只是续命,如果 redo 量本身异常偏高,要回到业务侧找源头:
- -- 用 performance_schema 找高峰期写入最多的表
- SELECT object_schema, object_name, count_write, sum_timer_write
- FROM performance_schema.table_io_waits_summary_by_table
- WHERE object_schema NOT IN ('mysql','performance_schema','sys')
- ORDER BY count_write DESC LIMIT 10;
- -- 大事务排查:一次改几十万行的一条 UPDATE,其 redo 是逐行累计的
- -- 业务侧拆批:每批 2000~5000 行,批间 sleep,把 redo 产生量在时间轴上摊平
复制代码
该案例顺带发现:凌晨批处理任务一条 UPDATE 扫 900 万行,凌晨本应平稳的 checkpoint age 也顶到 60%——拆批后凌晨峰值降到 22%。
步骤 4:复核与固化
- -- 高峰期再看 checkpoint age 占比(目标 < 容量的 40%)
- -- 再次采样脏页比例(目标长期 < 40%)
- SELECT variable_value/variable_total*100 AS dirty_pct
- FROM (SELECT
- MAX(CASE WHEN variable_name='Innodb_buffer_pool_pages_dirty'
- THEN variable_value END) AS variable_value,
- MAX(CASE WHEN variable_name='Innodb_buffer_pool_pages_total'
- THEN variable_value END) AS variable_total
- FROM performance_schema.global_status
- WHERE variable_name IN ('Innodb_buffer_pool_pages_dirty',
- 'Innodb_buffer_pool_pages_total')) t;
复制代码
治理前后对比
| 指标 | 治理前 | 治理后 | | redo 总容量 | 96MB(2×48M) | 8G(5.7: 4×2G) | | checkpoint age 峰值占比 | 91%(同步刷脏) | 33%(匀速刷脏) | | 高峰期 TPS | 8000 → 骤降 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 再看,成本极低。
|
|