Linux 基础体系 · 第 15/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 文件系统:ext4、XFS、日志、缓存、一致性和损坏恢复

文件系统不是“把文件名映射到磁盘扇区”的简单索引。它同时负责:

  • 将目录、文件、符号链接和元数据组织成持久化结构;
  • 将进程看到的字节偏移转换为块设备上的数据;
  • 管理空闲空间、权限、时间戳、配额和文件大小;
  • 在内核缓存与持久化介质之间协调读写;
  • 在断电、内核崩溃或设备错误后恢复文件系统自身的一致性;
  • 把底层 I/O 错误传递给应用或将文件系统切换为只读。

本文以现代主流 Linux 发行版中的 ext4 和 XFS 为重点,解释日志、页缓存、写回、一致性、fsync、崩溃恢复和损坏修复之间的关系。命令需要 root 权限,并且不同发行版的工具包、默认挂载参数和内核版本可能不同。


一、先区分四种“已经写入”

讨论文件系统时,“写入成功”至少可能表示四种不同状态:

  1. 应用把数据交给了内核
  2. 数据进入了文件系统和块层的缓存
  3. 块设备驱动接受了请求
  4. 数据已经稳定存储在断电后仍可读取的介质上

这四个状态不是同一个事件。

例如:

write(fd, buf, len);

返回 len,通常只说明内核接受了这次写入,并不等于数据已经落盘。即使后续调用:

fsync(fd);

也只是在文件系统和块设备语义允许的范围内,请求将该文件相关的数据和必要元数据持久化;它不能修复已经损坏的 SSD、失效的 RAID 控制器或错误报告“写入完成”的设备固件。

因此,分析一个存储问题时应分别问:

  • 可见性:其他进程何时能看到新内容?
  • 原子性:是否可能看到半个更新?
  • 一致性:文件系统内部结构是否满足约束?
  • 持久性:断电后更新是否仍存在?
  • 完整性:读出来的内容是否就是写入的内容?

日志主要解决文件系统结构的一致性问题,不自动解决应用数据的语义一致性,也不等于端到端数据完整性校验。


二、文件系统在 Linux I/O 路径中的位置

典型写路径可以简化为:

应用程序
  │ write / pwrite / mmap
  ▼
VFS(虚拟文件系统)
  │ inode、dentry、file、权限检查
  ▼
ext4 或 XFS
  │ 分配块、更新 inode/目录、建立日志事务
  ▼
页缓存 / writeback
  │ 脏页回写
  ▼
块层
  │ bio、队列、调度
  ▼
设备驱动
  │
  ▼
SSD、HDD、虚拟磁盘或 RAID

1. VFS:统一接口,不是具体文件系统

Linux 的 VFS 为 openreadwriterenameunlink 等系统调用提供统一入口。它维护几个重要对象:

  • inode:文件的元数据和数据块映射;
  • dentry:目录项,连接“目录中的名字”和 inode;
  • file:一次 open 得到的打开文件对象,包含当前文件偏移和打开标志;
  • 文件描述符:进程文件描述符表中指向 file 的整数句柄。

目录名不是文件本身。一个硬链接是多个目录项指向同一个 inode;符号链接则是一个独立 inode,其内容通常是目标路径字符串。unlink 删除的是目录项,只有当链接计数归零且没有进程打开该文件时,文件的数据空间才通常可以回收。

这解释了一个常见现象:

rm large.log
df -h

文件名消失了,但磁盘空间没有立即恢复。若某个进程仍持有该文件的打开描述符,inode 仍被引用。可以用:

lsof +L1

查找“已删除但仍被打开”的文件。该命令需要 lsof,并且输出受权限和 /proc 可见性影响。

2. 文件系统负责什么

ext4 或 XFS 会将逻辑操作分解为多个底层修改。例如创建文件可能需要:

  1. 在目录中加入文件名;
  2. 分配一个 inode;
  3. 更新 inode 的类型、权限、时间和链接计数;
  4. 分配数据块或记录文件为空;
  5. 更新块位图、inode 位图或其他空间索引;
  6. 更新目录的大小和校验信息。

这些修改必须满足内部约束。例如:

  • 目录项不能指向不存在的 inode;
  • 已分配的数据块不能同时被两个不允许共享的文件占用;
  • 空闲空间索引不能把正在使用的块当成空闲;
  • inode 的链接计数应与实际目录引用一致,除非处于特殊恢复状态。

文件系统日志的主要目标,就是让这些元数据更新在崩溃后回到一个可接受状态。


三、磁盘布局:分区、块设备、文件系统和挂载

一个文件系统通常建立在块设备之上,但中间可能还有多层:

物理磁盘 / 虚拟磁盘
  └─ 分区
      └─ RAID 设备
          └─ LVM 逻辑卷
              └─ ext4 或 XFS
                  └─ 挂载到目录树

因此,不能仅凭目录名判断文件系统边界。应先查看设备、类型和挂载关系:

lsblk -o NAME,TYPE,FSTYPE,SIZE,MOUNTPOINTS,UUID
findmnt -T /var/lib
df -T /var/lib

可能看到类似结果:

NAME        TYPE FSTYPE SIZE MOUNTPOINTS
nvme0n1     disk        1.8T
└─nvme0n1p2 part ext4   200G /
└─nvme0n1p3 part LVM    1.6T
  └─vg-data-lv data xfs 1.6T /var/lib

这里 //var/lib 可能属于两个不同文件系统。对 /var/lib 执行修复时,目标设备是对应的逻辑卷,而不是整块 NVMe 盘。

df 反映文件系统可用空间,du 统计目录树中可见文件所占的空间。两者差异可能来自:

  • 已删除但仍打开的文件;
  • 预留块;
  • 文件系统元数据;
  • 稀疏文件的“逻辑大小”与“实际占用”不同;
  • 快照、共享块或隐藏的内部结构。

四、ext4 和 XFS 的共同基础

ext4 与 XFS 都是 Linux 上成熟的日志文件系统,但内部空间管理和数据结构不同。它们共同提供:

  • inode 和目录;
  • 文件权限、ACL、扩展属性;
  • 稀疏文件;
  • 日志或日志化元数据;
  • 页缓存路径;
  • 挂载、卸载和崩溃恢复;
  • 工具链检查和修复。

“日志文件系统”不是指它们把每次普通文件写入都复制到一个文本日志里。日志通常是结构化的二进制区域,用于记录元数据事务,或者在特定模式下记录数据。日志的用途是让恢复程序知道某个更新是:

  • 尚未开始;
  • 已经完成;
  • 只完成了一部分;
  • 需要重放;
  • 需要撤销或丢弃。

它解决的是崩溃时结构修改处于中间状态的问题,而不是保证所有用户数据永不丢失。


五、ext4:块组、extent 和日志模式

5.1 ext4 的基本组织

ext4 将文件系统划分为块组,每个块组管理一部分数据块和 inode。现代 ext4 通常使用 extent 表示连续或大范围的数据块映射。

逻辑文件偏移到物理块的过程大致是:

文件偏移
  ▼
extent 树
  ▼
物理块范围
  ▼
块设备地址

一个 extent 可表示“从某个逻辑块开始,连续对应多少个物理块”。相比早期逐块间接索引,extent 更适合大文件和连续分配。

文件大小和块数仍不是一回事:

  • 普通文件可能存在内部空洞;
  • 稀疏文件的逻辑大小很大,但没有为未写区域分配物理块;
  • statSizeBlocks 因此可能差异很大。

示例:

truncate -s 1G sparse.img
du -h sparse.img
du --apparent-size -h sparse.img

truncate 创建一个逻辑大小为 1 GiB 的文件,但不必立即分配 1 GiB 的磁盘块。du 通常显示实际块占用,而 --apparent-size 显示逻辑大小。对稀疏文件执行普通复制、压缩或写入时,可能改变其实际占用。

5.2 ext4 的日志模式

ext4 常见的数据模式包括:

  • data=ordered:默认常见模式。文件数据通常在相关元数据提交前先写出,但这不是每个应用写入的持久化承诺;
  • data=writeback:元数据日志化,但数据写出顺序约束更弱,崩溃后旧数据或未初始化内容出现在新文件区域的风险更高;
  • data=journal:数据和元数据都写入日志,通常写放大更明显。

查看实际挂载参数:

findmnt -no SOURCE,FSTYPE,OPTIONS /

data=ordered 中的“ordered”不能理解成“调用 write 后数据立即持久化”。它主要约束文件数据回写与元数据提交之间的顺序,最终仍受回写线程、设备缓存和 fsync 等因素影响。

挂载选项是内核和文件系统实现的行为接口,不同内核版本可能有细节差异。生产环境不能只根据 /etc/fstab 推断当前状态,应查看实际挂载结果。


六、XFS:分配组、B+ 树和元数据日志

6.1 XFS 的基本组织

XFS 将文件系统划分为多个 allocation group,简称 AG。每个 AG 管理一部分空间和 inode,并使用 B+ 树等结构维护:

  • 空闲空间;
  • inode;
  • extent 映射;
  • 目录;
  • 配额和其他元数据。

