线上虚机断电后XFS文件系统损坏修复记录
线上虚机断电恢复后,Docker 容器显示运行中,但数据目录 /data 无法正常访问。重挂载只能短暂恢复,最终从内核日志中定位到 XFS 元数据损坏和文件系统主动关闭。
处理过程是停止业务、卸载数据盘、离线执行 xfs_repair,最后在普通修复被未重放日志阻断的情况下使用 -L 清除日志并修复。
-L 会丢弃尚未重放的元数据日志,存在数据丢失风险;文件系统修复结束后,还需要单独检查 Kafka 等业务的数据完整性。
前言
这次线上虚机发生断电,恢复后发现数据盘不能正常读写,第一反应是“磁盘坏了,变成只读了”。机器上通过 Docker 运行着 Kafka 和 Redpanda Console,数据盘挂载在 /data。
下面按历史命令整理当时的排查和处理过程,并补充再次遇到这类问题时应该做的检查。原记录止于修复后重新挂载、启动 Docker,后面的业务验证属于补充建议,不代表当时已经完成。
先明确一点:这次日志能确认的是 XFS 文件系统元数据损坏。断电是故障背景,但仅凭这些输出,还不能断定虚拟磁盘或底层物理磁盘已经发生硬件损坏。
容器还在运行,数据目录却打不开
最开始查看容器状态:
Kafka 和 Redpanda Console 都显示 Up 2 days,看起来进程还活着。但进入数据目录后执行:
返回的却是:
这时排查重点就从容器进程转到了文件系统。Up 只能说明容器主进程还在运行,不能说明它能正常读写数据,也不能证明 Kafka 能正常提供服务。
Input/output error 也不是普通的权限不足。目录遍历已经失败,单纯调整权限无法解决这个问题。
第一次重挂载,只是短暂恢复
当时先停止 Docker 及其 socket,再卸载、重新挂载:
停止 docker.socket 是为了避免排查期间访问 Docker 时,由 socket 激活机制再次拉起服务。实际处理时,还要确认容器已经退出;如果启用了 Docker 的 live-restore,停止 daemon 后容器仍可能继续运行。
重新挂载后,ls -al /data 一度可以正常看到目录:
于是又启动了 Docker:
容器重新显示运行中,但很快访问 /data 又出现了同样的 Input/output error。
这说明重新挂载并没有解决根本问题。能列出目录只是一个瞬时结果,一旦业务重新触发文件创建等操作,仍可能碰到损坏的元数据。
这里保留当时使用的 mount -a,它会按 /etc/fstab 尝试挂载所有适用条目。正式恢复单块数据盘时,在确认配置正确后,更适合使用 mount /data,将操作范围限制在目标挂载点。
从内核日志定位到XFS损坏
接着检查内核日志:
关键内容如下,省略调用栈和寄存器输出:
这些输出把前面的现象串了起来:
/dev/sda1上的 XFS 检测到了元数据异常。- 重挂载时执行了日志恢复,暂时又能访问目录。
- 随后的文件创建路径
xfs_create再次触发 inode 一致性检查错误。 - XFS 为避免继续扩大损坏,主动关闭文件系统,后续访问返回 I/O 错误。
日志中的调用进程是 java,这与机器运行 Kafka 的背景相符,但不能据此认定 Kafka 就是损坏的原因。它也可能只是访问到了已经损坏的元数据。
“只读”和XFS主动关闭的区别
遇到无法写入时,可以补充查看:
如果挂载选项中是 ro,说明该挂载确实处于只读状态。这次记录中的直接证据则是 Shutting down filesystem 和 Input/output error;XFS 主动关闭后,即便挂载选项仍显示 rw,也不代表文件系统还能正常使用。
因此,不能直接套用 mount -o remount,rw /data。元数据损坏没有修复,恢复写入并不能消除问题。
停止业务,确认需要修复的分区
确认文件系统损坏后,再次停止 Docker,并离开数据目录:
历史记录中尝试用 lsof 查看目录占用:
但目录本身已经无法读取,因此命令报错:
lsof +D 需要遍历目录,在这种情况下不能依赖它判断是否还有进程占用。可以结合 fuser -vm /data、服务状态及容器进程排查,并停止其他使用该数据盘的服务。
当时接着执行了:
fdisk 输出显示,数据盘是 200 GiB 的 /dev/sda,其中分区是 /dev/sda1;系统盘则是另一块 8 GiB 的 /dev/sdb。
umount -l 是懒卸载,只会先将挂载点从当前文件系统树中分离,旧引用可能仍然存在。它不是“现在可以安全修复”的保证,也不应作为离线修复的默认前置步骤。
推荐先释放占用,再正常执行 umount /data。如果已经懒卸载,或无法确定容器、其他挂载命名空间是否仍持有该文件系统,应进入救援环境,确认目标分区完全未挂载后再修复。不要在仍被使用的文件系统上运行 xfs_repair。
另外,fdisk 中的 Linux 分区类型并不能证明文件系统是 XFS。应在卸载前用 findmnt 确认挂载关系,再结合 lsblk -f、blkid /dev/sda1 和内核日志交叉核对。设备名可能变化,不能照搬这里的 /dev/sda1。
在进行会修改元数据的修复前,应先停止写入并保留故障盘快照或克隆。快照需要保存到独立存储,不能把同盘另一个目录当成备份。如果底层存储仍有持续 I/O 错误,应先处理宿主机或存储故障,必要时在磁盘副本上恢复。
普通修复被未重放日志阻断
确认目标分区离线后,当时执行:
工具在第二阶段停止,并给出了明确提示:
意思是 XFS 日志中还有需要重放的元数据变更,普通离线修复不能直接处理。-v 只表示输出详细信息,这条命令不是只读检查;如果需要先做不修改磁盘的检查,可以在完全卸载后使用 xfs_repair -n /dev/sda1,但遇到脏日志时其检查能力也会受限。
历史记录中连续执行了两次普通修复,结果相同。条件没有变化,重复运行不会自动消除脏日志。
优先尝试正常日志重放
按照工具提示,更完整的处理路径是在保留快照、底层存储稳定且业务停止的前提下,尝试正常挂载,让内核重放日志,再正常卸载:
每一步都需要检查结果,不能在挂载失败后直接继续。这种正常挂载可能写盘以重放日志,不属于只读取证操作。
这次前面已经出现过挂载恢复后再次损坏的现象,但原记录没有包含普通修复报错后,再单独尝试一次隔离挂载的过程。这里补上工具建议的路径,避免把 -L 写成遇到脏日志就必须执行的常规步骤。
最后使用-L清除日志并修复
当时最终执行的是:
工具首先提示:
-L 会清零 XFS 日志,丢弃尚未重放的元数据变更,然后尝试修复。它可能导致文件或目录丢失,不能理解成单纯“强制执行一下”。只有正常日志重放无法完成、已做好数据保护并接受相应风险时,才考虑这一步。
后续输出中可以看到空闲空间和 inode 映射扫描、元数据修正,以及目录连接关系检查:
其中 moving to lost+found 很关键:修复发现了与目录树失去连接的 inode,并将其放到了文件系统根目录下的 lost+found。重新挂载后,这里对应 /data/lost+found,需要检查其中是否有业务文件,不能直接当垃圾删除。
最后的 done 表示这次文件系统修复流程结束,不代表所有业务数据都完整,也不代表 Kafka 日志段的内容已经通过校验。
重新挂载与恢复验证
历史记录最后执行了:
到这里能确认的是修复命令执行结束,并进行了挂载和服务启动操作。更稳妥的恢复流程应先检查文件系统,再恢复业务。
先验证数据盘
确认 /etc/fstab 中的目标条目正确后,挂载并检查:
必须先确认 /data 实际挂载到了预期分区,再启动 Docker。否则挂载失败时,业务可能往系统盘上的同名目录写入数据。
确认挂载源和 rw 状态后,可以在数据盘上新建一个临时文件,测试写入、同步和读取:
检查是否出现新的 XFS corruption、shutdown 或块设备 I/O 错误。一次读写成功只说明基本访问恢复,仍需要观察后续业务负载下的表现。
再验证Docker和Kafka
除了容器是否启动,还要检查 Kafka 是否持续重启、是否出现日志段损坏或目录不可写,以及 topic、分区和副本状态是否正常。使用业务已有的测试 topic 做生产、消费验证,并核对关键数据和消费进度。
文件系统修复解决的是磁盘上的元数据一致性,Kafka 的数据恢复还要结合副本、备份和应用日志判断。不要为了让容器启动就直接删除 Kafka 数据目录或异常日志段。
这次排查留下的经验
这次最容易误判的地方,是容器一直显示运行中,以及重新挂载后目录曾经恢复访问。如果只看到这两个现象就结束处理,业务下一次写入时仍会再次触发故障。
以后遇到类似情况,我会先把容器状态、挂载状态和内核日志一起看。确认 XFS 损坏后,先停止写入、保留快照,再离线修复;普通日志重放能解决时优先重放,-L 留作最后手段。
后续还需要检查宿主机和底层存储的断电记录、写缓存保护及磁盘健康状态,并完善数据盘挂载失败时阻止业务启动的机制。同时保留独立备份、关注文件系统错误和 Kafka 副本告警,避免下次仍然只能依靠单盘修复恢复业务。
