林泠是在周三下午三点零七分接到电话的。
他记得这个时间,因为当时他正盯着屏幕上一个SqL查询的执行计划,想优化掉一个全表扫描。手机在桌上震起来,屏幕上是运维组老周的号码。他接起来,老周的声音语速飞快,说生产环境被人执行了一条删库命令,user表没了。
林泠拿着手机站起来,椅子往后滑出去撞在文件柜上发出一声闷响,旁边的同事小赵抬头看了他一眼。他问什么时候的事。老周说十分钟前,一个离职运维的账号没来得及注销,被人从外部登录进来,只执行了一条命令:dRop tAbLE user。干脆利落,没有拖泥带水。林泠已经走出了办公区,边走边问备份呢。老周说最近一次全量备份是凌晨三点,增量备份每小时一次,最近一次是下午两点。
下午三点零七分接到电话,备份在两点。也就是说,从下午两点到三点十分之间写入user表的所有数据——新增注册用户、密码修改、手机绑定、头像更新——全部没了。不是被删了,是随着那张表一起从这个数据库里彻底消失了,像从未存在过一样。
从办公区到机房要经过两段走廊和一个楼梯间。他走过第一段走廊的时候脑子里在快速算两件事:第一,两点到三点十分之间的增量数据量大概有多少;第二,这些数据有没有其他途径能捞回来。走过第二段走廊的时候他大概有了判断:user表平时每小时写入量不大,但今天是周三下午,公司App的推送刚在两点半发了一波促销活动,注册量比平时高了两到三倍。他推开机房的门,冷气从地板下面的送风管道往上灌,四排机柜同时运转,散热风扇发出持续的低频嗡鸣。老周已经蹲在一台服务器前面了,屏幕上的日志窗口还在往下滚。
林泠拉了把椅子坐下来,先确认了三件事。第一,那条dRop tAbLE命令的执行时间精确到秒——下午两点五十八分四十三秒,跟他接到电话的时间只差了不到十分钟。第二,全量备份确认完整可用,凌晨三点的备份文件校验和没有损坏。第三,增量备份的日志文件确认到了下午两点整,两点之后的binlog还在,没有被删除。
他说先别急着从备份恢复。全量恢复至少需要四十分钟,这四十分钟内所有依赖user表的业务模块都会挂掉。他让老周先把从凌晨三点到下午两点五十八分的全量备份加上增量备份串成一条完整的恢复链路,在测试环境里开始并行跑恢复流程。自己则打开数据库的binlog解析工具,把下午两点到两点五十八分之间的所有写入操作提取出来。
六十万条。这是他需要手动恢复的数据量。六十万条写入记录,包括四万三千个新注册用户、一万两千条密码修改记录、八万多条手机号绑定更新、四十多万条头像和昵称的修改。这些数据在binlog里都有完整的记录——INSERt、UpdAtE、dELEtE,每一条都有时间戳,每一条都有完整的字段值。它们没有被删,只是被dRop tAbLE命令从数据库里抹掉了表结构,数据本身还以二进制日志的形式存在于服务器磁盘上。
他开始写恢复脚本。脚本的逻辑分三步:先根据全量备份加上增量备份恢复到下午两点整的完整状态,再从binlog里提取两点到两点五十八分之间的所有增量数据,最后把这两段数据拼在一起,写入一张临时重建的user表。写脚本的时候手指在键盘上敲得飞快,老周在旁边看着他写,递了杯热水过来,他接过来喝了一口,眼睛没离开屏幕。
脚本写完之后先在测试环境跑了一遍。全量恢复耗时四十分钟,跟预估的完全一致。增量数据回灌跑得很快,六十万条记录在binlog解析后批量写入,不到十分钟就跑完了。他检查了一遍数据完整性——四万三千个新用户基本全部恢复,密码修改记录一条不少,手机绑定更新的时间戳全部连续,没有断点。测试环境通过。
下午四点二十三分,生产环境开始正式恢复。全量备份从凌晨三点的时间点开始回滚,屏幕上进度条一格一格地往前走,每前进一格代表一个数据文件的恢复完成。老周站在他旁边,两个人都没怎么说话。机房里只有硬盘读写指示灯在疯狂闪烁,闪成一片琥珀色的光海。
四点五十三分,全量恢复完成。五点零二分,两点之前的增量备份回灌完成。他深吸一口气,把下午两点到两点五十八分之间的增量数据恢复脚本推上了生产环境。屏幕上开始滚动写入日志——INSERt语句一条接一条地刷过去,速度快得看不清具体内容。五点十二分,六十万条增量数据全部回灌完成。
他打开user表的记录数校验查询,对比了凌晨三点加上全量备份恢复后的记录数,加上两点之前增量恢复的记录数,再加上下午两点到两点五十八分的binlog提取记录数。三个数字加起来,跟dRop tAbLE之前最后一秒的记录总数完全一致。
五点十五分他给老周和主任各发了一条消息:数据全部恢复,零丢失。然后靠在椅背上,感觉后背的衬衫被冷汗浸湿了一片,贴在皮肤上又凉又黏。
老周长长地出了一口气,说这次能恢复得这么快,binlog完整是关键。林泠点了点头,脑子里还在回想刚才恢复过程中的每一个步骤。全量备份加增量备份加binlog恢复,这个链路他之前在设计数据迁移方案的时候就反复推演过,但真正在生产环境上跑一次全流程紧急恢复,还是第一次。他想起自己在大暑那天在白马河做窗口期复盘直播时说过的话——数据库排查逻辑和鱼情分析逻辑本质上是同一套思维模型:划定时间窗口,定位异常变量,追踪因果链路。今天这次删库恢复,本质上也是一次窗口期内的紧急排查——划出数据丢失的时间窗口,定位dRop tAbLE的精确时间点,追踪从全量备份到增量备份到binlog的完整恢复链路。逻辑完全一样,只是压力不一样。
他站起来活动了一下僵硬的脖子,走到机房外面。走廊里的温度比机房里高了至少十度,暖暖的空气裹住他冰凉的胳膊,反而让他打了个哆嗦。窗外的天色已经开始变暗,下班的人群从办公楼里涌出来,三三两两地往停车场走。
第二天上午,公司发了全员通报邮件。标题很简短——“关于昨日生产环境数据安全事件的通报”,正文通报了离职运维账号未及时注销、外部人员利用未注销账号执行恶意操作的事实,处理措施列了三条:所有离职人员账号立即清理、生产环境操作权限全面升级为双人审批、数据库备份策略从每日全量升级为每日全量加每四小时增量。末尾特别加了一段致谢,提到数据组林泠在事件响应中完成数据零丢失恢复。邮件发出后不到十分钟,林泠的钉钉上弹出来七八条消息,有同事发的竖大拇指表情,有小赵发的一排感叹号,有平时不怎么联系的隔壁部门同事问技术细节。他一一回了谢谢,把手机屏幕朝下扣在桌上。
下午部门开会,主任专门提了这件事,说这次数据恢复不仅仅是技术过硬,更重要的是响应速度和判断力——从接到电话到定位问题到拿出恢复方案,整个过程不到十五分钟。
开完会回到工位,他翻了翻笔记本。
周五下班前,老周发来一条消息,说运维组的人想请他吃顿饭,感谢这次应急响应。林泠回了个好,然后低头看了一眼桌上那本摊开的笔记本。春季窗口期追踪表上,雨水的空白栏还等着他明天去填。删库是意外,钓鱼是日常。意外处理完了,该回到日常了。