多 AG 的设计有利于大型文件系统上的并发分配。多个 CPU 或多个线程可以在不同 AG 上进行操作,减少单一全局锁和索引热点。

XFS 的文件数据也通常通过 extent 映射。目录可能使用短格式、块格式或 B+ 树格式,具体取决于目录内容规模。

6.2 XFS 的日志

XFS 使用日志记录元数据事务。日志可以位于文件系统内部,也可以配置为外部日志设备。现代 XFS 还支持延迟记录、延迟分配等机制,以便合并操作和改善分配效率。

“延迟分配”意味着应用写入后,文件系统可能先在内存中记录脏页,而不是立即决定最终物理块。这样有机会把多个小写入合并成更连续的分配,但崩溃或空间不足时,写入可能在回写阶段才暴露错误。

XFS 默认并不等同于“所有文件数据都进入日志”。其日志重点是元数据。若应用需要特定数据持久化语义,仍应使用合适的 fsyncfdatasync 或更高层事务协议。

6.3 XFS 的重要特性边界

某些 XFS 文件系统创建时可启用特定功能,例如:

  • reflink:多个文件可以共享物理数据块,写时复制;
  • rmapbt:记录反向映射;
  • bigtime:支持更大范围的时间戳;
  • finobt:辅助 inode 空闲索引。

不能假设所有 XFS 都启用了这些功能。查看文件系统特征应使用:

xfs_info /mount/point

xfs_info 需要目标文件系统已挂载。卸载后的设备可用其他只读查询工具查看,但具体选项应以本机 man 页面为准。

启用 reflink 后,物理块占用、文件逻辑大小和“两个文件是否真正复制了数据”不能简单按普通文件理解。删除一个文件也不一定立即释放底层块,因为另一个文件可能仍引用共享块。


七、日志究竟保证什么

7.1 元数据事务的形式化描述

设文件系统状态为 SS,一次元数据操作为 TT,例如“创建文件并把目录项加入目录”。理想情况是:

S=T(S)S' = T(S)

崩溃可能发生在 TT 的任意中间步骤。没有日志时,磁盘上可能出现:

SpartialS_{\text{partial}}

它既不是旧状态 SS,也不是完整新状态 SS'

日志系统会先记录足够的信息,使恢复程序在重启后执行:

R(Spartial,L)SR(Spartial,L)SR(S_{\text{partial}}, L) \rightarrow S \quad \text{或} \quad R(S_{\text{partial}}, L) \rightarrow S'

这里的 LL 是日志记录。通常要求结果至少满足文件系统不变量,而不要求所有应用层操作都回到完整新状态。

例如创建文件时断电:

  • 目录项已经写入;
  • inode 尚未完全初始化;
  • inode 位图已更新;
  • 日志只记录了部分事务。

恢复程序可能重放完整事务,也可能丢弃未提交事务。最终目标是“不出现指向随机 inode 的目录项”,而不是保证用户一定能看到这个新文件。

7.2 日志提交不等于应用事务提交

数据库事务可能包含:

  1. 写入日志;
  2. 更新多个数据页;
  3. 修改索引;
  4. 提交事务;
  5. 对外返回成功。

文件系统日志通常只知道“块级元数据修改”,不知道应用的业务约束。例如:

扣减库存
写入订单文件
更新索引文件

即使三个文件的文件系统元数据都一致,断电后也可能只持久化了其中两个业务动作。文件系统不会自动保证这三个动作具有业务原子性。

7.3 日志通常不能保证用户数据内容

如果应用写入:

旧文件内容:ABCDEFGHIJ
新文件内容:1234567890

崩溃后可能出现:

  • 完整旧内容;
  • 完整新内容;
  • 新旧内容混合;
  • 文件大小或元数据已更新,但部分数据尚未持久化。

具体结果取决于写入方式、块大小、页缓存、文件系统、存储设备和同步调用。日志模式不能被当作通用的数据版本控制系统。


八、页缓存、脏页和写回

8.1 读路径

普通 read 的简化路径是:

read(fd, buf, n)
  │
  ├─ 页缓存中已有数据:直接复制到用户缓冲区
  │
  └─ 未命中:
       从文件系统映射到块设备
       发起 I/O
       数据进入页缓存
       再复制到用户缓冲区

页缓存按页管理,而不是按“一个文件一个缓存”。不同文件的页都可能竞争内存。

8.2 写路径

普通 write 通常先修改页缓存:

用户缓冲区
  ▼
页缓存页变脏
  ▼
文件系统记录元数据变化或延迟分配
  ▼
writeback 回写
  ▼
块设备

