“复制拓扑”这四个字,在数据库面试里出现的频率,跟实际动手规划过的人之少,形成了鲜明的反差。我最早接触这个概念时也以为,做主从复制就是搭一个从库挂上去完事。直到有一次业务大促前,架构评审会上被问到“复制拓扑怎么规划的”,我才意识到,主从复制不是一条链路,而是一张可以生长的网。复制拓扑决定了你今后怎么加从库、怎么做容灾、怎么拆分读写,也直接决定了故障发生时你能不能在几分钟内完成切换。这篇文章我就围绕配置和管理复制拓扑这件事,把从选型到落地再到日常运维的完整链路拆开讲清楚,适合刚把主从复制跑通、准备往更规范方向走的运维和 DBA 同学。
1. 复制拓扑的形态与选型:一主多从、级联、双主到底解决什么问题
很多同学搭复制拓扑,直接照抄网上的一主一从配置,抄完就以为高可用解决了。实际上,复制拓扑的形态选择,背后对应的是不同的业务压力和容灾目标。你选的每一种拓扑形态,都是在为某几个特定的问题买单,同时也在为某几个风险埋单。先把形态看清楚,再决定自己该用哪一张网。
1.1 一主一从:最小可用,但不等于高可用
一主一从是所有拓扑的基础,主库处理写入,从库异步拉取 binlog 回放,日常可以拿从库做备份、做只读查询。它的价值在于用最小的成本获得了数据冗余,但它完全不等于高可用。主库宕机时,从库虽然有数据,你还是要手动把业务流量切过去,期间要处理从库是否追平日志、应用侧连接串怎么切换、账号权限怎么同步一堆问题。
我见过不少团队把一主一从当成高可用方案来用,结果故障演练时发现切换耗时超过半小时。原因通常集中在两点:一是从库延迟没有提前监控,故障时根本不知道数据差了多少;二是从库的只读账号和主库不一致,切过去后应用报权限错误。所以,一主一从解决的是“数据有冗余”,高可用还要靠后续的切换工具和流程来兜底。
1.2 一主多从:读写分离与多业务隔离的经典解法
一主多从是一主一从的自然演进。主库只管写入,多个从库分别承担不同职责:一个从库跑报表查询,一个从库给数据分析团队跑临时查询,一个从库做备份导出源。这样做最大的好处是业务之间互相隔离,报表团队把从库查慢了,也不会影响线上主库。
配置一主多从时,最容易忽略的是每个从库的复制过滤规则。比如有个从库只负责某几个库的查询,那可以在复制链路配置里加上REPLICATE_DO_DB这类规则,避免无关数据也占到从库的磁盘和 IO。但要注意,过滤规则尽可能少用,因为一旦过滤规则写错了,从库数据不完整,你很难通过常规监控发现,这类问题属于复制拓扑里的隐形地雷。
1.3 级联复制:解决连接数压力和跨机房延迟
级联复制的典型结构是主库 A -> 从库 B -> 从库 C,C 不直接连 A,而是从 B 拉日志。这种结构有两个典型应用场景:一是主库连接数压力大,尽量让所有从库都直连主库会把主库的连接数拉得很高,级联后中间节点代替主库承担分发压力;二是跨机房场景,主库在机房一,从库在机房二,如果还有多个下游从库在机房二,与其让它们各自跨机房拉日志,不如先在机房二放一个级联节点,再由它分发给同机房的其它从库,延迟和带宽消耗都会明显下降。
级联复制最关键的一个参数是中间节点的log_slave_updates,必须设置为 ON。这个参数决定中间节点在回放完日志之后,是否把回放产生的变更再写一份到自己的 binlog 里。默认 OFF 时,从库 B 虽然应用了变更,但它自己的 binlog 是空的,C 从 B 拉日志就什么都拉不到。我踩过这个坑,当时排查了整整一个下午,从网络到权限查了一圈,最后发现就是log_slave_updates默认没开。
1.4 双主拓扑:读写不冲突时才建议用
双主拓扑里 A 和 B 互为主从,即 A 上写着 B 也在同步,B 上写着 A 也在同步。它的核心价值在于故障转移时可以快速切换,不用重新搭建复制关系。但双主最大的坑是写写冲突:如果业务同时往 A 和 B 写同一行同一条数据,就会产生主键冲突或数据不一致。
要让双主安全运行,通常要保证同一时刻只有一个主库接受写入,另一个只作为热备。同时在双主配置里必须设置自增参数:A 设auto_increment_offset=1,B 设auto_increment_offset=2,步长都设为 2,这样两边插入数据时生成的 ID 不会撞车。这套参数就是为双主设计的,千万别省。
1.5 多源复制:合并多个上游数据的场景
多源复制是一个复制通道同时接收多个主库的 binlog,默认在 MySQL 8.0 里已经支持,允许一个从库配置多个CHANGE REPLICATION SOURCE TO通道。典型应用是把多个分库分表的实例汇聚到一个只读从库上做汇总查询,或者把多个子公司业务库的数据合并到一个分析库。
配置多源复制时要注意每个通道必须有唯一的通道名。管理命令也变成了按通道操作,比如START REPLICA FOR CHANNEL 'channel_1'。日常巡检时记得逐个通道看状态,不要只看默认通道。汇总从库的表结构设计要提前想清楚,不同源库的同名字段类型不一致时,回放可能会报错。
我把上面这些形态的适用场景和主要风险整理了一张表,方便对照:
| 拓扑形态 | 典型结构 | 解决的核心问题 | 主要风险 |
|---|---|---|---|
| 一主一从 | 主 -> 从 | 数据冗余、备份源 | 不自动切换,无高可用 |
| 一主多从 | 主 -> 多从 | 读写分离、业务隔离 | 从库压力分散的管理成本 |
| 级联复制 | 主 -> 中继 -> 从 | 连接数压力、跨机房延迟 | log_slave_updates 忘开 |
| 双主复制 | 主主互备 | 快速切换、容灾 | 写写冲突、自增冲突 |
| 多源复制 | 多主 -> 一从 | 数据汇总、分库合流 | 通道管理复杂、表结构冲突 |
选型没有绝对最优,关键是看你的核心矛盾是什么。读压力大优先一主多从,跨机房带宽紧张优先级联,切换时间敏感优先双主加高可用工具,多业务汇总就上多源。
2. 开工前的三个技术决策:GTID、binlog 格式与半同步
很多人搭建复制拓扑时动手很快,但忽略了前期几个技术决策的重要性。这些决策不像配置文件那样能一眼看出来,等拓扑上线后再想改,基本等于重建复制关系。动手前先把这三个基础问题想清楚。
2.1 基于位点的复制还是 GTID 复制
基于位点的复制,从库通过主库的 binlog 文件名和偏移量(File+Position)来定位同步起点。它的优点是兼容老版本、实现简单,但缺点很明显:从库记录的位置一旦和主库 binlog 对不上,或者主库做了清理,复制就会报错,而且手动跳过错误位置非常麻烦。
GTID(全局事务标识符)则给每个事务分配了一个全局唯一的 ID,从库通过 GTID 自动判断哪些事务已经执行过、哪些还没执行。这样做的好处是复制关系搭建时不需要精确找位点,只需要指定从库从哪个 GTID 开始追即可,天然支持跳过已执行事务,拓扑变更时灵活得多。
我的建议是,新搭建的复制拓扑统一使用 GTID。MySQL 8.0 里开启 GTID 后,CHANGE REPLICATION SOURCE TO只需要指定SOURCE_AUTO_POSITION=1,复制关系自动基于 GTID 协商。老版本如果因为历史原因还在用位点,也要规划好迁移窗口,把 GTID 启用这件事排上日程。
2.2 binlog 格式:ROW 是默认且唯一推荐
binlog 有三种格式:STATEMENT、ROW 和 MIXED。STATEMENT 记录的是 SQL 语句本身,日志量小,但某些非确定性函数在不同机器上执行结果可能不一致;ROW 记录的是每一行数据的实际变更,日志量大,但最精确;MIXED 是前两者的混合,会在部分场景下自动切换。
对于复制拓扑来说,ROW 格式几乎是唯一推荐的选择。原因有两个:一是 ROW 格式的回放结果和主库实际变更完全一致,不存在因为函数、存储过程执行环境不同导致的偏差;二是很多运维工具(比如数据校验工具、数据订正工具)都要求 ROW 格式才能正常工作。代价是 binlog 体积会变大,尤其遇到大事务 UPDATE 时,整个表的行改动都会写进日志。这个代价在可控范围内,安全性和可维护性远大于日志体积的牺牲。
2.3 半同步复制:延迟和数据安全的折中
默认的异步复制里,主库提交事务后直接返回客户端成功,binlog 什么时候送到从库、从库有没有回放,主库完全不等待。这样性能最好,但主库宕机时可能丢失事务。简单说,异步复制丢数据的窗口是存在的。
半同步复制在事务提交时,会等待至少一个从库确认收到 binlog 后,才向客户端返回成功。它把数据安全的窗口大大缩小,代价是提交延迟增加,因为每次事务都要多一次网络往返。如果从库挂了,半同步会退化为异步,自动降级保证主库可用性,这是它的默认行为。
我的建议是:核心业务库务必开启半同步,分析型或报表型从库可以不要求半同步。开启方式是在主库安装rpl_semi_sync_master插件,从库安装rpl_semi_sync_slave插件,然后用INSTALL PLUGIN和SET GLOBAL配合启用。插件相关配置写入配置文件后,注意版本兼容性,5.7 和 8.0 的插件名称一致,但建议先查官方手册再动生产。
3. 三套典型拓扑的从零搭建:命令与参数逐一过
理论说了一大堆,实际操作才是检验理解的唯一标准。我按三种最常见的拓扑,用 MySQL 8.0 的语法走一遍搭建流程。
3.1 一主两从:五分钟内跑通的标准流程
主库mysql-master,从库mysql-slave1和mysql-slave2。主库配置文件加入:
server_id = 1 binlog_format = ROW log_bin = mysql-bin gtid_mode = ON enforce_gtid_consistency = ON从库配置文件加入:
server_id = 2 log_bin = mysql-bin gtid_mode = ON enforce_gtid_consistency = ON log_slave_updates = ON注意从库的server_id不能和主库重复,整个复制拓扑里所有实例的server_id必须是全局唯一的。然后分别在两个从库执行:
CHANGE REPLICATION SOURCE TO SOURCE_HOST='mysql-master', SOURCE_USER='repl', SOURCE_PASSWORD='repl_pass', SOURCE_AUTO_POSITION=1; START REPLICA; SHOW REPLICA STATUS\G重点看两个字段:Replica_IO_Running和Replica_SQL_Running都为 Yes,Seconds_Behind_Source为 0,复制就正常了。从库首次启动时如果主库已有数据,需要先做一次物理备份恢复到从库,再建立复制关系。
3.2 级联拓扑:中间节点最容易出错
级联结构是master -> relay -> slave3。relay节点既要作为从库拉取 master 的日志,又要作为主库给slave3提供日志,所以relay的配置里log_slave_updates必须为 ON,这一点前面已提过,这里放到操作场景里再强调一次。
relay上的操作:
CHANGE REPLICATION SOURCE TO SOURCE_HOST='mysql-master', SOURCE_AUTO_POSITION=1; START REPLICA;等relay追上主库后,在slave3上执行:
CHANGE REPLICATION SOURCE TO SOURCE_HOST='mysql-relay', SOURCE_AUTO_POSITION=1; START REPLICA;级联拓扑的延迟比一级从库更高,因为中间节点要比下游从库多一次落盘和网络传输。所以级联路径尽量不要超过三层,超过三层之后延迟放大效应会很明显,排查问题链路也变得又长又难。
3.3 双主拓扑:自增冲突必须提前处理
双主拓扑里 A 和 B 互为复制源。A 上的配置:
server_id = 1 auto_increment_offset = 1 auto_increment_increment = 2 log_slave_updates = ONB 上的配置:
server_id = 2 auto_increment_offset = 2 auto_increment_increment = 2 log_slave_updates = ON配置完成后,在 A 上把 B 加为复制源,在 B 上把 A 加为复制源。此时 A 和 B 都既是主又是从。要注意双主模式下,如果两边同时插入数据,即便自增参数规避了 ID 冲突,业务层的逻辑冲突仍然可能存在(比如针对同一数据行的更新)。所以双主拓扑建议明确角色,同一时间只有一个节点负责写,另一个作为纯备机。
4. 拓扑不是一成不变的:加节点、改结构的实操与顺序纪律
复制拓扑搭好之后,线上业务的变化会让拓扑跟着变。加一个从库、把级联节点晋升为主库、调整复制过滤规则,这些操作看起来简单,实际做起来有不少顺序上的讲究。
4.1 新从库上线:从备份恢复到 Clone,二选一
给现有拓扑加新从库,不能直接把空的从库实例拉起来建复制关系,那样它会从当前位点开始同步,历史数据全缺。正确做法是先用备份恢复出全量数据,再建立复制链路。
两种常用方案。方案一是用mysqldump导出主库数据,恢复后建立复制。优点是简单,小库(比如几十 GB 以内)够用;缺点是导入导出的过程中从库无法同步增量,数据导出期间的 binlog 要在建链路时通过 GTID 自动补齐。
方案二是用 MySQL 8.0 的 Clone 插件,直接从远端实例克隆数据文件。Clone 比逻辑备份快得多,适合大库。操作上,先在将要成为从库的实例上执行:
INSTALL PLUGIN clone SONAME 'mysql_clone.so'; CLONE INSTANCE FROM 'repl'@'mysql-master' IDENTIFIED BY 'repl_pass';克隆完成后会自动重启实例并应用增量数据,再执行CHANGE REPLICATION SOURCE TO ... SOURCE_AUTO_POSITION=1从当前 GTID 位置继续同步。注意 Clone 会导致目标实例数据被覆盖,只能在全新实例上操作,不要对已有数据的实例执行。
4.2 级联节点晋升:改拓扑而不是重建复制
当业务规模变大,原先的级联节点需要承担主库职责时,不需要把整个复制拓扑拆了重来。标准做法是先把要晋升的节点上的只读属性关掉,确认它已经追平上一级主库,然后把它的下游从库的复制源改到新主库上。
具体来说,如果原结构是A -> B -> C,要让 B 晋升为新的主库(A 退居为备份或下线),操作顺序是:
- 确认 B 的
Seconds_Behind_Source为 0; - 在 B 上执行
SET GLOBAL read_only=OFF,让 B 可写; - 把 C 的复制源改为 B,执行
STOP REPLICA; CHANGE REPLICATION SOURCE TO ...; START REPLICA;; - 确认 C 追平后,再决定 A 是否下线或转为 C 的从库。
这个过程中最容易出问题的是第 3 步:C 在切换复制源时,如果 B 还没有完全追平 A 的日志,C 会丢一段数据。所以晋升前检查追平状态是铁律。另外,所有变更操作建议放在维护窗口执行,哪怕理论上可以在线做,复制链路的变更涉及数据一致性,谨慎永远是对的。
4.3 拓扑变更时的顺序纪律
复制拓扑的任何变更,本质上是在一堆互相依赖的节点之间调整数据流向。顺序纪律只有一条:先确保数据完整,再动链路。也就是在动手改CHANGE REPLICATION SOURCE TO之前,先确认当前节点的复制已追上、报错已清零、跳过日志没有积压。
具体执行时,我惯用的顺序是先停复制,再改复制源,再启动复制,最后观察状态。停复制这一步很多人会省掉,直接执行CHANGE REPLICATION SOURCE TO,但从库在运行态直接改复制源,可能产生不可预期的状态。规范操作是:
STOP REPLICA; CHANGE REPLICATION SOURCE TO ...; START REPLICA; SHOW REPLICA STATUS\G变更完成后不要立刻离开,持续观察几分钟到十几分钟,确认 IO 线程和 SQL 线程都稳定,延迟持续走低而不是反弹。
5. 拓扑运行期的巡检、故障处理与主从切换
配置完成只是开始,复制拓扑真正的功力体现在日常维护和故障发生的应对上。这一节我把运行期间最常用的巡检手段、典型故障处理和切换流程梳理一遍。
5.1 日常巡检:四类关键状态要盯紧
复制状态巡检不能只看Replica_IO_Running和Replica_SQL_Running是不是 Yes,那是基本项。我每次巡检至少会看四类信息:
第一是延迟。看Seconds_Behind_Source,但这个值有局限性——如果主库长时间没有新写入,SQL 线程没有新事务可回放,即使从库落后很多这个值也可能是 0。更可靠的方式是配合查看从库当前执行到的Executed_Gtid_Set和主库的Executed_Gtid_Set,对比缺了哪些事务。这个对比才是真正的延迟判定。
第二是报错。Last_IO_Errno和Last_SQL_Errno两个字段只要不是 0,就要查是什么原因。IO 线程报错一般是网络或认证问题,SQL 线程报错通常是回放时遇到主键冲突、表不存在、字段类型不匹配等。
第三是 binlog 增长速率。从库的磁盘空间要结合 binlog 增长速度提前规划。有的从库 binlog 保留时间过长,日志会把磁盘塞满,导致 io 线程中断。定期清理过期 binlog 并配置合理的binlog_expire_logs_seconds是这个环节的关键。
第四是半同步状态。开启半同步后,如果从库长时间无响应导致半同步退化,要在监控里建立告警,否则你不知道主库已经悄悄退回异步模式。
5.2 典型故障处理:SQL 线程停止的排查链路
SQL 线程停止是复制拓扑里最常见的故障,标准排查链路我建议按下面顺序走:
首先看SHOW REPLICA STATUS里的Last_SQL_Error。最常见的错误是Duplicate entry 'xxx' for key 'PRIMARY',说明从库回放时试图插入一条主键已存在的记录。这种错误绝大多数是因为从库上恰好有并发写入了同一条数据,或者从库曾经被手动改过数据。
遇到主键冲突不要着急跳事务。先评估影响范围,确认冲突确实是人为写入造成的,再决定要不要跳过。跳过单个事务的方法是把 SQL 线程停掉,注入一个空事务,再启动 SQL 线程:
STOP REPLICA; SET GTID_NEXT='<报错事务的GTID>'; BEGIN; COMMIT; SET GTID_NEXT='AUTOMATIC'; START REPLICA;注意,注入空事务之后,丢失的真实变更不会再回放了,所以一定要确认跳过的那个事务对应的业务影响可接受。如果是大量事务批量报错,那说明复制链路本身的数据一致性已经出问题,不能靠逐个跳过解决,要从数据校验和修复入手。
5.3 主从切换:把流程写成脚本,而不是靠命令行现场敲
主从切换最怕的是现场临时想命令。我的建议是提前把切换流程固化成脚本或操作手册,并且定期演练。标准的切换流程分为五个阶段:
阶段一,确认目标从库追平主库,延迟归零;
阶段二,主库设置只读,停掉写入流量,确保没有新事务进入主库 binlog;
阶段三,等从库把 binlog 全部回放,确认两边数据一致(通过 GTID 对比或数据校验工具核对重点表);
阶段四,将从库设为可写,把业务连接切换过来,原主库降级为从库挂到新主库下面;
阶段五,更新监控,验证新主库的写入和从库的复制状态。
这套流程里,阶段三的数据一致性核对是很多团队会跳过的。实际故障中主从切换后才发现两边数据有差异,是最棘手的情况。有条件的话,切换前用pt-table-checksum这类工具做一轮校验,发现差异先处理再切换,宁可多花十几分钟,也不要切换到有数据问题的库上。
复制拓扑管理这件事,方法论说穿了就是两句话:拓扑形态跟着业务矛盾走,变更流程要保证数据先于链路。我在运维过程中体会最深的不是哪条命令多厉害,而是对拓扑结构变化的敬畏。加节点容易,减节点和换结构才是真正考验功力的时候。你每一次修改前多想一步“数据从哪里来、到哪里去、中间有没有丢失窗口”,生产事故就能避开一大半。这篇文章里的命令和参数都是最常用到的,真正要带走的是那套先确认数据、再动复制的思维习惯。最后再分享一个实用技巧:日常巡检时把 GTID 对比写进定时脚本,比单纯看Seconds_Behind_Source可靠得多,能看到真正的数据差距。