前言

relay_log_recovery=ON 是 MySQL 主从复制中常见的安全配置,官方文档描述它的作用是:从库重启时,丢弃未执行的 relay log,从主库重新拉取。听起来很安全,但在特定条件下,它不仅救不了你,反而会导致 数据静默丢失 — 复制显示正常,实际上数据已经少了。

本文通过一个完整的实验,从环境确认、故障触发、原因分析到最终修复,逐步展示这个陷阱的全貌。


一、实验环境

角色实例服务器
主库3308120.48.119.118
从库3309101.34.248.57

MySQL 版本:8.0.35,GTID 模式,半同步复制。

从库关键参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
mysql> SHOW VARIABLES LIKE 'relay_log_recovery';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| relay_log_recovery | ON    |
+--------------------+-------+

mysql> SHOW VARIABLES LIKE 'relay_log_info_repository';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| relay_log_info_repository| TABLE |
+--------------------------+-------+

mysql> SHOW VARIABLES LIKE 'slave_parallel_workers';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| slave_parallel_workers| 4     |
+-----------------------+-------+
-- ↑ 多线程复制,这是出问题的关键!

mysql> SHOW VARIABLES LIKE 'slave_parallel_type';
+--------------------+---------------+
| Variable_name      | Value         |
+--------------------+---------------+
| slave_parallel_type| LOGICAL_CLOCK |
+--------------------+---------------+

mysql> SHOW VARIABLES LIKE 'gtid_mode';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_mode     | ON    |
+---------------+-------+

配置总结:GTID + AUTO_POSITION + relay_log_recovery + MTS(4 worker)


二、完整复现流程

步骤 1:确认主从完全同步

主库 3308:

1
2
[root@master ~]# mysql -S /tmp/mysql3308.sock -uroot -p****** \
  -e "SELECT * FROM base01.t1; SELECT @@global.gtid_executed\G"
+----+------+
| 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:

1
2
[root@slave ~]# mysql -S /tmp/mysql3309.sock -uroot -p****** \
  -e "SELECT * FROM base01.t1; SELECT @@global.gtid_executed\G"
+----+------+
| 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
1
2
[root@slave ~]# mysql -S /tmp/mysql3309.sock -uroot -p****** \
  -e "SHOW SLAVE STATUS\G" | grep -E "Slave_IO|Slave_SQL_Run|Seconds|Auto_Pos"
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
        Seconds_Behind_Master: 0
                Auto_Position: 1

确认:主从数据一致,GTID 一致,延迟为 0。


步骤 2:停止从库 SQL 线程

1
2
[root@slave ~]# mysql -S /tmp/mysql3309.sock -uroot -p****** \
  -e "STOP SLAVE SQL_THREAD"

验证:

1
2
[root@slave ~]# mysql -S /tmp/mysql3309.sock -uroot -p****** \
  -e "SHOW SLAVE STATUS\G" | grep -E "Slave_IO|Slave_SQL_Run|Exec_Master|Executed_Gtid"
             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:在主库写入新数据

1
2
3
4
5
[root@master ~]# mysql -S /tmp/mysql3308.sock -uroot -p****** -e "
  INSERT INTO base01.t1 VALUES(4, 'd');
  INSERT INTO base01.t1 VALUES(5, 'e');
  SELECT * FROM base01.t1;
  SELECT @@global.gtid_executed\G"
+----+------+
| 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 线程未回放

1
2
3
[root@slave ~]# mysql -S /tmp/mysql3309.sock -uroot -p****** \
  -e "SELECT * FROM base01.t1; SHOW SLAVE STATUS\G" \
  | grep -E "^id|^[0-9]|Read_Master|Exec_Master|Retrieved|Executed_Gtid|Slave_IO|Slave_SQL_Run"
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 文件:

1
[root@slave ~]# ls -la /data/3309/log/relay-bin*
-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 文件

1
[root@slave ~]# rm -f /data/3309/log/relay-bin.000001 /data/3309/log/relay-bin.000002

验证:

1
[root@slave ~]# ls -la /data/3309/log/relay-bin*
-rw-r----- 1 mysql mysql 64 relay-bin.index     ← 只剩 index 文件

