拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL主从复制报错自动修复:从原理到工具落地实践

MySQL主从复制报错自动修复:从原理到工具落地实践

在 MySQL 运维这个圈子里,主从复制报错几乎是每个 DBA 都会遇到的日常。很多朋友第一次看见从库 SQL 线程变成 No 的时候,第一反应就是翻日志、找官档、手动改配置,折腾半小时终于把复制续上,结果第二天又来一次。标题里说的“自动修复”,并不是让你点一个按钮就万事大吉,而是把“检测—定位—补偿—验证”这条链路用工具串起来,让常见报错不用再等人半夜爬起来处理。说句实在话,生产环境里七成以上的复制报错并不复杂,复杂的是你不敢自动处理,也不知道处理完之后数据是不是真的对得上。

这篇文章适合三类人看:一是公司没有专职 DBA、后端同学兼任数据库运维的;二是已经有监控,但每次报警还是要人工登录库执行一堆命令的;三是刚接触 MySQL 复制、想系统搞懂报错原理和修复手段的新人。我会把个人在生产环境里用过的方案、踩过的坑,以及现在维持“能自动修复”的底线全部展开讲清楚。

1. 为什么先说“自动修复”,而不是“自动切换”

1.1 修复和切换完全是两个层面

故障转移(Failover)和复制修复(Recovery)经常被放在一起聊,但这俩其实是两件事。复制修复指的是从库的 IO 线程或 SQL 线程报错后,通过工具或脚本把复制链路重新拉起来,比如跳过错误事务、重建中继日志、补齐位点。故障转移则是指主库整体不可用之后,把读写流量切到某个从库上,让业务继续跑。

我见过不少同学一上来就搭了一套 MHA 或者 Orchestrator,以为主从报错会自动解决。实际上这些工具解决的是主库宕机后的切换,如果你的从库 SQL 线程早就挂了,它们不会帮你补数据,只会告诉你“当前拓扑里某个实例落后太多”,然后在切换时可能把数据缺口更大的库提升为主库,造成更大的混乱。这个认知非常关键,先搞清楚你缺的是“修复”还是“切换”,再去选工具。

1.2 自动修复真正要解决的问题

自动修复的核心可以拆成三件事。

第一,快速感知。从库复制线程停止后,必须在几十秒内被监控发现,而不是等业务侧报查询出错才反应过来。第二,准确判断。看到 1062 或者 1032 错误码时,要知道是哪些事务导致的、能不能跳过、跳过了会带来什么后续影响。第三,闭环验证。修复完成以后,要确认从库的延迟已经归零、GTID 集合和主库对齐、数据一致性没有被破坏。

很多自制的脚本只做到了第一点,第二点和第三点靠人肉补,所以才会出现“修了等于没修”的情况。我后面会专门讲怎么把第二点和第三点也做进自动化流程里。

1.3 不是所有报错都能自动修复

这句话值得写在你的值班手册第一页。自动修复适用于那些可以安全跳过或补偿的典型错误,比如从库误操作导致主键冲突、人为修改从库数据导致找不到行、binlog 被清理导致位点不可用等。但如果是硬件层面的磁盘损坏、中继日志文件崩溃、主从两端表结构不一致导致的大面积报错,自动修复方案非常有限,这种时候最重要的是保留现场,及时人工介入,而不是让脚本一遍遍重试,重试只会把问题放大。

所以说标题里这个“自动修复”,准确含义是把常见报错的处理流程标准化、自动化,而不是让数据库变成永不出错的神器。想清楚这一点,后面就不会在方案选型上走弯路。

2. 复制报错分类:先分清 IO 线程和 SQL 线程

2.1 两个线程的分工决定排查方向

MySQL 主从复制的原理是:主库把所有变更写入 binlog;从库的 IO 线程连到主库拉取 binlog,写入自己的中继日志 relay log;从库的 SQL 线程再读取 relay log 并把事务在本地重放。IO 线程负责“拉取”,SQL 线程负责“回放”,两者分工完全不一样。

排查的时候先看两个状态值:Slave_IO_Running和Slave_SQL_Running。IO 线程如果是 No,通常意味着从库连不上主库、认证失败、binlog 位置或者 GTID 集合对不上、主库的 binlog 被清理了。SQL 线程如果是 No,大概率是重放过程中和数据发生了冲突,比如主键冲突、找不到要修改的行、表结构不一致。这两个方向差别很大,别上来就复制粘贴网上的跳错命令。

2.2 我遇到的高频错误码和真实原因

下面按我实际处理的频率,把最常见的几种复制报错列一下。

