彻底搞懂 MySQL redo log:从崩溃恢复到性能调优
很多学MySQL的人学到事务、锁、MVCC甚至 binlog 都搞得明明白白可一到 redo log 就含糊了。老实说我早期也一样知道 redo log 是用来“崩溃恢复”的但具体怎么恢复、为什么能恢复、恢复时为什么那么慢完全讲不清楚。后来在线上处理过几次真实的故障——比如一台 8 核 16G 的 MySQL 实例在高峰期突然断电重启后将近二十分钟起不来再比如磁盘明明还有 30G 空闲redo log 却因为文件预分配问题把数据目录撑爆了。这些事故追根溯源全都能落在 redo log 身上。今天这篇就把 redo log 彻底讲透。我会从它可以做什么开始讲到它内部到底是怎么组织的、MySQL 的 InnoDB 引擎是怎么往里面写数据的再到线上如何观察它、排查问题、合理地调整相关参数。内容对于刚入门的开发者也足够友好不需要你提前掌握任何冷门知识跟着走一遍遇到 redo 相关的问题你至少不会慌。1. redo log 到底在解决什么问题1.1 崩溃恢复的“幕后日记”把 MySQL 想象成一家营业中的小卖部。客人买一瓶水正常流程是先收钱、记账、再把水递给客人。但卖货的老板很聪明客人太多他先把当天所有交易都快速记在一本临时小本子上等晚上打烊了再慢慢把账目整理到正式的大账本上。这样白天的营业速度就很快因为不需要每卖一瓶水就去翻大账本。MySQL 平时的操作也是类似的思路。你执行一条 UPDATE本质上是修改内存里数据页Buffer Pool 中的页同时顺手把这条修改记到 redo log 里。内存页更新很快redo log 写入也是顺序写速度也快。但真正的数据文件ibd不会立刻同步而是等到内存脏页积累到一定程度或者到了后台刷盘时机才批量落盘。这样做性能最好但引入了一个风险——如果还没最终落盘、内存里数据丢了怎么办比如突然断电、进程被 kill。这个时候小本子上的记录就派上用场了。redo log 就是这个小本子它保存了恢复数据所需的一切信息。MySQL 重启后InnoDB 会读取 redo log 里记录的修改把还没来得及写进数据文件的操作重新“演算”一遍保证最终数据不丢。这个机制叫 crash recovery也就是崩溃恢复。1.2 数据页的修改只有它能还原这里有个核心细节redo log 记录的不是 SQL而是物理修改结果。它记录的是“某个数据页的某个偏移量被写入了什么值”或者“某个索引页增加了什么记录”。为什么必须是物理级别的变化因为崩溃恢复时InnoDB 根本不敢重放 SQL——如果当时事务已经提交重放一遍 SQL 或许没事但如果事务还没提交只修改了部分数据重放完整 SQL 就会把一条半成品记录硬塞进去造成数据错乱。物理日志就不存在这个问题它只是把对应数据页的状态恢复到崩溃发生前那一刻InnoDB 再通过事务状态undo log、事务系统页决定哪些事务要回滚、哪些要保持。理解了这一点你和 binlog 的区别也就清楚了。binlog 是逻辑日志记录的是“执行了什么样的操作”主要服务于主从复制、数据回放redo log 是物理日志记录的是“哪个页怎么变了”核心目的是故障恢复。两者服务的目标完全不同缺一个都不行。1.3 一次典型的崩溃恢复过程我拿一个真实场景给你拆解。某台 MySQL 实例在凌晨两点被监控发现连接数飙升随后系统负载拉满最终 OOM内存溢出导致 mysqld 被系统杀掉。重启数据库后你第一次进入实例看到日志刷了一堆[Note] InnoDB: Starting crash recovery... [Note] InnoDB: Doing recovery: scanned up to log sequence number 165843216这个阶段InnoDB 就是在“读小本子”。它从检查点checkpoint位置开始扫描 redo log 中所有未被应用的变化然后逐一修改对应数据页。扫描和应用期间数据库无法对外提供服务只能等推进完毕。如果你平时 redo log 文件配置得很大而实际检查点推进得又不及时重启后恢复时间可能非常可观。理解了崩溃恢复你就明白了 redo log 所有相关机制的出发点既要保证性能又要保证不丢数据。接下来我们去看它是怎么写进磁盘的。2. redo log 的写入链路和内部组织2.1 redo log buffer 到磁盘的接力跑InnoDB 维护了一块内存区域叫 redo log buffer专门用来暂存即将写入 redo log 文件的数据。每次事务修改数据页时对应的 redo 记录会先写入这个 buffer再根据一定的条件刷入磁盘。这个 buffer 的大小可以通过参数innodb_log_buffer_size控制默认是 16M。早期版本很多老库还维持着 8M 甚至 4M在高并发写入场景下一个小事务产生的 redo 量可能不大但一个大事务比如一次批量 UPDATE 几百万行产生的 redo 量可以轻松超过 buffer。如果你遇到过“日志缓冲满了”导致的性能抖动那就是 buffer 太小事务被迫等待刷盘。写入路径大概是这样事务执行过程中每修改一个数据页生成一条 redo 记录。记录写入log_sys-bufredo log buffer。根据刷盘策略把 buffer 里的一段内容写入ib_logfile实际上在新版本里是#ib_redo*文件。事务提交时根据innodb_flush_log_at_trx_commit的配置决定是否需要把日志刷到磁盘才算提交成功。2.2 刷盘策略决定了数据安全等级这个参数特别值得掰开揉碎讲清楚。innodb_flush_log_at_trx_commit有三种取值取值行为数据安全性性能特征1每次事务提交都把 redo log 刷到磁盘后再返回最高MySQL 默认最慢但这是保证不丢数据的标准配置0提交时不刷盘由后台线程每秒刷一次最低进程崩溃或断电可能丢失最近 1 秒内的已提交事务最快适合对数据丢失不敏感、追求吞吐的场景2提交时只写到操作系统缓存每秒刷到磁盘中间档数据库进程崩溃不丢但整个机器断电会丢 1 秒左右比 1 快但不如 0 快我接手过很多业务系统不少开发觉得双 1也就是sync_binlog1和innodb_flush_log_at_trx_commit1性能差私自改成 0。改完之后表面上看写入延迟低了很多但实际上数据丢失窗口拉大了。如果业务允许丢最近一秒的“统计类数据”那 0 可以接受但凡涉及钱、库存、订单一律别碰 0。这也是我反复强调的性能调优之前先想清楚这个业务能不能丢数据。2.3 redo 文件的组织形态旧版本的 MySQL 里redo log 用的是ib_logfile0、ib_logfile1这种命名一组文件的整体容量等于innodb_log_file_size乘以innodb_log_files_in_group。MySQL 8.0.30 之后变了redo log 默认使用一组以#ib_redo开头的文件容量自动调整。你在数据目录下执行ls -l | grep #ib_redo会看到类似这样的文件-rw-r----- 1 mysql mysql 32768000 Aug 12 10:23 #ib_redo0 -rw-r----- 1 mysql mysql 32768000 Aug 12 10:23 #ib_redo1 -rw-r----- 1 mysql mysql 32768000 Aug 12 10:23 #ib_redo2这些文件的大小通常不会完全一致因为 InnoDB 会根据实际写入量动态扩容或者清理。MySQL 8.0.30 之后的参数也不一样了控制总容量的是innodb_redo_log_capacity默认 100M。对你没有看错8.0.30 之后默认只有 100M 的 redo 容量高并发下非常容易频繁触发刷盘。所以我强烈建议确认过版本之后把这项调大通常生产库直接给到 4G 到 8G甚至更高取决于你的写入负载。这里多说一句因为很多同学会踩坑在 8.0.30 之前的版本你修改innodb_log_file_size是需要把 MySQL 干净停掉、删除旧的 redo log 文件才能生效的。但 8.0.30 之后完全不需要直接用innodb_redo_log_capacity在线调整即可。升级到新版本运维方式和以前完全不同这个意识要建立起来。3. redo log 的生成过程亲自动手看看3.1 怎么感知 redo 的存在单纯讲原理容易飘我建议你自己动手验证一遍。一个很直观的验证方法新建一张测试表插入一条数据之后看 redo log 文件的变化。下面是一套在 MySQL 8.0 上的实验步骤。准备一个空闲实例先记录当前 redo 文件的情况ls -lh /var/lib/mysql/#ib_redo*然后执行CREATE DATABASE test_redo; USE test_redo; CREATE TABLE t1 (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100)); INSERT INTO t1(name) VALUES (hello);再回来看 redo 文件你会发现文件写入位置modified time如果有的话或容量产生了变化这说明一条普通的 INSERT 也在生成 redo 记录。但你可能想问它到底写了多少这里有个更方便的方式是直接查状态变量。3.2 用状态变量看 redo 的写入量InnoDB 暴露了一些和 redo 直接相关的状态计数最常用的就是SHOW GLOBAL STATUS LIKE Innodb_os_log_written;这个值表示自从实例启动以来往 redo log 写入的字节总数。你连续执行两次INSERT中间的差值就是这批操作产生的 redo 量。注意它单位是字节不是条目数。举例来说如果第一次查询返回 67108864第二次返回 67110144那差值 1280 字节就是你刚才的操作生成的 redo。你还可以从performance_schema里看 more比如SELECT * FROM performance_schema.file_summary_by_instance WHERE FILE_NAME LIKE %#ib_redo%;这里会列出每个 redo 文件的读写字节数、IO 操作次数等。把数据拉出来之后你可以统计每个文件的平均 IO 大小如果平均写 IO 非常小比如 512 字节说明每次刷盘的数据量很低但刷盘次数很高。这种“小而频繁”的写入对 SSD 寿命和延迟都不友好往往需要从应用层事务粒度去优化而不仅仅是调 MySQL 参数。3.3 大事务对 redo 空间的冲击我调过很多慢 SQL发现一个规律很多“看起来是慢查询”的问题实际上是“大事务把 redo 和 binlog 一起推爆”的问题。举例业务里有个定时任务凌晨三点跑批一次性更新三十万行记录。这条 SQL 执行期间每修改一行都会产生 redo。如果一条更新涉及的行数多产生的 redo 量可能非常可观。假设每行平均产生 200 字节 redo三十万行就是 60M如果这个数据量超过 redo log 总容量的一半InnoDB 就会一边写一边强制推进检查点、同时被迫刷脏页整个过程就像水管太细却强行灌水最终把磁盘 IO 打满。处理这类问题的思路不是单纯加大 redo而是从源头拆事务。比如批量 UPDATE 改成按照主键范围分批提交每批五千行。我实测下来把三十万行的大 UPDATE 拆成 60 批之后整体执行时间反而缩短了 30% 以上原因是每批提交后检查点可以及时推进redo log 不会被“堵死”后台刷脏压力骤减。这算是处理大事务问题的一个通用技巧。4. redo log 和检查点的配合关系4.1 检查点是什么讲到 redo log 就不能不提检查点也就是 checkpoint。简单说检查点就是一个标记它记录着“某个时间点之前的所有 redo 对应的数据页修改已经真正写入了数据文件”。换句话说检查点之前的数据已经安全落盘不需要靠 redo 来恢复检查点之后的数据才是崩溃恢复时需要重放的部分。InnoDB 的检查点有两种模糊检查点和尖锐检查点。日常运行中主要是模糊检查点它记录到 LSNLog Sequence Number日志序列号但不需要精确到某一页。崩溃恢复时从最近一次检查点的 LSN 开始向后扫描 redo log把期间涉及的页全部重放。检查点越频繁崩溃恢复时扫描的数据越少、恢复越快但代价是后台刷脏页更频繁可能影响正常业务。所以检查点频率是 InnoDB 自己在平衡的我们一般不直接干预但可以通过配置间接影响。4.2 LSN 到底是什么如果你刚开始接触 redoLSN 这个概念必须拿下来。它不是一个随机编号而是一个单调递增的日志序列号。你可以把它理解成“日志写到了第几个字节”。每一条 redo 记录都有自己对应的 LSNInnoDB 通过对比页上的 LSN 和 redo 记录的 LSN决定一条 redo 是否需要重放。判断逻辑是如果数据页的 LSN 已经大于等于 redo 记录的 LSN说明这条修改已经写入数据文件了跳过如果数据页的 LSN 小于 redo 的 LSN说明数据页比日志“旧”需要重新应用这条 redo。这套机制保证了崩溃恢复的准确性——即使某个页已经刷到磁盘一部分也不会重复应用。可以直接查看当前系统的 LSN 情况SHOW ENGINE INNODB STATUS\G输出里有类似这样的字段LOG --- Log sequence number 29584658120 Log flushed up to 29584658120 Last checkpoint at 29584590010这里的Log sequence number是当前已经写入到 redo log 的最大 LSNLog flushed up to是已经刷到磁盘的 LSNLast checkpoint at是最新检查点对应的 LSN。三个值之间的差能帮你判断写入压力。如果Log sequence number和Last checkpoint at差距太大比如超过 redo 总容量的 70%说明刷脏页跟不上写入速度这时你要小心了——一旦写入量继续飙升redo log 很快就会 wrapInnoDB 会被迫进入“同步刷盘”模式性能断崖式下降。4.3 检查点太久不推进怎么办线上排查时如果发现检查点长时间不推进我一般按下面顺序排查确认是否存在慢 SQL 或者大事务在阻塞。大事务不提交它修改的页就一直处于“活跃”状态后台刷脏也要避开这些页。确认磁盘 IO 是否有瓶颈。还有多少空间、系统 IO 是 100% 还是只用了 20%需要看iostat。确认 Buffer Pool 是不是过大。如果innodb_buffer_pool_size设置得非常大同时刷脏参数又不合理脏页积累也会很多刷不完检查点自然无法往前推进。查看SHOW ENGINE INNODB STATUS里的Modified db pages和Pending writes。如果Modified db pages长期在几千以上刷脏压力就非常大。这一块没有万能配方但有一个很实用的调优向量适当调大 redo log 总容量让 InnoDB 有更大缓冲空间同时适当调小innodb_max_dirty_pages_pct脏页比例阈值让后台刷脏更积极一些。注意这个参数在 MySQL 8.0 里已经改名为innodb_max_dirty_pages_pct_lwm效果方向不变。5. redo log 与 binlog、undo log 的区别别再搞混5.1 三个日志各管一头我见过太多人把 redo log 和 binlog 混为一谈甚至有面试直接问“binlog 不是也能恢复吗为什么还需要 redo log”。这里说清楚三个日志的区别非常明确日志类型记录内容主要作用清理机制redo log物理页的修改细节崩溃恢复、保证已提交事务不丢循环覆盖容量固定binlog逻辑 SQL 或行变更主从复制、基于时间点恢复保留期限可控可手动清理undo log事务回滚需要的数据前镜像支持事务回滚、MVCC按版本清理靠 purge 线程你平时说的“开启 binlog”其实是 MySQL 服务层的日志任何存储引擎外面都有。redo log 是 InnoDB 引擎专属的。MyISAM 引擎就没有 redo log它崩溃恢复能力非常弱这也是为什么 MyISAM 逐渐被淘汰的原因之一。5.2 为什么崩溃恢复不能靠 binlog这个问题很多人面试被卡过实际上原因非常简单binlog 记录的是“逻辑操作”它不关心具体某个数据页怎么变。比如执行一条DELETE FROM user WHERE id 1000binlog 记录的是“删除了哪些行的主键、旧值”或者“执行了这条 SQL”但 MySQL 崩溃时内存里可能有大量数据页还没有刷盘binlog 根本不知道这些数据页现在处于什么状态。它只能告诉你去执行一次 SQL没法告诉你“这个页的第三个字节应该更新成什么”。而 redo log 不一样它精确记录了每一个物理页的修改操作。崩溃恢复时InnoDB 需要把数据页恢复到“某个 LSN 节点”这一点只有物理日志能做到。你可以把 binlog 理解成电影剧本把它交给一个演员他可以演出整个故事但你没法用电影剧本去修复一帧损坏的具体画面。而 redo log 就是胶片本身它记录了每一帧的细节修复时直接按帧恢复。5.3 redo log 和 undo log 是配合关系还有人对 undo log 的认识也不够。undo log 记录的是数据被修改之前的样子它和 redo 记录的是同一操作的两个维度。拿一条 UPDATE 来说执行时 InnoDB 会同时写两条日志undo log记录“这一行原来长什么样”用于将来事务回滚或者 MVCC 读旧版本。redo log记录“这一行被改成了什么样”用于崩溃后重放让已提交事务的修改不丢。有趣的是undo log 本身也需要写 redo log 来保护。因为 undo 页也是普通的数据页它也会被修改、也可能在崩溃时还没落盘。如果只有 undo log 没有 redo崩溃后 undo log 自己都可能是错的谈何回滚。所以你在看 InnoDB 内部的时候会发现redo log 是“最底层”的日志几乎所有页的变化都由它兜底。6. redo log 日常运维观测、配置、常见故障处理6.1 如何判断当前 redo 容量是否够用关于 redo 调优很多开发遇到问题就是加配置、重启但我建议你还是先观察。通过以下 SQL 可以快速拿到 redo 的关键信息SELECT variable_name, variable_value FROM performance_schema.global_variables WHERE variable_name IN ( innodb_redo_log_capacity, innodb_log_file_size, innodb_log_files_in_group, innodb_log_buffer_size, innodb_flush_log_at_trx_commit );你还要看实际的生成量。最实用的指标是实例运行一段时间比如一周后对比Innodb_os_log_written的增长量计算平均每秒写入多少字节。如果平均每秒写入量达到 redo 容量的 1/20 以上说明 redo 容量偏小InnoDB 会频繁触发检查点强制推进。换句话说如果每秒写入 50M而你 redo 总容量只有 1G那么整个 redo 文件每 20 秒就要覆盖一轮检查点会持续高速运转性能基本不可能好。一个比较保守但有效的经验值redo 总容量建议大于“正常情况下 30 分钟到 1 小时产生的 redo 量”。这样可以给后台刷盘留足缓冲也能让崩溃恢复时的扫描范围小一点。按这个标准很多线上实例给到 4G 到 8G 是比较合理的。新版本在线调整SET GLOBAL innodb_redo_log_capacity 8589934592;8G 就是 8589934592 字节。修改之后不需要重启InnoDB 会自动调整 redo 文件数量。不过要注意在线调整过程中可能会有短暂的 IO 波动生产环境建议在低峰期操作。6.2 常见故障一磁盘被 redo 文件占满旧版本里redo log 文件是预分配的所以哪怕实际写入量不大文件也会占满设定的空间。出现“磁盘明明还有很多空间但目录被 redo 占了一半”的情况是很多刚接触 MySQL 的人容易困惑的地方。解决方案有两个层次临时处理清理 binlog、慢查询日志等不那么重要的文件释放磁盘空间。长期方案评估是否需要缩小 redo 容量。一般来说如果 redo 设置为 4G总共就是 4G 空间设计时就要把这种“占位但不清空”的特性考虑进去。MySQL 8.0.30 以后redo 文件不再固定保持预分配大小而是按需扩展这大大降低了磁盘空间浪费的问题。但如果你还在 5.7 环境一定要意识到这个问题规划磁盘空间时给 redo 预留足够空间。6.3 常见故障二启动时崩溃恢复非常慢实例崩溃后重启卡在 “InnoDB: Doing recovery” 很久不动这也是高发问题。原因通常是 redo log 太大或者检查点之后积累了太多待重放的日志或者磁盘随机读性能差。排查方法确认Last checkpoint at距离 redo log 末尾的 LSN 有多远差距越大恢复越慢。在恢复阶段观察iostat如果磁盘 IO 读写接近 100%说明恢复几乎是纯 IO 密集操作。如果经常出现这种慢恢复尝试调小 redo 容量或者把磁盘换成更高性能的 SSD。但从另一个角度redo log 又不能太小否则运行时刷盘压力大。这里的平衡点需要结合业务实测不能拍脑袋。我个人通常先用 4G 起步观察一个业务周期一周如果经常看到 6.1 里的告警就往上加。如果你面对一个全新的业务给 8G 起步也不会有太大问题新版本还能在线调低。6.4 常见故障三InnoDB log 写入慢导致整体性能卡顿有段时间业务反馈写入接口平均延迟从 5ms 涨到 80ms现象非常刺眼。排查时发现SHOW ENGINE INNODB STATUS里显示Log flush writes: 5000 Avg write duration: 15ms这说明 redo log 每次刷盘平均耗时 15ms远高于正常水平的 1ms 以内。进一步检查发现磁盘是共享的云盘同时还有另一个实例在做全量备份把 IO 带宽吃掉了大半。处理方案很简单备份错峰。同时给 MySQL 所在的云盘单独配置更高的 IOPS 能力。这个案例想提醒你的是redo log 写入性能直接决定事务提交延迟。因为提交事务时你必须等待 redo log 刷盘成功这一条链路跑不快你业务再多的连接也没用。所以 redo log 所在的存储设备一定是要保证低延迟的至少是 SSD别用机械盘扛核心业务。6.5 一个小技巧用 Redo Log 定位大事务最后分享一个排查大事务的技巧。当你怀疑某个时段有大事务产生异常多的 redo可以按下面步骤锁定使用 performance_schema 中的events_statements_summary_by_digest按SUM_ROWS_EXAMINED或者SUM_TIMER_WAIT排序找出耗时最高、扫描行数最多的 SQL。如果发现某个 SQL 在一个 statement 里 update 了上百万行基本可以断定它对 redo 的写入量贡献巨大。检查该 SQL 是否在事务里与其他语句合并提交。如果一条 UPDATE 后长时间不 COMMIT它产生的 undo 和 redo 都要一直保留严重影响检查点推进。排查到嫌疑对象后从业务层面拆事务、改批量处理往往比单纯调参数有效。我见过不少开发对 MySQL 参数很执着把innodb_buffer_pool_size调到物理内存 80%却忽略了业务上一条包含十万行更新的 SQL 才是元凶。调参可以解决表面容量问题但真正把大事务拆小才是治本。7. 写在最后的实操心得在 redo log 这个问题上折腾了好几个年头踩过的坑算起来一只手数不完。最想跟大家强调的是不要把 redo log 当成一个不用管的黑盒也不要在出了问题之后才去看它。平时定期观察一下SHOW ENGINE INNODB STATUS里的日志部分养成看Last checkpoint at和Log sequence number的习惯你会发现很多性能隐患是可以提前发现的。还有一个很多人不知道的小技巧MySQL 8.0 里可以临时把 redo log 参数调大但是调大之后最好在低峰期观察一两天再确认是否稳定。如果你用旧的innodb_log_file_size方式修改切记要干净关闭实例、移走旧 redo 文件否则恢复过程中可能出现文件不匹配直接导致启动失败。最后提醒一句对 redo log 的任何修改都要纳入变更流程评估回滚方案别在大促前夜手痒去动它。这套东西理解透了数据库在关键时刻不会让你失望。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →