
收到直接来。1. 并行复制和1032到底是个什么坎先说结论Last_Errno: 1032对应的 MySQL 内部错误是HA_ERR_KEY_NOT_FOUND翻译成人话就是在从库执行某个更新或删除语句时想要操作的那一行数据压根不存在。而Last_Error: Coordinator stopped because ...是并行复制MTSMulti-Threaded Slave特有的报错形式——协调线程Coordinator收到某个工作线程Worker的执行失败反馈后果断叫停整个复制通道。这俩凑一起基本可以断定是主从数据不一致了。很多第一次碰并行复制的人会懵因为老的单线程复制下出 1032就是简单直接的一条Error executing row event: ...一条SQL的事。到了并行复制错误信息开头先给你来个Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: ...后面的具体错误还是那一串熟悉的 1032。本质上问题没变只是从库已经把事务拆分给多个线程并行执行了协调者需要先“汇总”失败再决定是否继续。这就引出一个关键点并行复制不仅没有解决数据不一致反而因为并发执行让定位问题多了一层复杂度。我在公司环境里遇到的最常见场景是这样某天夜里任务调度平台跑了一个批量改历史数据的脚本主库刷刷刷更新了几万行。由于脚本逻辑和历史数据有出入一部分UPDATE语句影响行数为 0。在主库这是合法的——找不到匹配行就不更新不报错。但到了从库同样是这条语句如果从库本就缺失了对应行那执行就会失败1032 直接蹦出来。单线程复制下SQL线程会停在这个事务上你排查半天发现主库有、从库没也可能主库没、从库有。而并行复制下报错事务可能被分配到 Worker 3其他 Worker 还在正常执行但协调者一旦发现 Worker 3 失败了就会让所有 Worker 一起停下整个复制通道就断了。所以说1032 只是个表象真正要处理的是“主从数据不一致”。并行复制只是把表象变得更像“链路故障”而已。1.1 并行复制的历史与价值MySQL 5.6 开始支持slave_parallel_workers参数但当时的并行复制是按库schema并行的叫 DATABASE 模式。这意味着只有多个库之间的事务才能并行执行同一个库下面的写入依然串行效果有限。5.7 引入了 LOGICAL_CLOCK 模式也就是基于事务提交时间戳的并行回放不再受库的物理隔离限制——主库上同一提交组里的事务在从库上可以并行执行。这个改动让从库追主库的速度有了质的飞跃尤其是在大事务密集写入的场景下复制延迟从动不动几百秒压到秒级。我印象很深的一次优化是某业务库在做活动预热时主库写入 TPS 破万从库复制延迟一路拉到了 3000 多秒。调大slave_parallel_workers之后延迟肉眼可见地往下掉几分钟就追平了。这玩意儿确实是解决复制延迟的利器但也埋了个雷并发执行时如果某个事务因为数据不一致失败整个复制线程组都得停下来而且错误定位的上下文比单线程复杂得多。1.2 1032错误的直接含义具体到Last_Errno: 1032它对应的错误消息文本是Could not execute Write_rows event on table xxx; Cant find record in xxx, Error_code: 1032; handler error HA_ERR_KEY_NOT_FOUND; the events master log ...。翻译一下从库试图回放主库传来的行事件但在目标表里找不到对应主键或唯一键的行。注意这里有个容易被忽略的细节Write_rows、Update_rows、Delete_rows事件都可能触发 1032。Write_rows触发 1032 说明从库已经存在相同主键的行Update_rows和Delete_rows触发 1032 说明从库缺行。无论哪种都代表从库的数据和主库不在一个状态上。导致这个状态差异的原因常见的有这么几类从库曾被人工或应用直接写入过破坏了数据一致性。从库建表时漏了主键或唯一键导致某些更新能看到主库没有的“幽灵行”。主库执行了INSERT ... ON DUPLICATE KEY UPDATE或REPLACE INTO这类依赖唯一键的语句而从库表结构和主库不一致。曾经有过从库回放中断然后有人用SET GLOBAL sql_slave_skip_counter或sql_slave_skip_errors跳过事务跳多了就从根上歪了。所以排查时不能光盯着当前这条失败的语句而是得往历史的“脏操作”上想。2. 为什么Coordinator会停下来并行复制的架构里协调线程Coordinator负责从主库的 relay log 读取事务然后按提交顺序分发给多个 Worker 线程执行。Worker 线程各自维护一套表状态和 binlog 写入。当某个 Worker 执行事务报错时它不会像单线程那样就地停下而是把错误码和状态记录到本线程的上下文里然后通知 Coordinator。Coordinator 收到失败通知后会做两件事第一给其他还挂着的 Worker 发停止信号让他们结束当前正在执行的事务不再接收新事务第二把错误的细节汇总成一条Last_Error更新到SHOW SLAVE STATUS里并标记复制状态为Slave_IO_Running: Yes、Slave_SQL_Running: No。所以你看到Coordinator stopped because时不要慌它其实是并行复制的正常故障转移机制——宁可全停也不让复制线路带着错误继续跑。但这里有个坑Coordinator 汇总错误时可能会把多个 Worker 的错误信息叠加在一起。我在一次排查中就见过Last_Error里列了两三个事务的失败原因因为 Worker 1 和 Worker 3 都报错了。如果你光看最后一行就去处理修复完一个事务复位复制后紧接着又会撞到另一个未修复的事务上。正确做法是先按Last_Error里提示的Worker N failed的细节把所有报错的事务都找出来一起处理掉再启动复制。2.1 错误传导链从Worker到Coordinator用大白话理解这个传导链就像厨房里几个厨师同时在炒菜主厨Coordinator负责按订单顺序把菜分配到各灶台。某个灶台炒糊了一道菜主厨不可能装作没事继续接单他得让所有灶台都停下然后把糊了的那道菜信息记在公告板上Last_Error。公告板上的内容就是这个错误全过程的“黑匣子”。实际上你在SHOW SLAVE STATUS的Last_Error里会看到类似下面这样的完整上下文Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction ANONYMOUS in source log mysql-bin.000123, end_log_pos 567890. Error_code: 1032; handler error HA_ERR_KEY_NOT_FOUND; the events master log mysql-bin.000123, end_log_pos 567890.这里面最有价值的信息是source log mysql-bin.000123, end_log_pos 567890这一串。它告诉你从 relay log 里的哪个 binlog 文件的哪个偏移量开始事务执行失败了。通常拿到这个绝对定位就能用mysqlbinlog直接解析这段日志看到具体执行的是什么 SQL、涉及哪张表、哪个主键值。注意位置信息是“事务开始”的位置报错的具体行事件位置可能在事务中间。如果你只想定位表名和主键可以用mysqlbinlog --base64-outputdecode-rows -v --start-position567890 mysql-bin.000123多看几行直到出现### UPDATE \dbname.tablename 这种行事件描述就能确定是哪个表了。2.2 最易引发1032的几种操作如上所述1032 的本质是主从数据不一致。但具体到操作层面我见过的引发场景按出现的频率排序如下从库被人手动改了数据。最常见的是某个开发为了方便测试直接在从库上跑了几条UPDATE或DELETE没加WHERE或没提交事务事后忘了回滚。对我见过不止一次有的直接就是执行完就关了终端连事务都没提交但数据被锁着等事务超时后被回滚还算走运有的直接提交了然后主库一更新从库就 1032。主库执行了DELETE ... WHERE ... LIMIT 1且从库没有这行。这类语句在主库影响行数为 1但从库本来就缺这行于是从库影响行数为 0复制直接失败。从库的过滤规则replicate-ignore-db、replicate-do-db配置不当导致部分事务没执行导致后续依赖这些数据的事务在从库找不到记录。级联复制场景中中间节点的从库本身就没跟上主库又继续把 binlog 生成了下游下游自然就缺了一串数据。理解这些场景能帮你更准确判断“这 1032 是偶发还是规律性”。偶发的基本是人为操作或历史数据覆盖规律性的基本是表结构或过滤规则的问题。3. 一次完整的故障排查与恢复说实话我第一次处理 1032 时也是手忙脚乱的。查了一堆文档试了好几种方法最后才总结出一套稳妥的流程。这套流程后来我写成内部运维手册照着操作基本不会出大错。核心路径是读现场、定位事务、评估一致性、修复数据、恢复复制。3.1 第一步读取现场连上从库执行SHOW SLAVE STATUS\G重点看这几个字段Slave_IO_Running、Slave_SQL_Running、Last_Errno、Last_Error、Seconds_Behind_Master、Exec_Master_Log_Pos、Retrieved_Gtid_Set、Executed_Gtid_Set。如果开了 GTID先记录一下当前Executed_Gtid_Set方便后续确认哪些事务已经执行、哪些还没执行。然后根据Last_Error里的信息看看是哪个 Worker 报错、在哪个 binlog 位置报错。如果有条件把 relay log 相关的片段导出来mysqlbinlog --base64-outputdecode-rows -v --start-position报错位置 relay-bin.000012这么做不是要立刻改数据而是要搞清楚误在哪张表、哪个主键。先不动手让现场保持原样避免二次污染。3.2 第二步定位具体事务与表从mysqlbinlog输出里你会看到类似### UPDATE \user.order的行事件描述里面带着完整的字段值和前镜像、后镜像。重点看WHERE条件里的主键值以及UPDATE 目标里的新值。根据这些信息去主库查一下同一主键的记录当前到底是什么状态。如果主库和从库的记录对不上基本就实锤了。举一个我自己踩过的例子主库某个订单表的status字段被批量脚本更新为 2但从库那一行的status还是 1而且主键对应的记录从库也存在。这种属于主库先更新了从库执行的也是这个更新但报 1032说明从库那行的主键在那一刻根本不存在——后来查才发现是更早一次从库误删了那行。所以不要只看当前状态要推演一下从库的历史轨迹。定位到具体表和主键后把主库、从库各自对应的行内容都查出来用SELECT确认差异。这个动作不能省因为它决定了你后续是“补数据”还是“从主库重新导出该表”——如果只是主键缺失补一行即可如果整表都歪了就得全量导。3.3 第三步数据修复的几种路径修复方案选型取决于不一致的范围和表的大小。我常用的路径按优先级排一是小表或差异很小的情况直接在从库上补数。假设缺主键id10086主库上有对应记录那我就在从库执行INSERT INTO dbname.tblname (id, col1, col2) VALUES (10086, v1, v2);需要注意插入时要把所有字段都写上包括默认值字段不能留空否则就引入了新的差异。二是差异比较大但不涉及整表可以用pt-table-sync这类工具按主键范围修复。这个工具会先比较两边的行然后生成REPLACE INTO或UPDATE语句。用的时候要加上--nocheck-foreign-keys否则外键会烦死你。如果工具没法用比如某些环境不让装那就手动把主库的该表导出然后导入从库注意用--ignore-table避免影响其他表。三是差异覆盖大半个表就别费劲修差异了直接把该表从主库导到从库mysqldump --single-transaction --master-data2 --databases dbname --tables tblname tblname.sql mysql -h from_standby dbname tblname.sql如果是 MyISAM 表或需要保持一致性先锁表或停止复制再导。这一步虽然粗暴但能彻底解决该表的所有历史歪斜。3.4 第四步跳过与恢复主从修复完数据后不要急着START SLAVE还要考虑那个失败的事务本身。因为即使数据修复了事务在从库重放时如果用的是行模式可能还是会因为主键冲突或找不到行而报错。最温和的办法是在 GTID 模式下先手动注入一个空事务跳过失败的那一个事务SET GTID_NEXTxxxx:yyyy; BEGIN; COMMIT; SET GTID_NEXTAUTOMATIC;具体 GTID 值从SHOW SLAVE STATUS的Retrieved_Gtid_Set和Exec_Master_Log_Pos交叉推算或者直接看 relay log 里事务的 GTID 信息。如果没开 GTID还是传统的 binlog 位置那就要用STOP SLAVE; SET GLOBAL sql_slave_skip_counter 1; START SLAVE;但我强烈不建议一遇到 1032 就无条件跳过。跳过等于假装这个错误没发生如果数据没修复下次主库再改这一行还会再报一次 1032。跳过是权宜之计修复数据才是根治。4. 从源头避免这类问题在讲预防之前说一个我观察到的现象很多团队把“出了复制故障”当作小概率事件实际上从库数据不一致的概率远比你想象的高。特别是那些从库不止承担读流量还偶尔被开发拿去跑离线任务的架构里人为破坏更是防不胜防。4.1 从库只读是第一道防线这个真的老生常谈但我还是要强调。在从库的配置文件里一定要加read_only 1或者动态设置SET GLOBAL read_only ON;这个参数不是完全的“只读”拥有 SUPER 权限的账号还是能写但绝大部分应用账号和开发账号都不在 SUPER 权限池里这层隔离能挡掉 90% 的猪队友操作。配合创建只读账号比如只给SELECT, SHOW权限基本能从权限层面堵死误写。在并行复制场景下从库只读还有另一层意义防止复制延迟时有人从从库读到旧数据然后又写回从库造成复制链路的“脏写”。有了只读至少应用层面直接写从库这条路被堵死了。4.2 定期数据校验与监控预防不是靠事后排查而是靠提前发现。定期做数据一致性校验是我的标配。常用的方案是pt-table-checksum它可以按表逐行比对主键及其 CRC 值然后从库再根据主键查一遍生成校验值两边一对比就出来了。流程不复杂pt-table-checksum --hostmaster --databasesdbname --tablestblname --replicate-check-only跑完后查询校验结果表就能看到哪些表、哪些 chunk 的数据对不上。但更关键的是把校验做成定时任务比如每周六凌晨跑一次跑完把报告丢到监控系统里。如果发现差异在攻击面变大之前就处理掉而不是等复制报错了再来救火。监控维度上除了要盯Slave_SQL_RunningYes还要盯Seconds_Behind_Master的波动延迟突增往往是数据歪斜的前奏。另外建议给从库开 binloglog_slave_updatesON开了之后如果从库再被误写至少你能从 binlog 里找到肇事语句否则只能溯源到应用账号排查全靠嘴问。4.3 合理配置并行复制参数并行复制是把双刃剑参数配置不好也容易加大不一致的概率。我见过有人把slave_parallel_workers拉到 64觉得并发越高越快结果当某个 Worker 出错时其他 Worker 手里已经执行到一半的事务全部回滚回滚步骤本身就可能引发 1032回滚时改回旧值但从库已经有了新值。合理配置要考虑两点一是slave_parallel_type设置为LOGICAL_CLOCK不要用DATABASE二是slave_parallel_workers不要盲目调大我一般建议按主库并发事务粒度来设通常 16 到 32 足够。还有一个隐藏参数slave_preserve_commit_order1保证并行回放的事务提交顺序和主库一致减少因乱序提交导致的数据漂移。还有一个点是如果业务写模式是“大事务 大量小事务混跑”并行复制对小事务友好但大事务会作为边界被隔开不会和其他事务并行。这个大事务本身如果出 1032一卡就是一条长日志排查时也要习惯这一点。5. 踩坑实录与问题速查这部分是我最想写的因为网上科普基本都是“遇到 1032执行STOP SLAVE; SET GLOBAL sql_slave_skip_counter1; START SLAVE;”但实际生产环境里这种一刀切操作大概率会把问题搞大。5.1 我遇到过的几个典型坑先说一个最典型的坑并行复制下Last_Error里报的 Worker 编号不是固定的。比如一次报错 Worker 1你处理完重启复制结果又报 Worker 3以为出了新问题其实可能是多个 Worker 手头各有一个失败事务而Last_Error只展示了最后一个。处理的时候一定不要只看一条报错就开跑而是把所有可能失败的事务都扫描一遍。第二个坑是数据修复完成后直接START SLAVE但很快又报 1032而且报错位置跟刚才一样。这种情况十之八九是没把sql_slave_skip_counter或 GTID 空事务的计数器弄清楚。比如用sql_slave_skip_counter1跳过的是“一个事件”而不是“一个事务”。在行模式下一个事务里可能包含多个行事件跳过一个事件后后续事件可能继续报错。要跳过整个事务更靠谱的还是在 GTID 模式下处理。第三个坑是在replicate-do-db过滤配置下如果主库执行了跨库事务事务里的语句可能“部分执行成功部分被过滤”结果从库只应用了其中一部分导致逻辑上数据不一致之后一旦碰到涉及未同步数据的操作就 1032。这个坑排查起来极其烧脑因为直接看从库的数据你只会觉得“哦这不是存在吗”但之前过滤掉的那几步已经把数据状态扭歪了。遇到这种情况建议直接重建该库或全量重新同步。5.2 问题排查速查表现象可能原因应对方法Last_Errno: 1032Last_Error里报Write_rows从库已有同主键记录而主库是 INSERT删除从库对应行或改为REPLACE INTO手动补齐Last_Errno: 1032Last_Error里报Delete_rows从库缺失主键行从主库补插该行再跳过或重放事务Coordinator stopped且Last_Error里出现多个 Worker 报错多线程各自带着失败事务先导出所有失败位置逐一修复再重启复制修复后启动复制仍报同一位置 1032跳过方式错误或数据修复不彻底检查主从该行是否一致用 GTID 空事务方式跳过复制延迟突增伴随 1032存在单条大事务或数据大面积歪斜减少slave_parallel_workers或直接重建该表从库写入被权限拒绝但复制报 1032应用通过 SUPER 账号写从库或存在级联复制排查超级权限账号梳理复制拓扑遇到 1032 时别急着找“跳过”按钮先回答三个问题这一行数据在从库存不存在主库和从库的该行内容是否一致失败位置之前是否还有未处理的报错事务这三个问题有了答案处理方案基本就清楚了。最后再分享一个小经验我处理完这类故障后必做的一件事是把主库该表的 binlog 事件位置和从库的Executed_Gtid_Set记下来放进值班交接文档。下次再有人报警说复制断了不用重新抓日志直接对比交接记录就能判断是不是同一批数据又出了新问题。虽然多花了几分钟记录但后续排查省下的时间可远不止这几分钟。