接手过MySQL复制架构的DBA,基本都遇到过这样的需求:原来一主两从直接挂在主库下面,跑着跑着发现跨机房的从库延迟上去了,或者主库连接数被打满,这时候想把拓扑改成“主库→从库A→从库B”的级联结构;过段时间业务调整,又得从级联改回一主两从。这种“翻来覆去”的架构切换,如果用基于位点来做,每一步都得格外小心,因为位点这个东西错一个数字,轻则复制中断,重则数据翻倍。
这篇文章就围绕“MySQL基于位点”这一个核心,把一主两从和级联之间的双向切换讲透。我会把切换前要核对哪些参数、切换时怎么取位点、切换后怎么验证,以及常见故障怎么排查,全部按实际操作的顺序过一遍。不管你现在用的是MySQL 5.7还是8.0,只要复制架构是经典的binlog位点方式,这套思路都能直接落地。
1. 两种复制架构的本质差异与适用场景
1.1 一主两从与级联到底差在哪
先明确一下两种架构的形态。一主两从,也叫星型拓扑,主库Master下面挂着两个从库Slave1和Slave2,两个从库各自单独拉取主库的binlog,彼此之间没有依赖关系。级联架构,也叫链式拓扑,主库Master下面只挂一个从库Slave1,然后Slave2再挂在Slave1下面,形成Master→Slave1→Slave2的链路。
两个架构最本质的区别在于数据链路的依赖关系。一主两从里,任何一个从库挂了都不会影响另一个,运维上相对独立;级联架构里,Slave2的复制状态完全取决于Slave1,Slave1如果IO线程断了,Slave2立刻也跟着断。这个依赖关系决定了切换时必须逐层处理,不能先动上游再动下游,顺序反了很容易丢位点。
另外,级联架构对Slave1有个硬性要求:必须开启log_slave_updates参数。意思是从库在应用主库的binlog时,同时把相同的事务写入自己的binlog,这样下游的Slave2才能像连主库一样连上来继续拉日志。如果没有开启这个参数,Slave1自己的binlog里只有本地写入的事务,没有复制过来的事务,级联链路根本搭不起来。一主两从架构下,两个从库默认不需要开启这个参数就能正常工作。
1.2 什么场景需要做这种切换
我在实际维护中遇到比较多的情况有三类。
第一类是机房网络架构变化。比如Slave2所在机房和主库机房间的网络链路经常抖动,主库那边又没有多余的专线口,这时候把Slave2改挂到同机房的Slave1下面,就能避开跨机房的网络不稳定,延迟也会明显改善。
第二类是主库连接数压力过大。MySQL主库可以同时接受的复制连接数是有限的,每个从库都会占一个复制连接。当从库数量增长到十几个甚至更多时,主库光复制连接就吃掉一部分连接资源,这时候把部分从库改成级联方式,主库只需要维持少量复制连接,效果非常明显。
第三类是维护切换需要。比如某个从库要扩容、迁移或者做版本升级,但又不希望影响另一个下游从库的复制,临时把它挂到别的节点下面,等维护完再切回来。这类场景在业务连续性要求比较高的环境里很常见。
1.3 位点方案与GTID方案怎么选
既然MTTR很重要,那就先明确一下:如果你的环境已经开了GTID,并且所有节点都支持,那我其实更推荐GTID方案,因为架构切换会简单很多,MASTER_AUTO_POSITION=1会自动处理位点衔接。但位点方案依然有它的价值:很多老环境从MySQL 5.5或5.6一路升上来,GTID并没有真正启用;或者早年间批量搭建的复制架构,gtid_mode=OFF,业务依赖的运维脚本也都是基于show master status的位点信息来做的,这种环境下位点方案是唯一选择。
位点方案的核心就是两个值:binlog文件名和position偏移量。每次执行CHANGE MASTER TO时,必须给出这两个值告诉从库“你应该从主库binlog的哪个位置开始拉取”。如果是同一条binlog链路从上游直接切换,位点可以精准对齐;如果是跨节点切换,比如Slave2要从连Master改成连Slave1,位点就必须在Slave1上重新获取,这一点是很多新手最容易掉坑的地方。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 切换前必做的环境检查与参数准备
2.1 三个节点必须核对的参数清单
实际操作之前,我建议先做一个参数检查清单,把涉及的三个节点全部过一遍。不要凭记忆操作,生产环境的细节往往就坏在“我以为它开了”这句话上。
需要核对的核心参数包括:
| 参数 | 作用 | 一主两从中Master | 一主两从中Slave1 | 级联中Slave1(新角色) |
|---|---|---|---|---|
log_bin |
是否开启binlog | 必须ON | 建议ON | 必须ON |
log_slave_updates |
从库是否将复制事务写入自身binlog | 无要求 | 无要求 | 必须ON |
server_id |
节点唯一标识 | 必须全局唯一 | 必须全局唯一 | 必须全局唯一 |
binlog_format |
行格式或语句格式 | ROW | ROW | ROW |
gtid_mode |
是否开启GTID | 确认关闭或兼容 | 确认关闭或兼容 | 确认关闭或兼容 |
这里重点提醒一下log_slave_updates。这个参数在MySQL里默认是OFF,如果从一主两从切换成级联,Slave1必须开启它,否则即使你把CHANGE MASTER TO写对了,Slave2也连不上Slave1,因为Slave1的binlog里根本没有主库复制过来的事务。修改这个参数需要重启MySQL实例,所以要在业务窗口内提前评估好。
还有一个容易被忽略的点:server_id。很多环境里从库是从主库克隆来的,克隆后忘了改server_id,导致两个节点ID相同。在普通复制里,ID相同会导致复制错乱或者互相抢日志;在做级联切换时,如果Slave1和Slave2的server_id相同,切换后Slave2可能连日志都拉不到。我见过不止一次因为这个原因排查了半天的情况,所以检查清单里一定要有这一项。
2.2 复制账号与网络权限检查
级联切换时,Slave2的上游从Master变成了Slave1,这意味着Slave2要使用一个复制账号去连接Slave1。原来连接Master的复制账号,在Slave1上未必存在,很多环境里Slave1根从来不对外服务,根本没有任何复制账号。
所以切换前必须确认两点:Slave1上有复制账号,并且账号权限包含REPLICATION SLAVE和REPLICATION CLIENT。如果你习惯用同一个账号密码在整个架构里复制,那就要保证这个账号已经在Slave1上创建好了,否则切换时START SLAVE会一直报Access denied。
网络层面也要通。Slave2能访问Slave1的MySQL端口,通常默认3306;防火墙、安全组、iptables这些都要在切换前放通。还有一个细节,如果两个节点的bind-address配置限制了监听地址,也要调整成允许复制连接进来的IP范围。
我习惯在切换前先用mysql客户端手工测试一下:从Slave2的机器上执行mysql -u复制的账号 -p -h Slave1的IP -P 3306,能正常连上再继续。这一步能省掉后续复制线程连接失败的一大堆排查时间。
2.3 数据一致性基线:为什么必须追平再动手
位点切换有个铁律:下游节点在切换前必须先追平上游节点的数据,再取位点切换。这句话一定要刻在脑子里。
什么叫追平?简单说就是从库的Seconds_Behind_Master等于0,SQL线程已经把所有relay log应用完,此时从库的数据和主库当前的数据是一致的。只有在追平的状态下,你在上游节点执行SHOW MASTER STATUS拿到的位点才能安全地交给下游节点继续用。
如果不等追平就切,会分成两种情况。第一种:下游节点比上游节点慢,你取的上游位点比下游实际执行到的位点更早,那么下游切换后会把已经执行过的事务再执行一遍,出现主键冲突或记录重复。第二种:下游节点比上游节点快,你取的位点比下游执行到的位点更晚,那中间这部分事务就不会再执行,直接丢数据。不管哪一种,都很致命。
所以我的习惯是:切换动作的窗口尽量安排在业务低峰期,并且切换前要让主库暂停写入或者至少确保写入量极小。如果主库完全不能停写,那就必须做一次“位点对齐”的操作,也就是第3部分要讲的进阶做法。先保证能追平,再谈后面的切换。
3. 一主两从切换为级联的完整操作
3.1 切换动作详细拆解
现在进入核心环节。假设当前架构是Master→Slave1、Master→Slave2,目标架构是Master→Slave1→Slave2。也就是说Slave1变成中继节点,Slave2从Slave1拉取日志。
整体操作顺序是:先在Slave1上确认能作为上游,再在Slave2上停复制、改主指向、启动复制。千万不能反过来先动Slave2再动Slave1,那样Slave2会处于没有上游的状态,短暂不可用倒是小事,更麻烦的是位点会乱。
第一步,在Slave1上确认log_slave_updates=ON。如果之前没开,先改配置重启,再重新建立Slave1与Master之间的复制关系,确保Slave1的binlog里有复制过来的事务。
sql复制-- 在Slave1上查看关键参数
SHOW VARIABLES LIKE 'log_slave_updates';
SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'server_id';
第二步,确认Slave1和Slave2都已经追平Master。分别在Slave1和Slave2上执行:
sql复制SHOW SLAVE STATUS\G
-- 关注 Seconds_Behind_Master: 0
-- 关注 Slave_IO_Running: Yes
-- 关注 Slave_SQL_Running: Yes
第三步,如果两个从库都追平了,那么在Slave1上执行SHOW MASTER STATUS,拿到Slave1当前binlog的文件名和位移。这里有一个很关键的点:既然Slave1已经追平了Master,那么Slave1的binlog里已经包含了Slave2需要的所有事务,此时Slave1的当前位点就可以直接作为Slave2切换到Slave1下的起始位点。
sql复制-- 在Slave1上执行
SHOW MASTER STATUS;
记录下结果中的File和Position两个值。比如mysql-bin.000045和187403219。
第四步,在Slave2上停止复制线程,然后重新指定上游为Slave1。
sql复制-- 在Slave2上执行
STOP SLAVE;
CHANGE MASTER TO
MASTER_HOST='slave1.example.com',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='你的密码',
MASTER_LOG_FILE='mysql-bin.000045',
MASTER_LOG_POS=187403219;
START SLAVE;
注意MASTER_AUTO_POSITION这里不需要设置,因为我们是位点方案,不是GTID。执行完以后,在Slave2上再看一次SHOW SLAVE STATUS\G,确认Slave_IO_Running: Yes和Slave_SQL_Running: Yes都正常。
3.2 验证级联链路是否真正生效
很多人切换完只看了Slave_IO_Running和Slave_SQL_Running是Yes就完事了,其实还不够。位点方式下,复制链路是否真的通,还要看数据能不能流到最底层。
验证方法很简单:在Master上随便建一张临时表插入一行数据,然后分别到Slave1和Slave2上看这行数据是否同步过来。这里有个前提,Master上要允许写数据,如果业务不能动,可以自己建一张临时性的测试表,写入后立即删除。
sql复制-- 在Master上执行
CREATE DATABASE IF NOT EXISTS test_switch;
CREATE TABLE test_switch.t_check (id INT PRIMARY KEY);
INSERT INTO test_switch.t_check VALUES (1);
然后到Slave1和Slave2上分别查询:
sql复制SELECT * FROM test_switch.t_check;
如果两边都能查到这行数据,说明Master→Slave1→Slave2的整条链路是通的,Slave1的log_slave_updates确实生效了。查完之后把测试表和测试库清掉。
还有一个细节,切换后Slave2的SHOW SLAVE STATUS里会出现一个字段叫Master_Log_File和Read_Master_Log_Pos,这两个值对应的是Slave1的binlog文件名和位移,不再对应Master的。新手看到它和Master上的SHOW MASTER STATUS对不上就以为出了问题,其实这是正常的,因为Slave2的上游已经变成了Slave1。
3.3 如果不想停写,可以用位点对账法(进阶)
前面讲的是“先追平再切换”的稳妥方案,适用于低峰期或可停写窗口。如果业务完全不能停写,也可以用位点对账法,但操作复杂度会明显上升,而且风险也更大,我只建议有经验的DBA尝试。
基本思路是:在不停写的情况下,先在Master上拿到当前binlog位点,记为文件F_m和位置P_m,然后在Slave2上执行STOP SLAVE,记录SHOW SLAVE STATUS里的Relay_Master_Log_File和Exec_Master_Log_Pos,这两个值代表Slave2已经执行到Master主库的哪个位点。
因为Slave1和Slave2都是从Master复制,如果Slave1已经追平了Master且Slave2也是从同一个Master复制,那么理论上Slave2需要的后续日志在Slave1的binlog中都有。实际操作时,需要保证Slave1的位点领先于或等于Slave2已经执行到的位点。最稳妥的检查方式是:
在Slave1上执行SHOW MASTER STATUS拿到Slave1的当前binlog位点,再确认Slave1已经应用到Slave2记录的Relay_Master_Log_File和Exec_Master_Log_Pos对应的位点之后。由于binlog文件名在不同节点上可能不同,这个对应关系不好直接判断,可以用mysqlbinlog配合位点范围来确认Slave1的binlog里包含哪些Master上的事务。
坦白说这个操作在真实生产环境里比较繁琐,而且一旦计算错位,数据一致性就会出问题。我自己的经验是:除非真的无法设置只读窗口,否则不要用这种方式。如果实在没办法,建议先临时把Master设置为只读一小段时间,哪怕只有几分钟,也能换来稳稳的追平状态,然后在只读窗口内完成所有切换,最后再恢复写入,整体风险要低得多。
4. 级联架构回切为一主两从的完整操作
4.1 回切的核心难点与应对策略
回切的难点在于:Slave2的上游从Slave1改回Master,而它原来执行到的位点记录的是Slave1的binlog文件名和位移,Master和Slave1的binlog文件名大概率不一样,不能直接拿Slave2的Master_Log_File和Read_Master_Log_Pos去给Master用。
最稳妥的策略依然是先追平、再对齐、后切换。也就是说,先把Master→Slave1→Slave2整条链路都追平,然后在Master上取位点,把Slave2重新指向Master。这个思路和第3部分完全对称,唯一的差异是这里取位点的节点变成了Master,因为Slave2要直接连Master了。
有人可能会问,Slave2从Slave1拉日志时,Relay_Master_Log_File显示的其实是Slave1的binlog文件名,能不能在Slave1上找一个对应Master binlog的位点,然后把这个位点拿回Master上用?理论上可以,但需要分析binlog事件的具体内容,非常费劲。与其做这种高风险换算,不如直接追平后重新取Master的当前位点,简单可靠。
4.2 回切步骤详述
回切前,我会先确认一遍整条链路的健康状态。在Slave1和Slave2上分别执行SHOW SLAVE STATUS\G,确保Slave_IO_Running: Yes、Slave_SQL_Running: Yes、Seconds_Behind_Master: 0。
如果业务允许,先将Master临时设为只读,防止切换窗口期间新增数据,保证三个节点都在同一个静止点上。
sql复制-- 在Master上执行
SET GLOBAL read_only = ON;
SET GLOBAL super_read_only = ON;
只读设置生效后,再去三个节点确认一次追平状态。这一步不能省,因为前面确认追平到设置只读之间可能还有几秒钟时间差,期间如果有事务写入Master,Slave1和Slave2可能还没跟上。只读之后再确认一次,确保绝对静止。
确认追平后,在Master上执行SHOW MASTER STATUS,拿到位点:
sql复制-- 在Master上执行
SHOW MASTER STATUS;
假设结果为mysql-bin.000120和302881561。
然后到Slave2上执行切换:
sql复制-- 在Slave2上执行
STOP SLAVE;
CHANGE MASTER TO
MASTER_HOST='master.example.com',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='你的密码',
MASTER_LOG_FILE='mysql-bin.000120',
MASTER_LOG_POS=302881561;
START SLAVE;
启动后,在Slave2上查看复制状态,确认都正常,然后在Master上解除只读:
sql复制-- 在Master上执行
SET GLOBAL read_only = OFF;
SET GLOBAL super_read_only = OFF;
最后回到Slave2执行一次查询验证,确认新数据能正常同步。
4.3 回切后的校验清单
回切完成后,不能只看两台从库的状态就认为万事大吉。我建议按下面这个清单逐项确认:
- Slave2的
Master_Host是否已经指向Master,而不是Slave1。 - Slave1和Slave2的
Seconds_Behind_Master都保持为0,或者在业务恢复写入后短暂出现延迟后迅速追平。 - Master上
SHOW PROCESSLIST能看到来自Slave1和Slave2的复制连接,两个连接各自的command是Binlog Dump。 - 在Master创建一个测试表写入数据,确认Slave1和Slave2都能同步到,并且两边数据一致。
- 如果环境有
pt-table-checksum,跑一遍主从数据校验,确认没有差异。
这个校验清单看起来很基础,但实际价值很大。我遇到过回切后一周才发现某个表缺数据的案例,原因是回切时位点选错,SQL线程意外跳过了一部分事务,而当时只看了状态字段没做数据验证,导致问题潜伏了很久。所以务必养成回切后立即做数据校验的习惯。
5. 切换过程中的常见故障与排错速查
5.1 复制线程起不来的几类原因
切换后最容易遇到的故障是Slave_IO_Running: Connecting。这个状态说明从库正在尝试连接上游,但连接一直没建立成功。常见原因有三个。
第一个是复制账号或密码错误。尤其是从连Master改成连Slave1后,Slave1上如果没有对应的复制账号,就会一直连接失败。遇到这种情况,先在从库机器上手工用复制账号连一下目标节点,确认能连上。
bash复制mysql -urepl -p -h slave1.example.com -P 3306
第二个是防火墙或网络不通。很多公司内部MySQL端口只放通了特定IP段,Slave2到Slave1的网络如果没放通,IO线程就会一直Connecting。通过telnet或者nc测试端口连通性即可定位。
第三个是上游节点没有开启binlog或者log_slave_updates=OFF。从库连接上游时,如果上游线程发现没有binlog可供读取,连接会失败或者日志拉取为空。这需要在上游节点确认参数。
另外还有一个比较隐蔽的原因:如果上下游之间的server_id冲突,IO线程起来后会立刻被上游断开,日志里会看到类似“slave_io_thread started”然后又反复停止的现象。这个也是我前面强调检查server_id的唯一原因。
5.2 SQL线程报错主键冲突或记录不存在怎么办
SQL线程报类型基本都是1062 Duplicate entry或者1032 Can't find record。这类错误在基于位点的切换里非常典型,几乎都是同一个核心原因:起始位点选早或选晚,导致日志中的某个事务和当前从库上的数据无法对上。
遇到这种情况,不能直接SET GLOBAL sql_slave_skip_counter = 1跳过,因为跳事务是治标不治本,而且跳过的可能不止一个事务,会让数据裂缝越来越大。我的建议是停掉复制,先确认位点是否对齐。
sql复制STOP SLAVE;
SHOW SLAVE STATUS\G
-- 查看 Exec_Master_Log_File 和 Exec_Master_Log_Pos
然后对比该位点和当前上游节点的SHOW MASTER STATUS。如果差距很大,说明切换时选的起始位点有问题,需要重新做一次“追平再切换”。如果差距很小,可能是切换窗口内恰好有一个事务只写入了主库还没同步到从库,这时候需要把该事务补齐,再启动复制。
对于MySQL 8.0,如果出现少量错误且你确定可以跳过,可以用官方推荐的START REPLICA配合SQL_AFTER_MTS_GAPS等参数,但生产环境仍然不建议依赖跳事务来解决,正确做法永远是停写、对齐、重切。
5.3 延迟放大的监控与缓解
级联架构有一个天然的缺陷:链路每多一层,延迟就会多一份放大。Slave2的延迟往往是Slave1的两倍以上,尤其是在大事务、大批量更新场景下,这个放大效应非常明显。
切换成级联后,我建议在监控层面重点关注两个指标:Slave1的Seconds_Behind_Master和Slave2的Seconds_Behind_Master。如果两者差距持续拉大,优先检查Slave1的SQL线程是否存在大量重放慢查询,同时对比两个节点的负载情况。
缓解延迟有一个常用手段:在Slave1和Slave2上都开启并行复制。MySQL 5.7及以上版本支持MTS多线程复制,配置两个参数:slave_parallel_type = LOGICAL_CLOCK和slave_parallel_workers。一般建议slave_parallel_workers设为4到8,具体看CPU核数和业务负载。注意修改后要重启实例才生效。
ini复制[mysqld]
slave_parallel_type = LOGICAL_CLOCK
slave_parallel_workers = 8
另外,如果级联链路延迟长期偏高且无法缓解,我会考虑直接切换回一主两从架构,把复制压力拆成两条链路,而不是硬扛级联。毕竟级联架构在节约主库连接数上有优势,但在延迟敏感型业务里并不友好。
最后说两句
做了这么多年的MySQL复制维护,我最大的感受是位点切换本身并不难,难在对“状态”的掌控。每一次切换前都要先确认所有节点都处于可衔接的状态,每一次切换后都要验证数据真的通了,而不是只看那么几个状态字段。尤其是级联切换和回切这种拓扑变动操作,只要有一个节点没追平,后续的所有操作都会被带偏。
有条件的朋友,我建议先在测试环境完整演练一遍一主两从和级联之间的双向切换,把每一步的日志、输出、异常情况都记录下来,再上生产操作。真到了生产环境,照着演练过的步骤执行,心里会踏实得多。希望这篇文章能帮你少走一些弯路,更稳地拿捏基于位点的架构切换。
