一次删日志表引发的线上连锁事故记录
2026 年 4 月 17 日下午,SeaTunnel 先报了警。日志里最刺眼的一句是:Last offset stored 在 mysql-bin.026405 的某个位置,但 binlog reader 已经跑到了另一个位置,之前那个端点没了。 当时第一反应还以为是 SeaTunnel 自己抽风,后来顺着告警往下查,才发现问题根子根本不在同步任务,而在上游 MySQL 的 binlog 已经被阿里云 RDS 提前清掉了。SeaTunnel 这类 MySQL CDC 任务本质上依赖 binlog 位点恢复;如果任务重启时要回到的 binlog 文件已经不存在,连接器就会直接失败。SeaTunnel 的 MySQL CDC 配置本身就是围绕 binlog 位点启动的,而 Debezium 对这类场景的说明也很直接:如果连接器试图从一个 MySQL 已经不再保留的 binlog 位置恢复,就会报错并停掉。
后面继续查 RDS,才把事情串起来:这并不是单纯的“binlog 过期了”,而是实例存储空间已经逼近红线。阿里云 RDS for MySQL 的官方说明里提到,如果启用了本地日志保留策略中的可用存储空间保护,当存储使用率超过 80% 或者可用空间低于 5 GB 时,系统会自动删除更早的 binlog,直到空间回到安全区间。也就是说,到了这种时候,平台优先保活,binlog 的理想保留时间就不再是第一优先级了。我们当时嘴上都在说“内存快满了”,但更准确的说法其实是:RDS 实例的存储空间快满了。
再往前倒,就到了这次事故里最危险但也最容易被低估的一步:手工删日志表数据,试图给数据库腾空间。 这个思路本身并不完全错,因为本地和 OLAP 侧已经有冷备,线上老日志确实可以清。但问题在于,很多人以为 DELETE 掉了数据,磁盘空间就会立刻回来;对 InnoDB 来说,并不是这样。MySQL 8 里,DELETE 首先是逻辑删除:它会写 undo,保留旧版本供 MVCC 和回滚使用,真正的物理清理要等 purge 线程在“这些旧版本已经没人需要”的前提下再慢慢做。也就是说,删了不等于立刻把空间还给操作系统。如果目标是整表清空,TRUNCATE TABLE 确实更接近“直接重建表”,速度快,且在 file-per-table 且无外键阻塞的条件下能够回收表空间;但如果只是删一部分历史数据,通常还要配合表重建或 OPTIMIZE TABLE 之类的方式,才有机会把碎片化后的空间真正回收出来。
真正把事情推向失控的,不是“删了数据”,而是在一个已经快没空间的实例上,执行了一次可能非常重的删除操作。大批量 DELETE 对 MySQL 8 来说不是“减法”,而是先来一轮“加法”:它会产生 undo log、redo log,还会把变更写进 binlog;如果删除语句里带了复杂条件、子查询、联查或者物化过程,优化器还可能创建内部临时表。MySQL 官方文档写得很清楚:内部临时表可能先在内存里,撑大后再落盘;而从 MySQL 8.0.16 开始,磁盘上的内部临时表默认就是 InnoDB。也就是说,在磁盘本来就快见底的时候,一条重型 DELETE 不但不会立刻释放空间,反而可能先额外吃掉一截空间。这也是为什么“我只是想删点历史日志,结果一回来线上全炸了”这种事,技术上完全说得通。
我现在回看那个现场,整个链路其实很清楚:实例空间已接近极限,RDS 为了自保提前删旧 binlog,SeaTunnel 持久化的位点因此失效;与此同时,线上又执行了大批量删除,undo、redo、binlog 和可能出现的临时表一起继续放大存储压力;剩下那点空间被最后一波写放大吞掉,数据库直接被拖进故障状态。 RDS 在存储耗尽时会进入保护状态,严重时写请求会被拒绝;如果数据库进程发生异常退出,InnoDB 重启后还要做 crash recovery,把日志回放和未提交事务回滚完,业务恢复不会是瞬间完成的。
这件事最讽刺的地方在于,表面上看是“SeaTunnel 报错”,中间看是“阿里云强删 binlog”,落到执行层看是“删日志表把库删崩了”,但本质上其实是一个老问题:在容量已经逼近红线的情况下,还试图用高写放大的在线操作去做空间治理。 这时候任何一个动作都不是孤立的。CDC 盯着 binlog,RDS 盯着可用空间,InnoDB 盯着 undo/redo/purge,业务又盯着线上可写性。平时这些机制各干各的;一旦存储逼近上限,它们就会一起咬人。
这次事故给我的教训很直接。第一,CDC 链路不是“有库就行”,而是“binlog 保留窗口必须覆盖任务中断和恢复窗口”;只盯任务状态,不盯 binlog 保留策略和存储余量,迟早要出事。第二,想靠 DELETE 给 InnoDB 快速腾空间,本身就是个很容易误判的动作,尤其是在热库、日志表、大事务场景下。第三,真正危险的不是删数据,而是在快满的实例上做重型写操作;因为数据库先要为这次删除付出日志和临时空间成本,释放空间反而是后置的。第四,线上做这种清理,应该默认采用“小批次、按主键范围、可暂停、可观测”的方式,而不是一把梭。以上这些,不是事后诸葛亮,而是 MySQL 和 RDS 的工作机制本来就决定了它会这样发展。