错误码报错含义典型案例
1062主键或唯一键冲突从库被开发手动插入过同主键数据,主库又插入同主键记录
1032找不到要操作的行从库数据被删过或改过,导致重放回放时目标行不存在
1594中继日志文件损坏磁盘写异常、从库异常断电、relay log 被意外截断
1236从主库获取 binlog 失败主库 binlog 已过期清理,从库请求的位点或 GTID 不在binlog里
2003无法连接主库网络抖动、主库重启、防火墙策略变更
1677列类型不匹配从表结构被改动,和主库 binlog 里的 event 格式不一致

这里面 1062 和 1032 最容易自动修复,但要修复得安全,不能无脑跳过。1594 则比较麻烦,可能需要重建从库的复制链路。1236 要看具体情况,如果只是 binlog 被清理导致断点不可用,通常要用新主库重新建立复制关系,或者干脆重建从库。

2.3 看起来一样,性质完全不同的事故

有一类情况很容易误导人:Slave_SQL_Running: No,但Last_Error是空的,错误码也是 0。这种多半不是数据冲突,而是中继日志被截断或者内部状态异常。我第一次遇到时对着报错查了很久,反复跳错也没用,最后才发现是 relay log 文件在异常断电后损坏了,SQL 线程每次读到同一位置就退出。

所以这里有个经验:看到Last_Errno=0但 SQL 线程起不来时,别急着执行跳错命令,先看Relay_Log_File和Relay_Log_Pos,检查 relay log 相关文件是否完整。这种情况正确做法一般是STOP SLAVE; RESET SLAVE ALL;,然后用CHANGE MASTER TO重建复制关系,必要时直接从主库重新拉全量数据。

2.4 从库延迟过大算不算报错

Seconds_Behind_Master很大时并不一定代表复制线程报错,但通常是自动修复逻辑里的一个重要判断信号。如果从库延迟了几十分钟,说明它正在努力赶数据,此时做任何跳错或者切换操作都可能造成不可控的数据缺口。自动修复脚本里必须加一个延迟阈值,比如落后超过 60 秒就先告警不动作,等它自然追平再判断后续操作,这也算变相给自己上了保险。

3. 自动修复方案选型:自写脚本还是现成工具

3.1 从“人肉值班”到“自愈”的三个阶段

第一阶段是纯手工操作,监控发现报错后人登库,查询状态,翻日志,手动跳错或者重建复制。第二阶段是半自动化,写脚本监控状态,脚本自动跳常见错误,但跳完只记录日志不验证数据,整个流程能否成功还是依赖事后人工巡检。第三阶段才是完整的自愈体系,加入数据一致性校验、自动故障转移、修复后闭环验证,整个链路形成一套完整规则。

大部分团队从第一阶段往第二阶段走的时候,只需要一个简单的状态监控脚本,成本很低。但从第二阶段往第三阶段走,就该考虑引入成熟的工具了,因为数据一致性校验、选主策略、脑裂防护这些逻辑看起来简单,自己写还真的写不透。

3.2 主流的三个方案对比

我把比较常见的三个方向放在一起对比。

方案强项弱点适合场景
自写监控脚本轻量、完全可控、容易结合内部告警平台只擅长跳过 1062/1032,数据一致性验证薄弱小型业务、复制链路少、DBA 兼岗
MHA老牌主从高可用方案,自动故障转移成熟对 GTID 支持不如新工具直接,维护稍重长期稳定的传统主从架构
Orchestrator拓扑自动发现,可视化界面,自动恢复策略灵活部署配置有一定门槛,需要人员理解选主规则中大规模主从环境,需要自动 failover 和恢复

还有一个常用的辅助工具是 Percona Toolkit 里的pt-table-checksum,它不负责修复,但负责在修复之后验证主从数据是否一致。没有它,自动修复就等于蒙眼开车。

3.3 我为什么在多数场景选 GTID + Orchestrator

首先,GTID(全局事务标识符)让事务有了唯一编号,跳过错误时可以精确定位到具体事务,而不是像传统位点模式那样靠偏移量猜。其次,Orchestrator 能自动发现主从拓扑,提供 API 和 Web 界面,恢复策略可配置性很强,不会在一个简单报错上反复横跳。

但这里必须强调,Orchestrator 并不是装上就完事的。最典型的问题就是恢复策略配置不完整,比如RecoverMasterClusterFilters没写,Orchestrator 发现了故障却不会做任何动作。看起来监控面板一切正常,真正出事时它只是看客。这类“平台装了但配置没生效”的情况,比没有工具更危险。所以后面我会把配置里的关键项完整列出来。

