单库内核(存储、事务、日志、优化)齐了之后,生产还要面对三件事:副本怎么跟上、主挂了怎么切、读能不能摊到从库。分库分表走的是「容量扩展」主线(见 数据库扩展),本篇只讲单逻辑实例上的复制与高可用——这正是广告配置只读扩展与计费防丢两条诉求的交汇点。
本文是 MySQL 内核 系列的第 5 篇(主从复制与高可用 · 收官)。 全系列 5 篇:
- MySQL 内核深挖 · InnoDB 存储结构与 B+ 树索引
- MySQL 内核深挖 · 事务隔离级别、MVCC 与行锁
- MySQL 内核深挖 · redo、undo 与 binlog 机制
- MySQL 内核深挖 · EXPLAIN 计划与慢查询优化
- MySQL 内核深挖 · 主从复制链路与 HA 高可用(本篇)
一句话定位:复制 = 主库 binlog 事件经 Dump/IO/SQL(或并行 worker)在从库回放;半同步换「至少一副本收到」的耐久;HA 负责探活升主;读写分离必须显式处理复制延迟。
TL;DR
- 链路:Master 写 binlog → Dump Thread → Replica IO Thread 写 relay → SQL Thread / 并行 workers 应用。
- 异步 vs 半同步:异步提交即返回,主挂可能丢未传出事务;半同步等至少一个从库 ACK(落 relay),超时可降级。
- 并行复制:按 schema / logical clock / writeset 多 worker 回放,是压延迟的主力。
- 延迟来源:网络与 IO 拉日志、SQL 回放排队、大事务、无并行、从库硬件差。
- HA:传统主从 + MHA/Orchestrator 编排切换;或 MGR / InnoDB Cluster 多数派自动选主。
- 读写分离:写主读从提吞吐,但有复制延迟——配置变更后「写后读旧」会酿错误投放。
Table of contents
Open Table of contents
1. 复制在解决什么
复制在生产上要解决四件事:读扩展(配置/报表只读分摊到多从)、高可用(主挂提升从)、备份与分析(从库做备份、跑重 SQL,减少伤主),以及异地容灾(延迟与一致性权衡另议)。正因如此,这四件事的基石都在于 binlog 的可靠性与引擎状态的绝对一致,这也正是上一篇中我们死磕 双 1 与 2PC 的原因。随着复制位点机制演进出 GTID,集群切换与数据对账变得更加清晰,现代部署环境理应优先开启 GTID。
需要强调的是,复制过滤(如 replicate-do-table)、只读从库上的临时表,以及最致命的「人为在从库误开可写」,都会悄无声息地制造数据漂移。试想一下,广告配置库若存在「从库手工改一笔救急」的操作,一旦发生 HA 切换该从库被提升为主,这笔游离于 binlog 之外的修改就会变成长达数周的幽灵 bug。因此,从库必须严格只读,任何救急操作都必须走主库的正式变更打平数据。
和 数据库扩展 的边界再说一次:
| 问题 | 本篇(复制/HA) | 扩展系列 |
|---|---|---|
| 单库读顶不住 | 加只读从 | — |
| 单库写顶不住 | 最多垂直拆分思路 | 分库分表 / 队列削峰 |
| 主挂了 | 升从、MGR | — |
| 数据量无限涨 | 归档 + 从库备份 | 分片、冷热分离 |
不要指望「多挂几个从」解决写容量——写仍然压在主上,从库再多也只是分摊读。
2. 复制链路:三线程心智
延迟 = 拉日志延迟 + 回放排队;监控要拆开看 Seconds_Behind_Source 背后到底卡在哪段。
- Binlog Dump(主):把事件推给(或被)从库拉取;
- IO Thread(从):收事件写入 relay log;
- SQL Thread / Coordinator+Workers(从):解析 relay,应用到存储引擎。
这三条线程串起的就是经典的「三线程心智」,而复制延迟往往就潜伏在每一段交接的缓冲地带。在写密集的计费库场景下,旧版单线程的 SQL 线程回放吞吐不足,几乎必然导致复制回放积压;正是为了解决这个痛点,内核引入了并行复制机制,通过基于 schema、事务间依赖(LOGICAL_CLOCK)或更细粒度的 writeset 冲突检测,将无冲突的事务交由多个 worker 并行落库,这才真正把回放吞吐拉升到了生产可用的水平。
然而,大事务(如一次性更新几十万行)依然是并行的天然杀手:即便开启了多 worker,也挡不住单个庞大的 binlog 事件阻塞整条流水线。因此,在广告侧进行批量改价或刷状态等跑批操作时,务必在业务层切批以控制事务大小,绝不能让一笔大事务无端拉长整个集群的回放窗口。
回到刚才那条 UPDATE 路径,在 ROW 格式下,如果表没有主键或合适的唯一键,从库在回放更新时就不得不沿着叶子链表范围扫描大量行来定位目标数据,这会导致延迟飙升得非常离谱。正因如此,每张被复制的表都必须拥有主键,这不仅是关系型数据库的范式要求,更是线上复制场景不可逾越的硬纪律。
级联复制(A→B→C)曾用于跨机房,但排障复杂且延迟叠加;今日更常见的是主库扇出多个从,或用中间件与云产品做拓扑。计费库慎用「深度级联」当 HA 主路径。
并行复制调参直觉与踩坑经验:
- worker 数要匹配从库 CPU 与可并行事务比例;
- 单库内大表连写冲突多时,writeset 也救不了——还是要拆事务、降冲突;
- 从库若跑分析查询,请与「复制专用从」隔离,避免互相竞争 IO 资源。
3. 异步 vs 半同步
半同步保证的是「至少一从库收到并落 relay」,不是「已经应用完」——读从仍可能短暂落后。
| 模式 | 提交返回时机 | 主宕机风险 |
|---|---|---|
| 异步 | 本机提交完即可 | 未传出 binlog 可能丢 |
| 半同步 | 至少一个从库 ACK | 耐久更好;从库慢拖主库;超时降级 |
在半同步机制中,核心参数 rpl_semi_sync_master_wait_point 的取值(AFTER_SYNC 或 AFTER_COMMIT)决定了从库 ACK 与主库引擎提交的相对顺序,选型时必须严格对照版本文档核对行为。对于不容忍丢数据的计费或账本业务,我们强烈倾向于 双 1 + 半同步 的组合;而对于纯配置类的只读从库,虽然可以接受异步复制以换取极致性能,但架构上必须承认并容忍极端宕机时丢提交窗口的风险。
此外,必须通过监控持续观察半同步是否真的在工作:主库状态中应当能稳定看到 ACK 客户端数。如果因为网络抖动或从库磁盘打满导致频繁超时并降级成异步,这就等于你以为系统有耐久性保证,实则在裸奔——因此,务必对「降级次数」配置高优告警。当半同步阻塞在网络 IO 从而拖慢主库提交时,正确的做法是及时扩容从库或修复底层硬件,而不是默默忍受性能劣化。
对比前一种方案,全同步复制(等待所有从库或多数派应用完成)虽然一致性极高,但延迟往往扛不住。现代架构中,MGR(MySQL Group Replication)单主模式利用组复制协议在保证多数派一致性的同时实现了自动选主,其代价则是对事务冲突与网络抖动变得更加敏感。
4. 高可用拓扑
扩展容量(分片)不在本篇;这里只保证「一台逻辑主」的复制与切换。
4.1 传统主从 + 编排
MHA、Orchestrator、各类云厂商 RAS 的套路一致:探活 → 选最新从库 → 补齐差异 → 提升为主 → 改 VIP / DNS / 代理路由。要点:
- 切换演练要常做;
- 应用连接要能快速感知(短超时 + 重试 + 幂等);
- 脑裂防护:旧主必须 fence(杀连接、只读、摘路由);
- 探活不要只 ping 端口——要确认可接受复制进度与只读状态,避免「活着但已远远落后」的从被误提。
需要警惕的是,一旦发生误切换,其代价往往是集群出现短暂双主、GTID 分叉,甚至需要耗费数天时间来人工打平数据。正因如此,自动切换的阈值与人工确认的门槛必须按照数据库的关键等级严格区分:对于核心计费库,我们通常采用「自动置为只读 + 人工确认后升主」的保守策略;而对于容错率较高的配置库,则可以采用更激进的自动切换逻辑。
4.2 MGR / InnoDB Cluster
组复制基于多数派共识思想(与 Raft/Paxos 系列 同类问题),故障时自动选主。它适合希望少人工切换的中小集群,但也要注意网络分区时的可写性,以及多主模式下的冲突。广告配置库很多团队仍用「单主 + 半同步 + 编排」以求简单可控。
虽然 MGR 多主模式理论上支持多点可写,但当多个节点竞争同一资源导致冲突认证失败时,事务将被迫回滚。在广告业务中,「同行列高频修改预算」这类锁冲突极其密集,因此在实践里极少使用多主模式来扛计费写流量——相比之下,单主模式的事务排队与落盘行为更加可预期。
4.3 读写分离
Proxy(如 ProxySQL、云中间件)或应用层路由:UPDATE 走主,SELECT 走从。最大坑永远是:
写后立刻读从库 → 读到旧快照。
对策按场景组合:
- 关键读粘滞主库(会话级);
- 等 GTID / 读自己的写(wait for executed GTID);
- 业务接受短暂不一致(纯报表);
- 出价、开关、预算阈值这类,写后短窗口强制读主。
除了在代码层兜底,我们也可以在中间件层面实现「事务粘滞」:即同一个会话在发生写操作后的一段时间内,强制将其后续的读请求路由回主库。这种方案实现相对简单,但在连接池或多路复用场景下,必须严谨定义「会话」的边界,否则极易导致粘滞失效,或者将过多的读流量误打回主库,反而让主库扛不住。
三种常见架构组合(广告配置):
- 主 + 2 半同步从 + Proxy 读写分离:经典,因果读要代码兜底;
- MGR 单主 + Router:少人工切换,网络与冲突要评估;
- 云托管主从 + 只读地址:省运维,仍然逃不开写后读旧,SLO 自己定。
总而言之,架构选型不仅取决于团队的运维熟练度,更取决于业务「能不能接受短暂只读」。自动切换的策略越猛,就越要求应用层具备完善的幂等重试机制与连接回收能力。
5. GTID 与切换时要注意什么
GTID(全局事务 ID) 让「每个事务在集群里有唯一身份证」,好处:
- 换主、加点从库时,用
MASTER_AUTO_POSITION一类能力自动找位点,少手算 file / pos; - 应用可记录「我刚写下的 GTID」,再
WAIT_FOR_EXECUTED_GTID_SET做写后读从; - 排查「某个事务有没有上从库」更直观。
切换检查单(无论 MHA 还是人工):
- 确认旧主已 fence(只读或下线路由),防脑裂双写;
- 选中的新主 relay / 复制进度够新(半同步场景通常更安心);
- 代理 / DNS / VIP 原子切到新主;
- 应用连接排空与重试;幂等写避免「超时已成功又重放」;
- 从库重新挂到新主,并验证 GTID 集合一致。
虽然现代云托管 RDS 已经将上述很多繁琐步骤产品化,但需要强调的是,应用层幂等与连接失效处理仍然必须由业务代码来承担——这与我们在 高可用实践 中讨论的是同一套心智模型。
在真实的线上踩坑经历中,切主后最常见的「应用层症状」往往集中在以下几点:
- 连接池仍指向旧主 → 写入失败或写到只读;
- 重试把同一扣费打两次 → 无幂等键则双扣;
- DNS TTL 过长 → 流量切换迟滞。
正因如此,真正的高可用(HA)从来都不只是 DBA 手里的几段切换脚本,而是应用连接策略 + 幂等键兜底 + 全链路观测构成的三件套。
6. 延迟治理清单
- 开并行复制并验证 worker 数;
- 消灭超大事务与无主键表全表更新(无主键的 row 更新可能扫全表回放);
- 从库硬件不低于主太多(磁盘与 CPU);
- 半同步 ACK 慢时区分「网络 / 从库刷盘 / 主库等待」;
- 监控:
Seconds_Behind_Source、relay 积压、worker 活跃度、半同步状态与降级次数; - 只读从库别跑比主库更重的「暗跑全表分析」,否则自己制造延迟。
需要注意的是,Seconds_Behind_Source 只是一个估算值,在极端时钟跳变或多线程回放场景下可能会发生抖动;结合 relay 未应用字节数来综合判断会更加稳妥。
排障口诀:先看 IO 还是 SQL 积压;再查从库活跃大事务;对照主库 binlog 暴增;临时挪走分析流量;根治靠拆大事务与保主键。
至此,从底层存储到高可用架构,MySQL 内核 这条阅读路径已全部完结。让我们通过地图,回顾这一路走来的核心知识点。
系列地图(回顾)
| 篇 | 核心问题 |
|---|---|
| InnoDB 与 B+ 树 | 行怎么存、索引怎么找 |
| MVCC 与锁 | 并发怎么看见对的版本、冲突怎么串行 |
| redo/undo/binlog | 提交如何 crash-safe、如何可复制 |
| 执行计划 | 慢在哪、怎么改计划 |
| 本篇 | 副本怎么跟、主挂怎么切、读如何摊 |
参考
- MySQL 官方文档. Replication
- MySQL 官方文档. Semisynchronous Replication
- MySQL 官方文档. MySQL Group Replication
- MySQL 官方文档. Replication Channels / Multi-Threaded Slaves(并行复制相关章节随版本演进)
- MySQL 官方文档. GTID
- MHA / Orchestrator 等开源项目文档:故障切换实践。