fio误写数据卷打崩主备库?信创数据库恢复全复盘

📅 发布时间:2026/9/9 3:58:30
fio误写数据卷打崩主备库?信创数据库恢复全复盘 那台信创服务器上跑着国产数据库主备集群出事那天下午本来只是一个性能摸底测试。负责基础设施的同事想验证新到的一批SSD盘在真实业务负载下的写入能力就在数据库数据目录所在的逻辑卷上执行了一条fio命令。谁都没想到一条“常规”的磁盘压测命令最后把主备库一起打崩还把正在运行的备份任务也牵连了进去。等我和同事把整个故障处理完已经是第二天凌晨。这篇文章把这起fio破坏主备库、连带引发备份故障的完整过程写出来包括现场现象、排查路径、恢复步骤和根因复盘希望能帮到在信创环境下做数据库运维的朋友们避免重蹈覆辙。1. 事故发生前的一个小时那条fio命令是这样写出来的1.1 信创环境与性能摸底任务事情发生在一个典型的信创项目交付现场。服务器用的国产CPU平台操作系统是某款基于Linux内核的国产发行版数据库采用的是支持主备部署模式的国产关系型数据库。整体架构非常常见一台双路服务器本地挂了两块NVMe SSD做成了LVM逻辑卷数据卷/dev/mapper/vgdata-lvdb挂载在/data目录下数据库的主库实例和备库实例都跑在这台机器上主备实例的数据文件、控制文件、联机日志都在/data这个卷里。这种同机主备部署方式在中小规模的信创环境中并不少见。当时的背景是项目进入性能验收阶段第三方评测机构要求提供存储设备的随机写IOPS数据。基础设施组的同事以前在物理机上压测都是直接对裸盘跑fio觉得这是常规操作没有把“存储压测”和“数据库数据文件”这两件事在逻辑上分开。于是他从工具库复制了一个经典的fio压测命令登录服务器后打算直接对数据卷做压测。1.2 事故命令的逐步展开他执行的命令大概长这样fio --nameiodepth_test --ioenginelibaio --rwrandwrite --bs4k --direct1 --iodepth32 --numjobs1 --size100% --filename/dev/mapper/vgdata-lvdb这条命令的关键问题在于--filename直接指定了数据库所在逻辑卷的设备文件而不是一个普通测试文件。在fio的逻辑里--size100%表示对整个目标设备做全盘写--rwrandwrite表示随机写--direct1绕过文件系统缓存直接写裸设备。开始执行后fio会从这个逻辑卷的第一个块开始随机寻址并写入测试数据整个过程完全绕过文件系统语义。更要命的是fio是用root权限启动的。root对设备文件有完全访问权限不会出现“设备正忙”或“权限拒绝”的提示。数据库虽然也在打开这些数据文件但数据库并不会以锁设备文件的方式阻止另一个进程写同一个底层设备。两者之间没有任何一层防护机制fio就这样大摇大摆地开始覆盖数据库文件的物理存储区域。压测开始后大约几十秒数据库日志里开始出现大量的文件头校验错误和块读异常。我当时不在现场是数据库告警短信把我叫醒的。等我远程连上去的时候fio进程还在跑日志里已经刷了几万条I/O错误。1.3 数据库文件从什么时候开始受伤从数据库的角度看一个数据文件是由固定大小的数据块组成的文件头部保存着该文件的标识、检查点信息、日志序号等关键元数据。文件系统通过逻辑卷映射最终落到物理设备的不同偏移上。fio对这些偏移的随机覆盖相当于把数据库文件从物理层面“挖掉”了无数块。最先受损的是控制文件和数据文件头部。数据库后台进程在读取控制文件时发现内容已经变成了一堆随机字节就会认为控制文件损坏读取数据文件块时发现块内容不是合法的数据块格式就会报块损坏错误。最麻烦的是联机日志也在这个卷上联机日志被覆盖意味着崩溃恢复所需的事务重放信息也丢失了。事实上fio运行到约10%进度时数据库主库实例就已经处于半瘫痪状态后台写进程不断重试写入失败的块备库实例因为与应用进程无关反应稍微慢一些但随着数据卷损坏范围的扩大备库的数据库后台进程也陆续报错。到这一步主备库基本已经“同归于尽”。2. 主备库同时失联现场排查记录2.1 主库进程异常与日志线索那天晚上我远程连上服务器第一件事是看数据库实例状态。主库实例还挂着但会话基本卡死新连接无法建立。进入数据库日志目录看到的是大量重复刷屏的错误[ERROR] datafile /data/mydb/user_data_01.dbf: block 438272 read failed: Invalid argument [ERROR] datafile /data/mydb/user_data_01.dbf, block 438272: media recovery failed [ERROR] controlfile /data/mydb/control_01.ctl: read failed: Input/output error [ERROR] redo log file /data/mydb/redo_log_01.log: contents corrupted第一眼看到这些报错我的直觉是磁盘坏了但dmesg里没有发现SMART报错或NVMe控制器异常也没有出现task hung这类内核阻塞信息。这就很蹊跷底层存储硬件本身没问题但数据库读到的文件内容已经不对了。进一步查数据库的告警日志时间线发现第一个异常错误是数据文件块读取失败之后控制文件报错再之后联机日志报错。这个顺序说明在文件系统层面文件还能打开但实际读出来的内容已经被污染。这几乎可以断定是“有外部进程在物理层覆盖数据”而不是文件系统自身故障。2.2 备库同步中断与损坏确认查看备库实例状态时情况也很不乐观。主备同步进程早就断开备库日志里大量堆积“备库无法应用日志”“apply process aborted”之类的错误。因为备库的数据文件同样在/data卷上备库实例读取到的数据块也已经被覆盖即使把主库的联机日志完整同步过来备库也无法应用——日志内容对应的数据块底层数据已经不对了。我在备库上做了一次数据文件校验用数据库自带的全库校验命令扫描每个文件块结果异常块数量达到数千。主库和备库的数据文件都存在大面积块损坏常规的数据库恢复手段比如基于redo日志的前滚、基于undo的回滚已经无法处理这种物理层破坏。这里有一个很重要的判断经验如果只是数据库层面的控制文件不一致或日志断档通常还可以通过归档日志做恢复但如果是数据文件物理块内容被覆盖且覆盖范围非常大那么任何依赖原数据的恢复步骤都会失败。现场的关键动作就是尽快判断“损坏范围到底有多大”。2.3 把fio进程和数据库错误对上号登录服务器后我用ps命令查看进程列表一眼就看到还在运行的fio进程ps -ef | grep fio输出显示fio进程刚执行了不到20分钟CPU占用很高还在继续写入。再执行lsof -p fio进程号 | grep /dev/mapper确认它打开了/dev/mapper/vgdata-lvdb这个设备文件。到这一步事故原因已经非常清楚fio对数据库数据卷做了全盘随机写覆盖。我立刻把fio进程停掉。这里要提醒一点停fio本身不会让情况变得更糟但停止后不要马上对数据卷做任何写操作包括不要尝试初始化、不要重建逻辑卷、不要跑文件系统修复。后续所有恢复动作都要基于这个只读原则否则会把残存的可用数据再次覆盖。3. 备份也失败了压死骆驼的第二根稻草3.1 备份任务报错现象处理完主备库“停摆”这个表面问题后我习惯性地去检查一下最近的备份记录。结果发现当天晚上的全量备份任务在凌晨启动后不久就失败了备份日志停留在数据文件校验阶段[BACKUP ERROR] validate block failed on file /data/mydb/user_data_02.dbf, block 1280 [BACKUP ERROR] checksum mismatch for file /data/mydb/user_data_02.dbf [BACKUP ERROR] backup aborted due to critical file errors备份任务的时间点很尴尬它在fio压测开始之后才启动。也就是说备份进程读取数据文件时文件的一部分块已经被fio覆盖了备份出来的文件在块级别就是不完整的。备份工具自身做了块校验发现问题后主动终止了备份任务。本来这也不算致命因为备份失败最多是“没有新的可用备份”如果有历史全备和完整归档还是能恢复。但我继续往下查的时候发现历史全量备份文件存储在同一台服务器的另一个目录/backup下而这个目录竟然也挂在同一个逻辑卷vgdata-lvdb上。fio的随机写覆盖范围遍及整个逻辑卷备份目录里的历史备份文件同样没能幸免。3.2 备份文件为什么同样不可信我尝试对历史全量备份文件做一次内容校验结果备份文件的头部记录信息正常但读取到部分数据段时出现了校验和错误。原因很简单备份文件本身在写入后被fio覆盖了一部分它在文件系统层面仍然存在目录项也还能看到但物理内容已经不是当初的备份数据了。这时候我才真正意识到问题的严重性全量备份文件损坏、增量备份文件损坏、归档日志中也有部分文件被覆盖数据库实例层面主备同时无法正常启动。按照常规的“全备归档”恢复路径等于全部堵死。这个状态如果处理不好就是完全丢失数据的事故。3.3 盘点剩下的可用资产冷静下来后我列了一张资产盘点表把当前所有可能有用的数据来源都过了一遍数据来源状态可用性评估主库数据文件大面积块损坏不可直接使用需判断是否仍有部分完整数据文件可用备库数据文件同样大面积块损坏不可直接使用但损坏块分布可能不同历史全量备份位于被覆盖逻辑卷上已校验确认损坏当天归档日志部分被覆盖不完整无法作为增量来源远端归档副本备份脚本每天向异机目录推送一份归档当天数据已推送但尚未覆盖全部损坏范围相对完整是主要可用资产之一文件系统快照逻辑卷上开启了LVM快照快照创建于压测前约6小时高价值快照里保存了压测前的数据文件布局最后一行是最关键的救命稻草。LVM快照是小时级之前创建的保存了数据文件被覆盖前的物理内容。虽然快照数据会随着源卷的每次写入而保留旧版本但如果快照空间耗尽或源卷写入量过大快照会失效。我看到这台服务器的快照状态仍然是active说明它还没有被fio的大量写入撑到失效这是整场事故中最有希望恢复全量数据的资产。4. 恢复全程复盘从备库把主库捞回来4.1 先止血停业务、留现场、隔离损坏源确认无法依赖现有备份后我首先做了两件事一是通知业务方停止所有写入请求二是把数据库主备实例全部调整为离线状态防止任何后台进程继续对数据卷做写入。第三步是在LVM层面确认快照状态并把快照挂载为只读文件系统导出一份块级副本用于后续分析。lvdisplay /dev/mapper/vgdata-lvdb lvchange --refresh vgdata mkdir /mnt/snapshot_restore mount -o ro /dev/vgdata/snapshot_backup /mnt/snapshot_restore挂载成功后我先对比了快照里数据文件的文件大小、修改时间和源卷上对应文件的差异。快照中的数据文件是一个完整的、未被覆盖前的版本。虽然快照时间点距离故障发生有几个小时会丢失这段时间内的事务增量但至少能把数据库恢复到压测前的某个一致点之后再用远端异机同步的归档日志往前推进。在这个阶段另一个收获是备库数据文件虽然部分损坏但有个别数据文件在fio覆盖范围内的偏移恰好没有被命中文件块校验仍然通过。这些文件可以作为后续拼凑数据块时的参考但我不建议直接混合使用因为同一数据文件内部的不同块可能来自不同的时间点强行拼接会导致数据一致性问题。4.2 确定权威恢复源与恢复路径我最终确定的恢复路径是以LVM快照中的完整数据库文件集为基础恢复源以远端异机归档日志为增量来源把数据库恢复到故障前最近一个可确定的时间点。具体步骤分为四步第一步把快照中的数据文件目录整体拷贝到一个新的、与故障卷隔离的存储位置。不能直接原地使用快照挂载点作为数据库的数据目录因为后续数据库打开文件后会立即写入快照空间可能被耗尽。第二步在拷贝出来的文件集基础上启动数据库进入“离线恢复”模式。数据库会检查控制文件、数据文件头的日志序号和检查点信息这时需要先确认这些元数据是否一致。第三步应用远端异机归档日志。幸好备份脚本一直有异机推送归档的习惯虽然推送频率是每小时一次但故障发生时最近一次推送已经完成所以可以把数据库前滚到最近一次推送点满足业务对“尽量不丢数据”的诉求。第四步由于主库和备库原本是同一个数据卷快照里的数据文件对主备两个实例都适用。我先用这份文件集恢复了主库实例再重新初始化了备库实例重新搭建主备同步链路。4.3 重建主备关系并做一致性校验数据库文件恢复完成后我执行了主备初始化脚本重新搭建同步关系。这里有一个容易被忽略的坑如果备库的实例配置里还指向原有的数据文件路径重新初始化时一定要先把损坏的数据文件移动到备份目录再用主库的当前数据文件做基础覆盖否则备库会再次读取到损坏内容。主备同步恢复后我做了两轮校验。第一轮是数据库内部的一致性校验包括所有数据文件的块校验、控制文件和数据字典的一致性、主备两端的日志序号对齐状态。第二轮是业务层面的数据核对抽样了最近三个小时的关键业务表确认主备两端的记录条数和关键字段一致。最终从数据完整性角度业务数据恢复到了故障前最近一次异机归档同步点丢失了大约几个小时到半小时之间的部分增量事务。这部分数据通过业务系统自己的消息记录做了人工补录算是把损失降到了最低。5. 逃不掉的根因fio、数据库、备份三方如何互相踩踏5.1 fio写设备文件时发生了什么先说fio本身。fio是一个存储性能测试工具它的设计目标就是在指定设备或文件上模拟各种I/O模式。当--filename指向一个块设备时fio压根不关心这个设备上是否已有文件系统也不关心里面装的是什么数据它只按照--rw、--bs、--size等参数生成写请求。问题出在用户对工具的“语义边界”理解上。很多人把fio默认当成“会创建一个测试文件”的工具但实际上只要指定了--filename为已有路径fio就会直接覆盖该路径。如果这个路径是/dev/mapper/xxx这样的设备节点fio从设备起始偏移开始写入不会询问“设备上已有文件系统是否继续”。在这种情况下数据库对底层存储的所有读取都会得到fio生成的随机数据。比如数据库读某数据文件的第1000个块物理上会寻址到逻辑卷对应的某个偏移这个偏移已经被fio写入了随机内容读回来的块在数据库看来就是“块头非法”“块号不对”“SCN乱序”于是报出各种读失败和损坏错误。5.2 主备同步机制为什么没挡住脏写很多人的第一反应是主备同步不是应该能够提供高可用保护吗确实正常的主备同步可以应对实例崩溃、操作系统故障这类场景但它无法应对“底层数据被外部工具物理覆盖”这种极端情况。主备同步的本质是把主库的redo日志内容传输并应用到备库备库的恢复进程依赖的是“备库底层数据文件是主库数据文件的一个副本”这个前提。当底层数据卷同时被覆盖后备库数据文件的内容已经和主库的数据文件一样被污染了。备库的apply进程在工作时会发现要修改的块内容不对一致性校验失败最终导致同步中断。换句话说主备架构只能防“逻辑故障”不能防“物理层覆盖”尤其是在共享存储/共享卷的同机部署模式下一个卷被破坏主备一起遭殃。5.3 备份工具在物理损坏面前为何如此脆弱备份工具大多数采取“逻辑备份”或“物理备份”的方式读取数据库文件。无论是哪一种备份进程在读取数据文件时都要对文件块做校验。如果文件块已经被覆盖就会报告校验失败。但真正导致本次备份故障雪上加霜的是备份文件竟然存放在同一个被覆盖的逻辑卷上。这是运维规划上的硬伤它把一个“备份文件可用”的故障升级成了“备份完全不可用”的灾难。备份的可信性原则就是“必须和源数据分离”异地、异机、至少也要是独立文件系统。一旦把备份文件和源文件放在同一个物理卷/逻辑卷上卷损坏时备份也会跟着报销这在任何数据库环境里都是大忌。回顾整个链条它其实是一个叠加事故第一层是fio直接写了数据卷第二层是主备库共用此卷高可用失效第三层是备份文件与源数据同卷备份失效。每一层单独看都有机会止损但叠加在一起恢复难度就指数级上升。6. 压测之前必看以后我按这套规范操作6.1 fio压测的路径和参数红线经过这次事故我给团队定了几条硬性规定任何人在任何环境下跑fio之前都必须过一遍绝对禁止对生产数据库所在逻辑卷或裸设备执行带写操作的fio压测。如果只是想测试存储性能应该使用独立的测试卷或挂载一块不承载业务数据的新盘。如果确实需要在有文件系统的挂载点上测试必须在挂载点下新建专门的测试目录并确保测试文件名字与业务文件没有任何冲突。fio命令必须显式指定--size和--runtime不允许使用--size100%对设备做全盘写。加入--time_based --runtime30这类限时参数可以避免一次压测无限制跑下去。压测前用findmnt和lsblk确认目标路径对应的物理设备再用lsof确认该设备/目录下是否有数据库、文件服务等关键进程在使用。一律用非root用户执行fio避免误操作绕过权限限制。如果工具必须使用raw设备接口应该通过专人复核后在计划维护窗口执行。fio压测结束后检查测试目录里生成的测试文件大小是否正确及时清理避免测试文件残留占用空间。我建议把这条清单固化到团队的运维操作规范里而不是依赖个人经验。人在疲劳、赶进度的时候真的会随手执行命令规范的唯一作用就是让“随手”变得不可能。6.2 数据库备份与恢复演练的底线要求这次备份故障给我的教训还有一个备份不是“跑了就行”而是“恢复了才算”。一条可行的备份体系至少要满足三件事第一备份文件必须存放到与源数据物理隔离的存储位置可以是异机存储、对象存储、磁带库或其他独立卷绝对禁止和源数据放在同一个逻辑卷上。第二备份脚本必须包含备份完成后的可读性校验比如对备份集做md5校验、对备份生成的文件做随机采样恢复测试确保备份集不是“坏了的空壳”。第三每个季度至少做一次真实环境的恢复演练从备份集完整恢复出一个可用于查询的数据库实例确认备份链路在关键时刻真的能顶上去。在信创环境下这套底线并不因为软硬件栈变化而改变。国产操作系统、国产数据库在具体命令上可能有差异但备份隔离、校验、演练这些原则是通用的。一定要确认好你所用的国产数据库的备份工具支持哪些校验方式不能简单默认“导出成功就等于备份成功”。6.3 我的几条实战习惯最后分享几条我在这次事故之后养成的实战习惯不一定都是高深技术但关键时刻能救命。第一动生产环境之前我一定先看一眼这个目录或卷里到底有什么。哪怕只是执行一个lsblk、findmnt、lsof成本极低但能把“破坏范围”提前暴露出来。第二跑压测之前先手动创建一个测试文件确认写入路径不会串到业务目录再开始正式压测。第三恢复场景里永远先保住“现场”不要急着去修复、去启动、去重新初始化先备份现场元数据和日志否则一旦误操作后续可以做分析的材料就没了。这次事故最终在第二天凌晨处理完毕业务恢复数据损失控制在极小范围内。回想整个过程最让人后怕的不是fio有多强大而是我们离“完全不可恢复”的距离其实非常近如果LVM快照晚几个小时创建、如果备份脚本没有异机归档结果就会完全不同。希望这篇文章能把我们踩过的这个坑讲透让读到的人绕开它。