那天凌晨两点四十,监控群突然炸了。业务方发来一连串截图,后台管理页面全部报错,数据加载不出来。我揉了揉眼睛,第一反应是网络问题,但ping数据库服务器时发现,延迟直接超时。那一刻我知道,出大事了。

我一边往办公室赶,一边远程登录服务器。登录倒是成功了,但数据库服务进程已经挂了。看了下系统日志,磁盘空间满了,InnoDB的redo log写不进去,MySQL直接崩溃。这个库是公司核心业务库,保存着近三年的订单和用户数据,备份策略是每日凌晨一点全备,增量binlog每十分钟同步一次。而现在是两点多,也就是说,最近一个多小时的数据,全靠binlog撑着。
我先做的第一件事,不是急着启动服务,而是把崩溃现场的数据目录完整复制了一份。这一步极其关键,因为后面所有恢复操作,都基于这个原始副本进行,原库绝对不能动。我用rsync把整个数据目录同步到另一台备用机器上,大约花了二十分钟。这二十分钟里,我脑子里过了一遍恢复方案:先尝试直接启动,如果不行,就通过备份加binlog的方式追数据。
备用机上启动MySQL,报错信息很明确:InnoDB检测到redo log不完整,需要强制恢复。我加上了innodbforcerecovery=1的参数,启动成功,但只能以只读模式访问。查了下数据,发现最近一小时的数据确实缺失,但好消息是,表结构都还在,数据文件没有损坏。这个状态下,我赶紧用mysqldump把能导出的数据全部导出来,存成SQL文件。
然后我关掉强制恢复模式,开始走正规恢复流程。先找到昨晚凌晨一点的全备文件,解压后导入一个新建的空库,恢复到了凌晨一点的状态。接着,我需要把一点到崩溃时间之间的binlog找出来,重放进去。binlog文件存放在独立磁盘上,没有受到影响,这算是不幸中的万幸。
重放binlog用了大概四十分钟,过程并不顺利。中途报了几次错,有两条事务涉及的表被删除了,还有一条记录主键冲突。我一条条检查binlog内容,用mysqlbinlog工具把日志解析成可读的SQL,然后手工跳过有问题的语句,再继续执行。这里有个经验:binlog重放一定要用--stop-position或者--start-position来精确控制边界,不要一股脑全跑完,否则遇到一个错误就前功尽弃。
折腾到早上六点多,数据终于恢复到崩溃前一条成功提交的事务。我对比了业务侧的流水记录,确认数据量完全对得上,才敢把新库切换上线。整个过程花了将近四个小时,好在业务方是内部系统,凌晨使用量不大,没有造成太恶劣的影响。
这次事故的根因,说穿了其实很蠢。监控系统早就报了磁盘空间不足的告警,但值班同事认为是临时日志,没当回事,直接忽略了。结果日志文件越写越大,最终把磁盘彻底塞满。这提醒了我一个事:所有告警,哪怕看起来不起眼,都可能是大事故的前兆。尤其是磁盘空间和日志增长这类指标,必须设定自动清理策略,不能靠人工判断。
另外,备份策略本身也有漏洞。虽然每日全备加binlog增量听起来合理,但binlog同步的机器和数据库在同一台物理机上,磁盘满了之后,binlog写入也停了。如果当时把binlog同步到异地或者独立的存储设备,恢复起来会从容得多。现在我把binlog同步改成了双写,本地一份,远程对象存储一份,双保险。
还有一点值得说,就是恢复过程中的心态管理。凌晨干活,人本来就困,又遇到各种报错,很容易慌。我给自己定了个规矩:每做一个操作之前,先停下来想十秒钟,确认这个操作会不会对现有数据造成二次损坏。宁可慢一点,也不要做冒险动作。这个习惯,帮我避开了好几次可能把情况搞得更糟的坑。
事后复盘的时候,我整理了一份恢复操作清单,打印出来贴在工位上。清单上写清楚了每一步的命令、参数、注意事项,还有常见报错的应对方案。这次的经验告诉我,数据库恢复这种事,平时看着没用,真出事的时候,就是救命稻草。而且,恢复脚本和操作手册,一定要定期演练,不能光写在文档里吃灰。
说个细节,恢复完成之后,我没有马上睡回笼觉。我盯着监控看了一个多小时,确认新库运行稳定,没有任何隐藏的报错,才放心去补觉。数据库恢复这件事,不是把数据找回来就结束了,后续的稳定性观察同样重要。经历过这一次,我最大的感悟是:数据备份的意义,不在于备份本身,而在于恢复的时候,你能不能在最短时间内,把损失降到最低。


