删库跑路这件事,终究是轮到我了
工作两年多,终于亲手‘体验’了一次传说中的删库跑路。一次测试环境数据迁移演练中,误将删除旧字段的SQL执行到了生产环境。事故发生后通过业务止血,恢复字段,数据恢复等一系列措施,最终恢复了误删字段及历史数据
文章目录
删库跑路这件事,终究是轮到我了
“删库跑路”这个词,我以前只在各种程序员段子、梗图里见过。作为一个稳健的后端开发,平时也常把“敬畏生产环境”挂在嘴边,甚至还特意把 Navicat 中生产数据库的连接改成了醒目的红色,用来警示自己不要误操作。
理论上是这样的。
直到一个平常的傍晚,我正在测试环境演练支付服务新版本的升级流程,流程本身并不复杂:加字段、迁数据、升级服务,删除废弃的旧字段。一路演练下来十分顺利,我已经在思考晚饭吃什么了。

做到删除旧字段这一步时,刚好有人找我排查一个问题,于是我就顺手打开了生产数据库的连接查了几条数据,发现问题并不在我这,于是调侃了对面几句。
聊完之后,我继续刚才被打断的任务:“该到删除旧字段环节了”。于是我复制 SQL,粘贴,看了一眼,点击执行。转眼间,SQL 行云流水地跑完了。哟,这升级方案居然一次就跑通了,不愧是我!然后视线扫到了左边那个红色的数据库连接:这是生产环境。此时时间是17:51。
沉默是今晚的康桥。
我有些不可置信地看了看刚刚跑完的SQL:ALTER TABLE ... DROP COLUMN ...,那一刻,“删库跑路”这四个字从段子照进了现实。虽然严格来说我没有删库,也没有跑路,而是删了几个字段,但此时这两者对我来说没有什么区别。
It was at that moment, he knew, he f**ked up.
终于反应过来之后,大量报错也开始涌入告警群里。新支付系统虽刚正式上线不久,但已经推动了一个存量业务对接使用了,此时有不少真实的支付流量正打进来!
于是,本该是一次普通上线演练的傍晚,从此刻开始正式变成了生产事故处理现场。
先救业务,紧急止血
虽然当时的第一反应是“完了”,但事故显然不会因为我懵了几秒就暂停。我突然想起早上和+1讨论升级方案时,提到目前接入新支付系统的存量业务还保留着原来的支付实现,并且做了一个切流开关。一旦新支付系统出现问题,可以直接切回他们原有的实现。
没想到这个开关这么快就用得上了,此时时间是17:53,距离误操作已经过去了大概两分钟,我赶紧联系了对应负责人让他切一下开关,对方很快操作了开关,并告诉我这个配置有缓存,不能立即生效,还要等几分钟。
趁着这个时候,我赶紧找领导说明了情况,毕竟做错承认,挨打站稳。领导的反应倒是比我想象中平静得多,大概是这种场面见得多了。他让我想确认其他业务有没有受到影响,同时联系值班运维准备数据恢复。事实证明,事故现场保持淡定还是很有用的。
随后我挨个联系其他业务确认情况,确认影响面没有继续扩大。18:01,缓存终于失效,告警群暂时恢复了宁静。
此时数据库还没真正恢复,但是没有支付流量进来了,接下来可以慢慢处理数据库的问题。首先是恢复表结构,将误删的字段重新加回生产表,到这一步支付服务已经恢复了支付能力,但历史数据还处于不可用状态。毕竟 ADD COLUMN 可以加回字段,但无法加回丢失的数据。真正麻烦的部分,才刚刚开始。
几十G备份,慢慢下吧
好消息是,我们有备份。
运维上来之后,告诉我数据库开启了定时全量备份,最近一次全量备份是当天下午3点,配合3点后的 binlog 可以做到数据无损恢复。
那么理论上的恢复方案就很明确了:
方案有了,那就开工。但坏消息是,备份大小有几十个 G。
于是事故处理进入了戏剧性阶段:等下载
等待的过程中,我开始琢磨一个问题:阿里云 RDS 的备份机制如此完善,难道这种“将数据库恢复到某个时间点”这种操作,还需要用户自己把几十个 G 数据下载下来,再手动恢复、找重放起点、重放 binlog 吗?这不对吧?
于是我开始查阿里云的文档,还真让我查到了。阿里云 RDS 本身就提供了按时间点恢复数据库的功能。只需指定一个时间,平台会自动创建一个新的 RDS 实例,恢复指定时间之前最近的全量备份,再把后续 binlog 重放到指定时间点。
我喜出望外,赶紧和运维确认了一下新方案,确认可行后,也没理由继续和几十 G 的下载进度条培养感情了。
阿里云已经造好了轮子
既然决定换方案,接下来的事情就简单多了。在阿里云工程师的远程指导下,我们成功进入了数据恢复页面,现在的问题从“怎么恢复”变成了另一个看起来简单得多的问题:
恢复到几点?
从 Navicat 的历史执行记录来看,那条删字段的 SQL 是在 17:51:00 执行的,最理想的情况是恢复到执行前一秒,也就是 17:50:59,这样几乎不会损失任何正常数据,也便于我们后续定位 binlog。但想了一下,还是没敢这么玩。考虑到本地电脑和服务器之间可能有时间差,恢复时间点卡得这么严,万一把 DROP COLUMN 也一起重放进去,那就很有节目效果了。
所以我们最终保守地选择了 17:50:20。
19:20,运维在阿里云技术人员的协助下开始执行按时间点恢复。然后有到了熟悉的环节:等!
不过这次就快多了,不到半小时,新的临时 RDS 实例就创建好了,数据库数据回到了 17:50:20。
这四十秒里发生了什么
数据库虽然已经成功恢复到了 17:50:20,但距离误操作发生还有大约四十秒。
如果这期间有正常的支付请求写入数据库,那么恢复出来的快照里就少了这部分数据。理论上,我们还需要找到这段时间对应的 binlog,把误操作之前的正常变更重放回去。
于是接下来要解决的问题变成了:
17:50:20 到 17:51:00,这四十秒里到底发生了什么?
20:15 左右,我们开始下载事故时间点附近的 binlog,随后完成 dump。
打开文件之后,那种无力的感觉又回来了:大量事件、position、时间戳,以及事务信息和 SQL 混在一起。真要靠人力一点点找出哪些操作属于支付系统,哪些操作需要重放,多少有种大海捞针的味道…
不过,时代变了,大人!将 dump 后的 binlog 交给“天才程序员”,告诉它时间范围,涉及的数据库表,以及我们的目标:找到 17:50:20 到误删字段之间所有需要重放的正常业务操作。原本需要人工在大量日志中来回搜索校对的工作,变成了先由 AI 帮忙快速筛选归类,再人工对筛选结果进行确认。
不得不说,这种任务太适合交给 AI 了。(不过后面才想到,可以利用工具将 binlog 转为原始 sql,过滤掉删字段及之后的操作,再进行重放)
最终分析结果比预想中简单很多:该时间区间内,没有支付系统相关的数据变更需要重放。
至此,问题就简单很多了。剩下的事情,就是把恢复后的数据重新塞回生产环境。
把数据塞回去
现在我们手里已经有了两份数据:
- 生产环境:除了被我删掉的几个字段之外,其他数据都是最新的
- 临时 RDS:删字段时间点之前的完整快照
经过简单思考,我决定采用以下方案:
- 导出临时 RDS 的数据
- 在生产环境中新建一个
backup临时库,将这份数据导进去 - 使用
UPDATE JOIN语句通过主键跨库联表,完成缺失字段的回填
简化后的回填 SQL 类似如下:
UPDATE prod.t1 AS ptJOIN backup.t1 AS bt ON pt.id = bt.idSET pt.deleted_column = bt.deleted_column;确认无误后执行回补,之后再对记录数、关键字段值等数据进行校验。
20:47,数据恢复完成。
从 17:51 按下那个执行按钮,到现在过去了将近三个小时,至此这段闹剧总算告一段落。
后续复盘和整改
事后复盘时,误操作的原因其实并不复杂:人的注意力是有限的,在多个任务中切换时,“上下文”难免会混乱,导致本该跑在测试环境的 SQL 跑到了生产。
再谨慎的人,也有犯迷糊的时候;把“敬畏生产环境”建立在人不会犯错的前提下,显然不太靠谱。所以真正需要整改的,其实是长期以来生产数据库的访问权限过于宽松。
这次事故之后,这个口子被正式收紧了。生产数据库原本开放给开发人员使用的内网转发端口被收回,只能通过跳板机提供的中转链接进行访问。新的连接方式除了能留下完整的审计日志之外,也从权限层面限制了危险操作,类似 DDL 以及部分危险 DML,会被跳板机直接拒绝。
从这个角度看,这次事故也算推动了一件早该做的事:
“敬畏生产环境”,从依赖开发人员自觉,变成了程序帮你管控。
至于我自己,在这次事故中也算获得了一个“难得”的经验:
在有完善备份机制的前提下,如何将数据库恢复到某个时间点。