半夜两点被告警电话叫醒,打开手机一看:Slave_SQL_Running: No,从库已经停了快一个小时。这种感觉做过MySQL运维的人都懂。主从复制是MySQL高可用架构的基石,但也是日常故障中占比最高的环节之一。尤其是MySQL 8.0之后,很多老的排障思路已经过时了,还在用老方法排查大概率会走弯路。这篇文章是我在处理了大量主从复制问题之后的经验总结,把从原理到排障的完整链路拆开讲清楚,适合正在使用MySQL 8.0的DBA、运维和开发同学参考。
1. 先搞清楚主从复制在复制什么:Binlog的流转与三种复制方式
很多人一遇到主从故障就直接上SHOW SLAVE STATUS,却不理解主从复制到底是怎么跑起来的。这是排障思路混乱的根源。先把这条链路彻底搞明白,后面所有问题都会清晰很多。
1.1 从一条更新语句说起
主从复制的起点是主库的Binlog。你在主库执行一条UPDATE语句,MySQL会按照配置的binlog格式记录这条变更,这个动作发生在事务提交阶段。主库上有一个dump线程专门负责监听Binlog的变化,一旦有新的binlog事件产生,就把这些事件推送给从库。
从库这边有两个核心线程在干活:
- IO线程:负责连接主库,接收主库dump线程推送过来的binlog事件,并写入从本地的relay log(中继日志)中。
- SQL线程:负责读取relay log中的事件,并在从库上顺序回放,最终实现数据变更。
这个模型是MySQL异步复制的核心。理解这个模型之后,你就会明白:当IO线程报错时,问题通常出在从库到主库的网络、认证、主库dump线程状态上;当SQL线程报错时,问题通常出在relay log中要回放的SQL在从库上执行失败。这两个排查方向完全不同,在实际操作中第一步先分清是哪个线程挂了,能省去大量无用功。
1.2 异步复制、半同步复制与全同步复制的取舍
MySQL 8.0支持多种复制模式,实际使用中最常见的是异步复制和半同步复制,全同步复制(Group Replication)属于另一个体系。
- 异步复制:主库提交事务后,不关心从库是否收到和回放。好处是性能损耗最小,坏处是主库一旦崩溃且从库还没收到binlog,丢数据的风险很高。
- 半同步复制:主库在提交事务后,需要等待至少一个从库确认已经收到并写入relay log,然后才向客户端返回成功。相比异步复制,半同步显著降低了数据丢失风险,但会增加一定的响应延迟。MySQL 8.0中的半同步插件已经集成在
mysql-server包中,不需要像5.7那样手动安装插件包,但机制上继承了5.7的行为。 - Group Replication:基于Paxos协议的多主复制,本质上解决的是数据一致性问题,但运维复杂度高,日常使用中不是所有团队都能驾驭。
在排障时,搞清楚当前用的是哪种模式很有意义。半同步复制下如果从库超时或断开,主库会退化为异步复制继续提供服务,这是容易被人忽略的降级场景。你可能会在主库上看到大量semisync相关的状态信息,而业务无感知。要主动关注这些状态,避免在主库故障切换时才发现数据没有真正同步到位。
提示:MySQL 8.0中
SHOW SLAVE STATUS仍然可用,但从8.0.22开始官方推荐改用SHOW REPLICA STATUS。新环境建议直接使用新命令,旧脚本可以继续兼容,不过新项目没必要再写SLAVE了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 配置阶段最容易翻车的地方:server-id、GTID与Binlog格式
很多主从复制故障其实在配置阶段就已经埋下了雷。搭建好了之后能正常跑几天,一旦某个节点重启或切换,问题就集中爆发。配置文件里的每一个参数都不是随意写的,下面这几个坑我基本每次帮人排查都能遇到。
2.1 server-id重复导致的诡异故障
MySQL主从复制要求每个节点在同一个复制拓扑中必须有唯一的server-id。这个参数属于MySQL实例级别的标识,主库和从库之间、多个从库之间都不能重复。
最常见的场景是:用虚拟机镜像克隆出多台服务器,克隆完之后没有去修改server-id,结果几台机器一启动就出现日志报错,从库IO线程一直处于Connecting状态,或者一会儿连上、一会儿断开。因为MySQL会依据server-id来标识binlog事件的来源和过滤循环复制,重复的server-id会让主库的dump线程认为这是同一个从库的连接,直接把之前的连接踢掉。
另一个容易忽略的点是:不要在配置文件中写死server-id为1。多套环境、多个实例同时存在时,server-id=1的碰撞概率非常高。我个人的习惯是使用IP地址的末几位加上端口号组合成一个数字,比如192.168.1.15的实例用server-id设为15015,看起来不优雅但足够唯一。
2.2 GTID模式与Binlog格式的匹配关系
MySQL 5.6开始引入GTID,8.0中GTID已经是主从复制的默认推荐方案。GTID的全称是Global Transaction Identifier,每个事务在提交时会生成一个全局唯一标识,从库通过该标识来追踪哪些事务已经执行过,从而避免重复回放。
开启GTID后,binlog格式基本上必须使用ROW格式。虽然MySQL官方文档说MIXED模式也能配合GTID,但在实际使用中,ROW格式带来的数据一致性和可排查性远远优于STATEMENT模式。尤其是遇到数据不一致排查时,ROW格式能把每一行变更前后的值都记录在binlog中,这对后续使用工具修复数据至关重要。
配置时注意两个参数要配合好:
ini复制[mysqld]
server-id = 15015
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_format = ROW
log_replica_updates = ON
relay_log_recovery = ON
这里log_replica_updates常被忽略。如果从库还同时作为其他从库的主库(级联复制),这个参数必须开启,否则从库自己的binlog不会记录从主库同步过来的事务,下游节点就断链了。MySQL 8.0中该参数默认是开启的,但我依然建议在配置中显式写出来,明确意图。
2.3 初始化数据环节的常见错误
搭建主从复制前,先要保证从库有一份和主库一致的历史数据。常用做法是mysqldump导出主库数据,再导入从库。这一步看似简单,实际上问题很多。
mysqldump导出时要同时导出master-data信息,方便后续定位同步起点。在GTID模式下,更推荐这样导出:
bash复制mysqldump -uroot -p --single-transaction --set-gtid-purged=ON --all-databases > backup.sql
--set-gtid-purged=ON的作用是在备份文件中记录主库当前已经执行过的GTID集合。从库导入这些数据后,再执行CHANGE REPLICATION SOURCE TO时,MySQL会自动跳过这些已经存在的GTID事务,从缺失的部分开始拉取。
实际生产中,很多人图省事,直接用物理文件拷贝或者rsync复制整个数据目录。这种方式的坑在于:如果主库当时还在写入,拷贝出来的数据文件可能处于不一致状态;即使当场没有报错,后续也容易出现主从数据不一致。执行初始化操作前,必须确保主库的数据是静态的,或者在业务低峰期配合FTWRL全局锁来做。不要心存侥幸。
3. 排障实战:从SHOW REPLICA STATUS到线程状态解读
真到了故障现场,冷静下来按顺序排查比什么都重要。这一节我把主从复制最常见的几类故障的完整排查链路写出来,你可以直接照着做。
3.1 三个黄金命令:SHOW REPLICA STATUS、SHOW PROCESSLIST、ERROR LOG
第一件事不是去主库查,而是在从库上执行:
sql复制SHOW REPLICA STATUS\G
重点关注以下几个字段:
| 字段 | 含义 | 健康值 |
|---|---|---|
| Replica_IO_Running | IO线程是否运行 | Yes |
| Replica_SQL_Running | SQL线程是否运行 | Yes |
| Seconds_Behind_Source | 回放延迟秒数 | 0 |
| Last_IO_Errno / Last_IO_Error | IO线程最近一次错误 | 0 / NULL |
| Last_SQL_Errno / Last_SQL_Error | SQL线程最近一次错误 | 0 / NULL |
| Retrieved_Gtid_Set | 已经拉取到的GTID集合 | 持续增长 |
| Executed_Gtid_Set | 已经执行完的GTID集合 | 持续增长 |
看到Replica_IO_Running: Connecting时,说明从库根本没有和主库建立好复制连接。接下来看Last_IO_Error给出的具体报错,同时去主库执行SHOW PROCESSLIST,看看能不能看到从库IP过来的Binlog Dump线程。如果主库上根本没有这个线程,说明连接压根没建立成功。
在从库上执行SHOW PROCESSLIST也有价值,能直观看到IO线程和SQL线程的状态。如果IO线程卡在Sending to client状态且长时间不变化,大概率是网络延迟大或主库dump线程被大事务拖住了。
最后别忘了一点:任何排障都要先看错误日志。MySQL的error log会记录复制线程启动、重连、报错的详细调用链。很多人上来就执行SQL命令,忽略了日志文件,结果绕了一大圈才发现问题在日志里写得明明白白。
3.2 IO线程报错:连接不上主库、认证失败、网络问题
IO线程报错的典型错误码包括2003、1045、1236等,分别对应不同的原因:
2003 Can't connect to MySQL server:从库根本连不上主库的3306端口。先去主库执行TELNET或NC测试端口连通性,再看主库的bind-address是否限制了监听地址,以及防火墙/安全组是否放通了端口。MySQL 8.0默认配置只监听127.0.0.1,如果主库配置文件里bind-address没有改成实际网卡IP,从库必然连不上。1045 Access denied:复制账号认证失败。MySQL 8.0的默认认证插件是caching_sha2_password,如果在创建复制账号时没有指定mysql_native_password,并且从库版本过低,会出现认证不兼容的情况。解决办法是升级从库版本,或者创建账号时使用兼容的认证方式。具体创建复制账号的SQL建议这样写:
sql复制CREATE USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'StrongPass123';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
1236:这个错误非常经典,常常是Got fatal error 1236 from master when reading data from binary log。原因通常是从库要读取的binlog位置或GTID在主库上已经不存在了,比如binlog被清理掉,或者手动RESET MASTER过。处理思路是用CHANGE REPLICATION SOURCE TO重新定位到主库当前有效的binlog位置或GTID集合,如果是GTID模式,可以直接用MASTER_AUTO_POSITION=1自动定位。
3.3 SQL线程报错:主键冲突、表不存在、数据不一致
SQL线程报错意味着relay log里的SQL在从库上执行失败了。常见场景有:
- 主键冲突:错误码
1062。从库上已经存在相同主键的记录,而relay log里又插入一条同主键记录。这通常是因为从库被业务侧直接写入过数据,或者初始化数据时没有完全对齐。解决办法需要根据业务场景判断是否允许删除从库上多余的数据,然后手动处理冲突行,再用START REPLICA继续。 - 表不存在:错误码
1146。原因可能是初始化后数据库表被误删,或者表在从库上由于某些原因没创建成功。如果只是单张表缺失,可以把主库的表结构导出后在从库重建,然后跳过对应的GTID事务或binlog位置。如果大量表缺失,建议重新初始化从库,不要逐一修补。 - 数据不一致:错误码
1032,回放DELETE语句时影响行数为0。这意味着主库上删除的数据在从库上压根不存在。这种问题背后往往隐藏着更早之前的静默故障,比如曾有人手动修改过从库数据、或者从库上执行过SET SQL_LOG_BIN=0的操作。
这里我需要强调一个关键习惯:不要一看到SQL线程报错就无脑SET GLOBAL sql_slave_skip_counter = 1跳过。在GTID模式下这个命令直接就不能用了,更重要的是,盲目跳过会让缺失的数据被永久掩盖。正确做法是备份报错时的Last_SQL_Error信息,定位具体事务,评估影响范围,再决定是手动补数据还是重建从库。
4. 主从延迟的根因分析与优化手段
主从延迟不会直接中断复制,但危害往往更大。业务读到旧数据、切换时大量补binlog、主库故障后恢复时间过长,这些问题的根源都是延迟。
4.1 延迟的判定标准与常见误判
Seconds_Behind_Source是目前最常用但也是最容易被误读的指标。它表示的是SQL线程回放时间与IO线程最新接收时间之间的差距。在以下场景中会出现误判:
- 从库IO线程断开时,
Seconds_Behind_Source可能显示为NULL,这并不代表延迟为0。 - 主库长时间没有写入时,从库的SQL线程追平后,该值也会变成
0,但这不代表主从之间没有配置问题。 - 大事务回放期间,该值可能先暴涨再回落,不一定代表永久性延迟。
判断真实延迟更准确的方法是看Executed_Gtid_Set和Retrieved_Gtid_Set的差值,以及主库当前持有GTID的最新事务与从库执行到的GTID之间的差额。不要只盯着Seconds_Behind_Source这一个数字下结论。
4.2 从库单线程回放限制与并行复制
MySQL 8.0的默认复制回放模式是单线程的(Coordinator + Worker的模式下,很多简单场景还是退化为单线程),这就是主库写入压力大时从库容易延迟的根本原因。优化手段是开启并行复制:
ini复制[mysqld]
replica_parallel_workers = 4
replica_parallel_type = LOGICAL_CLOCK
LOGICAL_CLOCK模式下,MySQL会根据事务提交的先后顺序和锁竞争关系,自动判断哪些事务可以并行回放。这是基于主库的binlog_group_commit_sync_delay等参数来协调的。要注意:并行复制不是万能的。如果业务写入模式是单表单行高频更新,事务之间锁竞争严重,并行度上不去,延迟依旧存在。
4.3 大事务、DDL和热点更新导致的延迟
这是我最常遇到的三个延迟根因:
- 大事务:一条
UPDATE更新几百万行,binlog事件很大,从库解析和回放都要花较长时间。解决办法是拆分事务,控制在几千行一个事务,既降低主库锁持有时间,也减轻从库回放压力。 - DDL:
ALTER TABLE在从库回放时通常无法并行,而且会加锁。对超大表的DDL要引入gh-ost或pt-online-schema-change这类工具,避免在线DDL造成的同步延迟。 - 热点更新:某一行数据被高频更新,导致relay log中大量事件集中在这一行上,从库回放时只能串行等待行锁。这类问题通常需要从业务层面改造,比如引入队列削峰,或者调整主库的
innodb_thread_concurrency来限制对特定行的争用压力。
5. Docker部署MySQL 8.0主从实战中的坑
Docker装MySQL快是快,但踩过的坑一点不比物理机少。这里把容器环境下特有的问题集中说一下。
5.1 镜像拉取失败与版本选择
很多人在docker pull mysql:8.0这一步就卡住了。报错通常是Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection,这属于网络问题,解决思路是配置镜像加速器或使用能稳定访问Docker Hub的网络环境,同时注意确认registry-1.docker.io可以正常解析。
还有一点经常被忽略:mysql:8.0这个标签会持续跟随最新的8.0小版本。今天装的8.0和半年前别人文章里的8.0可能不是同一个版本,行为细节可能有差异。搭建环境时建议指定具体的tag,比如mysql:8.0.36,这样可复现性最好,也方便排查时对照版本特性。
5.2 docker compose环境下主从容器的初始化顺序
用docker compose up -d --build一次性拉起主库和从库时,会出现一个经典的初始化顺序问题:从库容器启动后会去连主库的3306端口,但如果主库容器还没完全准备好,从库的初始化脚本就会连不上,复制配置失败。
解决思路是给从库容器添加健康检查和依赖控制:
yaml复制services:
mysql-master:
image: mysql:8.0.36
environment:
MYSQL_ROOT_PASSWORD: root123
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-proot123"]
interval: 5s
timeout: 3s
retries: 10
mysql-replica:
image: mysql:8.0.36
depends_on:
mysql-master:
condition: service_healthy
environment:
MYSQL_ROOT_PASSWORD: root123
depends_on配合condition: service_healthy可以确保主库健康之后再启动从库。如果没有健康检查,最简单的兜底办法是在从库初始化脚本中写一个重试循环,每隔几秒检测主库端口,直到可连接再执行CHANGE REPLICATION SOURCE TO。这种脚本用shell写并不复杂,但能省去大量手工重试的麻烦。
5.3 容器重启后复制失效怎么办
容器重启后从库复制失效,是Docker部署的高频问题。现象是docker restart之后,SHOW REPLICA STATUS显示IO线程报Got fatal error 1236。
原因通常是容器重启后数据目录的binlog发生了变化,或者从库的复制位点还停留在重启前的位置,而主库的binlog在停机期间已经清理了。这种情况下,GTID模式的优势就体现出来了:如果开启了GTID,从库重启后执行STOP REPLICA、再START REPLICA,MySQL会尝试通过MASTER_AUTO_POSITION=1自动定位到正确的GTID位点,很多时候不需要人工干预就能恢复。
还有一个容器环境特有的坑:不要把MySQL的数据目录直接放在容器可写层里。容器一旦删除,数据全部丢失。务必通过Volumes或bind mount把/var/lib/mysql挂载到宿主机,否则重启一个容器就等于毁掉一个从库。
6. 数据不一致后的修复思路与日常巡检建议
主从复制跑久了,最怕的就是数据悄悄不一致。主键冲突往往只是表层的信号,真正的问题隐藏在更早的某个时刻。
6.1 用pt-table-checksum和pt-table-sync修复
当怀疑主从数据不一致时,最常用的开源工具是Percona Toolkit中的pt-table-checksum和pt-table-sync。
pt-table-checksum会在主库上对每张表计算校验和,再去从库上做同样的计算,对比结果找出不一致的表。执行时建议加上--no-check-replication-filters参数,避免因为复制过滤规则导致误报。它的原理并不复杂:对每一行做哈希计算,再聚合到块级别,最后比较主从两边块哈希。
发现不一致后,用pt-table-sync修复:
bash复制pt-table-sync --databases=testdb --replicate testdb.tb_name h=master_host,u=root,p=password
执行前务必先加--dry-run查看将要执行的SQL,确认影响范围之后再真正执行。这个工具默认会以主库数据为准来修正从库,执行过程中会产生一定的业务压力,建议在业务低峰期操作。
6.2 日常巡检的检查项
数据一致性不是靠出问题才修的,日常巡检非常关键。我自己的脚本里会定期检查这几项:
| 检查项 | 检查方法 | 告警阈值 |
|---|---|---|
| 复制是否中断 | SHOW REPLICA STATUS中的线程状态 |
任一线程不为Yes |
| 延迟是否过高 | Seconds_Behind_Source或GTID差值 |
连续5分钟大于30秒 |
| 主从GTID集合差距 | Retrieved_Gtid_Set与Executed_Gtid_Set比较 |
差值大于1000 |
| relay log是否膨胀 | 查看relay log目录大小 | 单文件超过1GB |
| 主库binlog保留时间 | SHOW MASTER STATUS和expire_logs_days/binlog_expire_logs_seconds |
保留时间小于24小时 |
把这些检查项接入Prometheus + Alertmanager或者简单的cron脚本定时执行,可以大大降低半夜被叫醒的概率。
另外,巡检时还要顺带确认从库的read_only参数是否开启。很多数据不一致问题的源头就是有人把应用程序连接串里的地址错误地指向了从库,业务直接在从库上写入了数据。MySQL 8.0中强烈建议将从库设置为read_only = ON和super_read_only = ON,这可以从数据库层面杜绝大部分这种误操作。
回到文章开头那个半夜告警的场景。现在你的排障思路应该很清晰了:先看SHOW REPLICA STATUS确认是IO线程还是SQL线程报错,再结合报错码和错误日志定位类型,是网络、认证、位点还是数据冲突,最后按对应策略处理。处理完之后,别忘了把根因记录下来,无论是重新初始化从库还是修复数据,都要确认复制恢复后Seconds_Behind_Source持续稳定归零。这套流程我已经跑通了很多次,每次都能用最少的时间定位问题。MySQL 8.0的主从复制本身很稳定,大部分故障都是配置不当和操作失误积累出来的。把这些前置工作做好,比掌握多少花哨的修复技巧都管用。
