第 20 章

主从复制原理与实战

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 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。

AFTER_COMMIT (传统半同步)

MySQL 5.5-5.6 行为。Source 在 COMMIT 阶段完成后才等待 ACK。

半同步复制配置

-- 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.ccdecide_logging_format() 中实现。

触发 ROW 切换的条件

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 上提交时:

  1. 分配一个 GTID,例如 3E11FA47-71CA-11E1-9E33-C80AA9429562:42
  2. GTID 被写入 binlog 作为事务的第一个事件 (Gtid_log_event)
  3. 事务提交后,GTID 加入 gtid_executed 集合
  4. Replica 接收到该事务后,先检查 GTID 是否已在自己的 gtid_executed 中——如果是则跳过(幂等性)
  5. 执行后将 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 多源复制注意事项

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-dbreplicate-do-dbSTATEMENT 格式下基于 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 批次中。

具体实现:

  1. Source 端:每个事务计算其修改行的主键/唯一键哈希值集合(WriteSet)
  2. 在 binlog 中记录每个事务的 last_committed(逻辑时钟序号)和 sequence_number
  3. 如果当前事务的 WriteSet 与最近 binlog_transaction_dependency_history_size 个事务都没有交集,则将 last_committed 设为更早的值——允许更多并行
  4. 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 提供了:

10.1 Single-Primary vs Multi-Primary

Single-Primary (推荐)
Multi-Primary

10.2 Certification(冲突检测)原理

当一个节点要提交事务时:

  1. 事务在本地执行(乐观执行),但不提交
  2. 提取事务的 WriteSet(修改的行的 PK/UK 哈希集合)和 ReadSet
  3. 将 WriteSet + 事务内容通过 XCom 协议广播给所有节点
  4. 每个节点执行 Certification:检查该 WriteSet 是否与最近已认证的事务冲突
  5. 如果无冲突且多数节点确认 → CERTIFY_OK,提交
  6. 如果有冲突 → 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 限制

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_ErrorLast_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 imagebinlog_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 上创建 Clusterdba.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)
本章评分
4.8  / 5  (11 评分)

💬 留言讨论