前言
relay_log_recovery=ON 是 MySQL 主从复制中常见的安全配置,官方文档描述它的作用是:从库重启时,丢弃未执行的 relay log,从主库重新拉取。听起来很安全,但在特定条件下,它不仅救不了你,反而会导致 数据静默丢失 — 复制显示正常,实际上数据已经少了。
本文通过一个完整的实验,从环境确认、故障触发、原因分析到最终修复,逐步展示这个陷阱的全貌。
一、实验环境
| 角色 | 实例 | 服务器 |
|---|---|---|
| 主库 | 3308 | 120.48.119.118 |
| 从库 | 3309 | 101.34.248.57 |
MySQL 版本:8.0.35,GTID 模式,半同步复制。
从库关键参数
| |
配置总结:GTID + AUTO_POSITION + relay_log_recovery + MTS(4 worker)
二、完整复现流程
步骤 1:确认主从完全同步
主库 3308:
| |
+----+------+
| id | name |
+----+------+
| 1 | a |
| 2 | b |
| 3 | c |
+----+------+
*************************** 1. row ***************************
@@global.gtid_executed: 5a8b61f5-5ba5-4ce7-94e2-cd401d2e747e:1-20,
5fe881c6-1c97-11f1-8224-525400ac4044:1-2,
da2593b2-18a0-11f1-be74-fa202030301e:1-18
从库 3309:
| |
+----+------+
| id | name |
+----+------+
| 1 | a |
| 2 | b |
| 3 | c |
+----+------+
*************************** 1. row ***************************
@@global.gtid_executed: 5a8b61f5-5ba5-4ce7-94e2-cd401d2e747e:1-20,
5fe881c6-1c97-11f1-8224-525400ac4044:1-2,
da2593b2-18a0-11f1-be74-fa202030301e:1-18
| |
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 0
Auto_Position: 1
确认:主从数据一致,GTID 一致,延迟为 0。
步骤 2:停止从库 SQL 线程
| |
验证:
| |
Slave_IO_Running: Yes ← IO 线程还在跑
Slave_SQL_Running: No ← SQL 线程停了
Exec_Master_Log_Pos: 3367 ← SQL 线程停在这个位置
Executed_Gtid_Set: ...:1-18 ← 最后执行到 GTID :18
此时:IO 线程像一根水管,持续从主库抽水(binlog)到水桶(relay log)。但 SQL 线程这个工人休息了,水桶里的水(事务)不会被处理。
步骤 3:在主库写入新数据
| |
+----+------+
| id | name |
+----+------+
| 1 | a |
| 2 | b |
| 3 | c |
| 4 | d | ← 新写入
| 5 | e | ← 新写入
+----+------+
@@global.gtid_executed: da2593b2-...:1-20 ← GTID 推进到 :20
主库新增了 GTID
:19(INSERT id=4)和:20(INSERT id=5)。
步骤 4:确认从库 IO 线程已接收,SQL 线程未回放
| |
id name
1 a
2 b
3 c ← 没有 4 和 5
Read_Master_Log_Pos: 3925 ← IO 已读到主库最新
Slave_IO_Running: Yes
Slave_SQL_Running: No
Exec_Master_Log_Pos: 3367 ← SQL 线程停在原地
Retrieved_Gtid_Set: da2593b2-...:18-20 ← 已接收 :19, :20
Executed_Gtid_Set: da2593b2-...:1-18 ← 未回放
查看 relay log 文件:
| |
-rw-r----- 1 mysql mysql 204 relay-bin.000001
-rw-r----- 1 mysql mysql 1263 relay-bin.000002 ← :19, :20 的事务在这里
-rw-r----- 1 mysql mysql 64 relay-bin.index
关键状态:relay-bin.000002 里存着 GTID
:19和:20的事务,等着 SQL 线程来回放。
步骤 5:模拟故障 — 删除 relay log 文件
| |
验证:
| |
-rw-r----- 1 mysql mysql 64 relay-bin.index ← 只剩 index 文件
灾难发生:relay log 数据文件被删了,但 index 文件还在。GTID
:19和:20的事务随着 relay log 文件一起消失了。
步骤 6:第一次重启从库 — relay_log_recovery 触发但失败
| |
查看 error log:
| |
[Warning] Recovery from source pos 3367 and file mysql-bin.000017
for channel ''. Previous relay log pos and relay log file
had been set to 705, /data/3309/log/relay-bin.000002
[ERROR] log relay-bin.000002 listed in the index, but failed to stat.
[ERROR] Error counting relay log space.
[ERROR] Failed to initialize the connection metadata structure
[ERROR] Failed to create or recover replication info repositories.
relay_log_recovery 失败了! index 文件里写着 relay-bin.000002,但文件已经不在了。
尝试 START SLAVE:
| |
ERROR 1872 (HY000): Replica failed to initialize applier metadata
structure from the repository
复制元数据已损坏,START SLAVE 也失败了。
步骤 7:第二次重启 — relay_log_recovery “成功”
| |
查看 error log:
| |
[Warning] Recovery from source pos 3367 and file mysql-bin.000017
for channel ''. Previous relay log pos and relay log file
had been set to 4, /data/3309/log/relay-bin.000001
Replica receiver thread for channel '': connected to source
'repl@10.100.0.1:3308'. Starting GTID-based replication.
第二次重启 relay_log_recovery “成功"了。 关键:
Recovery from source pos 3367— IO 线程被重置到 master pos 3367(GTID :18 之后的位置)。
步骤 8:检查结果 — 数据丢了,复制卡死
| |
id name
1 a
2 b
3 c ← 没有 4 和 5!数据丢了!
Slave_IO_Running: Yes
Slave_SQL_Running: Yes ← 显示正常!
Seconds_Behind_Master: 67 ← 持续增长
Slave_SQL_Running_State: waiting for handler commit ← 卡死
Retrieved_Gtid_Set: da2593b2-...:19-20
Executed_Gtid_Set: da2593b2-...:1-18 ← 一动不动
等待 30 秒后再查:
Seconds_Behind_Master: 99 ← 还在增长
数据:还是只有 id=1,2,3 ← 永远追不上
这就是最危险的地方:
Slave_IO_Running: Yes✓Slave_SQL_Running: Yes✓- 没有任何报错
- 但数据丢了,SQL 线程卡死,永远追不上
- 如果不看
Seconds_Behind_Master的变化趋势,完全发现不了问题
三、根因分析
relay_log_recovery 到底做了什么?
从库重启
│
▼
读取 mysql.slave_relay_log_info 表
→ 拿到 SQL 线程最后执行的主库位置:mysql-bin.000017, pos 3367
│
▼
删除所有 relay log,创建新的 relay log
│
▼
把 IO 线程的起始位置重置为 pos 3367 ← ★ 问题在这里!
│ 这一步用的是 file+pos,
│ 不是 gtid_executed!
│ 虽然配了 AUTO_POSITION=1,
│ 但 recovery 阶段还没走到
│ AUTO_POSITION 的逻辑
▼
IO 线程从 pos 3367 开始拉取
→ 只拿到 GTID :19 和 :20
→ 而从库 gtid_executed 只到 :18
→ :19 和 :20 需要等 SQL 线程回放
│
▼
SQL 线程尝试回放 :19
→ MTS coordinator 发现 worker 状态不连续(gap)
→ 卡在 "waiting for handler commit"
→ 永久卡死
三个条件缺一不可
| 条件 | 作用 |
|---|---|
slave_parallel_workers > 0 | MTS 模式下 coordinator 和 worker 之间有状态差异,重启后 gap 信息丢失 |
| relay log 被物理删除 | 包含未回放事务的 relay log 没了,无法重放来填补 gap |
relay_log_recovery = ON | 用 slave_relay_log_info 中的 file+pos 重置 IO 线程,而不是用 gtid_executed |
为什么 AUTO_POSITION=1 没有救场?
很多人以为 GTID + AUTO_POSITION 是万能的。确实,正常启动复制时 IO 线程会用 gtid_executed 告诉主库"我有哪些事务”。
但 relay_log_recovery 的恢复流程发生在 IO 线程启动 之前,它直接用 slave_relay_log_info 表里的 master binlog position 来重置 IO 线程的起点。在这个阶段,AUTO_POSITION 还没介入。
四、正确的修复方法
遇到这种情况怎么修?
| |
验证修复结果:
| |
修复原理
RESET SLAVE 清除了损坏的 relay log 和复制位置元数据(slave_relay_log_info 和 slave_master_info),但 不清除 gtid_executed。
重新 CHANGE MASTER TO ... MASTER_AUTO_POSITION=1 后,IO 线程启动时会把从库真实的 gtid_executed(:1-18)发送给主库。主库发现从库缺少 :19 和 :20,就把这两个事务发过来。SQL 线程正常回放,数据恢复。
五、生产环境防范
方案 1:MTS 场景下加上 commit order 保护
| |
这个参数保证 worker 按照 relay log 中的顺序提交事务,避免 gap 问题。
方案 2:永远不要手动删除 relay log
| |
如果非要清理 relay log,使用 SQL 命令而不是 rm:
| |
方案 3:relay log 丢失后的正确操作流程
错误做法:直接重启 mysqld,期待 relay_log_recovery 自动恢复
正确做法:
1. STOP SLAVE;
2. RESET SLAVE;
3. CHANGE MASTER TO ... MASTER_AUTO_POSITION=1;
4. START SLAVE;
5. 验证数据一致性
方案 4:监控告警
监控 Seconds_Behind_Master 的变化趋势。如果值 持续增长且不回落,同时 SQL 线程状态是 waiting for handler commit,大概率就是 MTS gap 问题。
| |
六、总结
| 项目 | 说明 |
|---|---|
| 现象 | 主从显示正常(IO/SQL 都 Yes),但数据缺失,Seconds_Behind 持续增长 |
| 触发条件 | MTS + relay log 被删 + relay_log_recovery + 重启 |
| 根因 | relay_log_recovery 用 file+pos 重置 IO 线程,跳过了未回放的事务;MTS worker gap 导致 SQL 线程卡死 |
| 修复 | STOP SLAVE - RESET SLAVE - CHANGE MASTER(AUTO_POSITION=1)- START SLAVE |
| 防范 | slave_preserve_commit_order=ON、不手动删 relay log、监控 Seconds_Behind 趋势 |
一句话记住:
relay_log_recovery能解决 relay log 损坏 的问题,但解决不了 relay log 丢失 + MTS gap 的组合问题。遇到 relay log 丢失,不要重启碰运气,直接 RESET SLAVE 重建复制。