先说个真实的场景。某天凌晨一点,值班电话把你从床上薅起来:线上 MySQL 主库负载飙到 90%,一堆统计类 SELECT 把机器差点拖垮。你敲下 SHOW PROCESSLIST,发现全是同一个报表查询,业务方还在群里 @你:能不能先保证写入不挂?这时候如果你提前搭好了主从复制,把读流量切到从库,主库压力瞬间就下来了。
mysql 主从复制这个主题,我在不同场合讲过很多遍:给应届生讲原理、给后端同事讲搭建、给运维讲排障。几乎每次都会有人问同一个问题:主从复制到底难不难?我的回答是:跑通一个 Demo 不难,两小时就能看到主库写入、从库同步;但要在生产环境里稳定跑几年不踩坑,需要理解的东西远比敲几条 SQL 多得多。这篇文章不打算写成纯文档式的步骤堆砌,我想把从原理到实操、再到故障复盘的过程完整过一遍,适合刚接触 MySQL 复制、以及搭过但没想明白"为什么"的人。
1. 先搞清楚一件事:主从复制不是高可用,但它是很多高可用方案的地基
1.1 它真正解决的三个问题
很多人一提到主从复制就想到"备份",这个理解偏差会让后续架构走很多弯路。主从复制解决的核心问题,按优先级排是这样的:
第一,读写分离,分摊读压力。 互联网业务典型特征是读多写少,可能 95% 的请求都是 SELECT。如果这些请求全打在一台 MySQL 上,再好的硬件也会在慢查询和并发尖峰面前败下阵来。搭建主从后,主库只处理写入和少量实时性要求极高的读,从库挂只读副本承接报表、搜索、后台管理等读流量。这是性价比最高的一种数据库横向扩展方式。
第二,为容灾切换提供一份"热"数据副本。 主库物理机宕机、磁盘损坏、机房网络故障时,从库上已经有一份几乎实时的数据。虽然主从复制是异步的,切换时可能丢少量事务,但总比连机器都起不来强得多。从库可以快速提升为主库,缩短业务恢复时间。
第三,把备份和统计分析任务从主库上剥离。 定时备份任务、大数据平台抽数、慢查询分析这类重活,如果在主库上跑,很容易影响线上写入性能。放到从库执行后,主库就能专注做它该做的事。
1.2 不要踩的认知误区
有不少文章会把主从复制和高可用画等号,这是最危险的误解。传统异步复制下,主库崩溃瞬间没来得及同步到从库的 binlog 事务会直接丢失,从库提升为主库后,这部分数据就没了。而且从库变主库不会自动发生,需要人工介入或额外编排工具。
所以如果你所在团队对可用性要求是"数据库挂了几分钟也没事",那主从复制够用;如果要求是"30 秒内自动恢复且不丢数据",你需要考虑的是半同步复制、MySQL Group Replication、或者数据库自动高可用方案。主从复制是上面的地基,但它本身不等于高可用。
这一点想透之后,你再看后面的搭建步骤,心态就不一样了——你不是在背命令,你是在为自己的系统加一层读扩展能力和一份应急副本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一条 SQL 在主库执行后,是怎么流到从库的
2.1 链路里的三个线程,缺一个都不转
我面试候选人的时候,喜欢让面试者手画主从复制的数据流向。能立刻说出"IO 线程、SQL 线程、Dump 线程"的人不少,能说清每一步发生在哪个节点、写哪个文件的人就不多了。
完整链路是这样的:
主库侧: 每个写事务提交时,都会被记录到主库的二进制日志 binlog 中,这一步由主库写入线程完成。当有从库发起同步请求时,主库会为每个从库派生一个 Binlog Dump 线程,负责读取 binlog 中的事件并发送给从库。
从库侧: 从库的 IO 线程专门负责连接主库、接收 binlog 事件,把它原样写入本地的中继日志 relay log。注意,这里只是把日志写下来,还没有执行。 紧接着,从库的 SQL 线程负责读取 relay log 中的事件,并在从库上顺序重放这些事务。
为什么从库要多出一层 relay log,而不是 IO 线程收到事件后直接执行?这个设计很像消息队列里的"生产者-消费者"模型。网络接收和执行 SQL 的速度天然不匹配,如果直接耦合,主库一旦发得快、从库执行不过来,网络缓冲就会堆积甚至断开。有了 relay log 这个中间层,IO 线程和 SQL 线程各自独立工作,IO 线程可以先把事件全部拉到本地,SQL 线程再按自己的节奏慢慢执行,同时断开重连等异常也不会影响已接收的日志。
MySQL 8.0 起,官方文档把 master/slave 术语换成了 source/replica,部分命令也做了更名,比如 SHOW SLAVE STATUS 老命令改为了 SHOW REPLICA STATUS。不过 8.0 版本里老命令仍兼容可用,网上大量教程还在用 CHANGE MASTER TO,我下文的命令以 5.7/8.0 普遍可用的写法为主,同时会标出新的叫法。
2.2 binlog 格式怎么选,为什么默认推荐 ROW
binlog 是一个写操作事件流,但记录事件有三种格式,这一步直接影响从库数据一致性:
STATEMENT:记录原始 SQL 语句。比如主库执行UPDATE t SET a=a+1 WHERE id>100,binlog 里存的就是这条 SQL,传到从库再执行一遍。优点是日志量小;缺点是依赖上下文,如果 SQL 里有NOW()、UUID()、RAND()这类非确定性函数,从库重放结果可能和主库不一致。ROW:记录每一行数据变更前后的值。它是"结果级"的复制,不依赖 SQL 上下文,特殊函数也不会产生不一致。缺点是日志量明显增大,一条 UPDATE 影响一万行,binlog 里可能就有上万条行变更事件。MIXED:让 MySQL 自己判断,默认用 STATEMENT,遇到可能产生歧义的语句自动切到 ROW。看起来两全其美,实际运维中 SQL 行为判断存在不可穷尽的情况,一旦判断失误导致从库数据不一致,排查代价远大于省下的磁盘空间。
我的建议很直接:新项目一律用 ROW,不要犹豫。 日志量大,那就扩容 binlog 磁盘、调大 max_binlog_size,这比某天从库对不上账轻松多了。做数据异构同步(比如通过 Canal 同步到 Elasticsearch)时,ROW 格式更是唯一靠谱的选择,因为它能提供完整的前后镜像。
2.3 position 和 GTID:两条复制路线
从库向主库报到时,需要说清楚"我上次同步到哪个位置了,请从这个位置继续发给我"。这个定位机制经历了两代:
基于 binlog 文件名 + position 偏移量的方式。 例如 MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=157。这种方式简单直接,但有一个致命弱点:如果从库 relay log 或本地状态丢了,你必须重新去主库找对应的文件和偏移量;做故障切换时,新主库上每个事务的位置和旧主库完全不同,重新对齐非常痛苦。
基于 GTID(全局事务标识符)的方式。 每个事务在提交时都会分配一个唯一 ID,格式是 服务器UUID:事务序号,比如 a3f2e9c8-1b2d-4e5f-8a7b-1234567890ab:1-2048。主从之间不需要再比较文件和第几字节,只要交换各自执行过的 GTID 集合,主库就知道从库缺哪些事务、从哪个事务继续发。这个特性在后续做切换、重建从库时优势极大。
动手搭建时,我强烈建议直接以 GTID 为目标,哪怕你第一次实验用的是 position 方式,也建议搞清楚两者差别,我们会在第 4 节详细讲 GTID 怎么落地。
3. 从零搭建 MySQL 8.0 主从:配置、命令与首次数据同步全程
3.1 规划参数之前,先把这个默认坑避开
如果你用的是 Linux 包管理器直接安装的 MySQL,或者是 Docker 起的 MySQL,第一件要核对的事是 server-id。MySQL 规定复制拓扑里每个节点的 server-id 必须全局唯一,默认值通常是 1。两台机器如果都保持默认 1,从库启动复制时 IO 线程会一直报错,这是新手踩得最密集的一个坑。
环境规划参考如下:
| 角色 | IP 示例 | server-id | 预期用途 |
|---|---|---|---|
| 主库 | 192.168.1.10 | 1 | 读写业务写入,也可承担部分强一致读 |
| 从库 | 192.168.1.11 | 2 | 只读,承接报表查询、备份、数据抽取 |
生产环境配置变更前,记得先备份原配置文件。下面是主库 my.cnf 里最小必要的一组配置:
ini复制[mysqld]
server-id = 1
log-bin = mysql-bin
binlog_format = ROW
sync_binlog = 1
innodb_flush_log_at_trx_commit = 1
binlog_expire_logs_seconds = 604800
从库配置:
ini复制[mysqld]
server-id = 2
log-bin = mysql-bin
relay-log = relay-bin
read_only = ON
super_read_only = ON
逐行说下为什么这样配。log-bin 不开启,主库根本没有复制数据源,这是前提。sync_binlog = 1 表示每次事务提交都把 binlog 刷盘,innodb_flush_log_at_trx_commit = 1 表示每次事务提交都刷 redo log,这两个参数配合才能保证主库宕机时 binlog 和 InnoDB 数据尽量不出现较大落差。binlog_expire_logs_seconds = 604800 是让 binlog 保留 7 天,防止磁盘被日志撑爆——注意 5.7 及之前用的是 expire_logs_days,单位是天,8.0 推荐用秒。从库我额外开启了 read_only,它能让非 SUPER 权限的账号无法写入;super_read_only 更进一步,连 SUPER 账号的普通写入也拦掉。从库一定要只读,否则应用误连从库写入数据,和主库数据 divergence,SQL 线程迟早报错中断,这是生产事故的高发区。
3.2 主库配置与复制账号
配置改完,重启 MySQL:
bash复制systemctl restart mysqld
然后在主库上创建一个专门用于复制的账号。不建议用 root 做复制,权限过大且一旦密码泄露影响面太宽。创建账号时注意 host 范围,尽量限定从库所在网段:
sql复制CREATE USER 'repl'@'192.168.1.%' IDENTIFIED WITH mysql_native_password BY 'Repl@2024!';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'192.168.1.%';
FLUSH PRIVILEGES;
为什么这里指定 mysql_native_password?MySQL 8.0 默认认证插件是 caching_sha2_password,它在非 SSL 连接下首次认证会要求额外的 RSA 公钥交换步骤,复制通道很容易报错,新手排查起来会很懵。用 mysql_native_password 是 8.0 环境里比较省事的做法。不过要注意它在新版本中已被标记为弃用,如果用的是 MySQL 8.4 及以上,更推荐给主从配置 SSL 或显式处理公钥传递,具体要看你所在版本的官方文档。
查看主库当前 binlog 位置:
sql复制SHOW MASTER STATUS;
输出类似:
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
|---|---|---|---|---|
| mysql-bin.000003 | 157 |
记下 File 和 Position,下面注册从库时要用。如果你的主库还没有任何写操作,Position 通常是 4 或 157 这类起始值。
3.3 存量数据怎么灌进从库,顺序不能乱
如果这是一套全新环境、主库没有业务数据,可以跳过这一步。但绝大多数情况是主库已经跑了一段时间,你必须在开启复制前先把存量数据对齐。
最稳妥的导数据命令:
bash复制mysqldump -uroot -p \
--single-transaction \
--master-data=2 \
--set-gtid-purged=OFF \
--all-databases > master_backup.sql
参数不是随便加的:--single-transaction 用 InnoDB 的快照读导数据,不锁业务表;--master-data=2 会在备份文件头部注释里记录导出那一刻主库的 binlog 文件名和位置;--set-gtid-purged=OFF 是因为当前还没启用 GTID,如果后续你计划按第 4 节切 GTID,这个参数值要相应调整。
把备份文件传到从库机器,然后导入:
bash复制mysql -uroot -p < master_backup.sql
导入完成后,从库的数据基线就和主库导出的那个时间点一致了。从这个时间点开始产生的增量 binlog,就交给复制链路去追。
3.4 从库注册主库并验证状态
在从库上执行:
sql复制CHANGE MASTER TO
MASTER_HOST='192.168.1.10',
MASTER_USER='repl',
MASTER_PASSWORD='Repl@2024!',
MASTER_LOG_FILE='mysql-bin.000003',
MASTER_LOG_POS=157;
这里的 MASTER_LOG_FILE 和 MASTER_LOG_POS,必须填第 3.2 步 SHOW MASTER STATUS 看到的实际值,而且要特别注意:这个位置必须能对应到从库导入的备份快照那一刻。如果你用的是 mysqldump --master-data=2,备份文件头注释里有一行类似 -- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=157;,你可以直接用它填,或者干脆把 --master-data=1 让备份文件自带未注释的 CHANGE 语句。但为了清晰可控,我习惯手工填。
接着启动复制:
sql复制START SLAVE;
检查状态:
sql复制SHOW SLAVE STATUS\G
重点关注这几行:
code复制 Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 157
Relay_Log_Space: 866
Seconds_Behind_Master: 0
Slave_IO_Running: Yes 表示 IO 线程已连上主库并持续拉取 binlog;Slave_SQL_Running: Yes 表示 SQL 线程在正常重放;Seconds_Behind_Master: 0 表示从库已追上主库,没有延迟。
做一次验证:
sql复制-- 在主库执行
CREATE DATABASE demo;
USE demo;
CREATE TABLE t_user (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50));
INSERT INTO t_user(name) VALUES('zhangsan'),('lisi');
然后到从库查询:
sql复制USE demo;
SELECT * FROM t_user;
能查到两条记录,恭喜,一个最小可用的主从复制链路就跑通了。是不是感觉没想象中复杂?复杂的是后面——链路在运行中会以各种姿势出问题,而且出问题的时间往往挑在半夜。
4. 从 position 升级到 GTID:故障切换时你会感谢这个选择
4.1 GTID 为什么能简化主从切换
我们在第 3 节搭建的是基于日志位置的复制。它有一个绕不开的痛点:每次从库重新连主库、或者主从角色互换时,你必须找到一个准确的 (file, position) 坐标。问题是,同一个事务在 A 机器上可能是 mysql-bin.000010:441,到了 B 机器上就变成 mysql-bin.000003:881,坐标本身是随机器而异的,没有全局可比性。
GTID 把这件事变成了"集合比对"。每个事务都有全局唯一 ID,主库发送时直接问从库:你执行过哪些 GTID?从库回答之后,主库把从库缺少的事务补发过去。这意味着:
- 从库重建后不需要手工找位置,只要
MASTER_AUTO_POSITION=1就行; - 主从切换时不需要关心 binlog 名和偏移量,只需要确认 GTID 集合的包含关系;
- 数据一致性判断也更直接:两边
gtid_executed值一致,说明事务集合完全一致。
4.2 落地步骤:先让两个节点都进入 GTID 模式
在配置文件中给主库和从库都加上:
ini复制[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON
重启后,GTID 不会自动为旧事务生成——它只针对新事务,所以你需要重建一次复制链路,让从库在主库生成的 GTID 基础上继续同步。顺序如下:
第一步,主库上确认 gtid_mode 已经是 ON:
sql复制SHOW VARIABLES LIKE 'gtid_mode';
SHOW VARIABLES LIKE 'enforce_gtid_consistency';
第二步,从库停掉旧复制:
sql复制STOP SLAVE;
第三步,把复制方式切换到自动定位:
sql复制CHANGE MASTER TO
MASTER_HOST='192.168.1.10',
MASTER_USER='repl',
MASTER_PASSWORD='Repl@2024!',
MASTER_AUTO_POSITION = 1;
注意这里不再写 MASTER_LOG_FILE 和 MASTER_LOG_POS。执行后再启动:
sql复制START SLAVE;
这时候如果一切正常,复制会基于 GTID 自动对齐。如果从库之前落后太多、或主库 binlog 已经过期清理,你会看到 IO 线程报错,错误里通常包含"does not contain the transaction"之类的字样,这是因为主库需要的 GTID 事务已经不在 binlog 里了。处理方式只能是从主库重新做一次全量备份灌入,没法靠增量补。
4.3 从库提升为主库时的标准操作顺序
真正让 GTID 发光发热的场景是主库故障切换。假设现在要人工把从库提升为新主库,标准的操作顺序长这样:
第一步,在旧主库上停掉写入。最好是在应用层直接切流量,同时数据库侧执行:
sql复制SET GLOBAL read_only = ON;
SET GLOBAL super_read_only = ON;
第二步,在旧主库上记录当前已执行的 GTID 集合:
sql复制SHOW MASTER STATUS;
记下 Executed_Gtid_Set 字段的值,比如 a3f2e9c8-1b2d-4e5f-8a7b-1234567890ab:1-2048。
第三步,在将要提升的从库上,等待它把旧主库的所有事务都执行完:
sql复制SELECT WAIT_FOR_EXECUTED_GTID_SET('a3f2e9c8-1b2d-4e5f-8a7b-1234567890ab:1-2048');
这个函数会阻塞直到该集合内的事务全部执行完毕,返回后你才能确信从库没有缺失任何已提交事务。
第四步,从库停止复制并清空复制状态:
sql复制STOP SLAVE;
RESET SLAVE ALL;
RESET SLAVE ALL 会删掉从库上 master_info 和 relay_log_info 等复制元数据,这一步执行前务必确认数据已经追平,否则清完之后旧主库的连接信息就丢了。
第五步,解除新主库的只读限制,并把写入流量切到新主库:
sql复制SET GLOBAL read_only = OFF;
SET GLOBAL super_read_only = OFF;
如果旧主库修复后要降级为新从库,它需要先清掉自己的历史复制状态,然后重新指向新主库:
sql复制RESET SLAVE ALL;
CHANGE MASTER TO
MASTER_HOST='192.168.1.11',
MASTER_USER='repl',
MASTER_PASSWORD='Repl@2024!',
MASTER_AUTO_POSITION = 1;
START SLAVE;
你会发现整个过程里没有再出现 "找 position" 这一步,这就是 GTID 给运维带来的最大红利。如果你现在还是 position 方案,我建议尽早规划切到 GTID——不是因为它更高级,而是它能实实在在降低你在故障发生时手忙脚乱的概率。
5. 真实故障复盘:IO 线程连不上、SQL 线程中断、主从延迟
5.1 故障一:Slave_IO_Running 永远是 Connecting
有个项目刚搭完主从,从库执行 START SLAVE 后,SHOW SLAVE STATUS\G 里 Slave_IO_Running 始终是 Connecting,过一会儿变成 No,循环往复。Last_IO_Error 提示连接超时或 Access denied。
完整排查链路应该是这样的,不要一上来就改配置凭感觉重试:
第一步,确认从库能通过 TCP 访问主库的 3306 端口。用系统命令测,不要用 mysql 客户端裸连——因为如果没加 -h,MySQL 客户端默认走本地 socket 文件,连不上时报的可能是 ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock',这个报错说明的是"你根本没走到网络层",和复制账号、主库远端配置没有关系,容易被带偏。正确的探测方式:
bash复制telnet 192.168.1.10 3306
nc -vz 192.168.1.10 3306
第二步,如果端口不通,看主库 bind-address 是不是绑定到了 127.0.0.1。MySQL 如果只监听本机回环地址,外部怎么都连不上。再检查云安全组和节点防火墙:
bash复制firewall-cmd --list-ports # CentOS/RHEL 7+
iptables -L -n # 传统方式
第三步,端口通了还报 Access denied,就要查复制账号的 host 授权。比如建账号时用了 'repl'@'192.168.1.%',但从库出网 IP 是 192.168.1.88,网上有说法建议用 %,但我更建议把网段写精确:账号授权越宽,密码泄露后的爆破面越大。确认方法:
sql复制SELECT user, host FROM mysql.user;
第四步,还有个隐蔽原因:从库机器如果是克隆出来的虚拟机,server_uuid 可能和某台历史节点重复,复制连接会被拒绝。查看并修正:
sql复制SHOW VARIABLES LIKE 'server_uuid';
MySQL 的 UUID 存在数据目录的 auto.cnf 文件里,如果重复,停库后删除或修改这个文件再重启,MySQL 会重新生成。
5.2 故障二:Slave_SQL_Running 变 No,Duplicate entry 刷屏
相比 IO 线程连不上,SQL 线程中断是更常见的"慢性病"。某天从库状态检查发现 Slave_SQL_Running: No,Last_SQL_Error 写着:
code复制Could not execute Write_rows event on table demo.t_user; Duplicate entry '1001' for key 'PRIMARY'
这类错误的根源几乎都是从库数据与主库基线不一致。常见场景包括:往从库导初始数据时主库还在写入,导致快照之后的增量事务和导入的数据撞了主键;或者有人不小心往只读从库里手工插入过数据。
这里最难的不是修复,而是忍住"跳过错误"的冲动。网上很多教程会让你执行:
sql复制STOP SLAVE;
SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1;
START SLAVE;
这个命令的含义是让 SQL 线程跳过下一个事务。它的适用场景非常窄:你必须确认跳过的只是一个事务,且这个事务在主从两侧没有造成数据差异。如果你连续遇到几十条 Duplicate entry,一条条跳等于在雪崩时数雪花,跳完后面的数据对账还是一团糟。
更可靠的处理链路是:评估从库数据差异范围。小范围差异、事务确认为冗余重放时,可以用 SQL_SLAVE_SKIP_COUNTER 跳过;但凡差异原因说不清,直接选择重建从库。GTID 模式下重建成本已经很低了:
- 从库
STOP SLAVE; - 主库做一次新的全量备份;
- 从库清空数据并导入备份;
- 重新
CHANGE MASTER TO ... MASTER_AUTO_POSITION = 1并START SLAVE。
5.3 故障三:Seconds_Behind_Master 看着是 0,业务却读到旧数据
有段时间业务反馈,刚提交的订单在列表页查不到。从库 Seconds_Behind_Master 明明是 0,看起来链路没有延迟。但真去查从库数据,确实少了最近几分钟的记录。
这个故障反映了 Seconds_Behind_Master 这个指标的盲区。它的计算逻辑是当前时间减去 SQL 线程正在执行的事件时间戳,不能等价于"主从数据实时一致"。如果 SQL 线程因为某个大事务被卡住、或者 relay log 获取出现空窗,这个值可能短暂显示为 0 或根本不涨。而且从库的数据读取是异步复制的结果,你永远不能假设"从库读到的就是最新"。
排查要从两个方向同时入手:
方向一,看从库当前是否有大事务在执行。从库执行:
sql复制SHOW PROCESSLIST;
如果看到 SQL 线程正在执行一个 UPDATE 或 DELETE 影响几十万行,那就是一个大事务拖慢了重放速度。这类大事务在 ROW 格式下会被拆成海量行事件,SQL 线程执行它们天然比主库原始语句慢得多,因为主库一条 UPDATE 可能走索引批量更新,而从库按事件逐行应用。
方向二,用更精准的方式监控真实延迟。我的团队后来引入了心跳表方案:定时从主库写入一条带时间戳的记录,再从从库读取这条记录的延迟。开源社区常用的 pt-heartbeat 就是干这个的。它比 Seconds_Behind_Master 可信得多,能反映真实的端到端延迟。
6. 上线之后才是开始:巡检清单、一致性校验与常见误操作
6.1 每天该看的几个状态,别等出了事才想起
主从复制搭完,最危险的心理是"只要状态看着正常就等于一直正常"。我建议把状态巡检固化到每天的例行任务里,最基础的检查项是:
| 检查项 | 命令或方法 | 期望结果 |
|---|---|---|
| 复制线程状态 | SHOW REPLICA STATUS\G |
IO 和 SQL 均为 Yes |
| 最近错误 | 同上 Last_IO_Error / Last_SQL_Error |
均为空 |
| 从库延迟 | Seconds_Behind_Master 或 pt-heartbeat |
长期保持低位 |
| 从库是否被误写 | SHOW GLOBAL VARIABLES LIKE 'read_only' |
ON |
| 主库 binlog 空间 | df -h + SHOW BINARY LOGS |
剩余空间充足 |
| 主从数据一致性 | pt-table-checksum(定期) |
无差异 |
数据库监控告警里,我特别建议把 Slave_IO_Running 和 Slave_SQL_Running 变成显式监控指标,不要只依赖错误日志。MySQL 的复制线程一旦中断,它不会主动发微信给你,只会安静地停在 No 状态,直到业务侧暴露问题。
6.2 复制不能替代备份,这俩是两码事
我见过不止一个团队,以为有了从库就等于有备份,主库的定时备份任务直接下线。这是整个数据库运维里最危险的一个错觉。
逻辑上想想就明白:如果你在主库上执行了一条错误的 DELETE FROM 并且没带 WHERE,这条删除会立刻被复制到从库执行,从库的数据也会被删掉。此时从库非但不是备份,反而是"帮凶",帮你把错误扩散到了第二台机器。
真正的备份应该满足"可以回到过去某个时间点"的能力。所以即便有主从复制,主库的物理备份或逻辑备份仍然要保留,binlog 也要按周期归档。否则你需要回溯数据时,会发现所有节点——主库、从库——都站在同一个错误时间点上,没有任何机器能救你。
6.3 网上一些"一键主从"脚本为什么不适合直接用
网上不少一键脚本会把若干危险配置打包进去,比如 slave-skip-errors = 1062,意思是遇到主键冲突自动跳过、SQL 线程永不中断。表面看是让复制"更稳定",实际是在系统性地掩盖数据不一致。跳过错误后从库和主库的数据差异会在后续查询中逐渐放大,等你发现时往往已经无法通过增量方式追平。
另一个常见误操作是在从库上执行大表 DDL,比如直接 ALTER TABLE 给几千万行的表加字段。MySQL 8.0 的 Instant DDL 能处理部分加列操作,但大量 DDL 在从库执行时依然会长时间持有元数据锁,阻塞 SQL 线程重放其他事务,表现为诡异的主从延迟。大表结构变更要遵循变更流程,在主库执行前评估其复制影响,必要时用 gh-ost 或 pt-online-schema-change 这类在线变更工具。
聊到这儿,主从复制从原理到落地的关键点基本都覆盖了。最后分享一个我个人养成的习惯:每次对复制拓扑做任何变更前,先把 SHOW MASTER STATUS 和 SHOW SLAVE STATUS 的完整输出存成带时间戳的文本文件,和变更记录放一起。这个习惯在故障复盘时帮了我大忙——很多问题查到最后,都需要对比"变更前"和"变更后"的复制状态。运维这种事,数据比记忆可靠,记录比自信可靠。
