先说个场景。很多人搜“MySQL 一主两从搭建”能找到一堆教程,但拓扑搭好之后,遇到“想把这个架构改成级联”就有点摸不着头脑了。尤其是基于位点(binlog position)而不是 GTID 的复制,切换时只要位点差 1 个事务,轻则主键冲突复制中断,重则静默丢数据。我这次就把一主两从到级联(Master → S1 → S2)的在线切换实战完整拆开,讲清楚每一步为什么要这么做、位点到底怎么对齐、踩过的坑怎么避。这篇内容适合手里正管着 MySQL 生产实例的 DBA、运维同学,也适合想在测试环境把复制拓扑玩明白的人。
1. 架构切换到底在解决什么问题
1.1 一主两从和级联的工作方式差异
一主两从未必是字面意义上只有两台从库,它强调的是“两个从库都直接指向同一个主库”。在经典复制拓扑里,主库 Master 同时给 S1、S2 各推一份 binlog,S1 和 S2 各自拉取、各自应用,互不依赖。这种架构简单直观,从库之间没有任何关联,任何一个从库出问题都不会影响另一个,排查故障时也容易定位。
但也正因为“各自都直接连主库”,当从库数量变多、或者从库分布在跨机房跨地域的网络环境里时,主库的网络连接数、binlog 推送线程数都会线性增加。更关键的是,如果机房 A 里有一台主库,机房 B 里放着两台从库,主库到机房 B 的网络链路完全绕不开,每次 binlog 传输都要跨机房,主库对外提供的连接质量就会受影响。
级联架构则把“分发压力”从主库上卸掉一部分:主库只推给下端第一个从库 S1,S2 不再直连主库,而是从 S1 拉日志。前提是 S1 必须开启 log_slave_updates,把应用过的日志继续写进自己的 binlog,这样 S2 才能把 S1 当“伪主库”来复制。数据流向变成 Master → S1 → S2,组成一条链。
这两种架构没有绝对的好坏。一主两从适合从库直连主库、网络拓扑简单、主库压力可控的场景;级联适合跨机房、从库数量多、想减少主库 binlog 分发压力的场景。但级联也带来新的问题:S1 这台中间节点变成了半主库角色,故障影响面变大;链路变长,延迟可能比直连更高。所以切换前要想清楚,你到底是图什么,别为了炫技把简单架构搞复杂。
1.2 为什么不用重建从库的方式硬切
有人可能会说,既然 S2 要从直连主库改成连 S1,那干脆把 S2 重新做一遍全量备份恢复,新拉一个指向 S1 的复制通道不就完事了吗?确实,如果 S2 是台新机器、数据量很小、业务允许停机,重建从库是最省脑子的方案。
但生产环境里很多从库是上百 GB 甚至上 TB 的量级,全量备份加恢复的时间可能是几十分钟到几小时,这期间 S2 上如果还挂着只读查询或报表任务,丢失的数据追平又要花时间。更重要的是,S2 上往往已经有接近实时的数据,重新备份恢复相当于把几百 GB 数据重新传一遍,网络带宽和磁盘空间都扛不住。这种场景下,在线切换、保留 S2 原有数据、只改复制链路,才是性价比最高的方式。
而在线切换最核心的动作,就是找出一个“位点”:在这个位点上,S2 已经应用完主库的所有历史事务,同时 S1 的 binlog 也从这里开始包含了后续所有新事务。只要位点对齐,S2 从 S1 继续拉日志就能无缝衔接,不需要回放旧数据。这就是基于位点切换的精髓。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 切换前必须确认的前置条件
2.1 log_slave_updates 是级联的地基
从库默认把主库推送过来的 binlog 事件应用到本地数据文件后,并不会把这些事件写进自己的 binlog,除非开启 log_slave_updates=ON。这个参数默认是 OFF,很多人在初始化实例时压根没注意过。
如果你在 S1 上没开这个参数,即使把 S2 的复制源切到 S1,S2 的 IO 线程也能连上 S1,但 S1 根本拿不出 binlog 给 S2 拉。表现就是 S2 的 Slave_IO_Running 一直是 Yes,但 Seconds_Behind_Master 永远跟不上,S1 上也没新增 binlog,整个链路是断的。
需要注意,log_slave_updates 不是动态参数,修改它必须重启 MySQL。所以切换前一定先确认目标中间节点(这里的 S1)上的配置:
sql复制SHOW VARIABLES LIKE 'log_slave_updates';
如果结果是 OFF,需要在 my.cnf 的 [mysqld] 段加上:
ini复制log_slave_updates=ON
然后重启 S1。这里有个经验:S1 作为中间节点,不仅要开 log_slave_updates,最好把 binlog 格式也统一成 ROW 格式,否则下游 S2 可能因为事件格式不兼容导致 SQL 线程报错。另外 S1 的 server_id 绝对不能和 S2 重复,也不能和主库重复,否则从库连接时会被直接拒绝。
2.2 环境信息核对与复制账号准备
切换前把三台实例的信息列成一张表,避免操作到一半发现账号密码不对、端口不对。我习惯先记录这些:
| 实例角色 | 主机 | 端口 | server_id | 是否开启 binlog | log_slave_updates |
|---|---|---|---|---|---|
| Master | 192.168.1.10 | 3306 | 1 | ON | - |
| S1 | 192.168.1.20 | 3306 | 2 | ON | ON |
| S2 | 192.168.1.30 | 3306 | 3 | ON | - |
这里 S2 作为级联的末端,虽然不一定要往外分发日志,但为了将来可能的拓扑调整,建议也把 binlog 开着。复制账号方面,S2 原来用的是主库上的复制账号,现在要改成能访问 S1 的账号。你需要在 S1 上创建一个专用的复制用户,或者复用 S1 上已有的复制账号。
创建复制账号时最少要给 REPLICATION SLAVE 权限:
sql复制CREATE USER 'repl'@'192.168.1.%' IDENTIFIED BY 'strong-password';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'192.168.1.%';
FLUSH PRIVILEGES;
权限不能只给 SELECT,复制进程在拉取 binlog 时依赖 REPLICATION SLAVE 权限。REPLICATION CLIENT 权限用于 show master status、show slave status 的监控,虽然不是必需的,但我建议一起给上,后面排查问题会方便很多。
2.3 位点切换的主要风险点
基于位点切换最怕三件事:
第一,位点算早。S2 从 S1 的 binlog 里继续读到的日志里,包含了 S2 在断开前已经执行过的事务,SQL 线程应用时会报主键重复。比如原本 S2 已经执行到主库 binlog 的 position 100,但你给它指到 S1 的 position 80,等于把 80 到 100 之间的事务又跑了一遍。
第二,位点算晚。S2 会漏掉中间一段事务,而且 MySQL 复制正常情况下不会主动报错,只有当后续数据产生关联影响时才会暴露,那时候很可能已经过了很久,排查起来非常痛苦。
第三,目标位点所在的 binlog 文件已经被清理。binlog 有 expire_logs_days 或 binlog_expire_logs_seconds 控制保留时间,如果 S1 的 binlog 刚好轮转并删掉了你需要的那个文件,S2 连接时会直接报错找不到日志文件。所以切换前要确认 S1 上需要的 binlog 文件还在,必要时先临时调大 binlog 保留时间,切换完成后再改回来。
3. 低峰期短时停写方案:最稳妥的在线切换
3.1 给主库加只读锁,让所有节点追平到同一位置
这是我认为生产环境中最值得采用的方式,核心思路是:让主库短暂只读,等待 S1 和 S2 都追平到主库的当前位点,此时三台机器数据完全一致。然后记录 S1 的 binlog 位点作为 S2 的新起点,S2 从该位点开始从 S1 拉日志,就不会多读也不会有遗漏。
执行只读前,选一个业务低峰期。如果是有赞、有订单的实时交易系统,凌晨三四点通常是比较安全的时间窗口。操作前先在业务侧确认可以接受这几十秒到几分钟的只读窗口,避免误伤在线请求。
在主库上执行:
sql复制SET GLOBAL read_only=ON;
建议用 read_only 而不是 FLUSH TABLES WITH READ LOCK。FTWRL 是会话级的锁,一旦执行命令的会话断开,锁会自动释放;read_only 是全局级的,更可控。如果主库配置了超级权限账号,记得业务连接用的账号不要带 SUPER 权限,否则 read_only 拦不住它们的写操作。
执行完 read_only 需要确认已经生效:
sql复制SHOW GLOBAL VARIABLES LIKE 'read_only';
看到 Value 为 ON 之后,再往下继续。这个确认步骤不能省,我见过有人以为设了 read_only 结果权限太高没拦住写请求,数据一直在变,后续位点全乱套。
3.2 在 S1 上等待追平并记录目标位点
主库只读后,S1 和 S2 的 SQL 线程还在继续应用 relay log,等它们追上主库之后就会停下来等待新的 binlog。在 S1 上执行:
sql复制SHOW SLAVE STATUS\G
重点看这两个字段:
- Slave_IO_Running: Yes
- Slave_SQL_Running: Yes
- Seconds_Behind_Master: 0
Seconds_Behind_Master 为 0,说明 S1 已经把主库发过来的 relay log 全部应用完。但要注意,这个值在某些情况下可能不可靠,比如 relay log 还没拉完时它也可能是 0,更稳妥的方式是再确认主库的位点已经和 S1 一致。不过低峰期只读场景下,我们稍后还会用 S2 做交叉校验,问题不大。
确认 S1 追平后,在 S1 上记录:
sql复制SHOW MASTER STATUS;
记下 File 和 Position 字段,比如:
text复制File: mysql-bin.000045
Position: 98765432
这个位点就是 S2 接下来要指向的目标。先把它写到便签里,或者直接记在操作文档中,后面要用。
3.3 在 S2 上追平并确认主库位点
接下来在 S2 上执行同样的操作,先看:
sql复制SHOW SLAVE STATUS\G
确认 Seconds_Behind_Master=0,Slave_IO_Running 和 Slave_SQL_Running 都为 Yes。由于主库已经只读,S2 不可能再追出新的差异,只要它追平,就在一个稳定状态。
此时再执行一次:
sql复制SHOW SLAVE STATUS\G
注意查看 Exec_Master_Log_Pos,也就是 S2 当前已经应用到的日志在主库 binlog 中的位置。理论上这个位置应该和 S1 记录的位置对应。这里不需要费劲把 S2 的 Exec_Master_Log_Pos 去和 S1 的 binlog Position 换算,因为我们已经通过主库只读这个操作把全局状态固定住了。
S2 停掉复制:
sql复制STOP SLAVE;
如果 S2 上还挂着业务查询或报表,STOP SLAVE 只影响复制线程,不影响本地查询连接。但要注意,如果 S2 上有写入操作(不该有,从库一般 read_only 也是 ON),会破坏一致性。确认 S2 的 read_only 也是 ON。
3.4 在 S2 上重新指定复制源为 S1
关键一步来了。把 S2 的复制源从主库改到 S1,执行:
sql复制CHANGE MASTER TO
MASTER_HOST='192.168.1.20',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='strong-password',
MASTER_LOG_FILE='mysql-bin.000045',
MASTER_LOG_POS=98765432;
这里的 MASTER_LOG_FILE 和 MASTER_LOG_POS 必须是 S1 上 show master status 记录的结果,千万不能写成 S2 原来从主库同步时看到的主库 binlog 文件名和位置。S1 的 binlog 文件名可能和主库的文件名不一样,加载的位点也不是同一个体系。
如果有 GTID 相关的历史配置,CHANGE MASTER TO 之前最好确认当前实例的 gtid_mode 状态。纯位点切换需要在执行 CHANGE MASTER 时明确设置 MASTER_AUTO_POSITION=0,避免 MySQL 优先用 GTID 自动定位导致位点被忽略。MySQL 5.7 及以上默认 MASTER_AUTO_POSITION 是 0,但有些新版本或自动化运维平台可能改过默认值,还是显式写一下更保险。
然后启动复制:
sql复制START SLAVE;
执行后立刻看状态:
sql复制SHOW SLAVE STATUS\G
正常情况下,Master_Host 应该显示 192.168.1.20,Slave_IO_Running: Yes,Slave_SQL_Running: Yes。如果 IO 线程是 Connecting,检查网络和账号权限;如果 SQL 线程报错,看 Last_SQL_Error 具体什么内容。
3.5 解除主库只读并做业务验证
确认 S2 复制正常后,回主库解除只读:
sql复制SET GLOBAL read_only=OFF;
解锁后先在测试环境里往一张业务表写一条数据,然后在 S1、S2 上分别查这条数据,确认链路 Master → S1 → S2 全部通。如果 S1 能看到新数据而 S2 看不到,重点检查 S1 的 log_slave_updates 是否真的开起来了,以及 S2 的 SQL 线程是否在正常跑。
如果是生产环境,建议在低峰期先做一轮数据一致性校验,用 pt-table-checksum 对关键表做比对,确认切换前后数据没有因为位点错位而丢失或重复。有人可能觉得切换过程很快,校验太费时间,但我见过太多“切换完看着没问题,第二天对账发现少了几条数据”的教训,一致性校验这一步别省。
4. 不停写的位点切换难点与替代思路
4.1 不停写时位点为什么这么难对齐
有些场景没法接受主库只读,比如 7×24 小时在线交易系统,几十秒的只读窗口业务侧也不批。这种情况下理论上也可以做基于位点的在线切换,但位点对齐的难度会明显上升。
问题在于,不停写时主库的 binlog 一直在增长,S1 和 S2 各自追平的时刻不同,它们看到的主库位点也不同。比如 S2 在 10:00:00 追平到主库 position 1000,S1 可能在 10:00:00.5 才追平到 position 1050,中间的 50 个 position 对应的事务,S2 如果从 S1 的当前位点开始拉就会漏掉。
要做不停写的位点对齐,核心难点在于“把 S2 的停止位点转换成一个 S1 binlog 中的精确位点”。具体方法是先停掉 S2 的复制,等到 S2 应用完所有 relay log,记录 S2 从主库已经追到的 binlog 文件号和位点,然后在 S1 的 binlog 里找对应位置。
理论上可以这样操作:S2 停止后,在 S1 上执行 SHOW BINLOG EVENTS IN 'S1 当前 binlog 文件',找到 S1 的 binlog 中对应主库 position 的那个事务,然后用 mysqlbinlog 工具解析 S1 的 binlog,精确定位到 S1 的 binlog 文件里该事务的起始位点。实际操作中这个“对应”的转换非常容易出错,因为主库的 binlog 文件名和位置与 S1 的 binlog 文件名和位置是两套体系,没有自动映射工具,纯靠手工推算很容易算错一个 event size 导致位点偏差。
4.2 实用建议:优先用 GTID 或临时灾备节点
既然不停写这么难,我给的建议是:如果生产环境支持,优先把复制模式切到 GTID,用 GTID 自动定位来规避位点问题。在 GTID 模式下,S2 从 S1 拉日志时,MySQL 会根据 GTID 集合自动跳过已经执行过的事务,不需要手工计算位点。只要保证 S1 的 binlog 里包含了 S2 需要的所有 GTID,S2 启动复制就能自动对齐。
如果历史原因必须保留位点复制,另一个折中方案是先在测试环境模拟一遍拓扑变更,算出对应的位点映射关系,再用低峰期快速操作窗口来执行。同样需要先追平 S2 的 relay log,然后在 S1 上根据时间戳或事务内容手工定位,过程繁琐且对操作者要求很高。我不建议在生产环境直接上手做不停写的纯位点切换,除非你已经有多次演练经验。
还有一个更稳的变通思路:在切换前临时把 S2 的数据追平到接近实时的状态,然后通过新拉一台备机,从 S1 全量备份恢复后作为新的 S2,再把老的 S2 下线。这样看起来是“重建从库”,但用在网络带宽允许的场景下反而比手工算位点更可控。
4.3 切换窗口的取舍经验
我在实际项目里遇到过很多次“不能停写”的需求,最后大多数都能通过沟通拿到一个 30 秒到 1 分钟的只读窗口。关键是前期要和业务对好“从库切换期间允许只读的时长”,而不是等到切换当天才问。数据库架构变更这种操作,业务方通常更担心的是数据丢,而不是几秒钟的写阻塞。
如果实在连 30 秒都拿不到,我会在切换前做一轮全量数据校验,同时启用慢查询审计追踪异常。但说实话,不停写切换的复杂度和风险远大于短时只读,除非有极强的理由,否则不建议硬上。
5. 切换后常见问题与排查手册
5.1 复制状态速查表
切换完成后,第一时间看 SHOW SLAVE STATUS\G 并记录关键字段。下面是我整理的一张速查表,覆盖最常见的异常场景:
| 现象 | 常见原因 | 排查思路 |
|---|---|---|
| Slave_IO_Running: Connecting | S1 网络不通、复制账号错误、防火墙拦截 | 在 S2 上 mysql -hS1 -urepl -p 测试连通性,查 S1 的 binlog 文件和 position 是否存在 |
| Slave_IO_Running: No,Last_IO_Error: Could not find first log file name | CHANGE MASTER 里写了主库的 binlog 文件名,而非 S1 的文件名 | 重新执行 CHANGE MASTER,使用 S1 上 SHOW MASTER STATUS 得到的 File |
| Slave_SQL_Running: No,主键冲突 | 位点设早,重复应用了 S2 已执行的事务 | 确认位点,必要时从 S1 重新定位后重做 CHANGE MASTER |
| Slave_SQL_Running: No,找不到表或列 | 表结构不一致,S1 的 binlog 事件无法应用到 S2 | 先检查主库和 S1、S2 的表结构是否一致,再处理复制积压 |
| Seconds_Behind_Master 持续增加 | S1 自身延迟大,或者 S1 的磁盘/网络有问题 | 看 S1 上 SHOW SLAVE STATUS,确认 S1 是否也落后于主库 |
排查时不要只看第一眼现象,要把 Last_IO_Error 和 Last_SQL_Error 两条都看全。IO 线程负责拉日志,SQL 线程负责应用日志,两个线程互相独立,一个报错另一个可能还在跑,很容易造成误判。
5.2 切换后的数据一致性校验
复制链路是否真正没问题,最终要看数据是不是一致的。校验工具我首选 pt-table-checksum,它可以对主从节点进行在线数据比对,通过把表按主键拆分 chunk,逐块计算 checksum,对比 Master 和 S1、S2 是否一致。
命令大致是:
bash复制pt-table-checksum \
--host=192.168.1.10 \
--user=root \
--password=xxx \
--databases=test_db \
--tables=orders \
--replicate=test_db.checksums
这里 --host 填主库地址,它会自动发现从库拓扑。切换后跑校验时,注意要把 --replicate 指定到一张专门存放校验结果的表,否则每次校验都会覆盖历史记录。如果校验发现差异,先用 pt-table-sync 把差异数据修正,再回头检查复制链路是否还有问题。
校验不是一次性的,建议切换后头一天做一次全量校验,之后一周内每天抽查几个核心表。级联链路的中间节点 S1 如果出问题,S2 的数据会逐步落后但不会报错,只有校验能发现这种静默延迟。
5.3 回退方案:切回一主两从
切换完成后如果发现级联链路问题很大,需要回退到一主两从,操作也不复杂,但要保证主库的 binlog 位点信息还在。
回退前先在主库上执行:
sql复制SHOW MASTER STATUS;
记录主库当前的 File 和 Position。然后在 S2 上执行:
sql复制CHANGE MASTER TO
MASTER_HOST='192.168.1.10',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='strong-password',
MASTER_LOG_FILE='主库的 File',
MASTER_LOG_POS=主库的 Position;
START SLAVE;
这里有一个前提:S2 从 S1 断开之前,必须先确认 S2 已经追平 S1。否则 S2 如果直接切回主库并从上一次主库位点继续,中间可能漏掉 S1 尚未转发给它的部分,或者重复应用已经通过级联拿到的事务。稳妥起见,回退时同样需要主库短时只读,把这个回退链路也当成一次独立的切换来对待。
我在项目里有一条铁律:任何拓扑变更,切换前记录主库位点,切换后记录各节点位点,回退时严格按照记录操作。不要靠记忆,把记录写进变更单里。
5.4 一些容易忽略的细节
切换中最容易被忽略的是 S1 的 binlog 保留时间。S1 作为中间节点后,S2 的追赶依赖 S1 上保留的 binlog。如果 S1 的 binlog 很快被清理,而 S2 因为网络或负载问题落后较多,S2 就会报错说找不到日志,被迫重新搭建。建议把 S1 的 binlog 保留时间调整为比主库更长,比如主库保留 2 天,S1 保留 3 到 4 天,给 S2 留出足够的缓冲。
另外,S1 的磁盘空间也要重点盯。开启 log_slave_updates 后,S1 每应用一个事务都要多写一份 binlog,等于写入放大了一倍。如果 S1 磁盘本来就紧张,很容易把磁盘写满导致实例 hang 住。切换前先看 S1 的磁盘剩余空间和 binlog 平均增长速度,估算保留周期内是否够用。
还有一个小点:S2 切换后,原本在主库上针对 S2 的监控项要同步调整。比如监控系统如果还在检查 S2 的 Master_Host 是否等于主库 IP,切换后就会误报复制中断。变更管理规范里应该同步更新监控规则,别改完架构就以为万事大吉。
6. 多次切换之后我的一些体会
MySQL 的位点复制像一把刻刀,用得熟练可以精准雕出各种拓扑,但稍微手抖就会把数据割坏。我经历过几次切换后才发现数据对不上,最后都是靠备份和 binlog 一点点回溯才复原的,过程非常难熬。所以我对这类变更越来越保守,越来越倾向于“先把环境控制住,再谈效率”。
以我个人的实际经验,无论计划多周密,都要在切换前做一次完整演练,哪怕只是两台虚机模拟同样拓扑。演练不只是验证命令,更是验证操作顺序和位点记录方式。真正上生产时,熟练度和肌肉记忆比临场推理重要得多。
最后再分享一个技巧:把整个切换过程中的关键 SQL、位点、时间点都写进操作记录模板,每次执行前逐条打钩。不要相信任何“应该没问题”的直觉,所有日志源、位点、账号权限都现场确认一遍。这套流程我用了很多年,不能说完全杜绝故障,但至少没有因为位点错误发生过数据丢失。希望这篇内容能帮你在做一主两从到级联的切换时少走几个弯路。
