先说个真实场景。上个月我帮一个客户做架构改造,他们原本是标准的一主两从架构——主库承担写入,两个从库各自拉主库的 binlog 做复制,一个用于实时报表,一个用于备份。后来随着业务量上来,主库的 dump 线程压力明显增加,而且其中一个从库要单独拉出去给大数据团队做数据源,需要尽可能减少对主库的复制链路挤占。当时我们就决定把架构从“一主两从”改成“主库 → 从库A → 从库B”的级联复制。
改完不到两周,另一个同事又找我说,级联链路中继节点磁盘告警,想临时把从库B重新挂回主库,也就是从级联切回一主两从。这一来一回,正好把“基于位点的架构切换”这个操作完整踩了一遍。
这篇文章就把这两次切换的完整过程、背后的位点原理、以及我在操作中踩过的坑,一次性讲清楚。不管你是刚接触 MySQL 主从复制,还是已经维护过一段时间主从环境,这篇文章都值得收藏,尤其是那些准备在现网环境做架构调整的同学,能帮你少走不少弯路。
1. 为什么要在“一主两从”和“级联”之间来回切
1.1 两类架构的核心差异
先看两张拓扑图对应的复制关系。
一主两从,是最常见的复制架构:
code复制主库(Master) ----→ 从库A(Slave A)
----→ 从库B(Slave B)
两个从库各自独立连接主库,分别从主库拉取 binlog,主库需要为每个从库各开一个 dump 线程。从库之间彼此独立,互不影响。
级联架构则多了一个层级关系,我这次使用的是单级级联:
code复制主库(Master) ----→ 中继从库(Relay Slave)
----→ 二级从库(Secondary Slave)
主库只面对一个从库(中继从库),中继从库继续把从主库接收到的 binlog 转发给二级从库。这个过程中,中继从库既要执行 SQL 线程应用日志,又要作为二级从库的 master 继续传输日志,因此它会成为新的日志源,本身会开启 log_slave_updates 来确保中继日志继续向下游传递。
两者的核心差异,我用一张表来对比:
| 对比维度 | 一主两从 | 级联复制 |
|---|---|---|
| 主库 dump 线程数 | 2 个 | 1 个 |
| 主库 IO 压力 | 相对较高 | 相对较低 |
| 从库获取日志路径 | 直接连主库,路径短 | 经过中继节点,路径长 |
| 整体延迟 | 路径短,延迟通常更低 | 每多一级,延迟线性增加 |
| 对中继节点的依赖 | 不依赖其他从库 | 中继节点故障会影响下游 |
| 运维复杂度 | 低,各节点独立 | 中继节点需要额外关注 |
| 适用场景 | 从库数量少、主库压力可接受 | 从库数量多、跨机房、需要分担主库压力 |
1.2 正向切换的常见动机
我在帮客户做这次切换前,他们其实已经就这个问题讨论了很久。最终促使他们下决心的因素有三个:
第一个因素,主库 dump 线程压力。 他们原本只有两个从库,主库压力虽然存在但还能接受。但随着报表团队在从库A上跑越来越多的复杂查询,从库A经常出现短暂延迟,从库B(备份节点)反而没有影响。这说明从库A的查询负载已经影响了日志消费和应用速度。把从库B挪到从库A下面后,主库只需要给从库A一个人喂日志,dump 线程的压力直接减半。
第二个因素,跨机房规划。 客户的从库B其实在另一个机房,专线带宽并不稳定。之前从库B直接连主库,一旦专线抖动,复制就中断。改成级联后,从库B只需要和从库A所在的同一机房通信,减少了跨机房链路的脆弱性。
第三个因素,备份链隔离。 从库B是备份节点,每天凌晨要跑全量备份。备份期间如果直接从主库拉日志,主库的 dump 线程会受到备份 IO 的影响。挂在从库A下面后,备份的流量完全隔离在从库A的出口方向上,主库完全不受影响。
1.3 反向切换的常见动机
有人说,既然级联能减少主库压力,为什么不一直用级联?答案很简单:级联带来了新问题。
第一个问题,延迟放大。 从库B的复制延迟,是“主库 → 从库A”的延迟加上“从库A → 从库B”的延迟。在主库正常情况下这个延迟很小,但一旦从库A上有大查询或者备份任务,从库B的延迟就会成倍上涨。
第二个问题,中继节点成为单点。 有一次从库A的磁盘接近写满,导致中继日志无法及时清理,从库A的复制进度停滞。结果从库B紧跟着延迟告警。正常情况下,一主两从中一个从库故障不会影响另一个,但级联架构下,中继节点故障会直接拉胯所有下游从库。
第三个问题,应急恢复路径变长。 如果主库出现意外需要切换,级联架构下的故障转移流程比一主两从复杂不少。所以很多团队在平时使用级联分担压力,遇到大促或者变更窗口,反而会临时把级联解开,切回一主两从,让每个从库都保持最短复制路径。
明白了这个背景,下面进入真正的技术环节——基于位点的切换具体怎么做。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前必须搞懂的位点基础
2.1 什么是“位点”
MySQL 主从复制的核心是 binlog。主库的每一个写操作都会按顺序写入 binlog 文件,每个事务对应一个起始偏移量。这个“binlog 文件名 + 文件内偏移量”就构成一个位点(position),它是复制能够精确衔接的唯一标识。
举个例子,mysql-bin.000012 这个 binlog 文件,偏移量 123456 代表某个事务开始的位置。从库只要记录了自己消费到了哪个文件的哪个偏移量,下次就能从那个位置继续往下拉日志。这就像读一本没有页码标注的长篇小说,你记录了“第 12 章第 3 段”,下次就能从那里继续读。
在主库上查看当前位点:
bash复制mysql> SHOW MASTER STATUS;
+------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000012 | 123456 | | | |
+------------------+----------+--------------+------------------+-------------------+
这里的 File 和 Position 就是主库当前的写入位点。
2.2 从库视角的位点字段
在从库上执行 SHOW SLAVE STATUS\G,会看到一大堆字段,很多人一看就晕。做位点切换,重点关注以下字段:
Master_Log_File和Read_Master_Log_Pos:这两个字段表示从库的 IO 线程已经从主库拉到了哪个 binlog 位点。位置是从主库视角计算的。Relay_Master_Log_File和Exec_Master_Log_Pos:这两个字段表示从库的 SQL 线程已经应用到了哪个位点,同样是从主库视角计算的。Slave_IO_Running和Slave_SQL_Running:分别表示 IO 线程和 SQL 线程是否正常运行。Seconds_Behind_Master:从库落后主库的秒数,注意这个值在某些场景下会是 NULL,不一定是 0。
理解这些字段的关键是:从库上记录的不只是“自己执行到了哪里”,还包括“自己从主库拉到了哪里”。两者之间的差距,就是 relay log 中等待 SQL 线程消费的部分。
2.3 为什么 change master 必须指定位点
CHANGE MASTER TO 命令里的 MASTER_LOG_FILE 和 MASTER_LOG_POS 参数,就是告诉从库:你该从哪个 binlog 位点开始拉日志。
这两个参数写错,后果非常直接:
- 位点写旧了,从库会重新从旧位点拉取本来已经执行过的日志,导致重复执行。
- 位点写新了,从库会跳过一部分日志,导致数据缺失。
- 位点写到了一个事务的中间偏移量,从库会尝试从错误的位置解析事件,大概率直接报错停止。
这条命令,是本次架构切换中最需要谨慎对待的环节。
2.4 位点切换与 GTID 切换的选择
现在很多 MySQL 环境都开了 GTID(全局事务标识符)。用了 GTID 之后,从库不需要手动指定 binlog 文件名和偏移量,只需要通过 MASTER_AUTO_POSITION = 1 让双方自动协商位点。
那为什么我在这次切换中依然选择基于位点的方式?两个原因。
第一个原因,客户环境是 MySQL 5.7 且部分业务库已经开启了 GTID,但还有一个历史遗留的从库没有开启。GTID 模式下,如果主库也开了 GTID,切换时反倒会因为 GTID 集合差异导致复制无法启动,处理起来更麻烦。基于位点的方式在这类混合环境下兼容性最好。
第二个原因,位点切换更可控。 级联切换的本质是“让新主节点从自己当前的位点继续为下游提供日志”。如果使用 GTID 自动协商,有时候会忽略中继节点自身在执行 relay log 时的位点偏移,反而需要额外校验。基于位点,每一步都可以明确核对,出问题更容易定位。
提示:如果你的环境从库数量不多、且全部都是 8.0 并启用了 GTID,基于 GTID 会方便很多。但如果你操作的是存量老环境,位点方式依然是基本功,必须熟练掌握。
3. 一主两从切换为级联:完整操作流程
3.1 操作前必须采集的信息
开始操作前,先在每个节点上采集当前的复制状态。这一步非常关键,尤其是要确认从库是否已经完全追平了主库。否则带着延迟直接切换,后续会很被动。
在主库上执行:
sql复制SHOW MASTER STATUS;
在从库A和从库B上分别执行:
sql复制SHOW SLAVE STATUS\G
重点核对从库的 Exec_Master_Log_Pos 和主库的 Position 是否一致。如果存在延迟,优先等待延迟归零,或者至少把延迟压缩到几秒以内再继续。
我的习惯是,切换窗口落在业务低峰期,然后按以下顺序操作:
- 收集主库当前位点。
- 收集从库A、从库B的当前复制状态,确认无严重延迟。
- 记录每个节点的 server_id 和 server_uuid,避免后续出现主键冲突。
- 准备一个全量备份,确保无论切换是否成功,都有完整的回滚数据。
3.2 第一步:全量初始化新链路
级联切换,最关键的一步是把从库B的“上游”从主库换成从库A。
这里有一个容易出错的地方。从库B的数据如果和主库完全一致,理论上可以直接重新 CHANGE MASTER。但如果从库B本身就有复制延迟,直接切换位点很可能会造成数据空洞。稳妥的做法是:先用从库A做一次全量备份,然后恢复到从库B,再在从库B上建立新的复制关系。
从库A上全量备份命令示例:
bash复制mysqldump -uroot -p --single-transaction --master-data=2 --all-databases > slaveA_full.sql
注意这里使用了 --master-data=2,会在备份文件的头部自动记录当时的 binlog 位点。这条信息非常重要。
恢复备份到从库B:
bash复制mysql -uroot -p < slaveA_full.sql
恢复完成后,从库B的数据就和从库A在当前备份位点上完全一致了。
3.3 第二步:在从库B上执行 CHANGE MASTER
打开备份文件,找到类似于这样的注释内容:
code复制-- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000012', MASTER_LOG_POS=123456;
这个就是备份时刻从库A对应的 binlog 位点。因为在备份期间从库A一直在运行,这里记录的位置,就是备份文件能够确保一致性的起始点。
然后在从库B上执行:
sql复制STOP SLAVE;
CHANGE MASTER TO
MASTER_HOST='从库A的IP',
MASTER_PORT=3306,
MASTER_USER='repl_user',
MASTER_PASSWORD='复制的密码',
MASTER_LOG_FILE='mysql-bin.000012',
MASTER_LOG_POS=123456;
START SLAVE;
注意:这里的 MASTER_LOG_FILE 和 MASTER_LOG_POS 必须使用从库A上的 binlog 位点,而不是主库的。这可能是整个操作中最容易混淆的地方。
3.4 第三步:验证新链路
执行完上面的操作,立即在从库B上检查复制状态:
sql复制SHOW SLAVE STATUS\G
确认以下三个关键项:
Slave_IO_Running: Yes—— IO 线程正常连接从库A。Slave_SQL_Running: Yes—— SQL 线程正常执行。Seconds_Behind_Master在合理范围内,并逐步趋近于 0。
如果 IO 线程为 Connecting,先不要慌,常见原因包括账号权限不足、防火墙未放通、或者从库A没有开 log_slave_updates。下面会详细说。
等到从库B的复制状态稳定后,还需要在主库上验证一个新的特征:主库的 dump 线程数量下降到了 1 个。
sql复制SHOW PROCESSLIST;
如果看到主库上只有一个 Binlog Dump 线程,说明级联链路已经建立成功。
3.5 中继节点必须开启的关键参数
这里单独强调一个参数:log_slave_updates。
log_slave_updates 的值决定了从库在应用完 relay log 之后,是否把执行过的日志继续写入自己的 binlog。如果没有这个参数,从库A虽然从主库接收了日志并执行了写入,但它自己产生的 binlog 里并不包含这些数据变更。那么从库B连到从库A上,什么都拉不到。
检查方式:
sql复制SHOW VARIABLES LIKE 'log_slave_updates';
如果返回 OFF,必须在配置文件 my.cnf 的 [mysqld] 区段中加上:
code复制log_slave_updates = ON
然后重启 MySQL 实例生效。
这个过程我当时已经提前确认过,但说实话,很多人第一次搭级联链路,最容易漏掉的就是这一项。如果你在切换后从库B的 IO 线程卡在 Connecting,大概率不是权限问题,而是这个参数没开。
4. 级联切换回一主两从:反向操作的完整流程
4.1 反向切换为什么比正向更难
正向切换(一主两从 → 级联)通常发生在计划内,可以在低峰期从容操作。但反向切换(级联 → 一主两从)往往是被动的:中继节点磁盘满了、中继节点所在机器要下线、或者中继节点的复制卡死了。
这次客户触发反向切换的原因,是从库A的磁盘告警。从库A既承担了从主库拉取日志的任务,又要给从库B提供日志,binlog 文件增长速度比之前快了近一倍。磁盘剩余空间不足,直接威胁到整条链路的稳定性。
反向切换的操作流程,本质上就是把从库B的 master 从从库A重新指回主库,恢复最初的复制拓扑。
4.2 操作步骤:级联改一主两从
第 1 步,先在从库B上确认当前复制状态,同时确认从库B已经几乎追平了从库A:
sql复制SHOW SLAVE STATUS\G
关注 Relay_Master_Log_File 和 Exec_Master_Log_Pos,确认这个位点对应的数据,在主库上已经包含。
第 2 步,停掉从库B的复制线程,避免继续消费中继节点数据:
sql复制STOP SLAVE;
第 3 步,在主库上查看当前位点:
sql复制SHOW MASTER STATUS;
第 4 步,在从库B上重新执行 CHANGE MASTER,指向主库:
sql复制CHANGE MASTER TO
MASTER_HOST='主库的IP',
MASTER_PORT=3306,
MASTER_USER='repl_user',
MASTER_PASSWORD='复制的密码',
MASTER_LOG_FILE='mysql-bin.000012',
MASTER_LOG_POS=123456;
关键的逻辑在于,从库B之前从主库拉数据,经过从库A中转,最终执行的位置依然是以主库 binlog 位点来记录的。所以这里填的位点,就是第 4 步主库当前的位置吗?这里有个细节要小心——如果从库B已经完全追平了主库,那么在从库B上执行 STOP SLAVE 之前,它的 Exec_Master_Log_Pos 对应的就是主库某个时刻的位点。但在 STOP SLAVE 之后,主库可能还在继续写入新的数据,所以不能直接使用从库B的 Exec_Master_Log_Pos 作为新起点,否则从库B会漏掉它在停止复制到重新连接之间主库产生的新数据。
正确做法是:在从库B STOP SLAVE 的同时,去主库执行 SHOW MASTER STATUS,获取当前位点,然后把从库B的 CHANGE MASTER 指向这个新位点。前提是从库B已经追平了主库(或差距极小),否则从库B在新位点开始拉日志,中间仍会有一段的空洞。
更稳妥的做法,是先在低峰期做一次数据校验,比如通过 pt-table-checksum 确认从库B和主库数据一致,然后再进行切换。
第 5 步,启动复制:
sql复制START SLAVE;
第 6 步,验证复制状态,确认 Slave_IO_Running 和 Slave_SQL_Running 都是 Yes,然后观察 Seconds_Behind_Master 是否稳定。
4.3 反向切换中的“断点续传”问题
反向切换有一个比较容易忽略的点:如果从库B和主库之间本来就存在延迟,而且这个延迟在两三分钟以内,你可能觉得无所谓。但 STOP SLAVE 到 START SLAVE 之间,主库会产生新日志。如果直接使用主库当时的最新位点,从库B会跳过它原本还没有执行的那部分日志,造成永久性数据不一致。
我以前有过一次类似的教训。当时以为延迟只有几秒,就直接切了,结果从库B刚好有个大事务没执行完,切换后那一部分数据永远对不上了。后来花了三个多小时做数据比对和回补,非常痛苦。
所以,反向切换前务必确认:
- 从库B和主库之间没有延迟,或者延迟在几秒以内且切换窗口足够低峰。
- 如果延迟较大,先等待其追平再切换。
- 切换完成后,必须做数据校验。
5. 位点切换高频踩坑记录与排查手段
5.1 常见错误 1062 和 1032 的触发链路
位点切换后,最常碰到的两个 SQL 线程错误是:
- 错误 1062(Duplicate entry):主键重复。通常是从库重复执行了已经应用过的事务,或者位点填得偏旧了。
- 错误 1032(Can't find record in table):更新或删除的行不存在。通常是从库漏了某些前置事务,或者位点填得偏新了。
这两个错误本质上说明一件事:位点没有落在正确的事务边界上。
遇到这类错误,一般处理流程是这样的:
先查看具体是哪个表、哪一行出了问题:
sql复制SHOW SLAVE STATUS\G
把报错信息记录下来,确认出错位点。如果确定是位点填错,最简单的做法是从备份重新恢复,重新设置正确的位点。如果你只是想在短时间内恢复复制,也有临时跳过错误事务的 sql_slave_skip_counter 参数,但这必须非常谨慎,因为跳过的可能不止一个事务。我的态度是:除非你非常清楚这个错误事务只是某个无关紧要的临时表写入,否则别轻易跳过。
5.2 IO 线程一直 Connecting 的排查
从库执行 CHANGE MASTER 后,Slave_IO_Running 如果一直显示 Connecting,按照优先级从高到低排查:
- 网络连通性:
telnet 目标IP 3306是否通。 - 账号权限:
repl_user是否在目标节点上有REPLICATION SLAVE权限。 - 目标节点的
log_slave_updates是否开启。 - 目标节点是否有防火墙或安全组拦截。
- 目标节点是否已经执行了
RESET MASTER或清理了 binlog。
另外,级联切换场景下,还要注意从库A的 server_uuid 和从库B是否相同。如果之前做过克隆、镜像复制之类操作,两台机器的 server_uuid 完全一样,会导致从库B认为自己在连接自己,直接报错。检查方式:
sql复制SHOW VARIABLES LIKE 'server_uuid';
如果相同,修改从库B的 auto.cnf 文件,重启后即可。
5.3 切换后数据校验的必要性
很多文章讲完切换操作就结束了,但实际工作中最关键的后续动作是数据校验。
我常用的校验手段有两种:
第一种,抽样校验。 选择几张业务核心表,分别在主库和从库上执行 COUNT(*),对比数据量。但要注意,MySQL 的 COUNT(*) 在大表上执行很慢,不适合频繁做。
第二种,使用 pt-table-checksum。 这是 Percona Toolkit 里的标准工具,能对主从数据做一致性和完整性校验。它的原理是在主库上对每个表计算校验和,然后把同样的语句在从库上执行,对比结果。在校验过程中,它会自动控制负载,不会对线上业务造成显著影响。
我的建议是,无论使用哪种方式,切换完成后一定要做一次全量校验,尤其是涉及权重的场景。别怕麻烦,这一步做一次,能省掉后面几十个小时的数据修复时间。
5.4 位点确实对不上时的回退策略
万一切换后数据不一致严重,无法直接修复,最快的方式是回滚到切换前的状态。这就要求你在操作之前,已经准备好了切换前各节点的全量备份。
回退流程很简单粗暴:
- 停掉所有从库复制。
- 用切换前的备份,把从库B的数据恢复到切换前状态。
- 把从库B的 CHANGE MASTER 重新指回主库(或指回从库A)。
- 启动复制,重新校验数据。
这个操作的前提是备份是完整可用的,并且备份时间点距离切换时间点不能太长,否则回退后还需要做增量回放,过程反而更复杂。所以业内常说的“切换前必有备份,切换后必做校验”就是这个道理。
6. 实操中的几个延伸经验
6.1 切换窗口别选在业务高峰
这个道理大家都懂,但我想说一个细节:即使你选择在凌晨 2 点操作,如果业务有定时任务(比如凌晨的批量计算、报表生成),同样会踩到高峰。我那次正向切换就是忽略了客户凌晨 1 点有个大规模数据归档任务,结果切换后从库B的延迟一直压不下来。后来查了进程列表才发现,归档任务正在从库A上跑。所以选窗口前,最好把目标实例上的定时任务清单拉出来看一眼。
6.2 复制的账号密码最好独立配置
在从库B的 CHANGE MASTER 命令中,我使用了专用的 repl_user 账号。这个账号权限只需要 REPLICATION SLAVE 和 REPLICATION CLIENT 两个权限,不要给太多。万一出现泄漏,影响范围也可控。
推荐的最小权限创建语句:
sql复制CREATE USER 'repl_user'@'%' IDENTIFIED BY '强密码';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl_user'@'%';
注意,从库A在执行 SHOW MASTER STATUS 时,也需要有 REPLICATION CLIENT 权限。如果没有,会出现权限不足的提示,虽然不影响已经建立的复制关系,但会影响切换操作。
6.3 每次切换后都记录一次基线
我在这次切换的整个过程中,建了一个操作记录表,把每次切换的时间、操作用户、切换前后的 binlog 位点、各节点状态全部记录下来。这个表在后续排障时非常有用。比如客户这次要回退到一主两从,我只需要去查之前记录的主库位点信息,而不需要重新去翻各种会话历史。
6.4 中继节点的 binlog 保留时间和大小
级联架构下,中继节点需要同时为下游从库保留 binlog。如果 binlog 过期时间设置得太短(比如默认的 7 天),而下游从库恰好因为故障停机了几天,恢复时可能会发现中继节点的 binlog 已经被清理了,导致无法继续复制。解决方式有两种:
- 调大中继节点
expire_logs_days或使用新的binlog_expire_logs_seconds参数。 - 把中继节点的 binlog 保留时间设置得比下游从库允许的最大停机时间更长。
我在这次客户的级联切换时,专门把从库A的 binlog 保留时间调到了 14 天。这样做虽然会占用更多磁盘,但换来的是更高的容错空间。
6.5 延迟突然升高时的检查思路
级联环境下,如果从库B的 Seconds_Behind_Master 突然升高,很多人第一反应是主库压力大。其实应该按照链路逐级排查:
- 先在从库B上
SHOW SLAVE STATUS,观察 IO 线程是否正常,如果 IO 线程正常,说明网络链路没问题。 - 再去中继节点(从库A)上查看它的复制状态和执行线程,判断是不是从库A本身的 SQL 线程较慢。
- 最后才回到主库上查看是否有大事务或者锁。
这个排查顺序,能帮你快速定位延迟是在哪一级产生的,而不是在主库上白白浪费时间。
从我这次两个方向都切过的经历来看,位置点切换的核心不在于命令多复杂,而在于你对当前复制状态心里有数。每一次切换,本质上是把“谁给谁喂 binlog”这个关系重新建立一次。搞清楚每一级的位点,搞明白切换前后数据的消费关系,操作起来就踏实多了。