4. 落地实操:搭建一套可用的自动修复链路

4.1 前置条件:GTID 开启、binlog 保留策略、监控账号

开始之前,先把基础环境检查一遍。打开从库和主库的 MySQL 配置文件,确认下面几个参数:

[mysqld] server-id=101 gtid_mode=ON enforce_gtid_consistency=ON log_bin=mysql-bin binlog_format=ROW expire_logs_days=7 log_slave_updates=ON

这些参数的解释不复杂。gtid_mode=ON让每个事务都带唯一标识,log_slave_updates=ON保证从库在重放主库事务的同时也记录自己的 binlog,这个参数在从库提升为主库之后特别重要,决定了它能不能带着更多从库继续跑。binlog_format=ROW更适合精准定位和校验数据,遇到行差异时能直接看到主键信息。

然后创建专门的监控账号,权限尽量收敛:

CREATE USER 'monitor'@'%' IDENTIFIED BY '这里换成一个足够强的密码'; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'monitor'@'%'; GRANT SELECT ON *.* TO 'monitor'@'%'; GRANT SUPER ON *.* TO 'monitor'@'%';

SUPER权限主要用于在 GTID 模式下跳过错误,给到特定监控账号即可,不要用 root 去跑自动化脚本。权限太大,脚本一旦出 bug,后果会比复制报错更严重。

用现有工具检查 GTID 集合的经典场景是:从库报错,你打开SHOW SLAVE STATUS\G,看到Auto_Position或者Master_UUID等字段异常。这时候要知道,GTID 模式下有一个从库执行但主库没有执行的 GTID 集合,叫Retrieved_Gtid_Set和Executed_Gtid_Set。修复前对比这两个集合,就能知道哪些事务已经在从库被执行了。

4.2 一个最小可用的自动修复脚本

先说清楚,下面这段脚本是我在生产环境踩过几次坑以后提炼出的最小雏形,适合刚搭建半自动化流程的团队。它做的事情是:检测 SQL 线程状态,遇到 1062 或 1032 时记录现场再跳过一个错误事务,并且跳完后立刻检查复制是否恢复。

#!/usr/bin/env bash # mysql_replica_helper.sh # 使用方式: bash mysql_replica_helper.sh SLAVE_HOST="127.0.0.1" SLAVE_PORT="3306" MONITOR_USER="monitor" MONITOR_PASS="你的监控账号密码" MYSQL="mysql -h${SLAVE_HOST} -P${SLAVE_PORT} -u${MONITOR_USER} -p${MONITOR_PASS}" LOG_FILE="/var/log/mysql_replica_helper.log" # 1. 获取复制状态 STATUS=$($MYSQL -e "SHOW SLAVE STATUS\G" 2>/dev/null) SQL_STATE=$(echo "$STATUS" | grep "Slave_SQL_Running:" | awk '{print $2}') IO_STATE=$(echo "$STATUS" | grep "Slave_IO_Running:" | awk '{print $2}') LAST_ERRNO=$(echo "$STATUS" | grep "Last_SQL_Errno:" | awk '{print $2}') LAST_ERROR=$(echo "$STATUS" | grep "Last_SQL_Error:" | sed 's/.*: //') # 2. 只有 SQL 线程报错且错误码属于可安全跳过范围才处理 if [ "$SQL_STATE" == "No" ] || [ -n "$LAST_ERRNO" ] && [ "$LAST_ERRNO" != "0" ]; then echo "$(date '+%F %T') SQL线程异常, errno=$LAST_ERRNO, error=$LAST_ERROR" >> "$LOG_FILE" case "$LAST_ERRNO" in 1062|1032) echo "$(date '+%F %T') 开始尝试跳过, 当前错误信息: $LAST_ERROR" >> "$LOG_FILE" $MYSQL -e "STOP SLAVE SQL_THREAD;" $MYSQL -e "SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1;" $MYSQL -e "START SLAVE SQL_THREAD;" sleep 2 # 3. 修复后复核 NEW_STATUS=$($MYSQL -e "SHOW SLAVE STATUS\G" 2>/dev/null) NEW_SQL_STATE=$(echo "$NEW_STATUS" | grep "Slave_SQL_Running:" | awk '{print $2}') if [ "$NEW_SQL_STATE" == "Yes" ]; then echo "$(date '+%F %T') 跳错成功, 复制已恢复" >> "$LOG_FILE" else echo "$(date '+%F %T') 跳错后仍异常, 需人工介入" >> "$LOG_FILE" # 这里调用你公司的告警 webhook,把日志内容发出去 # curl -X POST -d "message=...你的告警平台接口..." http://你的告警入口 fi ;; *) echo "$(date '+%F %T') 错误码不在自动处理范围,转人工" >> "$LOG_FILE" ;; esac fi

