MySQL redo log:WAL原理、崩溃恢复与调优实战
1. 为什么MySQL需要redo log:从一次写入抖动说起
先说一个我遇到过的场景。某业务库做了一次批量数据订正,单条UPDATE影响了几十万行,跑完之后业务侧反馈写入延迟从平时的 2ms 涨到了 300ms,监控上看磁盘 util 直接打满,但iostat里的写请求数并没有夸张到离谱的程度。把SHOW ENGINE INNODB STATUS拉出来一看,Log sequence number和Last checkpoint at之间的差值一路逼近 redo 容量上限,InnoDB 被逼着做了一次同步刷脏,整个写入链路被硬生生摁住。那次排查之后我才算真正把 redo log 这套机制从头到尾捋了一遍。
redo log 是 InnoDB 存储引擎层的一块固定大小的物理日志,记录的是"某个数据页上发生了什么修改",专门用来做崩溃恢复。它不负责回滚,不负责主从同步,也不负责给你查历史数据,这些活儿分别归 undo log 和 binlog 管。很多人第一次接触它的时候会把它和 binlog 混为一谈,实际上两者从产生位置、记录内容到使用场景全都不同。这篇文章适合三类人看:写过UPDATE但没深究过底层怎么写盘的开发、被innodb_flush_log_at_trx_commit参数绕晕过的运维、以及面试前想把这套机制讲清楚的同学。我会从"为什么需要它"讲到"怎么配、怎么调、怎么排障",尽量把每一个参数的来历和代价说明白,而不是甩一张参数表了事。
1.1 缓冲池与磁盘之间的那道时间差
要理解 redo log,得先理解 InnoDB 是怎么读写数据的。InnoDB 把数据按 16KB 一页组织,读写都走 Buffer Pool(缓冲池)。一条UPDATE语句进来,哪怕只改一行的某一个字段,InnoDB 也是把整页从磁盘读进内存,在内存里改,然后把这一页标记成"脏页",至于什么时候写回磁盘,由后台线程按策略决定。这个设计本身没问题,内存比磁盘快几个数量级,攒一批再写能省掉大量 IO。
问题出在"攒"这个动作上。脏页在内存里待着,万一这时候机器掉电、进程被杀、操作系统崩了,内存里的修改就全没了。而如果每次改完一页就立刻fsync到磁盘,那就是典型的随机写,一页 16KB,一个高并发写入场景下每秒几千次随机写,机械盘直接跪下,固态盘也扛不住写放大。这个矛盾就是 redo log 存在的根本原因:既要保证修改不丢,又不能让随机写把磁盘打穿。
所以 InnoDB 换了个思路——改页的时候,顺手把"我改了什么"这件事用极小的成本记到另一个地方,这个地方是顺序追加写的,写完就算数。真正的数据页可以慢慢刷,反正就算刷之前崩了,也能靠那本"流水账"把修改重新做一遍。这本流水账就是 redo log。
1.2 WAL:把随机写摊平成顺序写
这套机制在数据库领域有个专门的名字,叫WAL(Write-Ahead Logging,预写日志)。核心规则只有一条:修改数据页之前,必须先把对应的 redo 日志持久化。反过来,只要 redo 落盘了,数据页什么时候刷都行,哪怕一直不刷,恢复的时候也能重放出来。
我习惯用一个类比来解释:账本和流水单。真正记录余额的账本(数据页)改起来麻烦,一次只能改一页,还得翻到对应位置;而流水单(redo log)是往后接着写的,写上"3 号账户加 100"就完事,速度快得多。平时先在流水单上记账,等有空了再统一誊到账本上。万一誊到一半停电,翻开流水单从上次誊到的位置往后重放一遍就行。
这个转换带来的收益非常直观。一次批量更新 10 万行,如果直接改盘,可能是 10 万次随机写分散在几千个页上;走 WAL 之后,变成了几百 KB 到几 MB 的顺序追加写,加上后续 checkpoint 时合并后的批量刷脏。随机写变顺序写、多次写变批量写,这两条加起来,写入吞吐的提升往往是数量级的。代价也很明确:读的时候要保证内存里的页是最新的(这个 Buffer Pool 天然满足),以及崩溃恢复时要多花时间重放日志。
注意:WAL 不是 InnoDB 独有的,很多存储系统都用这套思路。但每家实现的细节差别很大,比如日志的格式、刷盘的时机、checkpoint 的策略,这些细节直接决定了崩溃恢复要多久、写入能扛多高。看别人讲 WAL 的时候,别把不同系统的参数混着套。
1.3 redo log与undo log、binlog的分工边界
这三个日志经常被放在一起问,我干脆在这里一次说清楚,后面几节还会展开。
| 维度 | redo log | undo log | binlog |
|---|---|---|---|
| 所属层 | InnoDB 引擎层 | InnoDB 引擎层 | MySQL Server 层 |
| 记录内容 | 数据页的物理修改 | 行的旧版本(逻辑) | 语句或行变更事件 |
| 写入方式 | 固定大小、循环覆盖 | 随事务增长、可清理 | 追加写、按文件滚动 |
| 主要用途 | 崩溃恢复(前滚) | 事务回滚、MVCC 快照读 | 主从复制、数据恢复 |
| 是否可关闭 | 不可关闭 | 不可关闭 | 可关闭(关了就没了复制能力) |
看这张表就能明白,redo log 只解决"已经落盘的修改不能丢"这一个问题。它不管事务有没有提交完成——未提交事务的修改同样会写 redo,恢复时靠 undo 再把这些修改回滚掉。它也不管主从——从库要的是逻辑上的变更事件,得靠 binlog。三者各干各的活,缺一不可。
有一点特别容易混淆:undo log 本身也是要写盘的,而 undo 页的修改同样会产生 redo。也就是说 redo log 里既有数据页的修改,也有 undo 页的修改,这个细节在做容量估算的时候会被忽略,实际写入量会比"只算业务数据变更"大出不少。
2. redo log的物理结构与循环写机制
搞清楚"为什么"之后,接下来得看"长什么样"。redo log 的物理结构有几个关键设计,理解这些设计,后面聊参数和排查才不至于云里雾里。
2.1 从重复记录到数据块:redo log长什么样
redo log 在磁盘上不是一个无限增长的文件,而是一组固定大小的文件。MySQL 5.7 和 8.0 早期版本里,这两个文件叫ib_logfile0和ib_logfile1,大小由innodb_log_file_size决定,数量由innodb_log_files_in_group决定,默认 2 个 48MB,总共 96MB。从 8.0.30 开始改成了另一套:所有 redo 文件放在数据目录下的#innodb_redo子目录里,默认切成 32 个文件,总容量由innodb_redo_log_capacity控制,默认 100MB。
不管哪套,文件内部的存储粒度都是512 字节的 block。每个 block 有 12 字节头部(记录 block 编号、数据长度、是否为某个 mtr 的首个 block、checkpoint 序号)和 4 字节尾部校验和,实际能放日志数据的只有 496 字节。一条 redo 记录如果超过这个长度,会跨 block 存储,用链表串起来。
redo 记录本身的类型很细化,不是笼统的"更新某行"。InnoDB 定义了几十种类型,比如MLOG_1BYTE、MLOG_2BYTE、MLOG_4BYTE、MLOG_8BYTE用于页头页尾的定长字段修改,MLOG_REC_INSERT、MLOG_REC_DELETE、MLOG_COMP_REC_UPDATE_IN_PLACE用于索引记录的增删改,还有专门针对二级索引、undo 页、空间分配的类型。所以业界常说 redo log 是"物理逻辑日志"——物理到页,逻辑到页内的具体操作。这个说法比单纯叫"物理日志"准确得多。
我在看某个开源工具解析 redo 文件时踩过一个坑:不同小版本的记录类型编号会变,硬编码类型映射的解析脚本很容易在新版本上读出乱码。要做 redo 解析,必须跟着源码里的枚举走,别信老博客里的表。
2.2 write pos与checkpoint:一条首尾相接的跑道
所有 redo 文件在逻辑上被当成一条首尾相接的环形跑道。跑道上维护两个指针:
- write pos:当前写到哪儿了,新日志从这里往后追加,写到末尾就绕回开头。
- checkpoint:当前可以安全覆盖到哪儿了。这个位置之前的日志,对应的脏页都已经刷到磁盘,日志没用了,空间可以被新日志覆盖。
两个指针之间的区域就是"还有效的日志",也叫 checkpoint age。当 write pos 追上 checkpoint 时,就意味着没有空闲空间了,InnoDB 必须停下来,强制刷一批脏页把 checkpoint 往前推,腾出空间才能继续写。这就是开头那个场景里写入延迟暴涨的直接原因。
这种环形设计的好处是空间恒定,不会像 binlog 那样越写越大需要清理。坏处也很明显:容量一旦定死,写得太猛就会撞墙。而且撞墙的时候不是简单地排队等待,InnoDB 会触发同步刷脏,把前台写入线程拖进去一起刷页,延迟抖动会非常剧烈。
这里有个反直觉的点:redo log 空间大不代表一定安全,太小也未必立刻出事,关键看写入速率和 checkpoint 推进速度能不能匹配上。一个每秒写 50MB redo 的库,配 1GB 容量只能撑 20 秒;一个每秒写 1MB 的库,配 512MB 能撑八分多钟。单纯看绝对值没意义,得算"能扛多久"。
2.3 LSN:贯穿buffer pool与磁盘的进度标尺
redo log 里所有进度都用LSN(Log Sequence Number)衡量,这是一个全局单调递增的字节偏移量,从日志文件组创建那一刻起一直往上加,不因为文件绕回而重置。任何一个数据页的页头里都存着一个FIL_PAGE_LSN,表示"这一页最后一次被修改时对应的日志位置"。
LSN 有几个衍生的关键值,在SHOW ENGINE INNODB STATUS的 LOG 段落里都能看到:
| 字段 | 含义 | 大小关系 |
|---|---|---|
| Log sequence number | 当前已写入 log buffer 的最新 LSN | 最大 |
| Log flushed up to | 已持久化到磁盘的 LSN | ≤ 上一个 |
| Pages flushed up to | 已刷盘脏页中最大的 LSN | ≤ 上一个 |
| Last checkpoint at | 最后一次 checkpoint 的 LSN | 通常等于 Pages flushed up to |
这四个值的差值就是排查问题的金矿。Log sequence number - Log flushed up to大,说明 log buffer 里攒了不少还没来得及落盘的日志,通常出现在高并发提交但磁盘 IO 跟不上的时候。Log sequence number - Last checkpoint at就是 checkpoint age,逼近容量上限时就是上面说的写满风险。
WAL 规则在这里体现得非常具体:一个脏页要刷到磁盘,前提是它的FIL_PAGE_LSN不能大于Log flushed up to。换句话说,如果日志还没落盘,对应的页就不允许刷出去。这条约束保证了磁盘上任何一页的状态,都能通过磁盘上的 redo 重放到最新。
3. 一条update语句背后:redo log的完整生命周期
光看结构还是抽象,我们把一条UPDATE从进入到落盘的全过程拆开看一遍,参数的意义就清楚了。
3.1 mini-transaction:InnoDB内部的原子小包裹
InnoDB 内部不是按 SQL 语句为单位生成 redo 的,而是按mini-transaction(mtr)。一个 mtr 是一组对页的原子修改,比如"更新聚簇索引上的一行 + 同步更新三个二级索引 + 修改页头的一些计数",这几件事必须一起成功或一起失败,否则索引和数据就对不上了。
mtr 执行过程中,所有的 redo 记录先攒在 mtr 自己的私有缓冲区里;等 mtr 提交时,一次性拷贝进全局的redo log buffer(由innodb_log_buffer_size控制,默认 16MB),同时拿到一段连续的 LSN 区间。多个 mtr 的日志在写入 log buffer 时会被尽量合并成一次连续写入,减少锁竞争。
这里有个细节值得说:事务提交并不等于 mtr 提交。一个大事务可能包含成千上万个 mtr,每个 mtr 提交都会往 log buffer 里写日志。所以一个还没提交的事务,它的修改早就已经进了 redo log,甚至可能已经落盘了。崩溃恢复时靠的不是"日志里有没有",而是"事务有没有提交标记"。
这也解释了为什么长事务对 redo 压力特别大:它持续产生 redo,把 checkpoint 往前推的压力一直存在,但事务又不结束,undo 也清不掉,整个系统的写入放大是持续叠加的。
3.2 redo log buffer到磁盘的四个触发时机
log buffer 是内存里的缓冲区,什么时候往磁盘刷,有四个触发点:
- 事务提交时。受
innodb_flush_log_at_trx_commit控制,这是最常被讨论的一个。 - log buffer 空间不足。默认 16MB,写到大概一半的时候,后台就会开始往磁盘刷,给后续写入腾地方。
- 后台线程每秒一次。有一个专门的线程按秒级节奏把 buffer 里的日志刷下去。
- checkpoint 或正常关机时。checkpoint 需要保证日志完整;正常 shutdown 会把所有残留日志刷干净。
注意"写"这个动作其实分两步:先write()到操作系统的页缓存,再fsync()落到磁盘介质。这两步的成本差着数量级,fsync才是真正的瓶颈。所有关于 redo 刷盘策略的讨论,本质上都是在讨论"什么时候调 fsync"。
innodb_flush_log_at_trx_commit三个取值的差别就在这里:
| 取值 | 提交时的动作 | 崩溃丢失窗口 | 性能 | 适用场景 |
|---|---|---|---|---|
| 1(默认) | write + fsync 都做 | 几乎不丢(掉电也不丢已提交事务) | 最慢,每次提交一次 fsync | 金融、订单等不能丢数据的场景 |
| 2 | 只 write,不 fsync | 数据库进程崩溃不丢;操作系统崩溃或掉电可能丢 1 秒左右 | 较快 | 能接受秒级丢失的一般业务 |
| 0 | 提交时什么都不做 | 依赖后台每秒刷,最多丢 1 秒,且进程崩溃也丢 | 最快 | 日志、埋点、可重算的离线数据 |
这里有个很多人搞错的点:取值为 2 的时候,只要机器不断电,即使 MySQL 进程被 kill -9,数据也不会丢,因为日志已经进了操作系统的页缓存,进程死了缓存还在,操作系统会负责写下去。真正危险的是操作系统崩溃或者直接断电,这时候页缓存里的东西全没了。取值为 0 则更激进,连页缓存都不一定进得去,MySQL 进程一崩就可能丢。
提示:这个参数是全局的,可以在线改,但改完只影响之后的新事务,已经提交的不受影响。生产上从 1 降到 2 之前,一定要先确认业务方能不能接受秒级数据丢失,别自己拍脑袋决定。
3.3 组提交:把N次fsync压成一次
如果每个事务提交都要 fsync 一次,那并发一高,磁盘的 fsync 能力立刻成为天花板。InnoDB 的解法是组提交(group commit):多个事务的日志先攒在一起,由一个线程统一做一次 fsync,然后大家一起返回。这样 N 个事务只花一次 fsync 的成本,吞吐量随并发数上升。
MySQL 8.0 对 redo log 子系统做过一次比较大的重构,把写日志和刷日志拆成了两个专门的线程——log_writer和log_flusher,写入路径基本做到了无锁化,高并发下的表现比 5.7 好不少。同时 redo 的组提交还会和 binlog 的组提交协同,尽量减少两边的 fsync 次数。
这里有个实践中的观察:组提交的效果依赖于并发度。单线程压测的时候,组提交几乎不起作用,每次提交还是一次 fsync,测出来的 QPS 会明显偏低。做性能评估的时候,一定要用接近真实业务的并发模型去压,否则结论会偏得很离谱。我之前见过有人拿 sysbench 单线程的结果去论证"参数从 1 改成 2 性能提升 30 倍",这个数字只有在高并发场景下才成立。
4. 崩溃恢复:redo log怎么把数据捞回来
redo log 的全部价值最终都要在恢复阶段兑现。这一段是面试高频区,也是实际运维中真正会踩坑的地方。
4.1 前滚与回滚:恢复的两段式流程
崩溃恢复分两个阶段。
第一阶段是前滚(roll forward)。从最近一次 checkpoint 的 LSN 开始,把磁盘上的 redo log 从头扫一遍,遇到需要重放的记录就把对应页从磁盘读进 Buffer Pool 并应用修改。扫到日志末尾(最后一个有效 LSN)就停下来。这一步保证了所有已经落盘的修改都体现在数据页上——包括那些还没提交的事务的修改,因为它们的 redo 同样在日志里。
第二阶段是回滚(roll back)。前滚结束后,数据页里可能包含未提交事务的修改,必须清掉。这时候靠 undo log 反向操作,把这些修改撤回去。所以恢复的完整流程是:redo 负责"多做",undo 负责"做减法"。
那么怎么判断哪些事务提交了?关键在于两阶段提交的设计(第 5 节会展开)。redo log 里每个事务的 prepare 记录携带了事务 ID(XID),恢复时 InnoDB 会把处于 prepare 状态的事务列出来,然后交给 Server 层去比对 binlog 里有没有对应的 XID:binlog 完整就提交,不完整就回滚。
有个经验值得分享:恢复时间主要花在第一阶段扫日志和随机读页上,而不是"重放"本身。一个几 GB 的 redo 文件组,如果 checkpoint 很旧,恢复时可能需要读几千个数据页,机械盘上这个耗时会非常可观。这也是为什么 checkpoint 不能拖太久,它直接影响故障恢复的 RTO。
4.2 双写缓冲:redo log救不了页断裂
很多人以为有了 redo log 就万无一失,这是个很危险的误解。redo log 是物理日志,它记录的是"在某个页的某个偏移位置写了什么",前提是这个页本身是完好的。如果页已经损坏,redo 就无从下手。
而页损坏是真实存在的:磁盘的扇区通常是 512 字节或 4KB,InnoDB 的页是 16KB,一次页写入会被拆成多次扇区写入。如果写到一半断电,可能出现"部分写"(partial page write),也就是页断裂——页头是新的,页尾还是旧的,整页不可用。
解决办法是doublewrite buffer(双写缓冲)。刷脏页的时候,先把页顺序写进一块连续的 doublewrite 区域并 fsync,然后再写到真正的数据文件位置。如果中途崩溃,恢复时可以从 doublewrite 区域找到完好的页副本,用它修复损坏的页,然后再应用 redo。代价是写放大了一倍左右,但换来的是页级别的可靠性。
注意:doublewrite 在 8.0.20 之后默认放在独立的
#ib_16384_0.dblwr文件里,不再占用系统表空间。如果你从老版本升级上来,会看到空间布局的变化,别以为是出问题了。
理解了这一层就能明白:redo log 保证的是"已落盘的日志不丢",doublewrite 保证的是"数据页本身不被写坏",两者是互补关系,不是替代关系。做高可靠架构的时候,两个都得在。
4.3 恢复慢、恢复失败的典型排查路径
实际遇到的恢复问题,大概有这么几类:
第一类是恢复特别慢。最常见的原因是崩溃前 checkpoint 太旧,需要重放的日志量大。其次是 Buffer Pool 太小,重放时页在内存里待不住,反复读盘。还有一种情况是恢复期间磁盘 IO 被其他进程抢占了,比如备份任务没停。
第二类是启动时报 redo 相关的错误。比如提示某个日志文件大小不匹配、日志文件损坏。8.0.30 之前如果在my.cnf里改了innodb_log_file_size但没做正常关闭,启动会直接失败。碰到这种情况,先备份数据目录,再按官方文档的流程处理,不要随手删日志文件。
第三类是恢复后数据"看起来不对"。这个多半不是 redo 的问题,而是业务逻辑本身在崩溃时处于中间状态,比如一个事务只写了一半。这种情况要靠业务侧的幂等设计来兜底,数据库层面只能保证事务的原子性。
实操中我习惯在恢复前先做两件事:一是把整个数据目录冷备一份,二是把错误日志和SHOW ENGINE INNODB STATUS的完整输出留存下来。恢复过程一旦出问题,这些是唯一能回溯的线索。
5. redo log与binlog:两阶段提交为什么省不掉
redo log 和 binlog 的关系是 MySQL 里最精妙的设计之一,也是理解数据一致性的关键。
5.1 两种日志的职责划分
redo log 在 InnoDB 层,是物理日志,用于崩溃恢复;binlog 在 Server 层,是逻辑日志,记录的是语句或行变更事件,用于主从复制和时间点恢复。两者是独立写入的,没有任何机制保证它们同时成功。这就是问题的根源。
假设没有协调机制,一个事务提交时先写 redo 再写 binlog,如果 redo 写完、binlog 写到一半崩溃了,重启后主库靠 redo 恢复了这条数据,但从库因为没收到完整的 binlog 而没有这条数据,主从就不一致了。反过来先写 binlog 再写 redo,崩溃后从库多了一条主库没有的数据,同样不一致。
5.2 两阶段提交的时序与两个崩溃点
MySQL 的解法是内部两阶段提交(internal 2PC),时序是这样的:
- InnoDB 写 redo log,把事务标记为prepare状态,记录 XID;
- Server 层写 binlog,并 fsync 到磁盘;
- InnoDB 写 redo log,把事务标记为commit状态。
崩溃可能发生在任意两个步骤之间,恢复时的判断逻辑是:
- 崩在步骤 1 之后、步骤 2 完成之前:redo 里是 prepare,binlog 里没有这个 XID(或不完整),判定为未提交,回滚。
- 崩在步骤 2 完成之后、步骤 3 完成之前:redo 里是 prepare,binlog 里能找到完整的 XID,判定为已提交,重做并补上 commit 标记。
判断的核心依据就是 XID 在 binlog 中是否完整存在。这个设计保证了无论崩在哪一步,主库和从库的最终状态都是一致的。
提示:binlog 的
sync_binlog参数(控制 binlog 什么时候 fsync)和innodb_flush_log_at_trx_commit是一对。业界常说的"双 1 配置"就是这两个都设为 1,代价是每次提交两次 fsync。对数据一致性要求极高的场景,这个代价必须付。
5.3 主从一致性里的redo log身影
redo log 在主从架构里不直接参与复制,但它间接影响复制的正确性。从库收到 binlog 后,由 SQL 线程(或 worker 线程)重放,重放过程中产生的修改同样会写从库自己的 redo log,走同样的 WAL 流程。所以从库的写入压力和数据安全性,同样受innodb_flush_log_at_trx_commit影响。
一个经常被忽略的点是:从库如果把innodb_flush_log_at_trx_commit调成 0 或 2 来提升重放速度,一旦从库崩溃,可能会有部分已接收的 binlog 没有真正应用,重启后需要从位点重新拉取。这在半同步复制下问题不大,在异步复制下可能导致数据不一致,需要评估。
6. 参数调优与容量规划:把redo log配到刚刚好
参数怎么配,是 redo log 话题里最实操的部分。这里我给一套可以照着做的思路。
6.1 innodb_log_file_size/redo_log_capacity怎么算
先说结论:redo 总容量应该能容纳系统在峰值写入下 20 到 60 分钟的日志量,同时不能小到让 checkpoint 频繁触发。
计算步骤分三步。
第一步,测出峰值 redo 生成速率。在业务高峰时段采样两次SHOW ENGINE INNODB STATUS里的Log sequence number,用差值除以时间间隔:
-- 第一次采样 SHOW ENGINE INNODB STATUS\G -- 间隔 60 秒后第二次采样,用两个 LSN 相减 -- 速率 = (LSN2 - LSN1) / 60,单位字节/秒第二步,算目标容量。假设峰值速率是 20MB/s,希望扛住 30 分钟,那容量就是20 × 60 × 30 = 36000MB,约 35GB。这个数字看起来很大,但对写入密集型的库来说很正常。
第三步,考虑改造成本。8.0.30 之前,改innodb_log_file_size需要改配置 + 重启,而且改小了不生效(InnoDB 不会主动缩小)。8.0.30 之后可以用innodb_redo_log_capacity动态调整,方便很多:
SET GLOBAL innodb_redo_log_capacity = 4294967296; -- 4GB参考一下不同业务形态的经验值:
| 业务特征 | 建议 redo 总容量 | 说明 |
|---|---|---|
| 读多写少,日变更量小 | 512MB ~ 1GB | 默认 100MB 往往偏小,稍微放大即可 |
| 一般 OLTP,峰值几千 TPS | 2GB ~ 8GB | 最常见区间,能覆盖半小时左右的峰值 |
| 批量导入、大批量订正 | 8GB ~ 32GB | 大批量操作会瞬间产生大量 redo |
| 高频写入 + 低延迟要求 | 16GB 以上 | 优先保证 checkpoint 不撞墙 |
容量也不是越大越好。redo 太大,崩溃恢复时要扫的日志更多,恢复时间变长;而且磁盘空间是实打实的成本。我一般的做法是先按 30 分钟的峰值量配,跑一段时间看 checkpoint age 的峰值水位,如果长期低于容量的 30%,可以适当缩;如果经常超过 75%,必须扩。
6.2 三个必须盯住的监控指标
redo 相关的监控不用做得很复杂,盯住三个就够:
第一,checkpoint age 水位。这是最核心的指标,直接反映"离撞墙还有多远"。通过information_schema.INNODB_METRICS可以拿到:
-- 如果查不到数据,先开启监控项 SET GLOBAL innodb_monitor_enable = 'log_lsn%'; SELECT NAME, COUNT FROM information_schema.INNODB_METRICS WHERE NAME IN ('log_lsn_current', 'log_lsn_last_checkpoint', 'log_lsn_checkpoint_age');log_lsn_checkpoint_age除以 redo 总容量就是水位百分比。超过 75% 就该告警了,因为到了这个水位,InnoDB 的 adaptive flushing 会开始激进刷脏,可能影响正常查询的 IO。
第二,redo 生成速率。定时采样log_lsn_current算差值。这个指标突然飙升,往往意味着有大事务、大批量操作或者索引设计有问题(比如更新了一个频繁变动的列,导致大量二级索引维护)。它是排查写入抖动的第一入口。
第三,日志等待时间。SHOW ENGINE INNODB STATUS里的Log wait/s或者 performance_schema 里的相关等待事件,能反映有多少时间花在等日志刷盘上。这个值高,说明 fsync 跟不上,该考虑换更快的盘或者调整刷盘策略了。
6.3 在线调整的实操步骤与风险控制
以 8.0.30 之后动态调整innodb_redo_log_capacity为例,我会这么操作:
- 确认版本和当前值:
SELECT @@version, @@innodb_redo_log_capacity;评估磁盘空间。调整过程中会新建 redo 文件,需要额外的磁盘空间,至少要留出目标容量的大小。用
df -h确认数据目录所在分区。选择低峰期操作,并且在操作前记录一次基线:
SHOW ENGINE INNODB STATUS\G -- 保存 LOG 段落的完整输出- 执行调整:
SET GLOBAL innodb_redo_log_capacity = 8589934592; -- 8GB观察 5-10 分钟,重点看
Last checkpoint at是否正常推进、Log wait是否异常、业务延迟有没有抖动。持久化到配置文件,否则重启后失效。
注意:调整 redo 容量会触发一次 checkpoint,如果当前 checkpoint age 已经很高,这次强制 checkpoint 会引发一次明显的 IO 抖动。所以千万不要在水位已经超过 80% 的时候去调,先等水位降下来。
8.0.30 之前的版本就没这么幸运了,innodb_log_file_size的修改需要:
- 正常关闭数据库(
mysqladmin shutdown,不要 kill); - 修改
my.cnf里的innodb_log_file_size; - 启动数据库,InnoDB 会自动重建 redo 文件。
如果在没正常关闭的情况下改了参数,启动会因为文件大小不匹配而报错。这时候的正确做法是把配置改回去,先正常启动,再走标准流程。千万不要直接删ib_logfile*文件,那等于把未刷盘的修改全丢了。
7. 常见问题与排查技巧实录
最后这一节是我这些年踩过的坑和一些容易被误判的场景,整理成速查表方便对照。
7.1 高频问题速查表
| 现象 | 可能原因 | 排查方式 | 处理方向 |
|---|---|---|---|
| 写入延迟周期性飙升 | checkpoint age 撞高水位,触发同步刷脏 | 看Log sequence number - Last checkpoint at | 扩容 redo、提升innodb_io_capacity |
| 启动恢复耗时数小时 | checkpoint 太旧,需重放日志量大 | 错误日志里的恢复进度 | 增大 redo 后恢复反而更慢,需权衡;提升磁盘 IO |
| 报日志文件大小不匹配 | 未正常关闭就改了innodb_log_file_size | 错误日志明确提示 | 改回原配置正常启动,再走标准流程 |
| 主从数据不一致 | 两边刷盘策略不一致或半同步配置问题 | 对比sync_binlog、innodb_flush_log_at_trx_commit | 统一配置,开启半同步 |
| 磁盘写入量远超业务量 | 写放大:redo + doublewrite + 脏页刷盘 | iostat、/proc/diskstats | 评估 doublewrite 必要性,优化批量提交 |
| redo 目录持续增长 | 8.0.30 后按 32 个文件切分,属正常 | ls -lh #innodb_redo | 无需处理,看总容量而非单文件 |
7.2 几个容易误判的场景
第一个误判:把 redo 写入量当成磁盘写入量。实际上一次更新至少涉及三处写:redo 日志、doublewrite 区、最终的数据页。乘以写放大系数,磁盘实际写入可能是 redo 量的 2-3 倍。做容量规划的时候只算 redo,很容易低估。
第二个误判:认为innodb_flush_log_at_trx_commit=2一定安全。前面说过,进程崩溃确实不丢,但一旦宿主机出问题,丢的数据量取决于操作系统页缓存的刷盘节奏,可能远超 1 秒。这个参数能不能用,取决于业务的 RPO 要求,不是技术问题。
第三个误判:把 redo log 当成可以随便删的"缓存"。有同事在数据目录空间紧张的时候想删 redo 文件腾地方,这是绝对禁止的操作。删了之后数据库要么起不来,要么静默丢失未刷盘的修改,后果比空间不足严重得多。
第四个误判:看到Log sequence number数字很大就以为有问题。LSN 是单调递增的绝对量,从实例创建起一直累加,数字大是正常的。真正要看的是它和 checkpoint 的差值,以及增长速率。
7.3 我在实际运维中的几点体会
第一点,redo 容量规划要跟着业务节奏走,不是一劳永逸的。业务量翻倍了,原来配的 4GB 可能就不够了。我习惯在容量水位告警里加一条规则:连续一周峰值水位超过 60%,就提工单评估扩容,不等它真的撞墙。
第二点,大批量操作前先看 checkpoint age。做数据订正、批量导入这类操作之前,先确认当前水位低于 30%,并且把这些操作拆成小批次提交。一次提交几十万行,redo 会瞬间飙上去,很容易把 checkpoint 顶到上限。
第三点,恢复演练比任何监控都重要。我在几个环境里定期做恢复演练:从备份 + redo 恢复到指定时间点,记录耗时。演练出来的数字才是真实的 RTO,监控上的估算值往往偏乐观。有一次演练发现恢复要 40 分钟,远超预期的 10 分钟,最后定位到是备份策略和 checkpoint 频率的问题,调整之后才降到可接受范围。
第四点,别迷信默认值,也别迷信调优文章。默认的 100MB redo 容量对很多业务来说确实偏小,但直接照搬网上"改成 4GB"的建议也不合适。还是要按自己的写入速率算一遍,跑一段时间看水位曲线,才知道合不合适。参数是给自己系统配的,不是给别人的文章配的。