一条 DELETE 如何拖垮三台从库:一次 MySQL 主从延迟事故复盘
一条 DELETE 如何拖垮三台从库:一次 MySQL 主从延迟事故复盘
前言
一条删除历史数据的 SQL,最终让三台从库同时出现小时级同步延迟,核心业务流程被卡住,并造成了实际资损。
事故发生前,我们的数据库采用一主三从架构:主库处理写请求,三台从库通过 binlog 同步数据并共同承担读流量。这样的架构看起来具备一定冗余,但这次事故暴露了一个容易被忽视的问题:
多个副本可以提高读能力,却不一定能够隔离复制链路中的共同故障。
本次事故的触发点,是一次按日期范围执行的千万级数据删除。删除条件没有可用索引,任务也没有拆分为小批次。从库当时只有一个工作线程负责顺序应用事务,大删除长时间占住唯一的同步通道,后面的订单、账户状态等核心变更全部被堵在 relay log 中。
本文将从事故链路、MySQL 复制机制、应急处置和长期治理四个方面,完整复盘这次问题。
为保护业务信息,文中的表名、日期和部分业务场景均已做脱敏处理。
一、事故是如何发生的
1. 数据库架构
事故发生时,数据库拓扑如下:
主库负责处理写请求,三台从库共同承担读请求。主库提交事务后,将变更写入 binlog;从库接收日志并写入 relay log,再由应用线程按顺序执行其中的事务。
当时每台从库只有一个工作线程处理同步逻辑。在日常小事务场景下,这套架构运行平稳,但它存在一个天然限制:前面的事务没有执行完成,后面的事务就只能继续等待。
2. 触发事故的 SQL
事故中的 SQL 可以抽象为:
DELETE FROM business_history
WHERE business_date >= '2026-01-01'
AND business_date < '2026-04-01';
这条 SQL 看起来并不复杂,但同时踩中了三个高风险点:
business_date没有可用索引,需要扫描大量数据;- 日期范围过大,一次命中数千万行;
- 没有通过
LIMIT或主键游标拆分为小批次事务。
执行后,主库需要完成大量数据扫描,并产生大量 Undo、Redo 和 binlog。更关键的是,这次操作形成了一个巨大的复制工作单元,并被传递到三台从库。
3. 业务侧的表现
大删除进入复制链路后,三台从库的同步延迟快速扩大。主库后续产生的订单更新、支付结果、账户状态等核心变更虽然已经提交,却迟迟无法在从库上完成回放。
业务读请求仍然访问从库,于是出现了以下现象:
- 查询不到刚刚写入或更新的数据;
- 读取到已经过期的业务状态;
- 依赖状态推进的后续流程无法继续;
- 上游不断重试,进一步放大系统压力;
- 人工补偿和数据核对成本持续增加。
事故持续数小时,并最终造成资损。
二、一条 DELETE 为什么能堵住所有核心数据
1. 单线程复制就像一条单车道
当时的从库只有一个工作线程顺序应用事务,可以把它理解为一条不能超车的单车道。
假设 relay log 中的事务顺序如下:
事务 A:删除数千万条历史数据
事务 B:更新订单状态
事务 C:写入支付结果
事务 D:更新账户余额
事务 E:推进业务流程
事务 B 到 E 本身可能只修改一两行,正常执行时间只有几毫秒。但只要事务 A 没有完成,后面的事务就无法越过它。
这就是典型的队头阻塞:不是后续所有 SQL 都执行缓慢,而是一个极慢的操作占住了唯一处理通道。
2. 主库执行完成,不代表风险已经结束
很多人评估慢 SQL 时,只关注它在主库执行了多久。这对于数据修改语句是不够的。
一次大范围删除至少会经过以下环节:
主库扫描和删除
↓
生成 Undo、Redo 和 binlog
↓
binlog 传输到从库
↓
从库读取 relay log
↓
从库定位并删除对应数据
↓
后续事务继续回放
主库提交成功,只说明写入阶段结束了。复制链路仍需要承担传输、定位、删除和提交成本。当从库的回放速度低于主库产生 binlog 的速度时,延迟就会持续增长。
三、技术关键:从库为什么会出现低效回放
1. 先区分 STATEMENT 和 ROW
MySQL 的复制行为与 binlog_format 有关。
在 STATEMENT 模式下,binlog 记录原始 SQL,从库会重新执行删除语句。日期条件没有索引时,从库同样要进行大范围扫描。
在 ROW 模式下,binlog 记录被修改行的镜像。从库不会简单地重新执行原始 WHERE 条件,但它仍然需要在本地表中找到对应记录,然后完成删除。
对于 ROW 模式,MySQL 会优先使用主键或合适的唯一索引定位目标行。如果表缺少可用主键、唯一索引,或者行镜像无法覆盖可用索引,从库的行定位可能退化为普通索引扫描、哈希辅助扫描,甚至全表扫描。
目标数据达到数千万行时,这种定位成本会被迅速放大。
2. 一个需要避免的错误结论
复盘时不能简单写成:
DELETE 的 WHERE 条件没有索引,所以 ROW 模式下从库每删除一行都会全表扫描。
这句话并不严谨。ROW 模式下是否发生全表扫描,还要取决于:
- 表中是否存在主键或合适的唯一索引;
binlog_row_image的配置;- 行事件中是否包含可用于索引定位的列;
- 从库实际选择的行搜索算法;
- 表结构在主库和从库上是否一致。
根据本次事故现象可以确定的是:删除任务在从库回放阶段没有获得足够高效的定位和执行路径,导致复制应用速度远低于正常业务 binlog 的产生速度。
要把技术结论完全坐实,还应结合事故时的表结构、binlog 配置、relay log、慢日志和复制线程状态进行确认。
四、为什么有三台从库,业务仍然全部受影响
三台从库都接收同一个主库的 binlog,并采用相同的回放配置。危险删除进入复制链路后,三台从库会处理同一批变更,也就会遇到相同的性能问题。
这意味着它们提供的是实例数量上的冗余,而不是故障模式上的隔离。
如果三台从库同时具备以下特征,那么同一复制事件就可能击穿全部副本:
- 接收完全相同的 binlog;
- 使用相同的单线程回放模式;
- 表结构和索引情况相同;
- 全部承载实时业务查询;
- 业务没有复制延迟下的读降级策略。
所以,“一主三从”并不天然等于高可用。副本数量只有与角色隔离、故障切换和业务降级结合起来,才能真正提升系统韧性。
五、根因不只是一条慢 SQL
把事故归因于“同事执行了一条错误 SQL”很容易,但这种结论无法阻止事故再次发生。
1. 直接原因
一次性删除数千万条数据,过滤字段缺少索引,并且没有分批提交,导致主库执行和从库回放都承担了远超日常事务的处理成本。
2. 放大因素
从库只有一个工作线程顺序回放事务,大删除长时间占住唯一应用通道,后续核心事务全部发生队头阻塞。
3. 架构问题
三台从库共享同一种故障模式;业务读链路又高度依赖从库,没有根据一致性要求准备切主读、限流、熔断或暂停流程等降级方案。
4. 流程问题
高风险数据变更缺少足够的执行前门禁:
- 没有强制检查执行计划;
- 没有估算扫描行数和影响行数;
- 没有评估事务大小和 binlog 规模;
- 没有确认目标表主键及相关索引;
- 没有规定大数据删除必须分批;
- 没有设置主从延迟的暂停阈值;
- 没有明确等待、跳过或重建从库的决策标准。
5. 用“五个为什么”继续向下追
为什么核心业务流程被卡住?
因为业务从从库读取不到最新状态,后续流程无法确认前置操作已经完成。
为什么从库长时间没有最新状态?
因为复制应用线程被一个大批量删除任务长时间占用。
为什么删除任务回放这么慢?
因为删除规模达到数千万行,缺少高效的扫描或行定位路径,并且没有分批提交。
为什么高风险 SQL 能直接进入生产?
因为变更流程没有对无索引大范围 UPDATE/DELETE、影响行数和事务规模设置强制门禁。
为什么三台从库同时失去实时服务能力?
因为所有从库共享相同的复制事件和回放模式,业务也没有复制延迟下的读路由与降级能力。
因此,人的操作只是触发点,SQL 治理、表结构、复制架构、监控告警和业务降级多层防线的缺失,才决定了事故最终规模。
六、事故发生后应该如何止血
应急处置的目标不是让延迟数字尽快归零,而是在数据一致性、恢复时间和业务影响之间作出可解释的选择。
1. 先确认复制链路的真实状态
需要同时确认:
- 主库删除事务是否已经提交;
- 从库接收线程和应用线程是否正常;
- 当前正在应用的事务及 relay log 位置;
- relay log 的增长速度和实际回放速度;
- 从库 CPU、磁盘、IOPS 和锁等待情况;
- 哪些业务必须读取最新数据,哪些业务能够容忍延迟。
不能只看单一的延迟秒数。大事务执行期间,部分延迟指标可能长时间不刷新,也无法准确反映剩余工作量。
2. 停止继续增加复制压力
暂停同类清理任务和其他大事务,冻结非必要数据库变更,避免继续向已经拥堵的复制链路增加负载。
如果主库事务尚未提交,是否终止需要评估回滚成本。一个已经执行很久的大删除,即使执行 KILL,也可能进入漫长回滚,继续占用主库资源。
3. 优先恢复核心业务链路
可以根据一致性要求采取分级措施:
- 将少量强一致核心查询临时切到主库,并设置严格流量上限;
- 暂停报表和非核心查询,为关键流程释放资源;
- 对可以容忍旧数据的功能明确降级;
- 暂停依赖最新状态的任务消费,将请求放入可补偿队列;
- 避免客户端和服务端进行无上限重试。
将全部读流量直接切到主库,可能把复制事故升级为主库事故,所以必须限流、分级并逐步切换。
4. 选择等待、跳过还是重建
| 方案 | 适用情况 | 主要风险 |
|---|---|---|
| 等待从库完成回放 | 剩余时间可以接受,一致性要求高 | 业务恢复较慢 |
| 优化资源后继续回放 | 瓶颈明确,能够安全提升回放速度 | 效果有限,操作不当会扩大故障 |
| 跳过事务并人工补数 | 删除允许暂缓,且有可靠补偿方案 | 主从立即不一致,后续校验复杂 |
| 从新快照重建从库 | 预计回放时间明显高于重建时间 | 占用主库、网络和存储资源 |
不能为了让监控恢复绿色而盲目跳过事务。跳过只改变复制位置,不会自动保证数据一致性。任何跳过操作都必须记录 GTID 或 binlog 位置、影响表、补偿方式和最终校验结果,并经过明确授权。
七、如何从根上避免同类事故
1. 在线业务表必须具备稳定主键
在 ROW 模式下,从库回放更新和删除事件时需要定位目标行。主键或合适的非空唯一索引,通常是最高效、最确定的定位路径。
建议对存量表进行扫描,识别无主键大表,并把“在线业务表必须有主键”加入建表规范和发布平台门禁。
2. 大数据删除必须拆成可观测的小批次
首先建立与清理条件匹配的索引,例如:
CREATE INDEX idx_business_date_id
ON business_history (business_date, id);
然后按小批次删除并提交:
DELETE FROM business_history
WHERE business_date < '2026-01-01'
ORDER BY business_date, id
LIMIT 2000;
任务循环执行时,每批之间适当让出资源,并持续观察:
- 单批执行时间和影响行数;
- 主库锁等待、磁盘和 IO;
- binlog 增长速度;
- 三台从库的复制延迟;
- 核心业务错误率。
当延迟或数据库负载超过阈值时,清理任务应自动暂停。批次大小需要通过压测和低峰期验证确定,不能机械地认为 2000 适合所有表。
如果历史数据长期按时间淘汰,还应评估时间分区、归档表和冷热分层。可以通过数据生命周期设计解决的问题,不应长期依赖超大 DELETE。
3. 建立危险 SQL 强制门禁
生产环境中的大范围 UPDATE、DELETE 至少应检查:
- 执行计划是否命中预期索引;
- 预计扫描行数和影响行数;
- 是否存在主键或合适的唯一索引;
- 预计事务大小和 binlog 规模;
- 是否支持分批、暂停和续跑;
- 执行窗口、观察指标和停止条件;
- 回滚、补偿和数据校验方案。
关键规则应该由数据库平台自动拦截,而不是只依赖执行人的经验和谨慎。
4. 评估并行复制,但不要把它当成万能解法
可以根据 MySQL 版本、硬件资源和事务依赖关系,评估启用或调优并行复制。MySQL 8.0.26 及以后使用 replica_parallel_workers,更早版本对应 slave_parallel_workers。
多线程复制可以并行回放相互独立的事务,减少普通事务之间的排队,但它不能把一个超大事务自动拆成许多小事务,也无法修复无主键、无索引带来的高回放成本。
合理的顺序应该是:先治理表结构和大事务,再通过压测选择合适的并行度。
5. 按业务一致性要求设计读策略
读写分离不能只有“读主库”和“读从库”两个全局开关。不同业务的要求并不相同:
- 写入后必须立即读取的请求,可以在限定时间内读主库;
- 支付、余额、状态机推进等操作,应采用更强的一致性策略;
- 列表、报表和历史记录可以容忍一定延迟;
- 延迟超过阈值时,非核心功能应限流、熔断或提示处理中。
这样才能避免一次复制延迟将所有业务流程同时拖住。
6. 完善复制监控和应急预案
除了常见的主从延迟,还应监控:
- relay log 堆积量和增长速度;
- 接收线程、协调线程和工作线程状态;
- 长事务、大事务和异常 binlog 增长;
- 从库 CPU、磁盘、IOPS 和锁等待;
- 业务侧旧数据、状态不一致和重试指标。
预案中需要明确谁负责数据库判断、谁负责业务降级、什么条件下允许切主读、何时选择重建从库、何时可以跳过事务,以及恢复后如何完成数据校验和业务补偿。
八、这次事故带来的四点认识
1. 副本数量不等于可用性
如果所有副本处理同一份危险事件、使用同一种策略并承载同一类流量,它们就可能同时失效。高可用不仅是增加机器数量,还要隔离故障模式。
2. 慢 SQL 的影响不会止于主库
评审数据修改 SQL 时,不仅要看主库执行时间,还要考虑事务大小、binlog 规模、从库行定位方式和回放吞吐。
3. 大事务是复制系统的天然敌人
一个数千万行的大事务会同时消耗 Undo、Redo、binlog、网络、relay log 和复制线程。即使使用并行复制,单个超大事务仍然难以被拆分。最有效的办法通常不是继续堆资源,而是从源头拆小。
4. 不要把系统问题归结为个人粗心
如果一条明显高风险的 SQL 可以未经执行计划检查、影响评估和限速控制直接进入生产,那么系统本身就缺少必要护栏。人的操作是触发条件,流程和技术防线决定了事故规模。
九、总结
本次事故的完整链路可以概括为:
无索引的大范围删除进入主库 → 数千万行变更形成巨大复制负担 → 从库回放效率过低 → 唯一应用线程被长时间占用 → 后续核心事务队头阻塞 → 三台从库同时出现小时级延迟 → 业务读取旧数据并停滞 → 最终造成资损。
要避免同类事故再次发生,不能只留下一句“以后执行 SQL 要注意索引”,而应该同时建立四层防线:
- 数据层:补齐主键和索引,做好分区与数据生命周期治理;
- 执行层:大任务分批、可暂停、可续跑,达到延迟阈值自动熔断;
- 复制层:评估并行回放,做好副本角色隔离、监控和恢复预案;
- 业务层:按一致性要求设计读路由、限流、降级和补偿。
只有把危险 SQL 从“依赖个人经验”变成“依靠系统强制约束”,把复制延迟从“数据库指标”升级为“业务风险”,才能真正降低下一次事故发生的概率。

浙公网安备 33010602011771号