跳到主要内容

主从复制与高可用

单机是不够的​

一台 MySQL 跑得好好的,为什么要折腾复制?因为两件事躲不掉:

  • 数据不能丢:磁盘坏了、误删库了、机房断电了,得有备份能拉回来——备份恢复的完整内容在上一篇《备份与恢复》。
  • 服务不能停:主库挂了要能切、读流量大了要能分摊、备份和报表不能压着主库跑——这一篇解决的就是它。

这两个需求对应两套机制:复制(实时同步,解决可用性和读扩展)和备份(定时快照,解决数据安全)。复制不能替代备份——DROP DATABASE 会瞬间同步到所有从库,那时候只有备份能救命。

先定 RTO 和 RPO
  • 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;
AUTO_POSITION=1 的含义

SOURCE_AUTO_POSITION = 1 表示用 GTID 自动定位,不用手写 MASTER_LOG_FILE 和 MASTER_LOG_POS。从库告诉主库「我已经执行过哪些 GTID」,主库把缺的补发过去。这是 GTID 最实用的地方:换主、重建从库都不用算位点。

验证与常用管理命令​

-- 从库上看复制状态(重点看这几行)
SHOW REPLICA STATUS\G
字段期望值含义
Replica_IO_RunningYes能连上主库并拉 binlog
Replica_SQL_RunningYes能正常回放
Seconds_Behind_Source0 或很小延迟秒数,不准但够用
Last_IO_Error空IO 线程报错,常见于网络、账号、权限
Last_SQL_Error空回放报错,常见于主键冲突、表不存在
Retrieved_Gtid_Set持续增长已拉到的 GTID 集合
Executed_Gtid_Set接近 Retrieved已执行的 GTID 集合
Auto_Position1使用 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 能直接对比主从差异。

三个必须知道的坑:

  1. 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 设进去,或者干脆清空数据目录重新初始化。
  2. enforce_gtid_consistency = ON 会限制某些语句:CREATE TABLE ... SELECT、事务里混用临时表和非事务表等。老应用迁移前先测。
  3. 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。