脚本里的SQL_SLAVE_SKIP_COUNTER=1是传统位点模式下的跳错手段,它在 GTID 模式下已经不是最优解。更严谨的 GTID 模式跳过,是使用SET GTID_NEXT直接标记某个事务:

STOP SLAVE; SET GTID_NEXT='主库UUID:事务序号'; BEGIN; COMMIT; SET GTID_NEXT='AUTOMATIC'; START SLAVE;

这样做的好处是精确跳过对应的 GTID,而不是像老办法那样“跳一个 event”,处理多语句事务时不容易把不该跳的内容也跳掉。这个细节是很多文档不会刻意提醒你的。

4.3 把半自动升级成带拓扑意识的方案

脚本方案在只有一两对主从时很管用,但对中大规模环境,我更推荐把 Orchstrator 加进来。它解决的最大问题不是跳错,而是自动选主和故障转移的一致性决策。

Orchestrator 启动后,会不断探测每个 MySQL 实例的主从关系,生成一张完整的拓扑图,并跟踪每个实例的 GTID 集合。当发现某个主库不可达,它会根据配置策略在从库里选一个新主,同时把其他从库的复制关系重新指向新主。关键配置项如下:

{ "Debug": false, "ListenAddress": ":3000", "MySQLTopologyUser": "orc_client", "MySQLTopologyPassword": "你的orchestrator专用密码", "MySQLTopologyCredentialsConfigFile": "", "BackendDB": "sqlite", "SQLite3DataFile": "/data/orchestrator/orchestrator.sqlite3", "RecoverMasterClusterFilters": [ "production.*" ], "RecoverIntermediateMasterClusterFilters": [ "production.*" ], "ReasonableReplicationLagSeconds": 10, "DetachLostSlavesAfterMasterFailover": true, "MasterFailoverDetachSlaveMasterHost": true }

这里面最容易踩的坑是RecoverMasterClusterFilters为空数组。很多同学部署完不配置这个选项,Orchestrator 就永远把自己当成“观察者”,不执行任何恢复动作。我建议配置好后至少做一次演练:把从库 SQL 线程手工停掉,确认 Orchestrator 能在秒级发现异常并把拓扑状态标红。

它执行恢复动作的方式有两种:一种是通过 Web API 手动触发,curl -X POST "http://orc地址:3000/api/recover/生产集群别名";另一种是在集群里配置自动恢复策略,一旦检测到主库故障,系统直接发起故障转移。生产环境我建议先把自动开关关着,跑上一段时间,确认你熟悉它的每次动作后,再打开自动恢复。

4.4 配置好检测后的三级告警和通知

自动修复方案不能缺了告警。我给团队定的通知规则大概是:复制中断 30 秒内发到 IM 群;如果自动跳过成功,只记录日志,不打扰人;如果脚本尝试一次后仍然失败,立即发更高优先级警报,并附上当前SHOW SLAVE STATUS的关键字段。告警消息不需要很花哨,但要能把异常实例、错误码、最近错误文本一起带出来,让人少点一次鼠标。

5. 修复之后的数据一致性检查不能省

5.1 先想清楚从库到底差了什么

很多自动修复方案最大的隐患,不是不会跳错,而是跳完以后根本不检查数据是否正确。跳过错误意味着这个事务永远不会在从库重放,如果事务里包含插入或更新,从库就缺了这部分数据。所以跳错前要记录被跳过的事务号,跳错后立刻用主从对比工具确认差异范围。

常见做法是用pt-table-checksum对主从对应表做校验。它的原理是对每个表按块计算校验和,把主库和从库的结果做对比。生产环境通常先在小表上跑:

pt-table-checksum h=主库地址,u=checksum_user,p=密码 \ --databases=yourdb \ --tables=users \ --replicate=percona.checksums \ --no-check-binlog-format

如果某个表的校验结果有差异,再用pt-table-sync修复。但这里必须提醒:pt-table-sync会直接改数据,执行前一定要备份或先在测试环境验证,并且尽量在低峰期操作。

5.2 差异修复比跳错更考验经验

我遇到过一种情况:从库因为人为误删行,SQL 线程报 1032,脚本自动跳过了错误,复制线程恢复正常,但被误删的行一直没有补回来。业务侧短期内没感知,直到某个报表查询结果不一致才发现。这就是典型的“复制恢复但数据没恢复”。

