Files
2026-05-24 11:42:38 +08:00

419 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [MySQL, 主从复制, Replication, Semi-Sync, GTID]
create time: 2026-05-16 10:30
---
# 主从复制
## 概述
MySQL 主从复制是通过 Binlog 将主库的数据变更传播到从库的过程。它是高可用架构的基础组件,也是读写分离的前提。
> [!tip] 生活中的类比
> 想象主库是一位「主播」,从库是「观众」。主播做了一个操作(比如画画),观众通过直播画面(Binlog)看到这个操作,然后在自己的画布上**照着做一遍**。
>
> 这个"看到 → 照做"的过程,就是主从复制的本质。
> [!question] 为什么需要主从复制?
> 一台 MySQL 能撑多久?当读请求暴增时,单机性能到顶了怎么办?
> 主从复制给出了两个关键答案:**读写分离**(主库写、从库读)和**高可用**(主库挂了,从库顶上)。
## 复制架构
复制的过程可以简化为三个步骤:**主库记录变更** → **网络传输** → **从库重放变更**。
```mermaid
flowchart LR
subgraph "Master"
M1["业务写入 DML"] --> M2["Server 层生成 Binlog"]
M2 --> M3["Binlog Dump Thread"]
end
subgraph "Network"
M3 -. "Binlog Stream" .-> I1["IO Thread"]
end
subgraph "Slave"
I1 --> S1["Relay Log 中继日志"]
S1 --> S2["SQL Thread"]
S2 --> S3["数据文件更新"]
end
style M2 fill:#C44569,color:#fff
style S2 fill:#00B6BC,color:#fff
```
### 三线程模型
| 线程 | 归属 | 职责 |
|------|------|------|
| **Binlog Dump Thread** | Master | 连接到一个 Slave 就创建一个,负责发送 binlog |
| **IO Thread** | Slave | 连接 Master,拉取 binlog 写入本地 Relay Log |
| **SQL Thread** | Slave | 读取 Relay Log 重放应用到本地数据库 |
> [!question] 为什么要多一个 Relay Log,而不是直接在从库上执行?
> 其实这和消息队列的「削峰填谷」是一个道理。Relay Log 起到了**缓冲区**的作用:即使从库暂时卡住了(比如正在做慢查询),主库发来的 Binlog 也不会丢失,先暂存在 Relay Log 里,等从库空闲了再慢慢回放。
## 同步模式
复制的「同步」和「异步」,核心问题只有一个:**主库写完后,要不要等从库确认?**
| 模式 | 等不等从库? | 数据安全 | 性能 |
|------|------------|---------|------|
| **异步** | 不等 | 低(可能丢数据) | 最高 |
| **半同步** | 至少等 1 个 | 中(大幅降低丢数据概率) | 中 |
| **组复制** | 等多数派共识 | 高 | 较低 |
下面的流程图展示了三种模式的完整过程:
```mermaid
flowchart TD
subgraph "Async 异步复制"
A1["Client 写入 Master"] --> A2["Master 刷盘返回 OK"]
A2 -->|"后续"| A3["异步发给 Slave"]
end
subgraph "Semi-Sync 半同步复制"
S1["Client 写入 Master"] --> S2["Master 刷盘"]
S2 --> S3["等待至少1个 Slave ACK"]
S3 -->|"超时10s"| S4["降级为异步"]
S3 -->|"收到 ACK"| S5["返回 OK"]
S5 --> S6["异步发给其他 Slave"]
end
subgraph "Group Replication 组复制"
G1["Client 写入"] --> G2["Paxos 共识"]
G2 --> G3["多数派确认"]
G3 --> G4["并行回放"]
end
style A3 fill:#FF9F43,color:#000
style S5 fill:#00D866,color:#fff
style G3 fill:#00B6BC,color:#fff
```
### 异步复制(默认)
最简单的模式——Master 写完就不管了,异步发给 Slave。
**在从库上执行以下命令**,告诉它"去连接哪个主库、用什么账号、从哪里开始复制":
```sql
-- MySQL 5.5 (已弃用)
-- CHANGE MASTER TO
-- MASTER_HOST='10.0.1.10', ...
-- ✅ MySQL 8.0+ 标准语法
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='10.0.1.10', -- 主库 IP
SOURCE_USER='repl_user', -- 专用的复制账号
SOURCE_PASSWORD='password',
SOURCE_PORT=3306,
SOURCE_AUTO_POSITION = 1; -- GTID 模式:自动找到正确的起始位置
START REPLICA; -- 启动复制!
```
> [!tip] 关键参数 `SOURCE_AUTO_POSITION`
> 设为 `1` 表示开启 GTID 自动定位——从库会自动告诉主库"我已经有哪些事务了",主库从缺失的部分开始发送。再也不用手动查 Binlog 文件和偏移量了。
> [!warning] 异步复制的风险
> Master 宕机后,尚未传输到 Slave 的数据会丢失。对于金融系统这是不可接受的。
### 半同步复制(Semi-Sync)
至少一个从库确认收到 Binlog 后,Master 才返回写成功。
半同步需要在**主库和从库两端都开启**插件:
```ini
# === Master 端配置 ===
plugin_load_add = semisync_master # 加载半同步主库插件
rpl_semi_sync_master_enabled = 1 # 开启
rpl_semi_sync_master_timeout = 10000 # 10 秒收不到 ACK 就降级为异步(保护性能)
rpl_semi_sync_master_wait_point = AFTER_SYNC # 先刷盘,再等从库确认(避免幻读)
```
```ini
# === Slave 端配置 ===
plugin_load_add = semisync_slave # 加载半同步从库插件
rpl_semi_sync_slave_enabled = 1 # 开启
```
> [!note] 半同步不是银弹
> - 从库收到 Binlog ≠ 已从磁盘持久化
> - 如果 Master 和从库同时断电,仍可能丢数据
> - 网络分区时 Master 会超时降级为异步
>
> **最佳实践**:Semi-Sync + 定期备份 + PITR 恢复组合使用
## GTID 复制
GTID(Global Transaction Identifier)给每个事务分配全局唯一的 ID,替代传统的 position-based 复制。
> [!question] 为什么传统 Position 复制让人抓狂?
> 传统模式下,从库靠 `mysql-bin.000003:154`(文件名 + 偏移量)来记录复制进度。
> 想象一下:主库挂了,你要把从库切到新主库上,但新旧主库的 Binlog 文件名和偏移量完全不同——你得**手动计算**该从哪个位置继续复制。
>
> GTID 的思路就简单多了:给每个事务贴一个「身份证号」。无论主库怎么切换,从库只要说"我已经执行到 23 号事务了",新主库就知道该从 24 号开始发。
```
GTID 格式: source_id:transaction_id
例: 3E11FA47-71CA-11E1-9E33-C80AA9429562:23
↑ UUID of server ↑ Sequence number
```
### GTID 复制流程
```mermaid
flowchart TD
A["Client 事务写入 Master"] --> B["Server 层分配 GTID"]
B --> C["写入 Binlog 事件, 带 GTID 标记"]
C --> D["Binlog Dump 线程发送 GTID 和事件到 Slave"]
D --> E["Slave IO Thread 写入 Relay Log"]
E --> F{"GTID Set 已包含此 GTID?"}
F -->|"是"| G["跳过重复事件"]
F -->|"否"| H["SQL Worker 重放事务"]
H --> I["更新 Slave gtid_executed"]
G --> J["继续下一个事件"]
I --> J
style B fill:#C44569,color:#fff
style F fill:#FFD166,color:#000
style H fill:#00B6BC,color:#fff
```
> [!note] GTID 的一致性约束
> 启用 `enforce_gtid_consistency = ON` 后,以下操作被**禁止**:
> - 对非事务引擎(如 MyISAM)的 DML
> - 创建/删除临时表
> - `CREATE TABLE ... SELECT`(混合语句级和语句级二进制日志)
> - 对视图的 DML(无法追踪来源表)
### GTID vs 传统 Position
一句话总结:**Position 靠「页码」定位,GTID 靠「身份证号」定位。**
| 特性 | Position-based | GTID-based |
|------|---------------|------------|
| **定位方式** | 文件名 + position 号(像书签) | GTID set(像身份证号集合) |
| **跨库切换** | 需要手动查 position | 自动定位 |
| **跳过事务** | 困难 | `SET gtid_next = '...'; BEGIN; COMMIT; SET gtid_next = AUTOMATIC;` |
| **一致性检查** | 手动比对 | `pt-table-checksum` + GTID 验证 |
> [!tip] 实际建议
> MySQL 8.0 中 GTID 已经非常成熟,**新项目强烈建议默认开启**。传统 Position 模式仅在兼容老版本时使用。
在主库和从库的 `my.cnf` 中添加以下配置即可启用 GTID:
```ini
# 启用 GTID
gtid_mode = ON # 开启 GTID
enforce_gtid_consistency = ON # 强制一致性:禁止不安全的 SQL 写法
master_info_repository = TABLE # 复制元数据存到系统表,比文件更可靠
relay_log_info_repository = TABLE
```
> [!tip] 热知识
> MySQL 8.0 中 `master_info_repository` 和 `relay_log_info_repository` 默认就是 TABLE,无需手动配置。
## 并行复制(Multithreaded Slave, MTS)
前面提到 SQL Thread 负责重放 Relay Log。问题来了——默认只有一个 SQL Thread,所有事务都**排队串行**执行。如果主库很忙,从库就很容易「越落越远」。
> [!tip] 用快递仓库来理解并行复制
> 串行回放 = 一个分拣员按顺序处理所有包裹。
> 并行回放 = 多个分拣员**同时**处理,只要包裹之间没有依赖关系(比如不会同时操作同一行),就可以并行。
MySQL 5.6+ 引入从库并行回放,大幅提升复制延迟处理能力。
在从库配置中开启并行复制:
```ini
# MySQL 5.x(已弃用)
# slave_parallel_type = LOGICAL_CLOCK
# slave_parallel_workers = 8
# ✅ MySQL 8.0+ 标准命名
replica_parallel_type = LOGICAL_CLOCK # 按事务依赖关系决定哪些可以并行
replica_parallel_workers = 8 # 并发 worker 数量(建议 CPU 核数的一半)
```
> [!TIP] worker 数量设多少合适?
> 不是越多越好。每个 worker 都占内存和 CPU,一般建议 **CPU 核数的一半**,上限不超过 16。设太多反而会因为锁竞争导致更慢。
### MTS 并行模式对比
```mermaid
flowchart LR
subgraph "DATABASE 默认"
D1["Relay Log 事件"] --> D2{"按 database 分桶"}
D2 -->|"db_a"| W1["Worker 1"]
D2 -->|"db_b"| W2["Worker 2"]
D2 -->|"db_c"| W3["Worker 3"]
W1 & W2 & W3 --> J1["写入数据"]
end
subgraph "GROUP_TRANSACTIONS"
G1["Relay Log 事件"] --> G2{"判断事务依赖关系"}
G2 -->|"无冲突"| A1["组1 并行执行"]
G2 -->|"有冲突"| A2["组2 串行等待"]
G1 -->|"无冲突"| A1
A1 --> J2["写入数据"]
A2 --> J2
end
style A1 fill:#00D866,color:#fff
style A2 fill:#FF9F43,color:#000
```
| 维度 | `DATABASE` | `GROUP_TRANSACTIONS` |
|------|-----------|---------------------|
| **分组依据** | database 名称 | 事务间的写集依赖关系 |
| **并发度** | 受限于 database 数量 | 取决于并发事务比例,通常更高 |
| **精确度** | 粗粒度——同一 DB 内串行 | 细粒度——无依赖即可并行 |
| **适用场景** | schema 设计良好的系统 | 多库共享、跨库事务较多的场景 |
| **引入版本** | MySQL 5.6 | MySQL 5.7+ |
> [!tip] 如何选择
> - 如果业务按 database 做数据隔离(每个 tenant 一个 DB),选 `DATABASE`
> - 如果多个业务共享少数几个 database,选 `GROUP_TRANSACTIONS`
### LOGICAL_CLOCK 原理
> [!info] `LOGICAL_CLOCK` = GTID 驱动的并行回放
> 无论选择 `DATABASE` 还是 `GROUP_TRANSACTIONS`,底层引擎都是 LOGICAL_CLOCK。
> 区别仅在于如何把连续事件流切分成可并行的逻辑组。
```mermaid
flowchart TD
A["Relay Log 事件流"] --> B{"按 database 分组"}
B --> C["DB1 事件队列"]
B --> D["DB2 事件队列"]
B --> E["DBn 事件队列"]
C --> W1["Worker 1 执行 DB1 事件"]
D --> W2["Worker 2 执行 DB2 事件"]
E --> Wn["Worker n 执行 DBn 事件"]
style W1 fill:#00D866,color:#fff
style W2 fill:#00B6BC,color:#fff
style Wn fill:#FF9F43,color:#000
```
> [!tip] Parallel Replication 的限制
> - 基于 database 的并行要求同一 database 内没有冲突事务
> - 跨 database 的操作无法并行
> - innodb_table_locks=OFF 且 autocommit=1 时才能并行(避免表锁冲突)
## 监控复制状态
复制搭好了,日常巡检同样重要。最常见的关注点:**IO 线程和 SQL 线程是否在正常运行?从库落后主库多少秒?**
```sql
-- MySQL 8.0+ 已弃用 SHOW SLAVE STATUS
-- SHOW SLAVE STATUS\G
-- ✅ 使用 performance_schema 视图
SELECT * FROM performance_schema.replication_connection_status;
SELECT * FROM performance_schema.replication_applier_status;
-- ⚠️ 以上两表各返回一行,多从库场景下需要关联:
-- connection: 一条代表 Master → Slave 的 IO 连接
-- applier: 每行代表一个 SQL Worker 线程
```
```sql
-- 实时延迟监控(MySQL 8.0+, GTID 模式)
SELECT
MAX(latency) / 1000000000 AS max_lag_seconds
FROM (
SELECT
MAX(clock_timestamp) - MIN(clock_start) AS latency
FROM performance_schema.replication_applier_status_by_worker
) lag;
```
## 常见问题与排查
生产环境中,主从复制最容易出问题的三个地方:**IO 线程断连**、**SQL 线程卡住**、**延迟越来越大**。下面逐一分析。
### IO 线程断开(Slave_IO_Running: No)
```mermaid
flowchart TD
A["IO Thread 断连"] --> B{"原因定位"}
B -->|"网络不通"| C["检查防火墙和安全组端口"]
B -->|"Binlog 已过期"| D["purge binary logs"]
B -->|"认证失败"| E["核对 user 权限和 password"]
B -->|"Server UUID 冲突"| F["重置 slave server_uuid"]
C --> G["CHANGE REPLICATION SOURCE TO"]
D --> G
E --> G
F --> G
G --> H["START REPLICA"]
H --> I["确认 IO 恢复"]
style A fill:#FF6B6B,color:#fff
style I fill:#00D866,color:#fff
```
**常见排查步骤:**
```sql
-- 1. 查看具体错误信息
SELECT LAST_ERROR_MESSAGE FROM performance_schema.replication_connection_status;
-- 2. 检查 Master Binlog 是否已被清理
SHOW MASTER LOGS;
-- 3. 如果 Binlog 已过期,需要全量恢复(mysqldump + --single-transaction)
-- 或使用 xtrabackup 做 In-place Slave 重建
```
### SQL 线程卡住(Slave_SQL_Running: No)
这通常是由于数据不一致导致的——例如从库被手动修改过。
> [!warning] 不要直接跳过!
> 盲目 `sql_slave_skip_counter` 可能导致数据静默丢失。必须先确认事务内容。
```sql
-- ⚠️ MySQL 5.7 及以下(已弃用)
-- SET GLOBAL sql_slave_skip_counter = 1;
-- STOP SLAVE; START SLAVE;
-- ✅ MySQL 8.0+ GTID 模式:跳过指定事务
-- 核心思路:假装已经执行过这个事务,让从库跳过它
SET GTID_NEXT='3E11FA47-71CA-11E1-9E33-C80AA9429562:100'; -- 指定要跳过的 GTID
BEGIN; COMMIT; -- 提交一个空事务, 标记该 GTID 为已执行
SET GTID_NEXT='AUTOMATIC'; -- 恢复自动分配 GTID
-- 如果需要从头重建从库的 GTID 记录(慎用!)
STOP REPLICA;
SET GLOBAL gtid_purged = '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100'; -- 声明从库已拥有这些 GTID
START REPLICA;
```
### 延迟过大如何诊断
```mermaid
flowchart LR
A["Seconds_Behind_Master 大"] --> B{"IO 延迟还是 SQL 延迟"}
B -->|"Relay_Log_Space 持续增长"| C["SQL Worker 回放太慢, 增加 replica_parallel_workers"]
B -->|"Read_Master_Log_Pos 跟丢"| D["网络带宽不足或 Master 写入量太大"]
B -->|"DDL 执行中"| E["大表 ALTER TABLE 阻塞, 使用 pt-online-schema-change"]
style C fill:#00B6BC,color:#fff
style D fill:#FF9F43,color:#000
style E fill:#C44569,color:#fff
```
---
## 关联笔记
- [[hhs/MySQL/07-高可用与分布式/29-Binary Log]] — Binlog 是主从复制的数据源
- [[hhs/MySQL/07-高可用与分布式/31-MHA与Orchestrator]] — 自动故障切换工具
- [[hhs/MySQL/07-高可用与分布式/32-Group Replication]] — Paxos 多主复制进阶方案