MySQL InnoDB数据损坏崩溃修复实战:从磁盘满到恢复重建
凌晨两点十五分手机上的监控告警像催命符一样响个不停。我睡眼惺忪地打开跳板机发现生产MySQL的连接数已经冲到几千然后下一秒进程直接退出了。一开始我以为是连接风暴重启了两次结果每次刚起来几十秒就又挂掉。翻看error log的时候看到一排排InnoDB页损坏的字样心跳直接漏了一拍——这种MySQL InnoDB数据损坏崩溃修复的case平时只在文档和培训里见过真落到自己头上压力完全是另外一个量级。这篇复盘会完整还原当时的排查思路、用innodb_force_recovery冒险恢复数据的过程、以及最终重建实例的细节希望能给同样负责数据库运行的人一些具体可操作的经验。1. 故障初现从业务侧“数据库无响应”到实例彻底宕机1.1 崩溃前几个小时我忽略过的一个信号下午五点左右其实我已经瞄过一次监控面板当时数据盘使用率已经到85%。那时我下意识觉得“还早”毕竟平时这个集群的磁盘增长并不算快而且还有定时清理任务。真正引发问题的是一批夜间批处理任务它们集中在凌晨开始跑本身就有一组重度生成临时表的操作会让InnoDB表空间自动扩展。到了凌晨一点半批处理任务开始大量写入临时表和日志表。数据盘使用率那会儿应该已经逼近100%只是我设置的监控页面没有单独对这种“磁盘空间剩余量”做告警只有CPU、内存和QPS的告警。这就是第一个被漏掉的信号。凌晨两点出头业务方开始在群里反馈“查询卡死”“登录失败”我还没有意识到这是磁盘问题看到MySQL的活跃连接数异常第一反应是“会不会是慢查询拖垮了”尝试进入实例执行show processlist会话直接卡在“waiting for table metadata lock”上根本拿不到结果。几分钟后监控显示mysqld进程不存在数据库彻底宕了。1.2 第一次启动失败错误日志里出现了“page corruption”我当时的操作顺序是先试着正常启动服务看能不能救起来。执行systemctl start mysql之后几十秒内进程就退出了。再看error log里面有大量InnoDB的表空间错误。关键的几行大概是这样的[ERROR] InnoDB: Database page corruption on disk or a failed file read of page [page id: space23, page number45] [ERROR] InnoDB: Page [page id: space23, page number45] in tablespace 23 is corrupted [ERROR] InnoDB: Operating system error number 28 in a file operation [ERROR] InnoDB: Error number 28 means No space left on device看到“No space left on device”的时候我头皮发麻。这意味着根因很可能不是MySQL本身的逻辑bug而是底层文件系统没有空间了导致InnoDB无论是扩展表空间还是写redo日志都出现了失败。更麻烦的是因为进程异常退出InnoDB启动时需要做崩溃恢复但部分redo日志可能没来得及完整落盘最终触发了页级别的校验失败。So错误日志才表现出“page corruption”的样子。第一次启动尝试失败之后我没有立刻反复重启。这是非常重要的一点一个已经在损坏边缘的InnoDB实例连续重启只会让它暴露更多问题甚至让原本可恢复的数据更难捞出来。我当时脑子里想的已经不是“怎么快点让库恢复正常”而是“怎么安全地把数据捞出来”。2. 先理解InnoDB为什么会“碎页”再谈怎么修2.1 页、LSN与校验和InnoDB的数据是按页存储的默认每个页大小是16KB。每一页都有自己的校验和checksum这个校验和是基于页内容计算出来的。正常的读写流程里MySQL从磁盘把页读入内存时会重新计算一次校验和与页头存储的默认值比对如果对不上就说明这页数据有问题。崩溃恢复的核心依赖是两个东西redo log和double write。redo log记录了物理页的修改数据库启动时会根据redo log做前滚double write则是为了解决“页写了一半”这种partial write问题——如果16KB的页写到一半断电redo log里可能只有修改的记录而没有完整的页这时double write是用来保存完整页副本的。听起来这套机制已经很完善了但为什么还是会出现“page corruption”关键在于这些保护机制都是在文件系统还健康的前提下才有效。当磁盘空间满了或者底层的I/O错误已经导致文件块写不完整那么redo和double write本身也可能处于不一致状态。这个时候InnoDB在启动时一读到那些损坏的页就直接抛出corruption错误压根走不到完整恢复流程。2.2 本次事故真凶文件系统没有可用空间我们从监控曲线和系统日志里很快确认了根因链条数据盘使用率从85%一路冲上99.9%。InnoDB的共享表空间ibdata1、独立表空间的扩展操作失败。redo log文件写完一个准备切换下一个时因为磁盘空间不足出现了写失败。mysqld收到I/O错误后无法优雅退出进程被强制“摔死”。重启后InnoDB需要从redo里恢复最后一段事务但redo的写入并不完整于是某些数据页在校验时失败进入corruption状态。这其实是一个“根因很朴素但破坏力极大”的故障链。在磁盘满的情况下任何“强制重启”“kill -9”都会加剧数据损坏的风险。最直接的教训是磁盘水位监控一定要单独配置而且告警阈值不能只依赖默认的“80%”之类。2.3 常见的其他诱因除了磁盘满InnoDB页损坏的常见诱因还有这么几类做了个表方便对照诱因典型症状规避方式突然断电页写一半redo缺失启动时报page corruption部署UPS数据库机器不要裸奔强制kill进程崩溃前没有完成checkpointredo断裂不要kill -9优先正常关闭磁盘坏道/硬件I/O故障读页时出现校验错误或直接读失败RAID冗余定期SMART磁盘检测内存不稳定页在内存中已经损坏写回磁盘后校验失败使用ECC内存做内存压测文件系统满扩展失败redo写入失败监控剩余空间设置紧急清理任务我们这次属于第一种和第五种的结合体说到底是基础设施监控不到位加上没有快速熔断机制。知道“为什么坏”之后修复才有方向。3. 恢复全过程一个DBA的冒险但清醒的夜晚3.1 第一步不是修复而是给现场“封存”很多人遇到数据库崩溃第一反应是“赶紧启动起来”但我建议先停下来做现场封存。当时我做的第一件事是把整个MySQL数据目录复制到另一块独立的、磁盘空间充足的硬盘上。命令大致是systemctl stop mysql mkdir -p /data/mysql_crash_backup cp -a /var/lib/mysql /data/mysql_crash_backup/这一步是纯物理复制相当于把“犯罪现场”完整保留下来。后面无论怎么折腾原目录至少还有一个最原始的副本可以回去。如果当时没有做这一步后面因为反复测试不同force_recovery级别而对数据目录产生二次写操作基本就是灾难。还有一个小细节复制完成后我在备份目录外挂了一个硬只读确保后续不会因为脚本误操作改写备份内容。实际操作中有时候会因为磁盘空间不足备份写到一半失败所以一定要先确认备份目录空间够大再开始拷。3.2 从innodb_force_recovery1开始逐级试探InnoDB提供了一个特殊启动参数innodb_force_recovery。这个参数可以强制MySQL跳过一部分崩溃恢复逻辑让实例以“尽量能启动”的优先方式起来。常见的级别是1到6级别行为风险1忽略损坏的页继续启动查询可能报错2阻止主线程运行不清理purge等操作只读事务无法完整回滚3不执行回滚段里的回滚操作只读数据更接近启动前的状态4不加载redo日志可能丢失最近事务5忽略undo log回滚信息丢失6不做前滚不做恢复检查基本是“破罐破摔”式修复我的策略是从最小的级别开始一点一点往上升能启动到什么程度就停在哪一级。改了配置后执行[mysqld] innodb_force_recovery 1第一次启动还是失败。错误日志里出现了关于undo回滚失败的信息说明实例能加载的页已经受损但回滚操作又卡住了。然后我把参数改成2启动成功。但是执行show tables还能过一旦对部分表做select count(*)特定表直接报错“表损坏”。接着试了3这次启动后基本稳定。这时的MySQL处于只读状态很多写入类操作会被拒绝。我知道级别越高说明它跳过的恢复逻辑越多潜在的数据丢失风险也越大所以到了3就决定不再往上加。force_recovery本身就是一种“明知要丢点数据也要先拿到能拿到的数据”的求生手段。如果直接上4或6可能整个库的表都打不开数据彻底没了那就失去了抢救意义。3.3 只读模式下抢救数据能导出的先导出实例能起来后我立刻开始做逻辑导出。因为此时服务是只读的业务流量已经被切走所以可以清空对外的访问专心做数据抢救。为降低互相影响我新开了一个MySQL实例用来接收数据。导出命令大概是mysqldump -uroot -p --all-databases --single-transaction --skip-lock-tables --no-tablespaces /backup/full_backup_$(date %F).sql注意在force_recovery模式下MySQL的很多操作都会受限--single-transaction不一定能拿到一致性快照但我当时的重点是“尽量把能读出来的数据都读出来”而不是追求事务一致性。备份过程中绝大多数表的导出是正常的最后有两个表直接报错一张是“会话日志表”业务上可以容忍丢失问题不大。另一张是订单相关的核心表部分分区的页完全读不出来。日志表我直接跳过核心表则需要另想办法。我当时没有在原库上抱着侥幸心理反复重试查询因为每触发一次损坏页扫描都可能让实例更不稳定。我把损坏的核心表记录在案准备用从库来补。3.4 核心表损坏部分的补救从库导出binlog追平这里要说一下我们的架构坏实例有一个主从架构的对等从库数据延迟大概在两到三分钟。由于从库平时一直在跑它那边的数据相对是完整的。于是我用从库导出了那张核心订单表的全量数据mysqldump -uroot -p --single-transaction 核心库 订单表 /backup/core_table.sql然后再结合主库导出的其他表数据和从库导出的核心表数据一起导入到新实例里。导入完成以后还有一个缺口从库导出那一刻到主库崩溃之前可能还有两分钟到五分钟的数据在主库里。这些数据还有没有救呢我们把主库的binlog文件搬到一台临时实例上用mysqlbinlog解析出崩溃前最后这段时间的事务再应用到新实例。mysqlbinlog --start-datetime2024-xx-xx 01:50:00 --stop-datetime2024-xx-xx 02:15:00 /backup/binlog.000012 /backup/last_transactions.sql应用binlog的时候要注意主键冲突。我是先把binlog导入到一个临时库再通过表名过滤只对缺失的部分执行replace into避免重复数据。整个过程非常琐碎但好在最终把核心表的数据追到了接近崩溃前的最后一个事务。3.5 重建实例与业务切换数据导入完成后我新建了一个独立MySQL实例配置调成正常模式innodb_force_recovery参数已经移除。然后做了一系列校验执行check table 核心表返回OK。抽样比对关键表的总行数与业务后台统计报表是否一致。跑了一遍应用侧的核心接口冒烟测试。查看binlog位点确认数据追到的位置。确认没问题后将应用连接串切到新实例数据库恢复对外服务。原损坏的数据目录和备份目录全程没有删除而是归档保留了一个月用来防止后面发现数据还有缺口时再回去查。新实例上我还额外加了一份定时物理备份避免再出现“裸奔到崩溃”的尴尬。4. 复盘总结怎么避免下一次同样的事故4.1 磁盘问题是元凶监控盲区是帮凶这次事故复盘下来最核心的问题是磁盘空间监控失效。原来监控面板上确实有磁盘使用率指标但阈值设置得太粗而且没有针对突发增长做趋势预测。我后来把相关的告警规则改成了双阈值使用率超过85%立刻告警超过90%触发自动排查超过95%直接通知on-call并触发紧急清理任务。同时增加了对InnoDB write失败、redo日志增长等关键指标的监控。另外一个教训是业务侧的批量任务最好在数据库负载低的窗口执行并且要评估它们对磁盘空间的消耗。临时表大量产生时磁盘增长可能比平时快很多不能用常态思维来预估。4.2 备份与高可用架构要配得上业务的重要性这次能从事故里恢复运气成分占了很大比重。如果没有一个数据相对完整的从库核心表很可能就只能丢数据了。我更倾向于把“运气”转变成“预案”备份存储必须和生产数据存储隔离最好跨机架或跨机房。备份磁盘被同一个数据盘写满这种事绝对要避免。定期做恢复演练不能只看备份文件存在就以为一定能恢复。至少每个季度抽一套备份完整拉起一个临时实例。高可用层面尽量把复制模式从异步改成半同步或者直接上组复制缩短从库延迟。如果这次从库延迟超过30分钟后续恢复成本会高很多。binlog要保留足够长的时间并且定期备份到异地。binlog是最后一道兜底有了它才能做时间点恢复。4.3 处理InnoDB损坏事故的“正确姿势”清单如果非要给这套处理流程做个提炼我会写成下面这些步骤贴在工位上发现数据库起不来先别反复重启立刻检查磁盘和系统日志。第一时间复制原始数据目录到另一块独立磁盘现场封存。从innodb_force_recovery1开始尝试启动能起来就停在最低级别。在只读状态下优先用mysqldump导出数据记录哪些表损坏。核心损坏表从从库、备份或binlog中找补能补多少是多少。新实例导入数据后务必做check table和业务冒烟测试。保留旧的损坏目录至少一个月不要急着删。4.4 这次经历沉淀下来的一个小细节最后分享一个我个人的小习惯在重建新实例时我把原来的二进制日志、慢查询日志和error log也一起做了归档而不是只备份数据库文件。后面核对时间线、确认数据恢复到哪个位置全靠这些日志。事故复盘时老板问“到底丢了多少数据”我也可以直接拿着binlog的位点和业务统计报告对给他看而不是凭感觉说“大概丢了几分钟”。经历过这一场“MySQL InnoDB数据损坏崩溃修复”之后我最大的感触是数据库损坏并不是多么罕见的事真正危险的是没有准备。把监控补上、备份做扎实、恢复流程写清楚哪怕下次再碰上凌晨的告警心里也会从“惊悚”变成“熟练工”。