脏页不会无限停留。内核通过后台回写和内存压力控制回写;应用也可以显式调用:

sync

或在程序中使用:

fsync(fd);
fdatasync(fd);

sync 会请求系统范围内的脏数据和相关元数据回写,代价和影响范围都比单文件同步更大。它不是替代应用事务设计的工具。

8.3 fsyncfdatasync 和目录同步

fsync(fd) 的语义是请求文件数据及恢复文件所需的元数据写入稳定存储。fdatasync(fd) 更偏向只保证数据和必要元数据,若不影响后续数据访问,某些元数据更新可以不必同步。

创建或替换文件时,仅同步文件本身可能不够。典型安全更新流程是:

int fd = open("config.new", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write_all(fd, data, len);
fsync(fd);
close(fd);

rename("config.new", "config");

int dfd = open(".", O_RDONLY | O_DIRECTORY);
fsync(dfd);
close(dfd);

逻辑如下:

  1. 写临时文件;
  2. fsync 临时文件,确保内容和必要元数据已持久化;
  3. rename 在同一文件系统内原子替换目录项;
  4. fsync 父目录,使目录项替换本身持久化。

如果进程在第 2 步前崩溃,旧文件通常仍在;如果在 rename 后、父目录 fsync 前断电,重启后目录替换是否持久存在不能按应用预期假设。不同文件系统、内核和设备实现会影响细节,但“文件同步”和“目录项同步”是两个不同对象。

rename 的原子性也有限制:

  • 源和目标必须位于同一文件系统;
  • 它保证目录项切换不会被观察为“半个名字”,但不保证文件内容已经持久化;
  • 它不等于多个文件同时替换;
  • 跨文件系统时通常失败并返回 EXDEV

8.4 内存映射写入

mmap 修改文件时,写入首先修改映射页。应用应使用:

msync(addr, length, MS_SYNC);

请求映射区域同步,但要注意:

  • msync 的语义与 fsync 不是完全相同的接口;
  • 文件扩展、元数据更新和目录项仍需单独考虑;
  • 映射访问可能在缺页或写回时异步暴露错误;
  • 收到 SIGBUS 可能表示文件被截断、底层空间或设备访问失败。

8.5 O_DIRECT 也不是“自动持久化”

O_DIRECT 试图绕过页缓存,但通常有对齐、长度和地址限制,具体要求取决于文件系统和设备。它不自动保证断电后数据已经稳定存储,应用仍可能需要 fsync 或设备级同步语义。


九、并发、可见性和持久性不是一回事

假设两个线程同时写同一文件:

线程 A:pwrite(fd, "AAAA", 4, 0)
线程 B:pwrite(fd, "BBBB", 4, 0)

最终内容取决于两次写入的排序。即使每次系统调用都成功,也不能从文件系统日志推导出业务上的“谁应该获胜”。

常见操作的语义应区分:

1. O_APPEND

使用 O_APPEND 时,每次写入前文件偏移会定位到文件末尾;在支持的本地文件系统上,定位和写入具有一定的单次操作原子性。但它不保证:

  • 多个写入在业务上不可交错;
  • 写入已持久化;
  • NFS 等远程文件系统有完全相同的语义;
  • 一个大写入一定作为业务记录完整保留。

2. 锁

flockfcntl 锁可以协调合作进程,但锁通常是协作式的:

flock -x /run/myjob.lock -c '/usr/local/bin/update-data'

不遵守锁的进程仍可能修改文件。锁也不替代 fsync:它解决并发互斥,不解决断电持久性。

3. 打开文件与删除

进程 A:open("a", O_RDONLY) 得到 fd
进程 B:unlink("a")

目录项被删除后,进程 A 仍可通过 fd 读取文件。新进程无法通过名字打开它,但数据要等打开引用消失后才可能回收。这是 inode 引用和目录项引用分离的结果,不是文件系统泄漏。


十、文件系统一致性、数据一致性和数据完整性

这三个词常被混用,但对象不同。

10.1 文件系统一致性

文件系统一致性指内部结构满足不变量,例如块分配、inode、目录和日志之间相互匹配。e2fsckxfs_repair 主要检查这一层。

10.2 应用数据一致性

应用数据一致性指业务规则成立,例如:

账户 A 余额减少 100
账户 B 余额增加 100

如果只完成其中一半,文件系统仍可能完全一致,但业务数据不一致。需要数据库事务、写前日志、版本文件、校验和或幂等恢复协议解决。

10.3 数据完整性

数据完整性关注“读回的字节是否被静默改变”。文件系统日志不一定能发现介质上的静默位翻转。可以采用:

  • 应用层校验和;
  • 数据库页校验;
  • RAID 校验;
  • 具备校验能力的存储栈;
  • 定期读取和备份验证。

应注意,RAID 的冗余主要提高可用性和部分错误恢复能力,不等于备份,也不一定能识别所有错误。


十一、ext4 与 XFS 的选择边界

不能用“某个文件系统永远更快”概括选择。应根据工作负载、运维能力、所需特性和恢复工具作决定。

ext4 常见特点

  • 发行版支持广泛,工具成熟;
  • 适合系统盘、通用服务器和中小规模文件系统;
  • 可在线扩展,通常也可离线缩小;
  • e2fsck 可执行检查和修复;
  • 具备较丰富的兼容特性与挂载选项。

XFS 常见特点

  • 适合大文件、大容量文件系统和高并发 I/O;
  • 在线扩容能力成熟;
  • 使用 xfs_repair 进行检查修复;
  • 通常不能缩小已创建的 XFS 文件系统;
  • 许多高级特性在创建时决定,不能简单通过挂载参数后补齐。

在线扩容示例:

# 先确认逻辑卷或底层设备已经扩容
lsblk
lvs

# XFS:对挂载点执行扩容
sudo xfs_growfs /data

# ext4:可以对设备或挂载点扩容,具体形式以本机 man 页面为准
sudo resize2fs /dev/mapper/vg_data-lv_data

xfs_growfs 的对象通常是挂载点;resize2fs 面向 ext 文件系统设备。扩容前必须先完成分区、LVM 或 RAID 层的扩容,否则文件系统看不到新增空间。任何扩容都应先确认备份和设备路径,避免把操作施加到错误的逻辑卷。


十二、空间不足不只是一种故障

检查空间时至少看四类指标:

df -h /data
df -i /data
sudo du -xhd1 /data | sort -h
sudo lsof +L1

1. 数据块耗尽

df -h 显示可用容量接近零。大文件、日志、快照或共享块都可能是原因。

2. inode 耗尽

df -i 显示 inode 使用率接近 100%。大量小文件会先耗尽 inode,即使还有很多字节空间。

3. 删除文件仍被打开

lsof +L1 找到进程后,应通过应用自身的日志轮转或重启流程释放描述符。直接杀进程可能造成业务中断。

4. 预留空间和保留区

ext4 可能保留一部分空间供特权进程和文件系统维护使用。普通用户报告“满了”,而 root 仍能写入,不一定是异常。具体比例可用:

sudo tune2fs -l /dev/sdXn | grep -E 'Reserved block count|Block count'

不要在不了解业务的情况下随意降低预留空间。根文件系统完全耗尽会使日志、临时文件和修复流程都变得困难。


十三、崩溃恢复的实际路径

系统崩溃或断电后,文件系统通常经历以下过程:

运行中
  │
  ├─ 应用修改页缓存和元数据
  ├─ 日志事务部分提交
  └─ 发生断电或内核崩溃
          ▼
下次挂载
  │
  ├─ 读取日志
  ├─ 重放已提交但尚未写入主结构的事务
  ├─ 丢弃未完成事务
  └─ 将文件系统置于可挂载状态

ext4 常可在挂载时自动进行日志恢复。XFS 也会在挂载时进行日志重放。日志恢复通常比全盘扫描快,因为它只处理日志中的近期事务。

但以下情况可能仍需要离线检查:

  • 日志本身损坏;
  • 底层设备返回 I/O 错误;
  • 文件系统元数据已经超过日志可恢复范围;
  • 文件系统被错误地多次强制断电;
  • 内核或驱动存在缺陷;
  • RAID、虚拟磁盘或存储控制器重排了写入;
  • 文件系统已被标记为需要检查。

十四、发现疑似损坏时的诊断顺序

先保存证据,再尝试修复。不要看到错误就立即运行修复工具。

14.1 查看内核和设备错误

dmesg -T | grep -Ei 'ext4|xfs|I/O error|blk_update|buffer|nvme|scsi|ata|readonly'
journalctl -k -b

重点关注:

  • I/O error
  • uncorrectable
  • medium error
  • journal aborted
  • remounting filesystem read-only
  • NVMe、SCSI、SATA 链路或控制器错误。

如果设备层已经报错,单独修复文件系统可能掩盖硬件问题。

14.2 查看挂载状态

findmnt -T /data
mount | grep ' /data '

若文件系统被重新挂载为只读,先检查硬件、线缆、虚拟化存储和 RAID 状态。只读通常是保护行为:继续写入可能扩大损坏范围。

14.3 查看 SMART 或设备健康状态

SATA/SAS 设备常使用:

sudo smartctl -a /dev/sdX

NVMe 可使用:

sudo nvme smart-log /dev/nvme0

工具未安装、虚拟机不暴露健康信息、RAID 控制器隐藏物理盘时,命令可能无法提供有效数据。SMART 正常也不能证明文件系统或数据绝对无误。


十五、ext4 检查和修复

15.1 必须先卸载

普通 e2fsck 不应对正在读写的 ext4 文件系统执行。先确认设备:

findmnt -T /data
sudo umount /data

如果是根文件系统,应进入救援系统、安装介质或使用发行版提供的维护环境。不能为了“方便”直接对正在使用的根分区做写修复。

15.2 只读检查

sudo e2fsck -f -n /dev/mapper/vg_data-lv_data

含义:

  • -f:即使文件系统看起来干净,也强制检查;
  • -n:假设回答否,不写入修复结果。

输出可能包含 inode、目录、链接计数、位图等阶段。只读检查适合初步了解问题,但它不能保证随后仍保持相同状态;如果设备继续坏,状态可能变化。

15.3 实际修复

sudo e2fsck -f /dev/mapper/vg_data-lv_data

工具可能询问:

  • 是否修复孤儿 inode;
  • 是否重建目录;
  • 是否修正链接计数;
  • 是否修复位图;
  • 是否清除损坏的日志。

每次修复都可能丢失无法重建的目录项或文件片段。应先做块级镜像或确认备份,尤其是出现大量 I/O 错误时。对故障盘直接反复运行修复,会把原始证据和可恢复结构进一步改变。

15.4 典型结果解释

  • clean:检查通过,不等于硬件健康;
  • 修复 orphan inode:文件曾经没有可见目录项,但仍有引用,工具将其放回恢复目录或释放;
  • 修复 bitmap:分配状态与实际引用不一致;
  • 目录项错误:可能导致文件名丢失、移动到 lost+found 或无法恢复;
  • I/O error:优先处理设备问题,而不是继续增加修复次数。

十六、XFS 检查和修复

16.1 XFS 的边界:fsck.xfs 通常不做常规修复

对 XFS,常见的 fsck.xfs 程序通常只提示应使用 xfs_repair,不会像 ext4 的 e2fsck 那样进行常规检查修复。

先卸载:

sudo umount /data

只读检查可以使用:

sudo xfs_repair -n /dev/mapper/vg_data-lv_data

-n 表示不写入修复。它仍可能需要读取大量元数据,设备故障时应谨慎。

实际修复:

sudo xfs_repair /dev/mapper/vg_data-lv_data

必须针对未挂载的 XFS 设备。修复过程可能重建索引、丢弃无法验证的元数据关系,并把孤立对象放入可访问的恢复结构或造成文件名、目录结构丢失。

16.2 日志损坏与 -L

有时 XFS 日志无法正常重放,工具可能提示需要清除日志。xfs_repair -L 会强制清除日志,可能丢失最近尚未完成的元数据事务:

sudo xfs_repair -L /dev/mapper/vg_data-lv_data

这不是普通的“再试一次”选项。使用前应:

  1. 确认文件系统确实已卸载;
  2. 采集内核日志;
  3. 检查底层设备和 RAID;
  4. 尽可能做镜像或快照;
  5. 评估最近写入的数据能否从备份恢复。

如果日志只是因为设备暂时不可用,先修复设备层可能比直接清除日志更安全。

16.3 XFS 文件系统扩容和缩容

XFS 通常支持在线增长:

sudo xfs_growfs /data

但常规 XFS 不支持缩小。需要减少底层卷大小时,通常流程是:

  1. 新建一个更小的文件系统;
  2. 停止业务并复制数据;
  3. 验证复制结果;
  4. 切换挂载点;
  5. 再处理旧卷。

直接缩小 LVM 或分区而不先缩小文件系统会截断仍在使用的块,通常导致严重损坏。


十七、先镜像,再修复:面对坏盘的正确优先级

如果存在持续的读错误,首要目标不是让文件系统“看起来干净”,而是最大化保留可恢复数据。

常见原则是:

原始故障设备
  ▼
尽量少写入
  ▼
块级镜像
  ▼
对镜像做检查和修复
  ▼
从镜像或备份恢复文件

GNU ddrescue 常用于有坏块的设备:

sudo ddrescue -f -n /dev/sdX /recovery/disk.img /recovery/disk.map
sudo ddrescue -d -r3 /dev/sdX /recovery/disk.img /recovery/disk.map

说明:

  • 第一步优先复制容易读取的区域;
  • 第二步尝试处理失败区域;
  • .map 文件记录进度,便于中断后继续;
  • /dev/sdX 必须确认无误,写反目标会摧毁数据;
  • 目标镜像需要足够空间;
  • 对有硬件故障的盘反复通电和重试可能加剧损坏。

更安全的操作对象是镜像文件或其只读映射。若镜像来自加密、RAID、LVM 等多层结构,还必须按正确顺序组装这些层,不能把任意中间层直接当作普通文件系统设备。


十八、挂载与恢复时的安全策略

18.1 只读挂载不是绝对零写入

sudo mount -o ro,noload /dev/mapper/vg_data-lv_data /mnt/recovery

对某些文件系统,ro 仍可能触发日志重放或其他元数据动作;noload 用于禁止加载日志,但具体支持与风险应查看本机文档。禁止日志重放后,看到的可能是崩溃前的中间状态,适合取证或复制前评估,不代表文件系统已正常恢复。

对于 ext4,ro,noload 常用于尽量避免日志回放;对于 XFS,具体行为和工具版本应以 mountxfs 文档及内核支持为准。

18.2 恢复数据时避免覆盖源

推荐将恢复文件写到另一块健康存储:

sudo mount -o ro /dev/mapper/vg_data-lv_data /mnt/source
sudo mount /dev/sdb1 /mnt/target
sudo rsync -aHAX --numeric-ids /mnt/source/ /mnt/target/

选项含义:

  • -a:保留常见属性并递归复制;
  • -H:保留硬链接关系;
  • -A:保留 ACL;
  • -X:保留扩展属性;
  • --numeric-ids:不依赖目标系统用户名映射。

rsync 的返回码、错误输出和目标空间都必须检查。只看到命令结束并不代表所有文件都成功复制。


十九、日志、缓存和设备写缓存之间的隐藏边界

即使文件系统发出了写入顺序,底层设备也可能有内部缓存。设备需要正确处理:

  • flush;
  • barrier;
  • force unit access;
  • 电源故障保护;
  • 写入重排。

Linux 会通过块层和文件系统向设备发出同步请求,但最终可靠性取决于设备是否正确实现协议。没有断电保护的消费级 SSD、虚拟化存储后端或错误的 RAID 控制器,可能在报告 flush 完成后仍丢失数据。

这会产生一个重要反例:

应用 write
文件系统提交日志
fsync 返回成功
突然断电
重启后仍缺少最近数据

若设备错误地确认了尚未稳定的数据,文件系统无法从空中恢复这些内容。fsync 是必要的语义请求,不是对不可靠硬件的物理保证。


二十、故障表现与原因对照

1. 启动时长时间检查

可能原因:

  • 上次未正常卸载;
  • 日志需要重放;
  • 文件系统确实存在结构问题;
  • 底层设备读操作很慢;
  • 大型文件系统正在做全量检查。

应结合启动日志和设备错误判断,不能仅根据“检查很久”断定文件系统损坏。

2. 文件系统自动变只读

常见原因:

  • 元数据写入返回 I/O 错误;
  • 日志提交失败;
  • 块设备或路径暂时不可用;
  • 文件系统检测到无法安全继续的状态。

此时反复执行 mount -o remount,rw 可能再次触发写入并扩大损坏。先收集日志、确认设备稳定、安排维护窗口。

3. 文件存在但内容为空或变旧

可能原因:

  • 应用只调用了 write,未同步就崩溃;
  • 数据写入页缓存后尚未回写;
  • 应用使用临时文件替换,但未同步父目录;
  • 存储设备丢失了已确认的写入;
  • 读取的是另一个挂载层或容器卷。

日志能保证文件系统结构可恢复,不保证应用最近一次内容更新一定出现。

4. No space left on device

除了容量和 inode 耗尽,还可能来自:

  • ext4 预留空间;
  • 项目配额、用户配额或组配额;
  • XFS 配额;
  • 文件大小限制;
  • 容器或虚拟文件系统的独立限制;
  • 目录所在文件系统与预期不同。

应同时检查:

df -h
df -i
ulimit -a
findmnt

二十一、检查工具不能替代备份

文件系统检查工具的目标是重建元数据结构,不是恢复所有历史版本。修复时可能发生:

  • 文件名与目录层级丢失;
  • 部分文件被放入 lost+found 或无法按原路径关联;
  • 文件内容只剩部分块;
  • 最近事务被丢弃;
  • 共享块、快照或 reflink 关系发生变化;
  • 原本可读的结构因设备继续失败而变得更差。

因此,恢复策略应分层:

  1. 先确认是否有可验证的备份
  2. 若设备有错误,先做镜像
  3. 在镜像或副本上检查和修复
  4. 恢复到新的文件系统
  5. 通过校验和、应用测试和目录抽样验证
  6. 保留原始介质和修复日志,直到确认数据完整

备份“存在”不等于备份“可恢复”。至少需要实际抽样恢复文件,验证权限、ACL、扩展属性、硬链接、符号链接和应用可读性。


二十二、一个可观察的安全更新示例

下面用 shell 演示文件替换流程的关键步骤:

set -eu

dir=/data/app
tmp="$dir/config.new"
dst="$dir/config"

printf '%s\n' 'version=2' 'mode=safe' > "$tmp"

# 让临时文件内容进入文件系统和块设备的同步路径
sync -d "$tmp"

# 同一文件系统内,目录项替换是原子的
mv -f "$tmp" "$dst"

# 同步父目录,使名称替换本身持久化
dirfd=$(exec 9<"$dir" && echo 9)
sync -d "$dir"
exec 9<&-

这个 shell 例子用于说明概念,但生产程序更应直接使用 openwritefsyncrename 和目录 fsync,并逐一检查返回值。sync -d 的可用性和行为依赖 coreutils 版本;不要把它当成跨平台、精确的应用事务接口。

更完整的 C 逻辑应处理:

  • write 的短写;
  • EINTR
  • ENOSPC
  • EIO
  • fsync 失败;
  • rename 失败;
  • 目录 fsync 失败;
  • 临时文件清理;
  • 崩溃后旧文件和新文件的选择策略。

如果配置内容超过单个文件,或者需要同时更新多个相关文件,就应使用数据库事务、版本目录加指针切换,或设计可重放的日志协议,而不是假设多次 rename 具有整体原子性。


二十三、哪些说法是错误的

错误一:有日志就不会丢数据

日志主要保证文件系统元数据恢复到一致状态。未同步的用户数据、设备缓存中的数据和业务事务仍可能丢失。

错误二:write 返回成功就是落盘

它通常只表示内核接受了写入。需要持久化语义时,应设计 fsyncfdatasync,并确认设备链路可靠。

错误三:rename 能保证配置不会损坏

rename 能提供同一文件系统内目录项切换的原子观察语义,但若没有先同步新文件或随后同步父目录,断电后仍可能得到旧版本、缺少新版本,或出现与预期不同的持久化结果。

错误四:XFS 不能修复

XFS 有 xfs_repair,但它要求文件系统通常处于卸载状态,且强制清除日志可能造成数据丢失。不能用 ext4 的思路直接运行 e2fsck

错误五:RAID 等于备份

RAID 主要处理设备故障和可用性问题,不能防止误删、恶意加密、应用错误写入或逻辑损坏。损坏会被同步到所有冗余副本。

错误六:只读挂载一定不会写任何东西

日志重放、挂载行为、设备层和文件系统特性存在差异。恢复现场应使用经过确认的只读策略,并尽量操作镜像。

错误七:删除大文件后空间必然立即恢复

若文件仍被进程打开,目录项虽已删除,数据块仍被打开文件引用。应检查 /proclsof +L1,通过安全方式释放进程描述符。


二十四、建立正确的判断模型

面对一个文件系统问题,可以按以下因果链分析:

应用操作
  │
  ├─ 是否完成了业务级原子性?
  ├─ 是否调用了合适的同步接口?
  ▼
VFS 与具体文件系统
  │
  ├─ inode、目录、extent、空闲空间是否一致?
  ├─ 日志事务是否提交?
  ▼
页缓存与写回
  │
  ├─ 是否仍有脏页?
  └─ 写回是否返回错误?
  ▼
块层与设备
  │
  ├─ 是否正确执行 flush?
  ├─ 是否发生介质或链路错误?
  └─ 是否存在 RAID/LVM/虚拟化层问题?
  ▼
断电或崩溃后的状态

这个模型能避免把不同层的问题混为一谈:

  • 文件系统修复工具处理的是结构;
  • fsync 处理的是同步请求;
  • 锁处理的是并发;
  • 数据库事务处理的是业务原子性;
  • 备份处理的是历史恢复;
  • RAID 处理的是部分设备故障;
  • 应用校验和处理的是内容完整性。

ext4 和 XFS 的核心差异在数据结构、分配策略、扩容缩容边界、特性开关和工具链;日志和缓存则是理解两者运行行为的共同基础。真正可靠的存储系统,需要把这些层的语义逐层验证,而不能用“写成功”“有日志”或“做了 RAID”替代对持久性、一致性和恢复路径的具体分析。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。