主从复制原理与实战
MySQL 复制完全指南
MySQL 8.0 / 8.4
~45 分钟阅读
源码级深度
MySQL 复制是构建高可用、读扩展和灾备架构的基石。本章从 Binary Log 的写入机制出发,深入异步复制、半同步复制和组复制三种模式的内部原理;手把手配置 GTID 复制和多源复制;覆盖并行复制(MTS)的调优、复制延迟的定位与修复、故障切换的最佳实践;最后讲解 InnoDB Cluster + MySQL Router 的完整高可用栈和读写分离模式。全部内容基于 MySQL 8.0/8.4 源码分析与生产实践。
1. 复制概述
MySQL 复制的核心思想:源服务器(Source)将数据变更写入 Binary Log,副本服务器(Replica)拉取这些日志并重放。这一机制从 MySQL 3.23(2000年)引入,经过二十多年演进已发展出多种模式。
1.1 复制能解决什么问题?
| 场景 | 说明 | 典型拓扑 |
|---|---|---|
| 读扩展 | 将读请求分散到多个副本,减轻主库压力 | 1 Source + N Replicas |
| 高可用 | 主库故障时快速切换到副本 | Source + Standby + Witness |
| 灾备 | 跨数据中心异地复制 | 跨 DC 异步复制 |
| 在线备份 | 在副本上做备份,不影响主库性能 | 专用备份从库 |
| 数据分析 | 在从库上运行报表和分析查询 | 分析专用从库 |
| 滚动升级 | 先升级从库,切换后再升级旧主 | 版本轮替 |
1.2 复制模式演进时间线
| 版本 | 里程碑 |
|---|---|
| 3.23 (2000) | 基于 STATEMENT 的异步复制首次引入 |
| 5.1 (2008) | ROW 格式 binlog + MIXED 模式 |
| 5.5 (2010) | 半同步复制插件 |
| 5.6 (2013) | GTID + 多线程从库(schema 级并行) |
| 5.7 (2015) | Group Replication 插件 + 增强半同步 + 逻辑时钟并行 |
| 8.0 (2018) | InnoDB Cluster + MySQL Router + 基于 WriteSet 的并行复制 |
| 8.0.22+ | 异步连接故障转移 + Replica 术语重命名 |
| 8.4 (2024) | Replica 术语全面替代 Slave;Group Replication 增强 |
1.3 核心概念术语
MySQL 8.0.22+ 开始用 Source / Replica 替代旧的 Master / Slave 术语。本文统一使用新术语,但会注明旧命令名以兼容老版本。
| 新术语 | 旧术语 | 说明 |
|---|---|---|
| Source | Master | 写入数据变更到 binlog 的服务器 |
| Replica | Slave | 读取并重放 binlog 的服务器 |
| Binary Log (binlog) | - | 记录所有数据变更事件的日志 |
| Relay Log | - | Replica 本地缓存的 binlog 副本 |
| I/O Thread (Receiver) | I/O Thread | 负责从 Source 拉取 binlog |
| SQL Thread (Applier) | SQL Thread | 负责重放 relay log 中的事件 |
| GTID | - | 全局事务标识符 (server_uuid:seq_no) |
2. 复制架构深入
2.1 异步复制 (Asynchronous)
异步复制是 MySQL 的默认模式。Source 提交事务后不等待任何 Replica 的确认就返回给客户端。这意味着:
- 零性能开销:Source 的提交延迟不受网络往返影响
- 数据可能丢失:Source 崩溃时,已提交但尚未传输到 Replica 的事务会丢失
- 复制延迟不可控:Replica 可能落后 Source 任意时间
异步复制内部流程
## Source side (each transaction commit):
1. InnoDB prepares transaction (redo log flush)
2. Binary log group commit: FLUSH stage → SYNC stage → COMMIT stage
3. Binlog dump thread sends new events to connected replicas
-- Source code: sql/rpl_binlog_sender.cc → Binlog_sender::send_events()
## Replica side:
4. I/O Receiver thread connects to Source via COM_BINLOG_DUMP_GTID
-- Source code: sql/rpl_replica.cc → handle_slave_io()
5. Receives binlog events → writes to local Relay Log
-- Source code: sql/rpl_replica.cc → queue_event()
6. SQL Applier thread reads Relay Log → replays events
-- Source code: sql/rpl_rli.cc → apply_event_and_update_pos()
7. Updates relay_log.info (or mysql.slave_relay_log_info table) with position
源码参考:sql/binlog.cc (MYSQL_BIN_LOG::ordered_commit), sql/rpl_binlog_sender.cc, sql/rpl_replica.cc
2.2 半同步复制 (Semi-Synchronous)
半同步复制在异步和全同步之间取得平衡:Source 在提交事务后,等待至少一个 Replica 确认已接收到 binlog 事件后才返回给客户端。注意 Replica 只需确认"已收到"(写入 relay log),不需要"已执行"。
两种确认点(AFTER_SYNC vs AFTER_COMMIT)
AFTER_SYNC (无损半同步)
MySQL 5.7+ 默认。Source 在 SYNC 阶段完成后、COMMIT 阶段之前等待 Replica ACK。
- 其他客户端看不到未确认的事务(phantom read 安全)
- Source 崩溃后,已返回客户端的事务保证在至少一个 Replica 上存在
- 这是推荐的模式
AFTER_COMMIT (传统半同步)
MySQL 5.5-5.6 行为。Source 在 COMMIT 阶段完成后才等待 ACK。
- 其他客户端可能看到未确认的事务
- Source 崩溃后可能出现"幻影事务"——客户端看到了提交但 Replica 上没有
- 仅在需要兼容旧版本时使用
半同步复制配置
-- Source: 安装插件 / Install plugin
INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';
SET GLOBAL rpl_semi_sync_source_enabled = 1;
SET GLOBAL rpl_semi_sync_source_timeout = 1000; -- ms, 超时退化为异步 / fallback to async
SET GLOBAL rpl_semi_sync_source_wait_for_replica_count = 1;
SET GLOBAL rpl_semi_sync_source_wait_point = 'AFTER_SYNC'; -- 无损模式 / lossless
-- Replica: 安装插件 / Install plugin
INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';
SET GLOBAL rpl_semi_sync_replica_enabled = 1;
STOP REPLICA; START REPLICA;
-- 验证状态 / Verify status
SHOW STATUS LIKE 'Rpl_semi_sync_source_%';
-- Rpl_semi_sync_source_status = ON → 半同步生效
-- Rpl_semi_sync_source_no_tx = 0 → 异步退化次数
-- Rpl_semi_sync_source_yes_tx = 12345 → 半同步确认事务数
超时退化: 当 Replica 在
rpl_semi_sync_source_timeout毫秒内没有返回 ACK,Source 会自动退化为异步复制,不会阻塞写入。Replica 恢复后自动回到半同步模式。这一设计确保了可用性优先于一致性——在网络分区时 Source 继续服务,但需要 DBA 关注Rpl_semi_sync_source_no_tx指标。
源码参考:plugin/semisync/semisync_source.cc → ReplSemiSyncMaster::commitTrx()
2.3 三种模式对比
| 特性 | 异步 | 半同步 (AFTER_SYNC) | 组复制 |
|---|---|---|---|
| 数据安全 | 可能丢失 | 已确认的不丢 | 多数派确认 |
| 写延迟 | 最低 | +1 RTT | +共识协议开销 |
| 自动故障切换 | 无(需外部工具) | 无(需外部工具) | 有(内建) |
| 冲突检测 | 无 | 无 | 基于 Certification(WriteSet) |
| 多写支持 | 否 | 否 | 可选(multi-primary 模式) |
| 最大节点数 | 无限制 | 无限制 | 9 |
| 适用场景 | 读扩展、备份、分析 | 要求零数据丢失的主从 | 全自动 HA 集群 |
3. Binary Log 格式详解
Binary Log(binlog)是复制的数据载体。MySQL 支持三种 binlog 格式,每种在确定性、数据量和兼容性之间做出不同取舍:
3.1 STATEMENT 格式
记录执行的 SQL 语句本身。这是最古老的格式。
-- binlog 中记录的内容 / What's recorded in binlog:
UPDATE orders SET status = 'shipped' WHERE id = 12345;
-- 优势 / Advantages:
-- • 日志量小(一条 SQL 可能影响百万行)
-- • 易于审计和理解
-- 劣势 / Disadvantages:
-- • 非确定性函数问题:NOW(), UUID(), RAND(), FOUND_ROWS()
-- • 存储过程/触发器可能产生不一致
-- • INSERT ... SELECT 需要更多锁(防止不确定性)
-- 危险示例 / Dangerous example:
UPDATE t SET val = RAND() WHERE id > 100 LIMIT 10;
-- Source 和 Replica 选择的行可能不同!
-- Source and Replica may select different rows!
3.2 ROW 格式
记录每一行数据变更的前后镜像。MySQL 8.0 默认格式。
-- binlog 中记录的内容(伪代码)/ What's recorded (pseudo):
-- Table: orders (id=12345)
-- BEFORE: {id:12345, status:'pending', updated_at:'2026-04-19 10:00:00'}
-- AFTER: {id:12345, status:'shipped', updated_at:'2026-04-20 14:30:00'}
-- 优势 / Advantages:
-- • 完全确定性——任何函数、存储过程都安全
-- • 更少的锁竞争(不需要语句级锁保证确定性)
-- • 精确的行级别数据变更审计
-- 劣势 / Disadvantages:
-- • 日志量可能很大(UPDATE 100万行 = 100万行数据记录)
-- • 不可直接阅读(需要 mysqlbinlog --verbose 解码)
-- 优化选项 / Optimization:
-- binlog_row_image = MINIMAL → 只记录变更列 + PK(默认 FULL)
-- binlog_row_image = NOBLOB → 排除未变更的 BLOB 列
3.3 MIXED 格式
默认使用 STATEMENT,当 MySQL 检测到语句可能不安全时自动切换为 ROW。"不安全"的判断在 sql/sql_parse.cc 的 decide_logging_format() 中实现。
触发 ROW 切换的条件
- 使用
UUID(),RAND(),SYSDATE(),FOUND_ROWS()等非确定性函数 - 使用用户定义函数 (UDF)
INSERT ... SELECT涉及 AUTO_INCREMENT- 含有触发器或存储过程的 DML
LIMIT与无ORDER BY的UPDATE/DELETE- 临时表相关操作(在某些条件下)
3.4 格式对比与选择建议
| 维度 | STATEMENT | ROW | MIXED |
|---|---|---|---|
| 确定性 | 低(依赖执行上下文) | 完美 | 高(自动切换) |
| 日志大小 | 最小 | 最大 | 中等 |
| 可读性 | 直接可读 SQL | 需解码 | 部分可读 |
| 生产推荐 | 不推荐 | 首选 | 可接受 |
生产建议: 始终使用
binlog_format=ROW(MySQL 8.0 默认)。配合binlog_row_image=FULL(默认),可以使用mysqlbinlog --verbose进行数据恢复和审计。如果 binlog 磁盘压力大,考虑binlog_row_image=MINIMAL但注意这会限制闪回能力。
3.5 Binary Log Group Commit
为了减少 fsync 调用次数,MySQL 将多个事务的 binlog 写入合并为一次 fsync——这就是 Group Commit(组提交)。组提交分三个阶段(在 MYSQL_BIN_LOG::ordered_commit() 中实现):
## Group Commit 三阶段 / Three stages:
FLUSH stage: Leader 收集队列中所有等待的事务
将它们的 binlog events 写入 binlog 文件(write, 不 fsync)
Leader collects all waiting transactions in the queue
Writes their binlog events to the binlog file (write, no fsync)
SYNC stage: 对 binlog 文件执行一次 fsync(合并多个事务)
One fsync call for the binlog file (batching multiple transactions)
-- 由 binlog_group_commit_sync_delay (μs) 控制等待时间
-- 由 binlog_group_commit_sync_no_delay_count 控制最小批量大小
COMMIT stage: 按顺序调用各事务的 InnoDB commit
Calls InnoDB commit for each transaction in order
-- 关键参数 / Key parameters:
binlog_group_commit_sync_delay = 0 -- 默认不额外等待
binlog_group_commit_sync_no_delay_count = 0 -- 不设最小批量
-- 在高并发写入场景下,适当增大 sync_delay 可以提升吞吐量
-- In high-concurrency write scenarios, increasing sync_delay can improve throughput
源码参考:sql/binlog.cc → MYSQL_BIN_LOG::ordered_commit(), change_mem_root()
4. GTID 复制详解
GTID(Global Transaction Identifier) 是 MySQL 5.6 引入的全局事务标识机制,格式为 server_uuid:transaction_id。每个事务在整个复制拓扑中拥有唯一标识,这极大简化了复制管理。
4.1 GTID 工作原理
每个 MySQL 实例都有一个唯一的 server_uuid(在 auto.cnf 中自动生成)。当一个事务在 Source 上提交时:
- 分配一个 GTID,例如
3E11FA47-71CA-11E1-9E33-C80AA9429562:42 - GTID 被写入 binlog 作为事务的第一个事件 (
Gtid_log_event) - 事务提交后,GTID 加入
gtid_executed集合 - Replica 接收到该事务后,先检查 GTID 是否已在自己的
gtid_executed中——如果是则跳过(幂等性) - 执行后将 GTID 加入自己的
gtid_executed
-- 查看 GTID 状态 / Check GTID status
SELECT @@global.gtid_executed\G
-- 输出 / Output:
-- 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-12345,
-- A1B2C3D4-71CA-11E1-9E33-C80AA9429562:1-500
-- 查看已清理的 GTID / View purged GTIDs
SELECT @@global.gtid_purged\G
-- GTID 集合运算(MySQL 8.0 内置函数)/ GTID set operations
SELECT GTID_SUBTRACT(@@global.gtid_executed, '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-12340');
-- 结果:3E11FA47-...:12341-12345 (Replica 落后的事务)
SELECT GTID_SUBSET('3E11FA47-...:1-100', @@global.gtid_executed);
-- 判断一个 GTID 集合是否是另一个的子集 / Check if one GTID set is a subset
4.2 GTID 配置步骤(从零搭建)
步骤 1:Source 配置
# /etc/my.cnf (Source)
[mysqld]
server-id = 1 # 每个实例唯一 / unique per instance
log-bin = mysql-bin
binlog-format = ROW
gtid-mode = ON
enforce-gtid-consistency = ON
log-replica-updates = ON # 链式复制必须 / required for chain replication
binlog-expire-logs-seconds = 604800 # 7天 / 7 days
sync-binlog = 1
innodb-flush-log-at-trx-commit = 1 # 双1配置 / dual-1 for durability
步骤 2:创建复制用户
-- 在 Source 上执行 / Run on Source
CREATE USER 'repl'@'192.168.1.%' IDENTIFIED BY 'StrongPassword123!';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'192.168.1.%';
FLUSH PRIVILEGES;
步骤 3:Replica 配置
# /etc/my.cnf (Replica)
[mysqld]
server-id = 2 # 与 Source 不同 / different from Source
log-bin = mysql-bin # 级联复制需要 / needed for cascading
binlog-format = ROW
gtid-mode = ON
enforce-gtid-consistency = ON
log-replica-updates = ON
relay-log = relay-bin
relay-log-recovery = ON # 崩溃后自动清理损坏的 relay log
read-only = ON # 普通用户只读 / read-only for non-SUPER users
super-read-only = ON # SUPER 用户也只读 / read-only even for SUPER
replica-parallel-workers = 4 # 并行复制线程数 / parallel applier threads
步骤 4:初始化 Replica 数据
# 方法 A: mysqldump(适合小数据集 full_backup.sql
# 在 Replica 上导入 / Import on Replica:
mysql -u root -p < full_backup.sql
# 方法 B: xtrabackup(适合大数据集)
# Method B: xtrabackup (suitable for large datasets)
xtrabackup --backup --target-dir=/backup/full -u root -p
xtrabackup --prepare --target-dir=/backup/full
# 将备份拷贝到 Replica 的 datadir / Copy backup to Replica's datadir
xtrabackup --copy-back --target-dir=/backup/full
# 方法 C: CLONE 插件(MySQL 8.0.17+,最简单)
# Method C: CLONE plugin (MySQL 8.0.17+, simplest)
-- 在 Replica 上执行 / Run on Replica:
INSTALL PLUGIN clone SONAME 'mysql_clone.so';
SET GLOBAL clone_valid_donor_list = 'source_host:3306';
CLONE INSTANCE FROM 'repl'@'source_host':3306 IDENTIFIED BY 'StrongPassword123!';
步骤 5:启动复制
-- 在 Replica 上执行 / Run on Replica
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = 'source_host',
SOURCE_PORT = 3306,
SOURCE_USER = 'repl',
SOURCE_PASSWORD = 'StrongPassword123!',
SOURCE_AUTO_POSITION = 1, -- GTID 自动定位 / GTID auto-positioning
GET_SOURCE_PUBLIC_KEY = 1; -- caching_sha2_password 需要
START REPLICA;
-- 验证复制状态 / Verify replication status
SHOW REPLICA STATUS\G
-- 关键检查项 / Key checks:
-- Replica_IO_Running: Yes
-- Replica_SQL_Running: Yes
-- Seconds_Behind_Source: 0
-- Retrieved_Gtid_Set: 与 Source 的 gtid_executed 匹配
-- Executed_Gtid_Set: 持续增长
4.3 GTID 与传统位点复制对比
| 特性 | 传统位点 | GTID |
|---|---|---|
| 故障切换 | 需要手动计算 binlog 位点 | 自动定位 (SOURCE_AUTO_POSITION=1) |
| 切换 Source | 复杂且容易出错 | 简单:CHANGE REPLICATION SOURCE TO 即可 |
| 跳过错误 | SET GLOBAL sql_replica_skip_counter = 1 |
注入空事务 / Inject empty transaction |
| 一致性验证 | 只能比对数据 | GTID 集合对比即可初步判断 |
GTID 跳过错误事务
-- 假设需要跳过 Source 的 GTID: 3E11FA47-...:100
-- Suppose we need to skip Source's GTID: 3E11FA47-...:100
STOP REPLICA;
SET GTID_NEXT = '3E11FA47-71CA-11E1-9E33-C80AA9429562:100';
BEGIN; COMMIT; -- 注入空事务 / inject empty transaction
SET GTID_NEXT = 'AUTOMATIC';
START REPLICA;
enforce_gtid_consistency 限制: 开启 GTID 后,以下操作被禁止:(1) 事务内混合 InnoDB 和 MyISAM 表的 DML;(2)
CREATE TABLE ... SELECT(MySQL 8.0.21 之前);(3) 事务内使用CREATE TEMPORARY TABLE。这些限制是为了保证每个 GTID 对应唯一的原子事务。如果你的应用使用了这些模式,需要先改造再启用 GTID。
源码参考:sql/rpl_gtid.h (Gtid_set), sql/rpl_gtid_persist.cc, sql/rpl_replica.cc → get_source_uuid()
5. 多源复制 (Multi-Source Replication)
多源复制允许一个 Replica 同时从多个 Source 接收复制数据。典型场景是将多个分库的数据聚合到一个中心节点用于分析和报表。MySQL 5.7+ 原生支持,通过命名**复制通道(Replication Channel)**区分不同 Source。
5.1 配置步骤
-- Replica: 配置通道 A(连接 Source-A)/ Configure channel A (connect to Source-A)
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = 'source-a.example.com',
SOURCE_PORT = 3306,
SOURCE_USER = 'repl',
SOURCE_PASSWORD = 'PasswordA!',
SOURCE_AUTO_POSITION = 1
FOR CHANNEL 'source_a';
-- Replica: 配置通道 B(连接 Source-B)/ Configure channel B (connect to Source-B)
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = 'source-b.example.com',
SOURCE_PORT = 3306,
SOURCE_USER = 'repl',
SOURCE_PASSWORD = 'PasswordB!',
SOURCE_AUTO_POSITION = 1
FOR CHANNEL 'source_b';
-- 启动所有通道 / Start all channels
START REPLICA;
-- 或者单独启动 / Or start individually:
START REPLICA FOR CHANNEL 'source_a';
START REPLICA FOR CHANNEL 'source_b';
-- 查看特定通道状态 / Check specific channel status
SHOW REPLICA STATUS FOR CHANNEL 'source_a'\G
SHOW REPLICA STATUS FOR CHANNEL 'source_b'\G
-- performance_schema 中的通道信息 / Channel info in performance_schema
SELECT * FROM performance_schema.replication_connection_status;
SELECT * FROM performance_schema.replication_applier_status_by_worker;
5.2 多源复制注意事项
- 库/表名冲突:不同 Source 上的相同库名会写入 Replica 的同一个库。使用复制过滤器 (
REPLICATE_REWRITE_DB) 或让每个 Source 使用不同的库名 - GTID 冲突:每个 Source 必须有不同的
server_uuid,GTID 自然不会冲突 - AUTO_INCREMENT 冲突:多个 Source 的表如果使用 AUTO_INCREMENT,可能产生主键冲突。使用
auto_increment_increment和auto_increment_offset错开,或使用 UUID 主键 - 通道独立性:每个通道有独立的 I/O 线程和 SQL 线程,一个通道故障不影响其他通道
- 仓库类型:必须使用
master_info_repository = TABLE和relay_log_info_repository = TABLE(MySQL 8.0 默认即为 TABLE)
6. 复制过滤器
复制过滤器允许 Replica 选择性地复制特定的库或表。过滤可以在两个层面进行:
| 过滤层 | 参数 | 说明 |
|---|---|---|
| Source 端 (Binlog) | binlog-do-db |
只为指定库生成 binlog(基于 USE db 当前库判断) |
| Source 端 (Binlog) | binlog-ignore-db |
忽略指定库的 binlog |
| Replica 端 (SQL Thread) | replicate-do-db |
只应用指定库的事件 |
| Replica 端 (SQL Thread) | replicate-ignore-db |
忽略指定库的事件 |
| Replica 端 (SQL Thread) | replicate-do-table |
只应用指定表的事件 |
| Replica 端 (SQL Thread) | replicate-ignore-table |
忽略指定表的事件 |
| Replica 端 (SQL Thread) | replicate-wild-do-table |
通配符匹配表名 |
| Replica 端 (SQL Thread) | replicate-rewrite-db |
库名重写 (from->to) |
过滤器陷阱:
binlog-do-db和replicate-do-db在 STATEMENT 格式下基于USE db当前库判断,而不是基于表的实际所在库。例如执行USE app; UPDATE logs.audit SET ...时:如果过滤器是replicate-do-db=logs,该语句会被忽略(因为当前库是app)。在 ROW 格式下则基于表的实际库名过滤,行为更符合预期。这是推荐 ROW 格式的又一个理由。
-- MySQL 8.0.22+ 在线设置过滤器(无需重启)/ Online filter setup (no restart)
STOP REPLICA SQL_THREAD FOR CHANNEL 'source_a';
CHANGE REPLICATION FILTER
REPLICATE_DO_DB = (orders, inventory)
FOR CHANNEL 'source_a';
START REPLICA SQL_THREAD FOR CHANNEL 'source_a';
-- 查看当前过滤器 / View current filters
SELECT * FROM performance_schema.replication_applier_filters;
SELECT * FROM performance_schema.replication_applier_global_filters;
7. 并行复制 (Multi-Threaded Applier)
单线程的 SQL Applier 是复制延迟的最大瓶颈。为了加速回放,MySQL 支持多线程并行应用 relay log 中的事务。并行粒度从粗到细经历了三代演进:
7.1 并行策略对比
| 策略 | 版本 | replica_parallel_type |
并行粒度 | 限制 |
|---|---|---|---|---|
| 按库并行 | 5.6+ | DATABASE |
不同库的事务可并行 | 单库写入多时退化为单线程 |
| 逻辑时钟并行 | 5.7+ | LOGICAL_CLOCK |
同一 Group Commit 批次内的事务可并行 | Source 并发低时批次小 |
| WriteSet 并行 | 8.0+ | LOGICAL_CLOCK + transaction_write_set_extraction |
不冲突的事务可并行(即使不在同一批次) | 需要额外内存跟踪 WriteSet |
7.2 并行复制配置
# /etc/my.cnf (Replica)
[mysqld]
# 并行 worker 线程数(推荐 = CPU 核心数 / 2 到核心数)
# Parallel worker threads (recommended = CPU cores / 2 to cores)
replica-parallel-workers = 16
# 并行类型:LOGICAL_CLOCK(MySQL 8.0 默认)
replica-parallel-type = LOGICAL_CLOCK
# 保持提交顺序(默认 ON)——保证从库读一致性
# Preserve commit order (default ON) — ensures read consistency on replica
replica-preserve-commit-order = ON
# Source 端优化:增大 group commit 窗口提升 Replica 并行度
# Source optimization: increase group commit window for better Replica parallelism
binlog-group-commit-sync-delay = 100 -- μs
binlog-group-commit-sync-no-delay-count = 20
# WriteSet 追踪(Source 端启用)
# WriteSet tracking (enable on Source)
binlog-transaction-dependency-tracking = WRITESET
transaction-write-set-extraction = XXHASH64
7.3 WriteSet 并行原理
WriteSet 并行的核心思想:如果两个事务修改的行没有交集(WriteSet 没有交集),它们在 Replica 上可以安全地并行执行,即使它们不在 Source 上的同一个 Group Commit 批次中。
具体实现:
- Source 端:每个事务计算其修改行的主键/唯一键哈希值集合(WriteSet)
- 在 binlog 中记录每个事务的
last_committed(逻辑时钟序号)和sequence_number - 如果当前事务的 WriteSet 与最近
binlog_transaction_dependency_history_size个事务都没有交集,则将last_committed设为更早的值——允许更多并行 - Replica 端:
last_committed相同的事务可以并行执行
-- 查看 binlog 中的并行信息 / View parallelism info in binlog
$ mysqlbinlog --verbose mysql-bin.000042 | grep -A2 "last_committed"
-- 输出示例 / Example output:
#260420 14:30:01 ... last_committed=100 sequence_number=105
#260420 14:30:01 ... last_committed=100 sequence_number=106
#260420 14:30:01 ... last_committed=100 sequence_number=107
#260420 14:30:01 ... last_committed=105 sequence_number=108
-- 事务 105, 106, 107 可以并行(last_committed 相同)
-- Transactions 105, 106, 107 can run in parallel (same last_committed)
-- 事务 108 必须等 105 完成后才能开始
-- Transaction 108 must wait for 105 to complete
7.4 并行复制监控
-- 查看各 Worker 线程状态 / View worker thread status
SELECT WORKER_ID, LAST_APPLIED_TRANSACTION,
APPLYING_TRANSACTION, LAST_ERROR_NUMBER, LAST_ERROR_MESSAGE
FROM performance_schema.replication_applier_status_by_worker;
-- 查看协调器状态 / View coordinator status
SELECT * FROM performance_schema.replication_applier_status_by_coordinator;
-- 衡量并行效率:比较 worker 空闲时间 / Measure parallelism efficiency
SELECT WORKER_ID,
LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP,
LAST_APPLIED_TRANSACTION_START_APPLY_TIMESTAMP,
TIMESTAMPDIFF(MICROSECOND,
LAST_APPLIED_TRANSACTION_START_APPLY_TIMESTAMP,
LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP) AS apply_us
FROM performance_schema.replication_applier_status_by_worker
WHERE WORKER_ID > 0;
源码参考:sql/rpl_rli_pdb.cc (Slave_worker), sql/rpl_replica.cc → slave_start_workers()
8. 复制延迟监控与排障
8.1 延迟的度量方式
| 方法 | 命令/工具 | 精度 | 注意事项 |
|---|---|---|---|
Seconds_Behind_Source |
SHOW REPLICA STATUS |
秒级 | 基于 SQL 线程正在处理的事件时间戳与当前时间的差值。I/O 线程断开时显示 NULL。长事务期间可能突然跳变。不准确。 |
| GTID 差集 | GTID_SUBTRACT() |
事务级 | 只能看到"落后多少事务",不能直接换算为时间 |
| 心跳检测 | pt-heartbeat | 亚秒级 | 最可靠的方法。Source 定期写入时间戳,Replica 读取并计算延迟 |
| performance_schema | 复制表 | 微秒级 | MySQL 8.0 原生高精度时间戳 |
使用 pt-heartbeat(推荐)
# 在 Source 上创建心跳表并启动守护进程
# Create heartbeat table on Source and start daemon
pt-heartbeat --database=percona --create-table \
--update --daemonize -h source_host -u monitor -p
# 在 Replica 上监控延迟
# Monitor lag on Replica
pt-heartbeat --database=percona --monitor -h replica_host -u monitor -p
# 输出 / Output:
# 0.05s [ 0.03s, 0.04s, 0.03s ] ← 当前/1m/5m/15m 平均延迟
# 或者检查一次 / Or check once:
pt-heartbeat --database=percona --check -h replica_host -u monitor -p
# 输出: 0.05
使用 performance_schema 高精度监控
-- MySQL 8.0 高精度复制延迟 / MySQL 8.0 high-precision lag
SELECT
CHANNEL_NAME,
LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP AS last_apply_ts,
LAST_APPLIED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP AS source_commit_ts,
TIMESTAMPDIFF(MICROSECOND,
LAST_APPLIED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP,
LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP
) / 1000000.0 AS lag_seconds
FROM performance_schema.replication_applier_status_by_worker
WHERE LAST_APPLIED_TRANSACTION != ''
ORDER BY lag_seconds DESC
LIMIT 1;
8.2 复制延迟常见原因与修复
| 原因 | 诊断方法 | 解决方案 |
|---|---|---|
| 单线程 SQL 回放 | replica_parallel_workers = 0 或 1 |
启用并行复制 (workers = 16, LOGICAL_CLOCK + WRITESET) |
| Source 大事务 | binlog 中出现超大 Write_rows 事件 | 拆分大批量操作为小批次(每次 1000-5000 行) |
| Replica 缺少索引 | ROW 格式下 Replica 逐行查找 PK,无索引则全表扫描 | 确保 Replica 表结构与 Source 一致 |
| Replica 磁盘 I/O 瓶颈 | iostat 显示磁盘利用率接近 100% |
升级磁盘(SSD/NVMe),或 Replica 使用 flush=2, sync_binlog=0 |
| Replica 上的查询竞争锁 | SQL Applier 等待行锁 (lock wait timeout) | 避免在 Replica 上运行长事务/大查询;或使用专用分析从库 |
| 网络延迟 / 带宽不足 | I/O 线程的 Retrieved_Gtid_Set 远落后于 Source | 优化网络 / 启用 binlog 压缩 (binlog_transaction_compression=ON, 8.0.20+) |
| DDL 阻塞 | 一个大表的 ALTER TABLE 阻塞所有后续事务的回放 | 使用 pt-online-schema-change 或 gh-ost 做在线 DDL |
8.3 Binlog 压缩(MySQL 8.0.20+)
-- Source 端启用 / Enable on Source
SET GLOBAL binlog_transaction_compression = ON;
SET GLOBAL binlog_transaction_compression_level_zstd = 3; -- 1-22, 默认3
-- 监控压缩效果 / Monitor compression effectiveness
SELECT *
FROM performance_schema.binary_log_transaction_compression_stats\G
-- COMPRESSION_TYPE: ZSTD
-- TRANSACTION_COUNT: 50000
-- COMPRESSED_BYTES_COUNTER: 125000000
-- UNCOMPRESSED_BYTES_COUNTER: 500000000 ← 75% 压缩率
9. 故障切换流程
9.1 计划内切换 (Planned Switchover)
计划内切换用于维护窗口(升级、硬件更换等),可以做到零数据丢失。
## 步骤 1: 停止 Source 写入 / Step 1: Stop writes on Source
SET GLOBAL read_only = ON;
SET GLOBAL super_read_only = ON;
## 步骤 2: 等待 Replica 追上 / Step 2: Wait for Replica to catch up
-- 在 Replica 上检查 / Check on Replica:
SELECT GTID_SUBTRACT(
(SELECT @@global.gtid_executed), -- Replica 已执行的
(SELECT @@global.gtid_executed) -- 这里应传入 Source 的值
);
-- 当结果为空集时,Replica 已完全追上
-- When result is empty set, Replica is fully caught up
-- 或更简单 / Or simpler:
SHOW REPLICA STATUS\G
-- 等待 Seconds_Behind_Source = 0 且 Retrieved = Executed GTID Set
## 步骤 3: 停止 Replica 的复制 / Step 3: Stop replication on Replica
STOP REPLICA;
RESET REPLICA ALL; -- 清除复制配置 / Clear replication config
## 步骤 4: 提升 Replica 为新 Source / Step 4: Promote Replica to new Source
SET GLOBAL read_only = OFF;
SET GLOBAL super_read_only = OFF;
## 步骤 5: 将旧 Source 配置为新 Replica / Step 5: Configure old Source as new Replica
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = 'new_source_host',
SOURCE_USER = 'repl',
SOURCE_PASSWORD = '...',
SOURCE_AUTO_POSITION = 1;
SET GLOBAL read_only = ON;
START REPLICA;
## 步骤 6: 更新应用端连接 / Step 6: Update application connections
-- VIP 漂移 / DNS 切换 / ProxySQL 后端切换
-- VIP drift / DNS switch / ProxySQL backend switch
9.2 非计划故障切换 (Unplanned Failover)
Source 突然宕机时的紧急切换。关键在于选择数据最完整的 Replica 作为新 Source:
## 步骤 1: 确认 Source 确实不可恢复 / Step 1: Confirm Source is truly unrecoverable
-- 避免脑裂:确保旧 Source 不会自行恢复后继续接受写入
-- Avoid split-brain: ensure old Source won't recover and accept writes
## 步骤 2: 找到最新的 Replica / Step 2: Find the most up-to-date Replica
-- 在所有 Replica 上执行 / Run on all Replicas:
SELECT @@global.gtid_executed;
-- 选择 gtid_executed 最大(事务最多)的 Replica
-- Pick the Replica with the largest gtid_executed (most transactions)
## 步骤 3: 等待候选 Replica 应用完 relay log / Step 3: Let candidate finish relay log
STOP REPLICA IO_THREAD;
-- 等待 SQL Thread 处理完剩余的 relay log
-- Wait until SQL Thread finishes remaining relay log
## 步骤 4: 提升为新 Source / Step 4: Promote to new Source
STOP REPLICA;
RESET REPLICA ALL;
SET GLOBAL read_only = OFF;
SET GLOBAL super_read_only = OFF;
## 步骤 5: 将其他 Replica 指向新 Source / Step 5: Point other Replicas to new Source
-- GTID 模式下非常简单 / Very simple with GTID:
STOP REPLICA;
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = 'new_source_host',
SOURCE_AUTO_POSITION = 1;
START REPLICA;
Errant Transaction 问题: 如果某个 Replica 上有"误操作事务"(直接在 Replica 上写入产生的 GTID,不存在于 Source 上),在故障切换后这些事务会被推送给其他 Replica,可能导致不一致。预防方法:(1) 始终开启
super_read_only=ON;(2) 使用GTID_SUBTRACT()定期检查 errant GTID。
-- 检查 Errant GTID / Check for Errant GTIDs
-- 在每个 Replica 上运行:/ Run on each Replica:
SELECT GTID_SUBTRACT(
@@global.gtid_executed,
'source_uuid:1-N' -- 替换为 Source 的 server_uuid 和 GTID 范围
) AS errant_gtids;
-- 如果结果非空,说明有 errant GTID!
-- If result is non-empty, errant GTIDs exist!
9.3 自动故障切换工具
| 工具 | 维护方 | 特点 |
|---|---|---|
| InnoDB Cluster / Group Replication | Oracle (官方) | 内建共识协议,自动选主,无需外部工具 |
| Orchestrator | GitHub / Shlomi Noach | 拓扑可视化,智能故障检测和切换,支持伪 GTID |
| MHA (Master High Availability) | Yoshinori Matsunobu | 经典工具,支持非 GTID 模式,通过 relay log 差异补偿 |
| ProxySQL | ProxySQL Inc. | 智能代理 + 故障检测 + 读写分离 |
| Vitess | PlanetScale / CNCF | 分片 + 复制管理 + 自动故障切换(YouTube 规模验证) |
10. 组复制 (Group Replication)
Group Replication (GR) 是 MySQL 5.7.17+ 引入的基于 **Paxos 变种协议(XCom)**的多节点复制方案。与传统主从不同,GR 提供了:
- 自动故障检测和选主:节点宕机后自动重新选主,无需外部工具
- 事务冲突检测(Certification):在提交前检测写冲突,防止脏写
- 多数派确认:事务必须被多数节点确认后才提交
10.1 Single-Primary vs Multi-Primary
Single-Primary (推荐)
- 只有一个节点接受写入,其他为只读
- 无写冲突可能,性能更可预测
- Primary 故障后自动选新 Primary
- 与传统主从架构的应用兼容性最好
- 适合 99% 的场景
Multi-Primary
- 所有节点都可以接受写入
- 写冲突时后提交的事务被回滚
- 不支持外键级联、序列化隔离级别
- 跨节点的 DDL 操作需要额外协调
- 适合写入分散、冲突概率低的场景
10.2 Certification(冲突检测)原理
当一个节点要提交事务时:
- 事务在本地执行(乐观执行),但不提交
- 提取事务的 WriteSet(修改的行的 PK/UK 哈希集合)和 ReadSet
- 将 WriteSet + 事务内容通过 XCom 协议广播给所有节点
- 每个节点执行 Certification:检查该 WriteSet 是否与最近已认证的事务冲突
- 如果无冲突且多数节点确认 → CERTIFY_OK,提交
- 如果有冲突 → CERTIFY_FAIL,事务在发起节点被回滚
源码参考:plugin/group_replication/src/certifier.cc → Certifier::certify()
10.3 Group Replication 配置(3 节点 Single-Primary)
# /etc/my.cnf — 每个节点都需要(修改 server-id 和地址)
# /etc/my.cnf — required on each node (change server-id and address)
[mysqld]
server-id = 1 # 每个节点不同: 1, 2, 3
log-bin = mysql-bin
binlog-format = ROW
gtid-mode = ON
enforce-gtid-consistency = ON
log-replica-updates = ON
binlog-checksum = NONE # GR 要求 / Required by GR
# Group Replication 配置 / Group Replication settings
plugin_load_add = group_replication.so
group_replication_group_name = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" # UUID 格式
group_replication_local_address = "node1:33061" # GR 通信端口(非 MySQL 端口)
group_replication_group_seeds = "node1:33061,node2:33061,node3:33061"
group_replication_start_on_boot = OFF # 首次手动启动 / Manual start first time
group_replication_single_primary_mode = ON # Single-Primary 模式
group_replication_bootstrap_group = OFF
# 安全与认证 / Security & authentication
group_replication_recovery_use_ssl = ON
group_replication_ssl_mode = REQUIRED
-- Node 1: 引导集群 / Bootstrap the group
SET GLOBAL group_replication_bootstrap_group = ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group = OFF;
-- Node 2, Node 3: 加入集群 / Join the group
START GROUP_REPLICATION;
-- 验证集群状态 / Verify cluster status
SELECT MEMBER_ID, MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
-- +------+-------+------+---------+--------+
-- | ... | node1 | 3306 | ONLINE | PRIMARY|
-- | ... | node2 | 3306 | ONLINE | SECONDARY|
-- | ... | node3 | 3306 | ONLINE | SECONDARY|
-- +------+-------+------+---------+--------+
10.4 Group Replication 限制
- 最多 9 个节点(Paxos 协议限制)
- 所有表必须使用 InnoDB 且必须有主键或非空唯一键
- 不支持 gap lock(REPEATABLE READ 下的间隙锁在 Multi-Primary 中不工作)
- 大事务限制:超过
group_replication_transaction_size_limit(默认 150MB)的事务会被拒绝 - 网络敏感:节点间网络延迟应 < 10ms(同一数据中心或同一区域)
- 写吞吐量:受限于共识协议开销,单节点异步复制的写吞吐量更高
11. InnoDB Cluster
InnoDB Cluster 是 MySQL 官方的完整高可用解决方案,由三个组件组成:
| 组件 | 作用 |
|---|---|
| Group Replication | 底层复制引擎,提供数据同步和自动故障检测 |
| MySQL Shell | 管理工具,通过 AdminAPI 简化集群配置和管理操作 |
| MySQL Router | 智能代理,自动路由读写请求到正确的节点 |
11.1 使用 MySQL Shell 创建集群
# 使用 MySQL Shell 连接到第一个节点 / Connect to first node
$ mysqlsh root@node1:3306
// 检查实例是否满足 GR 要求 / Check if instance meets GR requirements
mysql-js> dba.checkInstanceConfiguration('root@node1:3306')
// 自动修复配置问题 / Auto-fix configuration issues
mysql-js> dba.configureInstance('root@node1:3306')
// 创建集群(在 node1 上引导)/ Create cluster (bootstrap on node1)
mysql-js> var cluster = dba.createCluster('myCluster')
// 添加其他节点 / Add other nodes
mysql-js> cluster.addInstance('root@node2:3306')
mysql-js> cluster.addInstance('root@node3:3306')
// 查看集群状态 / Check cluster status
mysql-js> cluster.status()
// {
// "clusterName": "myCluster",
// "defaultReplicaSet": {
// "topology": {
// "node1:3306": { "status": "ONLINE", "memberRole": "PRIMARY" },
// "node2:3306": { "status": "ONLINE", "memberRole": "SECONDARY" },
// "node3:3306": { "status": "ONLINE", "memberRole": "SECONDARY" }
// }
// }
// }
// 手动切换 Primary / Manual Primary switchover
mysql-js> cluster.setPrimaryInstance('root@node2:3306')
// 从集群中移除节点 / Remove node from cluster
mysql-js> cluster.removeInstance('root@node3:3306')
11.2 InnoDB ClusterSet(跨数据中心)
MySQL 8.0.27+ 引入 InnoDB ClusterSet,允许在不同数据中心部署多个 InnoDB Cluster 副本,通过异步复制连接。主集群(Primary Cluster)处理写入,副本集群(Replica Cluster)提供灾备和本地读取。
// 创建 ClusterSet / Create ClusterSet
mysql-js> var cs = cluster.createClusterSet('myClusterSet')
// 在远端 DC 创建 Replica Cluster / Create Replica Cluster in remote DC
mysql-js> var replicaCluster = cs.createReplicaCluster(
'root@dc2-node1:3306', 'dc2Cluster')
mysql-js> replicaCluster.addInstance('root@dc2-node2:3306')
mysql-js> replicaCluster.addInstance('root@dc2-node3:3306')
// 灾难切换:提升 Replica Cluster 为 Primary / DR switchover
mysql-js> cs.setPrimaryCluster('dc2Cluster')
// 查看 ClusterSet 状态 / View ClusterSet status
mysql-js> cs.status({extended: 1})
12. MySQL Router
MySQL Router 是轻量级中间件代理,通常与应用服务器部署在同一主机上。它从 InnoDB Cluster 的元数据自动获取拓扑信息,将读写请求路由到正确的节点。
12.1 引导 Router
# 引导 Router(自动生成配置)/ Bootstrap Router (auto-generates config)
$ mysqlrouter --bootstrap root@node1:3306 \
--directory /opt/mysqlrouter \
--conf-use-sockets \
--account routeruser
# 启动 / Start
$ /opt/mysqlrouter/start.sh
# 默认监听端口 / Default listening ports:
# 6446 — Read/Write (Classic protocol) → 路由到 Primary
# 6447 — Read-Only (Classic protocol) → 轮询到 Secondary
# 6448 — Read/Write (X protocol)
# 6449 — Read-Only (X protocol)
# 8443 — REST API
12.2 Router 配置细节
# /opt/mysqlrouter/mysqlrouter.conf(自动生成,可调整)
[routing:myCluster_rw]
bind_address = 0.0.0.0
bind_port = 6446
destinations = metadata-cache://myCluster/?role=PRIMARY
routing_strategy = first-available
protocol = classic
max_connections = 1024
connection_sharing = 1 # MySQL 8.0.33+ 连接共享
[routing:myCluster_ro]
bind_address = 0.0.0.0
bind_port = 6447
destinations = metadata-cache://myCluster/?role=SECONDARY
routing_strategy = round-robin-with-fallback
protocol = classic
max_connections = 2048
[metadata_cache:myCluster]
cluster_type = gr
router_id = 1
user = routeruser
metadata_cluster = myCluster
ttl = 0.5 # 元数据刷新间隔(秒)
auth_cache_ttl = 300
auth_cache_refresh_interval = 2
12.3 Router 监控
# REST API 监控 / REST API monitoring
$ curl -s http://localhost:8443/api/20190715/routes | python3 -m json.tool
# 查看活跃连接 / View active connections
$ curl -s http://localhost:8443/api/20190715/routes/myCluster_rw/connections
# 查看后端健康状态 / View backend health
$ curl -s http://localhost:8443/api/20190715/routes/myCluster_rw/destinations
13. 读写分离模式
读写分离是利用复制架构提升读吞吐量的核心策略。根据实现层级不同,有以下几种模式:
13.1 实现模式对比
| 模式 | 实现方式 | 优势 | 劣势 |
|---|---|---|---|
| 应用层 | 代码中区分读写数据源 | 最灵活,可精确控制哪些查询走从库 | 侵入性强,需要改代码 |
| 框架层 | Spring AbstractRoutingDataSource, Django db router |
透明于业务代码 | 事务内的读必须走主库 |
| 代理层 | ProxySQL / MySQL Router / MaxScale | 完全透明,支持查询重写和缓存 | 增加网络一跳延迟 |
| 驱动层 | MySQL Connector/J jdbc:mysql:replication:// |
零代码修改,驱动自动路由 | 语言/驱动特定 |
13.2 ProxySQL 读写分离配置
-- ProxySQL Admin 接口(默认端口 6032)/ ProxySQL Admin (default port 6032)
-- 添加后端服务器 / Add backend servers
INSERT INTO mysql_servers (hostgroup_id, hostname, port, max_connections, weight)
VALUES
(10, 'source', 3306, 200, 1000), -- HG 10: 写组 / Write group
(20, 'replica1', 3306, 200, 500), -- HG 20: 读组 / Read group
(20, 'replica2', 3306, 200, 500);
-- 配置用户 / Configure user
INSERT INTO mysql_users (username, password, default_hostgroup)
VALUES ('app', 'AppPassword!', 10);
-- 读写分离规则 / Read-write splitting rules
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES
(1, 1, '^SELECT .* FOR UPDATE', 10, 1), -- FOR UPDATE → 写组
(2, 1, '^SELECT', 20, 1), -- 普通 SELECT → 读组
(3, 1, '^(INSERT|UPDATE|DELETE|REPLACE)', 10, 1); -- 写操作 → 写组
-- 配置复制延迟检查 / Configure replication lag check
UPDATE mysql_servers SET max_replication_lag = 1
WHERE hostgroup_id = 20; -- 延迟 >1s 的从库自动摘除 / Auto-remove replicas with >1s lag
-- 加载配置到运行时 / Load config to runtime
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL USERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
SAVE MYSQL USERS TO DISK;
SAVE MYSQL QUERY RULES TO DISK;
13.3 读写分离的一致性问题
读写分离的核心难题是复制延迟导致的读一致性问题:用户写入数据后立即读取,可能从延迟的从库读到旧数据("写后读不一致")。
解决方案
| 策略 | 实现 | 适用场景 |
|---|---|---|
| 写后读走主库 | 写操作后 N 秒内的读强制路由到主库 | 简单,大多数场景够用 |
| GTID 等待 | 写入后记录 GTID,读从库前调用 WAIT_FOR_EXECUTED_GTID_SET() |
精确,延迟最小化 |
| 因果一致性 (MySQL Router 8.0.27+) | Router 自动追踪写入 GTID,路由到已应用该 GTID 的从库 | 零代码改动,与 InnoDB Cluster 集成 |
-- GTID 等待示例 / GTID wait example
-- 写入后,获取当前 GTID 位点 / After write, get current GTID position
-- (应用代码记住这个值 / application code remembers this value)
SELECT @@global.gtid_executed AS write_gtid;
-- 在从库上读取前,等待 GTID 回放到位 / On replica, wait for GTID before reading
SELECT WAIT_FOR_EXECUTED_GTID_SET('3E11FA47-...:1-12345', 1);
-- 第二个参数是超时秒数。返回 0 表示成功,1 表示超时
-- Second parameter is timeout in seconds. Returns 0 on success, 1 on timeout
-- 超时则 fallback 到主库读取 / On timeout, fall back to reading from primary
14. 生产环境配置模板
14.1 Source (主库) 完整配置
# /etc/my.cnf — Source (Primary) 生产配置
# Production configuration for Source (Primary)
[mysqld]
# === 基本标识 / Identity ===
server-id = 1
report-host = source.example.com
report-port = 3306
# === Binary Log / GTID ===
log-bin = /var/lib/mysql/binlog/mysql-bin
binlog-format = ROW
binlog-row-image = FULL
binlog-expire-logs-seconds = 604800 # 7 days
max-binlog-size = 256M
sync-binlog = 1 # 每次提交 fsync / fsync on every commit
gtid-mode = ON
enforce-gtid-consistency = ON
log-replica-updates = ON
binlog-checksum = CRC32
binlog-transaction-compression = ON # 8.0.20+
# === 并行复制优化 / Parallel replication optimization ===
binlog-transaction-dependency-tracking = WRITESET
transaction-write-set-extraction = XXHASH64
binlog-group-commit-sync-delay = 100 # μs
binlog-group-commit-sync-no-delay-count = 20
# === 半同步复制 / Semi-sync replication ===
plugin-load-add = semisync_source.so
rpl-semi-sync-source-enabled = 1
rpl-semi-sync-source-timeout = 1000 # ms
rpl-semi-sync-source-wait-point = AFTER_SYNC
rpl-semi-sync-source-wait-for-replica-count = 1
# === InnoDB 持久化 / InnoDB durability ===
innodb-flush-log-at-trx-commit = 1 # 双1 / dual-1
innodb-flush-method = O_DIRECT
# === 连接与安全 / Connection & security ===
max-connections = 500
require-secure-transport = ON
14.2 Replica (从库) 完整配置
# /etc/my.cnf — Replica 生产配置
# Production configuration for Replica
[mysqld]
# === 基本标识 / Identity ===
server-id = 2
report-host = replica1.example.com
# === Binary Log / GTID ===
log-bin = /var/lib/mysql/binlog/mysql-bin
binlog-format = ROW
gtid-mode = ON
enforce-gtid-consistency = ON
log-replica-updates = ON
# === Relay Log ===
relay-log = /var/lib/mysql/relaylog/relay-bin
relay-log-recovery = ON # 崩溃恢复自动清理 / Auto cleanup on crash recovery
relay-log-info-repository = TABLE
master-info-repository = TABLE
# === 并行复制 / Parallel replication ===
replica-parallel-workers = 16
replica-parallel-type = LOGICAL_CLOCK
replica-preserve-commit-order = ON
# === 只读 / Read-only ===
read-only = ON
super-read-only = ON
# === 半同步复制 / Semi-sync replication ===
plugin-load-add = semisync_replica.so
rpl-semi-sync-replica-enabled = 1
# === InnoDB(从库可放宽)/ InnoDB (relaxed for replica) ===
innodb-flush-log-at-trx-commit = 2 # 放宽以提升性能 / Relaxed for performance
sync-binlog = 0 # 从库不需要 fsync binlog
innodb-flush-method = O_DIRECT
# === 复制延迟保护 / Lag protection ===
slave-net-timeout = 30 # Source 无响应超时 / Source unresponsive timeout
14.3 监控关键指标汇总
| 指标 | 获取方式 | 健康阈值 | 告警动作 |
|---|---|---|---|
| Replication Lag | pt-heartbeat / perf_schema | < 1s | 检查并行复制配置、大事务、I/O |
| Replica I/O Thread | SHOW REPLICA STATUS |
Running: Yes | 检查网络和认证 |
| Replica SQL Thread | SHOW REPLICA STATUS |
Running: Yes | 查看 Last_Error,修复后 START REPLICA |
| Semi-Sync Status | Rpl_semi_sync_source_status |
ON | 检查 Replica 连接和 ACK 超时 |
| Semi-Sync No Tx | Rpl_semi_sync_source_no_tx |
0 (或趋势不增长) | 检查网络延迟和超时设置 |
| Errant GTIDs | GTID_SUBTRACT() |
空集 | 立即修复!可能导致切换后数据不一致 |
| Binlog Disk Usage | SHOW BINARY LOGS |
磁盘空间充足 | 调整 expire_logs 或加磁盘 |
| GR Member Status | replication_group_members |
所有节点 ONLINE | 检查离线节点的网络和日志 |
-- Prometheus mysqld_exporter 复制相关指标 / Replication-related metrics
-- 推荐的 Grafana 告警规则 / Recommended Grafana alert rules:
# 复制延迟告警 / Replication lag alert
ALERT MySQLReplicaLag
IF mysql_slave_status_seconds_behind_master > 5
FOR 2m
LABELS { severity = "warning" }
ANNOTATIONS { summary = "MySQL replica lag > 5s on $labels.instance "}}" }
# 复制断开告警 / Replication broken alert
ALERT MySQLReplicaBroken
IF mysql_slave_status_slave_sql_running == 0
OR mysql_slave_status_slave_io_running == 0
FOR 1m
LABELS { severity = "critical" }
ANNOTATIONS { summary = "MySQL replication broken on $labels.instance "}}" }
# 半同步退化告警 / Semi-sync fallback alert
ALERT MySQLSemiSyncOff
IF mysql_global_status_rpl_semi_sync_master_status == 0
FOR 1m
LABELS { severity = "critical" }
ANNOTATIONS { summary = "Semi-sync disabled on $labels.instance "}}" }
15. 源码阅读指引
理解 MySQL 复制的内部原理,阅读源码是最权威的方式。以下是关键源码文件和入口函数的导航图:
15.1 Binary Log 写入路径
| 文件 | 关键函数 | 功能 |
|---|---|---|
sql/binlog.cc |
MYSQL_BIN_LOG::ordered_commit() |
Group Commit 三阶段(FLUSH/SYNC/COMMIT)的核心实现 |
sql/binlog.cc |
MYSQL_BIN_LOG::write_event() |
将单个 binlog 事件写入 binlog 文件 |
sql/log_event.cc |
Write_rows_log_event::write_row() |
ROW 格式中写行数据事件的编码 |
sql/binlog.cc |
MYSQL_BIN_LOG::new_file_impl() |
Binlog 文件轮转(rotate)逻辑 |
sql/rpl_gtid_persist.cc |
Gtid_table_persistor::save() |
将 GTID 集合持久化到 mysql.gtid_executed 表 |
15.2 Replica 端复制线程
| 文件 | 关键函数 | 功能 |
|---|---|---|
sql/rpl_replica.cc |
handle_slave_io() |
I/O Receiver 线程主循环:连接 Source、接收 binlog、写入 relay log |
sql/rpl_replica.cc |
handle_slave_sql() |
SQL Applier 线程主循环(单线程模式) |
sql/rpl_rli_pdb.cc |
Slave_worker::slave_worker_exec_event() |
并行复制 worker 线程执行单个事件 |
sql/rpl_replica.cc |
queue_event() |
I/O 线程将接收的事件写入 relay log |
sql/rpl_rli.cc |
apply_event_and_update_pos() |
执行事件并更新位置信息 |
sql/rpl_replica.cc |
slave_start_workers() |
启动并行复制的 coordinator 和 worker 线程 |
15.3 Binlog Dump 线程(Source 端)
| 文件 | 关键函数 | 功能 |
|---|---|---|
sql/rpl_binlog_sender.cc |
Binlog_sender::run() |
Binlog dump 线程主循环 |
sql/rpl_binlog_sender.cc |
Binlog_sender::send_events() |
从 binlog 读取事件并发送给 Replica |
sql/rpl_binlog_sender.cc |
Binlog_sender::send_heartbeat_event() |
发送心跳事件(防止连接超时) |
15.4 半同步复制
| 文件 | 关键函数 | 功能 |
|---|---|---|
plugin/semisync/semisync_source.cc |
ReplSemiSyncMaster::commitTrx() |
Source 等待 Replica ACK 的核心逻辑 |
plugin/semisync/semisync_source.cc |
ReplSemiSyncMaster::readSlaveReply() |
读取 Replica 的 ACK 响应 |
plugin/semisync/semisync_replica.cc |
ReplSemiSyncSlave::slaveReply() |
Replica 发送 ACK 给 Source |
15.5 Group Replication / Certification
| 文件 | 关键函数 | 功能 |
|---|---|---|
plugin/group_replication/src/certifier.cc |
Certifier::certify() |
事务冲突检测的核心算法 |
plugin/group_replication/src/gcs_operations.cc |
Gcs_operations::send_message() |
通过 XCom 协议广播消息 |
plugin/group_replication/src/applier.cc |
Applier_module::apply_action_packet() |
应用通过共识的事务 |
plugin/group_replication/src/member_info.cc |
Group_member_info::update_member_status() |
成员状态变更处理(ONLINE, RECOVERING, OFFLINE 等) |
16. 复制故障排查手册
16.1 常见错误代码与修复
| 错误码 | 含义 | 典型原因 | 修复方法 |
|---|---|---|---|
| 1062 | Duplicate entry | Replica 上已存在该行(之前可能跳过了 DELETE) | 用 pt-table-sync 修复,或跳过该 GTID |
| 1032 | 找不到行 | Replica 上缺少该行(之前可能跳过了 INSERT) | 用 pt-table-sync 修复数据差异 |
| 1146 | 表不存在 | Replica 上缺少表(复制过滤器配置错误) | 从 Source 导出表结构并在 Replica 上创建 |
| 1236 | 无法读取 binlog | Source 上的 binlog 已被清理或损坏 | 使用备份重建 Replica |
| 1205 | Lock wait timeout | Replica 上的 SQL Applier 等待锁超时(有长查询在 Replica 上运行) | 终止 Replica 上的长事务,或设置 slave_transaction_retries |
| 1756 | 跨库事务违反 GTID 一致性 | 事务中混合了 InnoDB 和 MyISAM | 将所有表迁移到 InnoDB |
16.2 mysqlbinlog 常用命令
# 查看 binlog 事件摘要 / View binlog event summary
mysqlbinlog --no-defaults --base64-output=DECODE-ROWS --verbose \
mysql-bin.000042 | head -100
# 按时间范围过滤 / Filter by time range
mysqlbinlog --start-datetime="2026-04-20 10:00:00" \
--stop-datetime="2026-04-20 10:30:00" \
mysql-bin.000042
# 按 GTID 过滤 / Filter by GTID
mysqlbinlog --include-gtids="3E11FA47-...:100-110" \
mysql-bin.000042
# 排除特定 GTID(用于恢复时跳过某些事务)/ Exclude specific GTIDs
mysqlbinlog --exclude-gtids="3E11FA47-...:105" \
mysql-bin.000042 | mysql -u root -p
# 查看远程 binlog / View remote binlog
mysqlbinlog --read-from-remote-server \
--host=source_host --port=3306 \
--user=repl --password \
mysql-bin.000042
# 解析 ROW 格式的数据变更 / Parse ROW format data changes
mysqlbinlog --base64-output=DECODE-ROWS --verbose --verbose \
mysql-bin.000042 | grep -A5 "### UPDATE"
# 双 --verbose 会显示列类型信息
# Double --verbose shows column type information
16.3 数据一致性校验
# 使用 pt-table-checksum 校验一致性 / Verify consistency with pt-table-checksum
# 在 Source 上运行,会自动计算和比对所有 Replica 的数据校验和
# Run on Source, auto-computes and compares checksums across all Replicas
pt-table-checksum \
--replicate=percona.checksums \
--databases=app_db \
--tables=orders,users,products \
--chunk-size=5000 \
--max-lag=2 \
-h source_host -u monitor -p
# 查看差异报告 / View differences report
pt-table-checksum --replicate=percona.checksums --replicate-check-only \
-h source_host -u monitor -p
# 使用 pt-table-sync 修复差异 / Fix differences with pt-table-sync
# 先 dry-run 查看会做什么 / First dry-run to see what would happen
pt-table-sync --print \
--replicate=percona.checksums \
h=source_host,u=monitor,p=xxx
# 确认无误后执行 / Execute after confirmation
pt-table-sync --execute \
--replicate=percona.checksums \
h=source_host,u=monitor,p=xxx
16.4 复制拓扑可视化
# 使用 Orchestrator 可视化复制拓扑 / Visualize topology with Orchestrator
# Orchestrator 提供 Web UI 和 CLI
# CLI: 查看拓扑 / CLI: View topology
orchestrator-client -c topology -i source_host:3306
# 输出 / Output:
# source_host:3306 [OK,5.7.42,rw,ROW,>>,GTID]
# + replica1:3306 [OK,5.7.42,ro,ROW,>>,GTID]
# + replica2:3306 [OK,5.7.42,ro,ROW,>>,GTID]
# + replica3:3306 [OK,5.7.42,ro,ROW,>>,GTID] (delayed: 3600s)
# CLI: 手动切换 / CLI: Manual switchover
orchestrator-client -c graceful-master-takeover \
-i source_host:3306 -d replica1:3306
# 或使用 MySQL Shell 查看 InnoDB Cluster 拓扑
# Or use MySQL Shell for InnoDB Cluster topology
mysqlsh -- cluster status --extended=1
16.5 复制性能调优清单
| # | 检查项 | 推荐值 | 影响 |
|---|---|---|---|
| 1 | replica_parallel_workers |
8-16 | 并行回放事务,减少延迟 |
| 2 | replica_parallel_type |
LOGICAL_CLOCK | 比 DATABASE 更细粒度的并行 |
| 3 | binlog_transaction_dependency_tracking |
WRITESET | 即使 Source 并发低也能并行 |
| 4 | binlog_group_commit_sync_delay |
100-1000 (微秒) | 增大 Group Commit 批次提升 Replica 并行度 |
| 5 | binlog_transaction_compression |
ON (8.0.20+) | 减少 60-70% 网络传输和磁盘 I/O |
| 6 | Replica 磁盘 | NVMe SSD | I/O 是 Replica 最常见的瓶颈 |
| 7 | innodb_flush_log_at_trx_commit (Replica) |
2 | Replica 放宽持久性换取性能 |
| 8 | sync_binlog (Replica) |
0 | Replica 不需要每次 fsync binlog |
| 9 | 避免大事务 | 每批 ≤ 5000 行 | 大事务无法并行且阻塞其他回放 |
| 10 | slave_transaction_retries |
10 | 自动重试暂时性错误(死锁) |
16.6 复制健康检查脚本
-- 一键复制健康检查 / One-click replication health check
-- 1. 基本复制状态 / Basic replication status
SELECT
CHANNEL_NAME,
SERVICE_STATE AS io_state,
LAST_ERROR_NUMBER AS io_err
FROM performance_schema.replication_connection_status;
-- 2. SQL Applier 状态 / SQL Applier status
SELECT
CHANNEL_NAME,
SERVICE_STATE AS sql_state,
LAST_ERROR_NUMBER AS sql_err,
LAST_ERROR_MESSAGE AS sql_err_msg
FROM performance_schema.replication_applier_status;
-- 3. Worker 线程状态 / Worker thread status
SELECT
CHANNEL_NAME,
WORKER_ID,
LAST_APPLIED_TRANSACTION,
APPLYING_TRANSACTION,
LAST_ERROR_NUMBER
FROM performance_schema.replication_applier_status_by_worker
WHERE LAST_ERROR_NUMBER != 0;
-- 4. 复制延迟精确计算 / Precise replication lag
SELECT
CHANNEL_NAME,
TIMESTAMPDIFF(SECOND,
LAST_APPLIED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP,
LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP
) AS apply_lag_seconds,
TIMESTAMPDIFF(SECOND,
LAST_QUEUED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP,
LAST_QUEUED_TRANSACTION_END_QUEUE_TIMESTAMP
) AS transport_lag_seconds
FROM performance_schema.replication_applier_status_by_worker
WHERE LAST_APPLIED_TRANSACTION != ''
ORDER BY apply_lag_seconds DESC LIMIT 1;
-- 5. Group Replication 成员状态 / GR member status
SELECT
MEMBER_HOST,
MEMBER_PORT,
MEMBER_STATE,
MEMBER_ROLE,
MEMBER_VERSION
FROM performance_schema.replication_group_members;
-- 6. Group Replication 统计 / GR statistics
SELECT
MEMBER_ID,
COUNT_TRANSACTIONS_IN_QUEUE AS cert_queue,
COUNT_TRANSACTIONS_CHECKED AS certified,
COUNT_CONFLICTS_DETECTED AS conflicts,
COUNT_TRANSACTIONS_ROWS_VALIDATING AS certifier_rows,
TRANSACTIONS_COMMITTED_ALL_MEMBERS AS applied_gtids
FROM performance_schema.replication_group_member_stats;
17. 常见问题 (FAQ)
Q1: GTID 模式下能否回退到传统位点复制?
可以,但需要按特定顺序操作。MySQL 支持在线切换 GTID 模式(无需停机): 1. 所有节点:SET GLOBAL gtid_mode = ON_PERMISSIVE(允许混合) 2. 所有节点:SET GLOBAL gtid_mode = OFF_PERMISSIVE(停止生成 GTID) 3. 等待所有 Replica 的 Ongoing_anonymous_transaction_count 为 0 4. 所有节点:SET GLOBAL gtid_mode = OFF 5. Replica 上重新配置为基于位点的复制 但强烈不建议回退——GTID 在所有方面都优于传统位点。
Q2: 复制突然中断,如何快速定位?
按以下步骤排查: 1. SHOW REPLICA STATUS\G — 查看 Last_IO_Error 和 Last_SQL_Error 2. I/O 线程停止:通常是网络问题、认证失败、Source binlog 被清理。检查 Source 是否可达,复制用户权限是否正确,SHOW BINARY LOGS 确认需要的 binlog 还在 3. SQL 线程停止:通常是数据冲突。常见错误:1062 (duplicate entry)、1032 (row not found)、1146 (table doesn't exist) 4. 查看 Replica 的 error log(SHOW VARIABLES LIKE 'log_error')获取详细信息 5. 用 mysqlbinlog 解析出错的 binlog 事件,理解具体是什么操作失败
Q3: 如何在不停机的情况下将异步复制升级为半同步?
半同步可以在线启用,无需停机: 1. 先在所有 Replica 上安装并启用半同步插件 2. 重启 Replica 的 I/O 线程:STOP REPLICA IO_THREAD; START REPLICA IO_THREAD; 3. 在 Source 上安装并启用半同步插件 4. 验证:SHOW STATUS LIKE 'Rpl_semi_sync_source_clients' 应显示已连接的半同步 Replica 数 注意顺序:先 Replica 后 Source。如果先启用 Source,它会因为没有半同步 Replica 而超时退化为异步。
Q4: Group Replication 和传统半同步复制如何选择?
选择取决于你的需求: - 选 Group Replication / InnoDB Cluster:需要自动故障切换、不想依赖外部工具、节点数 <= 9、同一数据中心或低延迟网络、可以接受所有表必须有 PK 的限制 - 选半同步 + 外部工具:节点多于 9 个、跨高延迟数据中心、有 MyISAM 表或无 PK 的表、已有 Orchestrator/MHA 等成熟工具链、需要更灵活的拓扑(级联复制等) 趋势上,MySQL 官方越来越推荐 InnoDB Cluster 作为标准 HA 方案,但传统方案在很多大型互联网公司中仍然广泛使用。
Q5: 如何处理复制中的 1032 (row not found) 错误?
错误 1032 表示 Replica 在执行 UPDATE 或 DELETE 时找不到对应的行。这通常意味着 Source 和 Replica 的数据已经不一致。处理步骤: 1. 短期修复:跳过该事务(GTID 模式下注入空事务) 2. 根因分析:检查是否有人直接在 Replica 上写数据、是否 Replica 之前跳过过事务、是否有 errant GTID 3. 一致性验证:使用 pt-table-checksum 检查 Source 和 Replica 的数据差异 4. 数据修复:使用 pt-table-sync 将不一致的数据从 Source 同步到 Replica 5. 预防:确保 super_read_only=ON,定期运行 pt-table-checksum
Q6: binlog 占用太多磁盘空间怎么办?
多管齐下: - 调整保留期:binlog_expire_logs_seconds(默认 2592000 = 30天)。根据备份策略设置:如果每天做全备 + binlog 备份,可缩短到 3-7 天 - 手动清理:PURGE BINARY LOGS BEFORE '2026-04-15 00:00:00',但先确认所有 Replica 已经处理过这些 binlog - 启用压缩:binlog_transaction_compression=ON(MySQL 8.0.20+),可减少 60-70% 体积 - 使用 MINIMAL row image:binlog_row_image=MINIMAL 减少 ROW 格式的数据量(但会影响闪回能力) - 独立磁盘:将 binlog 放到独立磁盘/分区,避免影响数据目录
Q7: 如何设置延迟复制(Delayed Replication)用于数据恢复?
延迟复制让 Replica 故意落后 Source 一段时间,作为"时间机器"——如果 Source 上误删了数据,可以在延迟从库上还没应用到该操作之前恢复。 CHANGE REPLICATION SOURCE TO SOURCE_DELAY = 3600; -- 延迟 1 小时 START REPLICA; 恢复流程:发现误操作后,立即 STOP REPLICA,然后用 START REPLICA UNTIL SQL_BEFORE_GTIDS = '...' 精确回放到误操作之前的 GTID,最后从这个延迟从库导出数据恢复。
Q8: replica_parallel_workers 设多少合适?
没有万能公式,但以下是经验指导: - 起步值:CPU 核心数的一半到与核心数相等(例如 16 核机器设 8-16) - 观察指标:通过 performance_schema.replication_applier_status_by_worker 查看各 worker 的负载是否均衡 - 不是越多越好:worker 之间有协调开销,设太大(如 128)反而可能因锁竞争降低吞吐量 - 配合 Source 端优化:增大 binlog_group_commit_sync_delay 让更多事务可以并行,比单纯增加 worker 数更有效 - 实际瓶颈:很多时候瓶颈不在线程数,而在磁盘 I/O 或大事务
Q9: MySQL Router 挂了怎么办?会不会成为单点故障?
MySQL Router 的推荐部署方式是与应用部署在同一主机上(sidecar 模式)。这样: - 如果 Router 挂了 = 该应用实例也有问题,负载均衡器自动摘除 - 不需要额外的 Router 高可用机制 - 应用通过 localhost 连接 Router,零网络延迟 如果必须将 Router 部署为独立服务,则需要在 Router 前面加一层 VIP(Keepalived)或负载均衡器,但这种模式不推荐。
Q10: 如何从传统主从复制平滑迁移到 InnoDB Cluster?
迁移路径: 1. 前置检查:确保所有表使用 InnoDB 且有主键;启用 GTID(如果还没有);binlog 格式为 ROW 2. 升级到 MySQL 8.0+(如果还在用 5.7) 3. 在现有 Replica 上验证:dba.checkInstanceConfiguration() 检查每个实例 4. 用 dba.configureInstance() 修复所有配置问题 5. 在 Source 上创建 Cluster:dba.createCluster('myCluster', {adoptFromGR: false}) 6. 逐个添加 Replica:先停复制,再 cluster.addInstance() 7. 部署 MySQL Router 并逐步切流量 整个过程可以在线完成,但建议在维护窗口内执行最终切换步骤。
[← 上一章:锁与死锁](/books/high-performance-mysql/lock-deadlock)
[返回目录](/books/high-performance-mysql)
[下一章:查询优化 →](/books/high-performance-mysql/query-optimization)