忙了一整天终于把生产环境那个诡异的主从复制问题压下去了。想写这篇内容很久了一直在等一个合适的时机。MySQL 集群一致性问题听起来像是教科书里才会有的概念但真到上线跑业务的时候你会发现它无处不在——主从延迟、半同步降级、脑裂后的双写、切换时的数据丢失每一个都足以让你在凌晨两点的会议室里和 DBA 同事面面相觑。这篇内容不是给你复述原理也不是给你念官方文档。我打算把这些年踩过的坑、排查过的故障、以及最终沉淀下来的那套“先找原因再动手”的思路用尽量直白的方式讲清楚。如果你正在维护一个 MySQL 集群或者正准备从单机迁移到主从架构这篇东西应该能帮你少走不少弯路。而且很多问题是经典的、可复现的看完你至少知道出了事先看哪里别慌着重启。1. 先弄清一个问题MySQL 集群的“一致”到底指的是什么1.1 从主从复制说起你集群里其实不止一份数据单机 MySQL 不存在集群一致性问题你只有一份数据落盘了就落盘了。一旦走上主从复制这条路数据就变成了多份主库一份从库若干份每个副本之间通过 binlog 接力。你看到的“MySQL 集群”本质上是一套数据分发系统主库产生 binlog从库的 IO 线程拉过来写进 relay log再由 SQL 线程回放执行。问题就出在这个“接力”上。主库写成功了不代表从库已经执行了从库执行了也不代表执行结果和主库完全一样。这种数据多副本之间的偏差就是我们常说的 MySQL 集群一致性问题。为了把这个问题说透我举个生活化的例子你给全组发通知说“明天早上十点开会”发出去并不等于所有人都收到了有人可能在电梯里没看手机有人看到了但记错了时间最后的结果就是会议室里坐的人参差不齐。MySQL 集群也一样主库说“事务已提交”从库那边可能压根还没开始处理。所以在深入排查之前你得先接受一个事实主从架构下的一致性是“最终的”不是“实时的”。大多数业务能接受从库在几秒甚至几十秒内读到旧数据但绝对不能接受从库最终数据和主库对不上。前者叫延迟后者叫不一致性质完全不同。1.2 三种复制模式异步、半同步、增强半同步MySQL 主从复制有三种常见模式每一种对一致性的保障力度不同代价也不同。异步复制主库提交事务后立即返回成功binlog 异步发送给从库。性能最好但主库刚提交就宕机事务可能在从库上不存在数据直接丢。半同步复制lossy semi-sync主库在事务提交后必须等待至少一个从库确认收到 binlog 才返回成功。MySQL 5.7 之前的实现是“提交后等待确认”如果主库在确认到达前宕机虽然从库可能收了 binlog但主库还没有落盘依然存在丢失风险。增强半同步lossless semi-syncafter_sync主库在存储引擎提交之前等待从库确认确认到达后再提交。主库和至少一个从库同时持有这个事务的 binlog掉电也不怕数据不丢。我在项目里推荐的是第三种。虽然牺牲了一点性能但对绝大多数业务来说那点延迟完全可以接受换来的是至少一份从库与主库保持一致。有些团队为了性能选了异步复制结果故障演练时一主库宕机数据缺了一截恢复起来想把运维逼疯。我的原则是能用增强半同步就用除非你的业务能明确接受丢失最近几秒的写入。1.3 一致性问题的覆盖面比想象中广很多刚接触集群的人以为只要主从不报错、没有复制中断数据就一定是好的。这个认知害了不少人。MySQL 集群一致性问题不仅仅包含“从库落后主库”还包括主库和从库数据内容不一致比如某张表主库有 100 行从库只有 99 行复制中断后恢复从库执行了错误的跳过语句导致整张表数据错乱双主或多主架构下不同节点写入了同一条主键产生主键冲突网络分区引发的脑裂导致两个节点都认为自己才是主库数据侧写切换过程中旧主库 binlog 没完全同步到新主库导致数据丢失这些场景我都经历过有的还不止一次。所以下面我把它们拆开来讲记录一下每个问题的现象、根因以及我在当时的处理思路。这部分内容比较多但每一节都值得仔细看因为任何一个都可能在你的生产环境里冒出苗头。2. 我踩过的那几类典型故障现场2.1 半同步复制偷偷降级订单丢了半个小时才被发现这是让我印象最深的一个事故。那是一个电商类模拟项目某天运营突然反馈有一批订单在后台查不到。数据库层面没有任何报错主从状态看起来也算正常从库的 Seconds_Behind_Master 显示为 0。查了半天最后在监控面板上看到半同步复制的状态指标不对劲。半同步复制有一个超时机制主库等待从库确认的时间如果超过rpl_semi_sync_master_timeout默认是 10000 毫秒也就是 10 秒就会自动降级为异步复制继续提交事务。这本是保护写入可用性的设计但副作用就是——模式降级之后主库不再等待从库确认binlog 还没发过去就可能发生故障数据自然就丢了一截。当时的情况是从库所在机器的磁盘 IO 出现了一次长时间停顿导致确认迟迟不能到达主库半同步直接超时降级。紧接着主库又发生了重启那一批尚未传走的 binlog 事务就彻底消失了。整个过程没有任何人收到警报因为我们监控的是复制线程状态而不是半同步是否还在生效。这个案例给我最大的教训是半同步复制不是靠“开启之后一直生效”的心智模型来理解的你必须把它当成一个“可能随时降级”的保护措施来对待。后来我在所有项目里都加了一个监控项实时检查Rpl_semi_sync_master_status一旦从ON变成OFF就立即告警同时要求运维关注半同步相关的 status 变量yes 和 no 的计数防止这种静默丢失。2.2 主从延迟的经典场景一张千万级大表更新另一个高频问题就是主从延迟。有个项目上线初期一切正常某天数据同步团队需要清理一张千万级大表的过期数据。他们直接在主库执行了一条影响几百万行的DELETE执行了大概两分钟。结果从库的延迟从 0 一路涨到三千多秒也就是一个多小时。很多人不理解为什么主库执行两分钟从库要执行一个多小时。这里面有两个原因第一从库默认是单线程回放或者并行度配置不当一个几百万行的大事务在主库执行完后binlog 会作为一个事务整体发到从库从库必须把这个大事务完整重放一遍期间无法并行处理其他事务导致延迟呈雪崩式增长第二删除操作在 binlog 里生成的日志量远超实际涉及的行数尤其使用 ROW 格式时每一行删除都会被记录从库要逐行执行套上索引 B 树更新的开销自然慢得多。这个问题的处理思路不是“让从库跑快点”而是“消灭大事务”。后来我要求所有批量操作都必须分批执行比如每批 500 或 1000 行循环提交这样每批事务都很小从库可以快速回放延迟基本能控制在几秒以内。并且把大表清理、大数据量更新这类操作统一收到变更流程里禁止直接在生产主库上跑全表范围的 DML必需走审批也要评估对同步链路的影响。2.3 脑裂与双写网络断开的十分钟里两边都在写脑裂这件事听起来更像分布式系统的术语但在 MySQL 高可用架构里照样会发生。某次机房网络设备抖动运维看到的是主从之间的 TCP 连接断开了。主库这边业务还在照常写入没人感知到任何异常。从库这边高可用切换组件检测到主库失联就把从库提升为新的主库开始接收业务流量。等网络恢复原来的主库也恢复写入能力两个节点都认为自己才是主库。结果就是同一份数据在两个节点上各自演进等我们发现后尝试把旧主库加回集群时主键冲突、数据覆盖、自增 ID 错位各种乱象全冒出来了。最麻烦的是这种冲突没有办法自动合并只能靠人工比对和订正工作量巨大。这个案例最核心的根源在于自动切换机制不具备完整的“防脑裂”能力或者说切换决策没有考虑网络分区两边同时存活的情况。后来我们做高可用时加了一条铁律任何自动切换必须先确保旧主库已经被强制下线比如通过 fencing 机制切断它的存储或网络再提升新主库。同时禁止“双主”这种拓扑直接对业务开放两侧写入写入流量必须固定在单一节点上。用大白话说同时把两个节点都当成主库用就是把灾难提前预订了。2.4 切换演练翻车MHA 切换后少了一段 binlog最后再讲一个和“切换”直接相关的案例。某团队做高可用切换演练模拟主库故障通过 MHA 把流量切到备库。一切看起来都很顺利但等旧主库修复重启后校验数据才发现切换后新旧主库之间缺了十几个事务。原因是 MHA 在切换时确实会尝试从旧主库补拉 binlog但当时旧主库已经处于假死状态补拉失败了。如果业务对一致性要求很高这就是不可接受的数据丢失。MHA 本身不是银弹它对 binlog 补拉有依赖对旧主库的可用性有一定要求一旦旧主库彻底起不来最后那一点 binlog 就只能听天由命了。后来我们引入了 MGRMySQL Group Replication作为另一套方案的验证因为 MGR 的数据一致性保障依赖 Paxos 共识协议事务提交必须经过组成员确认从机制上解决了“旧主库未同步事务被丢弃”的问题。当然 MGR 也有自己的限制比如性能开销、网络要求、以及 DDL 的兼容性但它确实比“靠运气补拉 binlog”靠谱得多。3. 排查定位从现象到根因的四步路线3.1 第一步看一眼复制线程和位点状态每次怀疑集群一致性问题我都不先看业务日志而是直接连上从库执行SHOW SLAVE STATUS\G重点关注几个字段Slave_IO_RunningIO 线程是否还在拉取 binlog如果为 No说明主从之间的网络或认证有问题。Slave_SQL_RunningSQL 线程是否在正常回放如果为 No说明回放出错中断了。Seconds_Behind_Master从库落后主库的秒数但这个值不能全信。Master_Log_File/Read_Master_Log_PosIO 线程已经拉取到主库 binlog 的哪个位点。Relay_Master_Log_File/Exec_Master_Log_PosSQL 线程已经回放到哪个位点。如果Master_Log_File和Exec_Master_Log_Pos之间的差距在持续扩大说明回放速度跟不上 binlog 产生速度如果 IO 线程和 SQL 线程的位点长期不动说明复制已经卡住了。这些字段组合起来能快速定位问题出在“拉取”环节还是“回放”环节。提示Slave_IO_Running和Slave_SQL_Running都显示 Yes只代表两个线程活着不代表数据一定一致。线程活着但一直卡在某个大事务上也是常见的假象。3.2 第二步不要迷信 Seconds_Behind_Master这是一个必须反复强调的点。Seconds_Behind_Master的计算逻辑是从库当前系统时间减去 SQL 线程正在执行的事务在 binlog 里记录的时间戳。这个值在很多场景下都会失真。举例来说IO 线程断开了但 SQL 线程已经把 relay log 全部回放完此时Seconds_Behind_Master会显示为 0因为从库已经没有可回放的新事务了。但它主库已经写入的最新数据完全看不到阻塞其实很大。再比如一个大事务回放进行到一半这个值会一直保持不变你根本看不出它是在涨还是在跌。更精准的做法是用pt-heartbeat在主库上周期性更新一张心跳表然后从库去计算主库心跳表的最新时间与当前时间的差距。这样拿到的延迟是“真实的数据传输延迟”不受 relay log 耗尽或大事务回放位置的影响。如果你还没有部署这个工具强烈建议补上闲着没事就 nob 的刷一刷排查的时候能省很多时间。3.3 第三步做一次数据一致性校验复制线程正常、延迟不高只能说明同步在推进不能证明数据内容一致。真正要较真必须做主从数据对比。最常用的是pt-table-checksum。它会按表分块计算 checksum在主库上执行并对比从库上相同块的 checksum。不一致的数据会被标记出来。下面是基本用法pt-table-checksum h主库地址,uchecksum_user,p密码 \ --databases你的库名 --tables用户表 --recursion-methodhosts注意这个工具运行时会消耗一定资源建议在业务低峰期执行或者设置在从库上通过--chunk-size控制每次检查的行数。如果发现不一致再配合pt-table-sync修复。但我在实际操作中从不直接依赖自动修复而是先把不一致的数据导出来比对一下到底丢了多少、错在哪评估影响后再决定修复方案。因为自动修复可能会覆盖掉从库上一些意外的额外写入你要先确认哪些数据才是可信的。3.4 第四步查 binlog 和事务类型如果连上从库但发现延迟居高不下就得回到主库看 binlog 里到底有什么“重型武器”。我有一次排查延迟问题看了半天的参数和容量最后才发现罪魁祸首是一条没有人注意到的ALTER TABLE主库执行 5 秒从库回放时因为要重建整张表跑了整整 8 分钟。所以不要盯着复制参数死磕多看看主库的 binlog 内容mysqlbinlog --base64-outputdecode-rows -vv mysql-bin.000123 | grep -E ### (DELETE|UPDATE|INSERT) | head也可以直接看表结构变更记录找出那些 DDL 和大事务。通常延迟的根因就藏在里面。如果看到的是大量行级变更就基本可以确定是业务侧批量操作不规范导致的后面要推动流程管控。4. 修复与预防从应急处理到架构级规避4.1 应急修复先止血再想为什么在正式修复之前务必先确认到底是要挽回数据还是保证后续一致。如果从库落后得不多可以直接等待 SQL 线程追平如果落后得离谱比如几个小时就需要评估是否值得等待还是重新搭建从库更快。我处理过一次从库落后两天的情况。落地的做法是先在从库上记录当前位点然后把从库停止复制用mysqldump或物理备份数据目录把数据重新恢复到从库再重新配置主从关系。整个过程虽然要花时间但比让 SQL 线程硬追两天 binlog 要可靠得多因为长时间追 binlog 期间从库的本地数据可能已经因为磁盘问题、空间不足等产生了各种边界异常。如果是半同步降级导致的数据丢失第一件事是先把降级节点重新恢复到正常状态确认半同步生效然后立即开启相关告警。至于丢失的那部分数据只能通过业务日志、备份或其他消息补偿。这个层面没有快捷方案功夫都在日常。4.2 架构规避单主写入和规范的切换流程我在生产环境中强烈坚持一个原则集群可以有一主多从也可以有双主备用但业务写入流量只会进入一个节点。另一节点的责任是备份、只读查询和随时准备接管而不是直接对业务开放读写。这样做的好处是彻底隔绝了“双写冲突”这类最棘手的数据矛盾。如果确实需要多地域写那就不应该使用普通的 MySQL 主从复制而要考虑分布式数据库或者更底层的方案而不是在 MySQL 层面硬撑。切换流程也要规范化。我现在的标准流程是提前确认旧主库状态至少在切换前做一次数据完整性校验。切换前从业务侧摘除旧主库写入流量。在旧主库上执行FLUSH TABLES WITH READ LOCK记下 binlog 文件和位点。确认新主库已经追平到该位点。再切换流量把新主库对外服务。旧主库恢复后不要自动加回集群先走数据校验流程。这条流程每一步都可以写脚本固化但千万不要因为自动化就省略“人工确认位点追平”这一步。我在现实中见过太多次因为自动化切换而翻车的情况反面教训比正面经验多得多。4.3 参数调优半同步、并行复制和刷盘策略如果集群本身没有架构缺陷调整一组参数也可以显著提升一致性表现。先看半同步复制相关配置# 主库 plugin-loadrpl_semi_sync_mastersemisync_master.so rpl_semi_sync_master_enabled1 rpl_semi_sync_master_timeout10000 rpl_semi_sync_master_wait_pointAFTER_SYNC rpl_semi_sync_master_wait_no_slaveON # 从库 plugin-loadrpl_semi_sync_slavesemisync_slave.so rpl_semi_sync_slave_enabled1AFTER_SYNC就是增强半同步必须配置别再用默认的AFTER_COMMIT。超时时间我建议设置得保守一点比如 10 秒甚至更大因为网络抖动大多数都在几秒内就能恢复让出一点时间换数据安全是划算的。再看并行复制参数。MySQL 5.7 以后可以配置# 从库 slave_parallel_typeLOGICAL_CLOCK slave_parallel_workers8 binlog_transaction_dependency_trackingCOMMIT_ORDER这里要说一个容易踩的坑并行复制不能简单调大 worker 数量。如果 CPU 核数不够、磁盘 IO 有瓶颈worker 线程反而会互相争抢资源延迟不降反升。我在一个低配机器上开过 16 个 worker结果复制直接变成了轮流等锁最后调回 4 个才稳定。刷盘策略也直接关系到“数据是否真的落地”。主库上如果sync_binlog不等于 1binlog 可能还在 OS 缓存里掉电就会丢。从库上可以保持innodb_flush_log_at_trx_commit2提高性能但主库强烈建议和sync_binlog1一起配合把双一配置用起来innodb_flush_log_at_trx_commit1sync_binlog1这是 MySQL 数据不丢的基础底线。4.4 DDL 和批量操作的流程管控很多一致性问题本质上是“人祸”一条 DDL、一条大事务直接打爆同步链路。业务系统运行期间尽量不要直接在主库执行高影响 DDL。如果一定要做优先考虑使用pt-online-schema-change或者gh-ost这类在线变更工具它们通过增量同步的方式避免长时间锁表。批量 DML 的流程管控也类似。我的规则很简单单个事务影响行数不超过 2000 行分批执行每次提交之间间隔 100~200ms不在业务高峰执行必须有回滚方案这套规则看起来保守但能保证从库的 SQL 线程不会被大事务压垮也能保证主库的 binlog 不会瞬间膨胀。长期下来集群的稳定性比那些追求最高写入性能的团队要踏实得多。5. 常见问题速查表与几条实战心得5.1 故障场景速查表把上面提到的问题归纳成表格方便你在排查时快速对照。现象可能原因第一反应排查方向推荐处理从库延迟激增持续追不平大事务、DDL、单线程回放SHOW SLAVE STATUS对比位点看 binlog 找大事务拆分批次主从数据不一致但复制线程正常语句级复制差异、回放错误被跳过pt-table-checksum 校验导出差异数据评估影响pt-table-sync 修复半同步复制未生效或已降级插件未配置、超时降级查Rpl_semi_sync_master_status检查参数开启告警切换后数据丢失旧主库 binlog 未完全同步检查切换工具日志和 binlog 位点使用 MGR 或增强同步机制双主同时写入后冲突脑裂或配置不当看自增 ID、主键冲突强制单主写入故障演练验证复制 SQL 线程中断从库执行错误、数据不一致SHOW SLAVE STATUS 的 Last_SQL_Error对比差异不要随意跳过从库只读查询正确但数据偏旧从库延迟pt-heartbeat 精确测量调整并行复制参数或划分一致性要求5.2 几个常年有效的小原则第一从库必须开启read_only。很多事故都是因为某个人在从库上误执行了一条更新语句导致从库数据和主库分道扬镳。开启read_only后普通账号无法在从库写入能挡住 90% 的手滑。第二复制错误不要轻易用SQL_SLAVE_SKIP_COUNTER跳过。跳过一条错误事务表面上看复制恢复了实际上从库可能已经和主库存在结构性差异。跳过之前先确认这个事务的具体内容评估影响再决定是修复还是重建。第三主从之间尽量用内网专线或低延迟网络。半同步复制对网络延迟非常敏感跨地域的长链路大概率导致频繁超时降级最终退化成异步复制。如果你必须在异地部署从库那就不要对一致性抱有幻想或者换一种分布式数据方案。第四定期做切换演练。不要等故障真的来了才第一次尝试“把流量从主库切到从库”。演练过程会暴露很多纸面设计上根本想不到的问题比如应用连接池的连接保持、DNS 缓存、从库延迟监控阈值等等。5.3 最后分享一个实战习惯我在排查 MySQL 集群一致性问题时养成的一个习惯是不管现象看起来多简单都会先把主库当前的 binlog 位点、从库已回放的位点、半同步状态这三个信息抓出来做一次快照。哪怕是同一个故障不同时刻抓到的位点数据差异很大从快照里能看出复制是“在跑、在等、还是已经断了”。这个习惯救过我很多次。有一次看起来像是主从延迟我按快照对比后才发现根本不是延迟而是 IO 线程连接断了但 SQL 线程还在空转查询接口一直读到旧数据。如果只盯着延迟数值看可能又要浪费半小时去查根本不存在的“大事务”。最后再强调一遍MySQL 集群一致性问题不是一个“有就修没有就安心”的状态它是一个持续需要你关注和维护的过程。把该开的参数开好、该加的告警加上、该做的演练做掉比任何花哨的工具都管用。