大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
有一次线上告警:数据库突然不响应任何写操作了。应用侧报"Lock wait timeout",但查锁等待,没有明显的行锁冲突。
连上去看,SHOW ENGINE INNODB STATUS的输出里,LOG段显示Last checkpoint at和Log sequence number之间的差距非常大。再看Innodb_log_waits,持续增长。
问题找到了:Redo Log写满了,InnoDB暂停了所有写入,等待脏页刷盘。而根因,是一个跑了半小时还没提交的长事务。
Redo Log这东西,平时没人关注它。它不像Buffer Pool那样有命中率可看,也不像锁那样有等待可查。它安安静静地循环写,直到写满的那一刻,整个数据库的写入瞬间停止。
要理解为什么会写满,得先看它是怎么循环的。
一、Redo Log的循环写与checkpoint的约束
Redo Log的总大小由innodb_log_file_size × innodb_log_files_in_group决定。假设每个文件1GB,两个文件,总共2GB。
写入是循环的:写到文件末尾,回到文件开头继续写。但不能直接覆盖还没刷盘的日志。如果某段日志对应的脏页还在内存里没落盘,这段日志就必须保留------万一数据库崩溃,恢复时还需要它。
checkpoint的位置,标记的就是"哪些日志可以安全覆盖"。checkpoint之前的日志,对应的脏页已经刷盘,可以覆盖;checkpoint之后的日志,对应的脏页还没刷盘,不能动。
写入位置不断向前推进,checkpoint也必须跟着推进,才能腾出空间。如果checkpoint推不动,写入位置迟早会追上checkpoint,Redo Log就满了。
二、checkpoint为什么推不动
checkpoint推进的条件是:所有LSN小于checkpoint位置的脏页,都已经刷盘。有三个因素会阻止这件事发生。
长事务是第一个,也是最常见的一个。
一个事务修改了大量数据页但还没提交。这些页是脏页,但对应的Redo Log不能被覆盖------事务回滚时还需要这些日志。长事务不结束,checkpoint就无法越过它涉及的日志范围。
我那次遇到的长事务,是一个批量更新任务,一次修改了上百万行,跑了半小时。这半小时里,checkpoint被卡死,Redo Log可用空间持续缩小。
第二个因素是脏页刷盘速度跟不上日志产生速度。
即使没有长事务,如果写入量太大,脏页产生速度超过刷盘速度,checkpoint也会滞后。innodb_io_capacity决定了每秒能刷多少页,默认200。如果业务写入远超这个速度,checkpoint追不上。
第三个因素是日志文件太小。
默认值在很多版本中是48MB,对于写入量大的业务远远不够。可用空间本身就有限,稍微一点滞后就会写满。
三、从写满到暂停的完整链路
完整的触发链路是这样的:
-
长事务开始,修改大量数据页,产生大量Redo Log
-
事务不提交,它涉及的日志不能被覆盖,checkpoint被卡住
-
新的写入继续产生Redo Log,写入位置向前推进
-
写入位置接近checkpoint位置,可用空间越来越小
-
Redo Log写满,InnoDB暂停所有写入
-
InnoDB加速刷脏页,等待checkpoint推进
-
checkpoint推进后,写入恢复
这个问题的可怕之处在于:它不是慢慢变慢,是突然完全停止。 前面的写入一直正常,直到Redo Log写满的那一刻,所有写入瞬间阻塞。应用侧的感知是"数据库突然不响应了",但数据库本身并没有宕机。
四、怎么排查和确认
第一步:看checkpoint和LSN的差距
bash
SHOW ENGINE INNODB STATUS\G
在LOG段中,对比Log sequence number和Last checkpoint at。差距越大,说明checkpoint越滞后。如果差距接近Redo Log总大小,说明快写满了。
第二步:看等待次数
bash
SHOW GLOBAL STATUS LIKE 'Innodb_log_waits';
这个值持续增长,说明已经发生过多次"Redo Log写满等待"。
第三步:找长事务
bash
SELECT trx_id, trx_state, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec,
trx_rows_modified
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDER BY duration_sec DESC;
这个查询列出运行超过60秒的事务,按持续时间排序。trx_rows_modified显示事务修改了多少行------修改行数越多,对checkpoint的阻塞越严重。
第四步:看脏页比例
bash
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_total';
脏页占比过高(超过50%),说明刷盘跟不上。
五、解决方案:控制事务大小是根本
方案一:控制事务大小。
这是最根本的方案。不要把上百万行的修改放在一个事务里。拆成每批几千行,提交后再跑下一批。长事务被拆成短事务,checkpoint就不会被长时间卡住。
我那次遇到的长事务,后来改成了分批更新,每批5000行,提交后再处理下一批。同样是一百万行,Redo Log不再堆积,checkpoint正常推进。
方案二:调大innodb_log_file_size。
给Redo Log更多缓冲空间。但这是治标------如果长事务持续存在,再大的日志文件也会被写满。MySQL 8.0.30之后用innodb_redo_log_capacity统一控制,可以动态调整。
方案三:调大innodb_io_capacity。
加快脏页刷盘速度。SSD环境可以调到2000-5000。这样checkpoint推进得更快,Redo Log空间释放得更及时。
方案四:监控Innodb_log_waits和长事务。
把这两个指标纳入监控。Innodb_log_waits持续增长是明确的预警信号。长事务查询定期跑,发现超过60秒的事务及时处理。
六、小结
Redo Log写满导致数据库暂停写入,根因往往不在磁盘慢,在长事务阻塞了checkpoint推进。长事务涉及的日志不能被覆盖,checkpoint被卡住,写入位置追上checkpoint时Redo Log就满了。排查的核心是对比Log sequence number和Last checkpoint at的差距、监控Innodb_log_waits、找出长时间运行的事务。最根本的解决方案是控制事务大小------拆批执行,让checkpoint有机会推进。调大日志文件和刷盘能力是辅助手段,不能替代事务拆分。
小耶在手,SQL 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~