这种场景下,最稳妥的做法是按照被跳过事务的 binlog 内容,从主库找到对应的原始操作,评估影响行,再定向修复。比如找到主键后,把主库对应行的数据查出来,插入或更新到从库,然后再继续复制。不要为图方便直接重建整张表,大表重建的成本往往比定向修复高很多。

5.3 每周巡检清单

自动化不等于彻底放手,我建议每周做一次最小化巡检:第一,确认所有实例的Slave_SQL_Running和Slave_IO_Running都是 Yes;第二,检查Seconds_Behind_Master是否持续高于合理阈值;第三,随机抽 5 张核心表做pt-table-checksum,看看有没有数据漂移;第四,检查 binlog 磁盘剩余空间,避免因为磁盘写满引发复制中断。把巡检脚本挂定时任务,输出结果到日报,比等出了问题再补救省心得多。

6. 常见问题与排查实录

6.1 高频问题速查表

现象可能原因建议处理
Slave_IO_Running: No,报 1236请求的 binlog 位点已过期重建复制;评估是否需要延长 binlog 保留时间
Slave_SQL_Running: No,报 1062主键冲突确认冲突事务影响后,定向清理或跳过
Slave_SQL_Running: No,报 1032目标行不存在从主库或备份找回数据,再继续复制
复制线程反复启动又报错中继日志损坏或表结构异常检查 relay log,必要时重建从库复制关系
跳错一次后复制恢复但延迟持续增长大事务执行时间过长分析 binlog 里的大事务,优化写入方式后再继续

6.2sql_slave_skip_counter=1跳错了事务

我用这个参数时踩过一个很典型的坑。它跳过的其实是一个 event,而不是一个完整事务。如果一个事务包含多条 SQL,跳一个 event 可能只是跳了事务的一部分,后续 event 重放时照样继续报错,甚至会报出更难懂的 1032 或 1062。所以如果是 GTID 环境,建议尽量用SET GTID_NEXT精确跳过错误事务;如果是传统位点模式,至少先看一眼 relay log 里这个事务包含几条语句,不要连续无脑执行跳错命令。

6.3 中继日志损坏后越修越乱

从库异常断电后,最容易出现[ERROR] Slave SQL for channel '': Worker thread ... Error reading packet这类问题。这时候不要先执行跳错,正确动作是把 relay log 清理干净并重建。在 GTID 模式下,可以这样操作:

STOP SLAVE; RESET SLAVE ALL; CHANGE MASTER TO MASTER_HOST='主库IP', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='复制账号密码', MASTER_AUTO_POSITION=1; START SLAVE;

注意,RESET SLAVE ALL会清掉复制配置,执行前一定要确认主库的 binlog 还保留着从库缺失的 GTID 区间。如果主库 binlog 已经被清理,就必须用备份重建从库,步骤会复杂很多,这也是我一直强调 binlog 保留时间不要太短的原因。

6.4 从库 binlog 没开导致无法继续级联

有的团队把从库当成单纯的“读库”,嫌浪费磁盘,没有开log_slave_updates。这个配置平时不显眼,但一旦从库提升为主库,后面的级联从库就会全部断掉,因为新主库根本没有记录完整 binlog。在搭建主从的那一刻就该把log_slave_updates=ON加上,并且写入建库规范里。这不是自动修复脚本能帮你解决的问题,属于基础设施层面的长期债。

6.5 云数据库实例的自动修复边界

如果你用的是托管云数据库,RESET SLAVE ALL这种操作大概率没有权限。这时候不要强行模仿自建集群的跳错流程,而是优先使用云厂商提供的复制管理和任务重建功能,同时把异常情况第一时间反馈给数据库服务商。云实例的自动修复思路更偏“拓扑重新搭建”,而不是直接在从库上改内部状态。

7. 给值班同学和 DBA 的几条实操建议

自动修复工具解决的是效率问题,不是运维人员的思考问题。我自己的使用习惯是:脚本里跳错之前永远先落一条日志,记录当时的状态和错误文本;跳错之后强制延时 2 到 5 秒再查状态,不要刚START SLAVE就断言成功;修复成功后保留至少 7 天的日志,方便做周期性复盘,看看哪些错误是被自动跳过的,哪些是重复出现的。

最后再分享一个很多人忽略的小细节:自动化脚本里所有涉及mysql命令行的地方,尽量使用配置文件存放账号密码,不要在命令行明文拼接数据库口令,否则进程列表里会泄露凭据。把这些杂七杂八的细节做好,主从复制自动修复这套体系才能真正从“能用”变成“好用”。

返回列表