主从复制与高可用
单机是不够的
一台 MySQL 跑得好好的,为什么要折腾复制?因为两件事躲不掉:
- 数据不能丢:磁盘坏了、误删库了、机房断电了,得有备份能拉回来——备份恢复的完整内容在上一篇《备份与恢复》。
- 服务不能停:主库挂了要能切、读流量大了要能分摊、备份和报表不能压着主库跑——这一篇解决的就是它。
这两个需求对应两套机制:复制(实时同步,解决可用性和读扩展)和备份(定时快照,解决数据安全)。复制不能替代备份——DROP DATABASE 会瞬间同步到所有从库,那时候只有备份能救命。
- RPO(恢复点目标):最多能丢多少数据。决定备份频率和是否开半同步。
- RTO(恢复时间目标):多久能恢复服务。决定用冷备、热备还是自动切换。
这两个数定不下来,方案就只能拍脑袋。核心交易库通常要求 RPO 接近 0、RTO 小于 5 分钟,那半同步 + 自动切换 + 定期演练就是必须的。
主从复制原理
MySQL 复制是异步的,核心就三件事、三个线程:
主库
├─ 事务提交 → 写 binlog
└─ dump thread 把 binlog 发给从库
↓
从库
├─ IO thread 接收 binlog → 写 relay log
└─ SQL thread 读 relay log → 重放(apply)
- binlog dump thread(主库):每个从库连接对应一个,负责从指定位置读 binlog 发出去。
- IO thread(从库):连主库、拉 binlog、写本地 relay log。它跟网络和主库相关,断了会重试。
- SQL thread(从库):读 relay log 重放 SQL/行变更。它跟从库自身性能相关,回放慢就延迟。
为什么能异步:主库提交事务只需要写本地 binlog,不等从库确认。所以主库性能不受从库影响,代价是主库宕机时可能丢最后一段还没传过去的数据。
配置主从复制
准备两台机器(示例:主 10.0.0.11,从 10.0.0.12),版本必须一致,server-id 必须不同。
主库 my.cnf:
[mysqld]
server-id = 11
log-bin = /data/mysql/binlog/mysql-bin
binlog_format = ROW
binlog_row_image = FULL
binlog_expire_logs_seconds = 604800
max_binlog_size = 512M
# GTID(推荐开启,8.0 默认关闭)
gtid_mode = ON
enforce_gtid_consistency = ON
# 半同步(可选,装好插件再开)
# plugin_load_add = semisync_source.so
# rpl_semi_sync_source_enabled = ON
从库 my.cnf:
[mysqld]
server-id = 12
relay_log = /data/mysql/relaylog/relay-bin
log_slave_updates = ON
read_only = ON
super_read_only = ON
gtid_mode = ON
enforce_gtid_consistency = ON
几个参数别漏:
binlog_format = ROW:8.0 默认就是 ROW,一定要保持。STATEMENT 格式在NOW()、UUID()、LIMIT不确定顺序等场景下主从数据会不一致。log_slave_updates = ON:从库回放后也写自己的 binlog。想做级联复制或者把从库当新主,这个必须开。read_only+super_read_only:防止误写从库。super_read_only连超级用户也挡,切换时需要先关掉。binlog_expire_logs_seconds:binlog 保留期,默认 30 天。磁盘告警的头号原因是 binlog 没清,保留期按备份周期定,一般 7 天够用。
建立复制账号(主库执行):
CREATE USER 'repl'@'10.0.0.%' IDENTIFIED BY 'Repl@Strong#2024';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.0.0.%';
拿到一致性起点。最稳妥的做法是用备份里的位点,别手工 FLUSH TABLES WITH READ LOCK:
# 全备,同时把 binlog 位点记到 dump 文件头部
mysqldump -uroot -p \
--single-transaction --source-data=2 \
--all-databases --routines --triggers --events \
> /backup/full_$(date +%F).sql
# 看记录下来的位点
head -n 30 /backup/full_*.sql | grep -E "CHANGE|GTID"
--source-data 是 8.0.26 才改的名,之前的版本(以及很多教程)叫 --master-data=2,作用完全一样。--set-gtid-purged 则是 GTID 环境专用的:备份给从库重建用 ON,导入到一个全新的独立实例用 OFF,选错了会带出一堆 gtid_purged 冲突。
在从库启动复制(8.0.23 之前的语法):
CHANGE MASTER TO
MASTER_HOST = '10.0.0.11',
MASTER_PORT = 3306,
MASTER_USER = 'repl',
MASTER_PASSWORD = 'Repl@Strong#2024',
MASTER_AUTO_POSITION = 1;
START SLAVE;
8.0.23 及以后(推荐,CHANGE MASTER 已废弃):
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = '10.0.0.11',
SOURCE_PORT = 3306,
SOURCE_USER = 'repl',
SOURCE_PASSWORD = 'Repl@Strong#2024',
SOURCE_AUTO_POSITION = 1;
START REPLICA;
SOURCE_AUTO_POSITION = 1 表示用 GTID 自动定位,不用手写 MASTER_LOG_FILE 和 MASTER_LOG_POS。从库告诉主库「我已经执行过哪些 GTID」,主库把缺的补发过去。这是 GTID 最实用的地方:换主、重建从库都不用算位点。
验证与常用管理命令
-- 从库上看复制状态(重点看这几行)
SHOW REPLICA STATUS\G
| 字段 | 期望值 | 含义 |
|---|---|---|
Replica_IO_Running | Yes | 能连上主库并拉 binlog |
Replica_SQL_Running | Yes | 能正常回放 |
Seconds_Behind_Source | 0 或很小 | 延迟秒数,不准但够用 |
Last_IO_Error | 空 | IO 线程报错,常见于网络、账号、权限 |
Last_SQL_Error | 空 | 回放报错,常见于主键冲突、表不存在 |
Retrieved_Gtid_Set | 持续增长 | 已拉到的 GTID 集合 |
Executed_Gtid_Set | 接近 Retrieved | 已执行的 GTID 集合 |
Auto_Position | 1 | 使用 GTID 自动定位 |
日常管理命令:
-- 停止/启动复制
STOP REPLICA;
START REPLICA;
-- 只停 SQL 线程(排查回放错误时常用,IO 继续拉)
STOP REPLICA SQL_THREAD;
-- 跳过一条报错的事务(GTID 模式下)
SET GTID_NEXT = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:100';
BEGIN; COMMIT;
SET GTID_NEXT = 'AUTOMATIC';
START REPLICA;
-- 看主库上有哪些从库在连
SHOW PROCESSLIST;
-- 或
SELECT * FROM performance_schema.replication_connection_status\G
SET GTID_NEXT 注入空事务能跳过一条冲突,但主从数据从此不一致。正确流程是:先记录跳过的 GTID、事后用 pt-table-checksum 校验、pt-table-sync 修复。能重建从库就别跳。
GTID 复制要点
GTID(Global Transaction Identifier)给每个事务一个全局唯一编号,格式是 server_uuid:transaction_id,比如 3e11fa47-71ca-11e1-9e33-c80aa9429562:23。
好处:
- 自动定位:从库断线重连后告诉主库自己的 GTID 集合,主库自动补发,不用人算位点。
- 切换简单:主从切换后,其他从库执行
CHANGE REPLICATION SOURCE TO SOURCE_HOST='新主'就能接上。 - 可校验:
SHOW REPLICA STATUS里的Executed_Gtid_Set能直接对比主从差异。
三个必须知道的坑:
gtid_purged不能有交集。用备份重建从库时,如果备份里带的 GTID 集合跟从库已有的冲突,会报The slave is connecting using CHANGE MASTER TO MASTER_AUTO_POSITION = 1, but the master has purged binary logs containing GTIDs that the slave requires。解法是把备份的gtid_purged设进去,或者干脆清空数据目录重新初始化。enforce_gtid_consistency = ON会限制某些语句:CREATE TABLE ... SELECT、事务里混用临时表和非事务表等。老应用迁移前先测。gtid_mode只能逐级切换:OFF → OFF_PERMISSIVE → ON_PERMISSIVE → ON,不能跳步。
-- 查看 GTID 执行情况
SELECT @@gtid_mode, @@enforce_gtid_consistency;
SHOW VARIABLES LIKE 'gtid_purged';
SHOW MASTER STATUS; -- 8.2+ 也可用 SHOW BINARY LOG STATUS
半同步复制
异步复制的窗口是「主库提交了、binlog 还没到从库就宕机」。半同步把窗口收窄:主库提交后至少等一个从库确认收到 binlog(注意是收到,不是执行完)才返回客户端。
-- 主库装插件并开启(8.0.26+ 的插件名是 semisync_source)
INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';
SET GLOBAL rpl_semi_sync_source_enabled = ON;
SET GLOBAL rpl_semi_sync_source_timeout = 1000; -- 毫秒
-- 从库
INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';
SET GLOBAL rpl_semi_sync_replica_enabled = ON;
代价是每次提交多一个网络 RTT,写入 QPS 会下降。rpl_semi_sync_source_timeout 到了还没确认,主库会自动降级回异步——可用性优先,但这期间数据就有丢失风险,要配合监控盯 Rpl_semi_sync_source_status。
| 模式 | 数据安全 | 性能 | 适用 |
|---|---|---|---|
| 异步 | 可能丢最后一段 | 最好 | 日志、报表、可容忍少量丢失的业务 |
| 半同步 | 最多丢一个事务 | 略降 | 大多数核心业务 |
| 组复制(MGR) | 多数派确认 | 明显下降 | 金融级、要求自动选主 |
高可用方案
一句话版本:MHA 是上一代事实标准,Orchestrator 是它的现代替代,云上直接用 RDS 多可用区,实在要自己搞就上 MGR。
| 方案 | 原理 | 优点 | 局限 |
|---|---|---|---|
| MHA | 管理节点 + SSH,检测到主库故障后选最新从库提升并切换 VIP | 成熟、切换快(10~30 秒) | 依赖 SSH 免密、需自己写脚本、社区已不活跃 |
| Orchestrator | 持续探测复制拓扑,基于 Raft 做故障检测,自动重构复制关系 | 拓扑可视化好、支持复杂拓扑、切换可控 | 部署组件多,要理解它的故障检测逻辑 |
| 云 RDS | 厂商托管的双节点/多可用区,主库故障自动切 | 零运维、有 SLA、自带备份 | 成本高、参数受限、跨云迁移麻烦 |
| MGR | 组复制,多数派确认,自动选主 | 数据强一致、无外部依赖 | 性能损耗明显、对网络延迟敏感、限制多 |
配套还要考虑读写分离:应用连 ProxySQL / MySQL Router / 云上的代理,写走主、读走从。读从库要接受「刚写就读不到」的延迟,强一致读必须回主库。
自动切换最怕选到一个延迟很大的从库。切换脚本里必须检查 Seconds_Behind_Source 或对比 Executed_Gtid_Set,追平了才提升。宁可多等几秒,也别把 10 分钟前的数据当最新。
小结
- 复制三线程:主库 dump 发 binlog,从库 IO 线程写 relay log,SQL 线程回放;异步是默认,半同步收窄丢失窗口。
- 配置四件套:
server-id唯一、binlog_format=ROW、GTID 两个开关、read_only防误写。 - GTID 让位点计算成为历史,但要注意
gtid_purged冲突和enforce_gtid_consistency的语法限制。 - 高可用选型:MHA 老旧、Orchestrator 现代、云 RDS 省心、MGR 强一致但重;切换必须校验从库已追平。
架构搭起来了,接下来是日常运维:下一篇《运维与监控》讲慢查询怎么定位、关键指标看哪些、mysqld_exporter 怎么接 Prometheus。