1. 故障复盘:一场fio压测怎么让国产信创库主备和备份同时出事
我在处理国产信创库故障时,见过不少自作主张的“存储压测”,但这次还是让我印象最深:一套跑在信创服务器上的国产数据库主备集群,数据库主备通过日志同步,数据盘和归档盘共用同一套文件系统。结果有同事为了验证存储IO能力,直接对数据盘对应的块设备跑fio随机写,主库最先崩溃,触发failover切到备库后,备库日志回放也跟着失败。等我想从当天的物理备份做恢复,备份任务居然也处于失败状态,备份目录里还躺着一堆fio生成的测试文件。
如果你也是DBA、系统运维或信创数据库迁移负责人,这篇复盘可以直接当处理手册用。我先说结论:fio本身不是病毒,但它默认是个“会老老实实按你参数去写盘”的工具。只要参数里出现randwrite、write、trim这类破坏性动作,而又把filename指向了数据库数据块设备或数据目录,那它就可以瞬间把数据库文件、redo日志、归档日志甚至文件系统元数据全部冲掉。最麻烦的是它不像普通误删文件那样还有回收站,很多块被覆盖之后,唯一可靠的恢复路径就是物理备份加归档日志做时间点恢复,所以备份能否用、备份放在哪里,往往决定了这个事故要丢多少数据。
这套环境的具体配置是什么呢?主机是ARM架构的国产服务器,操作系统用的麒麟,数据库是一套基于PostgreSQL内核的国产信创库,下面统一叫X库。主备模式采用物理流复制,主库数据目录在 /data/xdb,备库数据目录在另一台机器相同路径上,归档日志和备份暂存目录也放在 /data 下的独立目录。所有节点服务器的磁盘基本都是本地NVMe盘,数据目录挂载在独立的文件系统上。这就意味着,fio只要写到数据盘对应的底层设备,主备两块盘都会被冲,情况比Oracle单机加归档还棘手,因为Oracle的故障处理经验在PG内核数据库上很多命令并不能直接套用。下面先用时间线还原现场,看完你就能理解为什么主备库和备份会“全军覆没”。
1.1 事故时间线:从一条压测命令到全套备份失效
| 时间点 | 发生了什么 | 现场表现 |
|---|---|---|
| 14:00 | 运维在数据盘块设备上启动fio随机写压测 | 存储IO延迟迅速飙升,fio命令行包含 ioengine=libaio、direct=1、rw=randwrite |
| 14:02 | 主库写入redo日志开始超时 | 应用连接大量报错,数据库日志出现“could not write to log”类错误 |
| 14:05 | 高可用组件判断主库故障,触发failover | 主库被标记为故障,虚拟IP漂移到备库,业务短时闪断 |
| 14:10 | 备库开始加载主库日志并回放 | 备库日志回放遇到WAL CRC错误或页面校验失败,实例处于恢复中断状态 |
| 14:30 | 值班人员准备登录检查 | 发现主库无法正常启动,备库处于recovery失败状态,数据库服务整体不可用 |
| 15:00 | 定时物理备份任务启动 | 备份进程在读取数据文件时遇到坏页,或备份目标路径因磁盘空间满而写入失败,备份任务退出 |
从压测开始到备份彻底失败,前后不过一个小时。这里有一个很关键的判断点:如果备库是独立的两块本地盘,fio只误伤了主库的盘,那么备库大概率还能承担业务;但这次主备数据都在同一个磁盘组/同一台存储阵列的不同分区上,fio误把整个控制器设备或卷组当作目标,备库底层同样被冲掉。还有另一种常见情况是fio没有直接写块设备,只是在 /data 目录下创建了一个几十GB的测试文件,但压测过程把文件系统空间占满,主库后续的WAL段写不进去,备库同步也就断了,备份任务则因为目标目录写不进去而失败。后面这几类情况我在第五部分会单独讲隔离规则,先把最严重的恢复流程说清楚。
1.2 fio的破坏路径:为什么会同时影响主库、备库和备份
fio是一个著名的IO压力测试工具,本身是用来评估磁盘性能的,常见用法是在指定路径上创建测试文件,然后按顺序读写或随机读写,测试完删除文件。大部分人在测试环境玩过,在 /tmp 下建个文件跑randwrite,没出过问题。但生产环境就完全不同了:如果filename参数不是文件系统里的普通文件,而是 /dev/sdb 或 /dev/mapper/vg_data-lv_xdb 这类块设备,并且指定了 direct=1,fio就会绕过文件系统的page cache,直接在块设备上以块粒度写随机数据。这时它写的已经不是“某一个文件”,而是整块盘的物理扇区。数据库文件、文件系统元数据、日志、空闲块统统会面临覆盖风险,和你手动执行 dd if=/dev/urandom of=/dev/sdb 本质是一样的。
破坏主备库的原理不难理解。数据库主库在运行过程中会产生WAL/redo日志,日志文件和数据文件都存在同一个文件系统里。fio随机写覆盖到数据文件的page,轻则让数据库在读到该页时报“invalid page header”,重则让控制文件、提交日志、系统表空间直接损坏。主库crash后,高可用组件会把ip漂移到备库。备库正常工作的前提是能连续接收并回放主库产生的日志,可一旦fio写盘的这几十秒内主库日志本身已经被写坏,备库收到的WAL段就有CRC错误,回放立刻中断。所以,不是备库本身“不行”,而是它的输入源已经被污染了。备份失败就更好解释:物理备份工具读取数据文件时,如果读到的page头部信息异常或checksum校验失败,备份进程通常会直接报错退出;就算备份工具勉强把文件拷贝走了,这个备份集也是一个带病备份,后续恢复时大概率还会二次爆炸。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 紧急处置:停fio、留现场、评估主备与备份受损面
碰到这类故障,最忌讳的是脑子一热就重启。数据库崩了,你重启它,等它启动到某个坏页位置又崩一遍,日志越打越多,存储负载也下不来。正确做法是先把还在跑的fio进程干掉,记录现场,再去做检查。不要先跑fsck,不要马上umount,也不要立刻用另一个空的文件系统覆盖原盘。只要当前文件系统还没有被修复工具改动过,后续还有通过底层取证辅助恢复的机会。
2.1 先停fio并固定证据
如果fio还在跑,第一时间执行:
bash复制pkill -9 fio
ps -ef | grep fio
确认进程已经消失。接着想办法把发起压测的人当时输入的完整命令留下来,可以看执行用户的历史记录,也可以从运维平台的命令审计日志里找。完整命令非常重要,它能直接告诉你fio到底写到了哪个设备、覆盖多大范围、写了多久,比如:
bash复制fio -filename=/dev/sdb -direct=1 -ioengine=libaio -rw=randwrite -bs=4k -iodepth=32 -size=50G -runtime=60 -name=test -time_based
如果命令行里的filename是块设备或数据库数据目录,这就是灾难级操作;如果只是写了一个自定义的testfile,那还需要继续判断这个testfile是否落在 /data 下面。记录完命令后,用 lsblk -f 和 mount 命令把当前挂载关系留存下来,再查一下系统日志:
bash复制lsblk -f
mount | grep /data
dmesg | tail -100
这些输出建议都另存为文件,后面分析恢复范围和一些纠纷扯皮时都用得上。这个时候不要急着把故障节点从集群里踢掉,如果厂商高可用组件反复触发自动拉起,建议先停掉高可用守护进程或把自动切换改成手工模式,避免它在你的恢复过程中突然又做一次错误切换,把原本还正常的备用节点拖下水。这是很多人没想到的坑,因为DB挂掉后,集群管理软件往往会按照“健康检查失败自动重启/自动切换”的规则动作,它不知道当前是物理数据损坏,越自动化越容易把事情搞复杂。
2.2 数据库侧需检查的关键点
停掉高可用自动动作后,再到数据库节点上做检查。注意全程要小心,不要在写模式下做任何查询或日志清理。下面这几个检查点基本够用:
| 检查项 | 命令或方法 | 关注点 |
|---|---|---|
| 数据盘挂载状态 | df -h /data、`mount |
grep /data` |
| 数据库进程状态 | `ps -ef | grep xdb` |
| 主备角色与复制状态 | xdb_ctl query -D /data/xdb |
查看当前角色、发送和回放位置是否异常 |
| 数据目录完整性 | ls -la /data/xdb |
关注是否存在乱码文件、文件大小异常、目录缺失 |
| 数据库启动日志 | tail -200 /data/xdb/log/xdb.log |
定位是哪个文件、哪个页面先出错 |
| 文件系统剩余空间 | df -h |
排查是否因fio测试文件把空间写满 |
很多人发现备库还能启动,就觉得备库没事,直接promote。这里要特别提醒:备库能启动不代表它回放的所有日志都是健康的。备库可能只是启动时读到的WAL还完整,后面一旦遇到损坏的历史日志,照样会回放失败。主备两边的状态要在同一时间点交叉验证,不要只看“进程活着”就下结论。
2.3 五分钟判断恢复路径
现在把手里信息汇总一下,判断整体恢复难度。我的习惯是快速画一张表,把所有可用资源列出来:
| 受损情况 | 处理建议 |
|---|---|
| 只有主库数据文件损坏,备库日志完整可用 | 立刻将备库提升为新的主库,再从新主库重建原主库 |
| 主备物理文件都被写坏,但有独立全量备份和归档 | 用最近一次全量备份配合归档做PITR,恢复到fio启动前时间点 |
| 备份文件本身也有问题,只剩更早的全备 | 先用早期全备恢复,再尽可能从备库导出未损坏的增量数据 |
| 当前实例还能启动,只是部分业务表损坏 | 先把还能读的schema导出来保底,再针对损坏表做单表恢复 |
这张表做完,心里就有底了。如果备份都不完整,那就要立刻联系原厂或资深DBA分析能不能通过底层工具解析损坏页,但这类操作成功率不稳定,最稳妥的思路还是尽可能保住未损坏部分,不要拿生产时间赌恢复工具。我处理完现场后,通常会先把“全备路径、归档路径、备份保留天数、备份文件是否独享存储”四项信息发给客户确认,因为没有备份的恢复方案和在真金白银的备份池里捞数据,风险和操作节奏完全不一样。
3. 主备库恢复实操:failover、PITR与备库重建
到了真正动手恢复阶段,最核心的原则是“先恢复一个能对外提供服务的节点,再去考虑把集群补全”。不要试图一上来就同时修好主备两个节点,那样反而容易互相干扰。实际操作中我发现,很多人喜欢直接把损坏节点上的文件从备库拷贝过来,觉得“文件不都一样吗”,其实在PG系数据库里,数据文件结构、时间线、lsn位置都强相关,跨节点拷贝文件很容易造成新的不一致。正确的做法是走真正的备份恢复路径,或者用工具做基础备份重新同步。
3.1 备库可用时的failover方案
如果经过检查,备库日志回放没有出现CRC类错误,备库节点上的数据文件也没有被fio覆盖,那就直接把它提升为主库。以X库为例,操作类似:
bash复制# 在备库节点执行,将备库提升为新的主库
xdb_ctl promote -D /data/xdb
提升后第一时间验证新主库能否正常读写,并检查是否有复制槽残留,然后修改应用连接配置或把VIP切到备用节点。这步执行后,不要急着删原主库的数据目录,因为你可能还要从原主库抢救一些最新日志。fio只是覆盖了部分块,不代表所有WAL都坏了。如果原主库里还有完整且连续的WAL段,可以先把它们拷贝到一个安全目录,再决定是否需要用来补日志。不过从PG系数据库的恢复逻辑来看,如果原主库已经发生过非正常停止,直接拿这些日志去给新主库补数据很危险,因为时间线可能已经分叉,强行应用会导致分裂。稳妥做法是把这些WAL作为参考,而不是直接塞进新主库。
3.2 主备都不可用:基于全量备份和归档做PITR
最坏的情况就是主备都坏了,此时不要考虑在坏盘上“低温修复”,直接转入重装系统盘+数据盘还原的流程。先找后台的物理备份,包括最近一次全量备份和它之后连续归档的日志。备份工具通常都有列表命令,以常见的 gs_probackup 或 xdb_probackup 为例:
bash复制xdb_probackup show -B /backup/xdb
输出里能看到每个备份集是否有效、时间点、备份类型、wal位置等信息。找到故障前最近一次完整备份的ID后,再结合归档日志做恢复。在设计恢复目标时间时,务必找fio启动前一个“安全时间点”,最好用告警产生前的一两分钟,而不是故障发生时。因为故障发生后很多page已经被改,把恢复目标设到那之后,等于又引入了脏数据。恢复命令的大体模板如下:
bash复制# 损坏的数据目录要先清空,注意保留归档
rm -rf /data/xdb/*
# 用全量备份集恢复到原目录,并设置恢复到指定时间点
xdb_probackup restore -B /backup/xdb \
-D /data/xdb \
--instance xdb \
-i <FULL_BACKUP_ID> \
--recovery-target-time "2025-01-10 14:00:00" \
--recovery-target-inclusive=false
恢复完成后,数据库可能处于recovery状态,需要启动实例让它追日志,一直追到目标时间点再退出恢复模式。观察日志时,要重点看有没有 invalid page header、could not read block、WAL contains references to invalid pages 这类错误。如果一切正常,数据库会启动到可用状态。我还会顺带执行一次逻辑查询,把核心表都 select count(*) 一遍,确认能读到完整数据再让应用接入。
3.3 用基础备份重建备库
主库恢复后,备库不能直接沿用旧数据目录。因为旧备库的数据文件可能已经被覆盖,而且时间线早就乱了。重建备库的方式,是在新的存储/格式化后的目录上,从主库拉一个基础备份。命令类似:
bash复制# 在备库节点执行,基础备份主库数据
xdb_basebackup -h <新主库IP> -p 5432 -U repluser -D /data/xdb -X stream
拉取完成后,需要在数据目录创建