灾难发生:relay log 数据文件被删了,但 index 文件还在。GTID :19:20 的事务随着 relay log 文件一起消失了。


步骤 6:第一次重启从库 — relay_log_recovery 触发但失败

1
[root@slave ~]# systemctl restart mysqld3309

查看 error log:

1
[root@slave ~]# tail -20 /data/3309/log/error.log | grep -E "Recovery|relay|Failed" -i
[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:

1
[root@slave ~]# mysql -S /tmp/mysql3309.sock -uroot -p****** -e "START SLAVE"
ERROR 1872 (HY000): Replica failed to initialize applier metadata
structure from the repository

复制元数据已损坏,START SLAVE 也失败了。


步骤 7:第二次重启 — relay_log_recovery “成功”

1
[root@slave ~]# systemctl restart mysqld3309

查看 error log:

1
[root@slave ~]# tail -10 /data/3309/log/error.log | grep -E "Recovery|Starting GTID"
[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:检查结果 — 数据丢了,复制卡死

1
2
3
[root@slave ~]# mysql -S /tmp/mysql3309.sock -uroot -p****** \
  -e "SELECT * FROM base01.t1; SHOW SLAVE STATUS\G" \
  | grep -E "^id|^[0-9]|Slave_IO|Slave_SQL_Run|Seconds|Retrieved|Executed_Gtid|SQL_Running_State"
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 > 0MTS 模式下 coordinator 和 worker 之间有状态差异,重启后 gap 信息丢失
relay log 被物理删除包含未回放事务的 relay log 没了,无法重放来填补 gap
relay_log_recovery = ONslave_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 还没介入


四、正确的修复方法

遇到这种情况怎么修?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- 1. 停掉复制
STOP SLAVE;

-- 2. RESET SLAVE 清除损坏的复制元数据和 relay log
RESET SLAVE;

-- 3. 重新建立复制
--    AUTO_POSITION=1 让从库根据真实的 gtid_executed 重新对齐
CHANGE MASTER TO
  MASTER_HOST='10.100.0.1',
  MASTER_PORT=3308,
  MASTER_USER='repl',
  MASTER_PASSWORD='******',
  MASTER_AUTO_POSITION=1;

-- 4. 启动复制
START SLAVE;

验证修复结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
mysql> SELECT * FROM base01.t1;
+----+------+
| id | name |
+----+------+
|  1 | a    |
|  2 | b    |
|  3 | c    |
|  4 | d    |        回来了!
|  5 | e    |        回来了!
+----+------+

mysql> SHOW SLAVE STATUS\G
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
        Seconds_Behind_Master: 0               完全同步
      Slave_SQL_Running_State: Replica has read all relay log;
                                waiting for more updates

修复原理

RESET SLAVE 清除了损坏的 relay log 和复制位置元数据(slave_relay_log_infoslave_master_info),但 不清除 gtid_executed

重新 CHANGE MASTER TO ... MASTER_AUTO_POSITION=1 后,IO 线程启动时会把从库真实的 gtid_executed:1-18)发送给主库。主库发现从库缺少 :19:20,就把这两个事务发过来。SQL 线程正常回放,数据恢复。


五、生产环境防范

方案 1:MTS 场景下加上 commit order 保护

1
2
# my.cnf
slave_preserve_commit_order = ON    # MySQL 8.0.27+ 推荐

这个参数保证 worker 按照 relay log 中的顺序提交事务,避免 gap 问题。

方案 2:永远不要手动删除 relay log

1
relay_log_purge = ON    # 让 MySQL 自动管理 relay log 的清理

如果非要清理 relay log,使用 SQL 命令而不是 rm

1
2
-- 安全的方式
PURGE RELAY LOGS BEFORE '2026-03-25 00:00:00';

方案 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 问题。

1
2
3
4
5
6
-- 检查是否存在 MTS gap 问题
SELECT * FROM performance_schema.replication_applier_status_by_coordinator\G
SELECT * FROM performance_schema.replication_applier_status_by_worker\G

-- 如果所有 worker 的 LAST_APPLIED_TRANSACTION 都是空的,
-- 但 SQL 线程显示 Running: Yes,就是卡死了

六、总结

项目说明
现象主从显示正常(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 重建复制。