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

XFS 深入:Allocation Group、Journal、在线扩容、修复和大文件

XFS 是一种面向大容量文件系统和高并发 I/O 的日志文件系统。它的设计重点不是“让每个操作都立即落盘”,而是把元数据组织、空间分配、日志提交和延迟写回拆成多个可并行推进的组件。

理解 XFS,不能只记住“它支持大文件、支持在线扩容”。真正需要建立的是下面这条因果链:

  1. 文件系统被划分为多个 Allocation Group(AG,分配组)
  2. 每个 AG 管理自己范围内的空闲空间和 inode;
  3. 文件的 extent 尽量在合适的 AG 中分配,以减少全局锁竞争;
  4. 元数据变化先进入 Journal(日志),日志提交后,崩溃恢复可以重建一致的元数据状态;
  5. 文件数据通常通过 Page Cache 延迟写回,日志并不等价于“所有文件内容都已经持久化”;
  6. 文件系统扩容本质上是向数据区域增加可管理的 AG 或扩展最后一个 AG;
  7. 文件系统损坏时,先利用日志恢复,再使用离线 xfs_repair 重建索引和修正引用关系。

一、XFS 的基本对象:块、extent、inode 和 AG

1. 文件系统块不是磁盘扇区

XFS 使用固定大小的文件系统块。创建文件系统时通常由 mkfs.xfs 选择块大小,常见值是 4 KiB,但实际值由设备、发行版和创建参数决定。

可以用下面的命令查看已有文件系统:

xfs_info /data

典型输出会包含类似字段:

meta-data=/dev/mapper/vg-data-lv  isize=512    agcount=8, agsize=...
         sectsz=512              attr=2, projid32bit=1
         crc=1                   finobt=1, sparse=1, rmapbt=0
data     =                       bsize=4096   blocks=...
         imaxpct=25              sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0, ftype=1
log      =internal               bsize=4096   blocks=...
realtime =none                   extsz=4096   blocks=0, rtextents=0

这些字段的含义不是装饰信息:

  • bsize 是文件系统块大小;
  • agcount 是 AG 数量;
  • agsize 是典型 AG 大小;
  • log=internal 表示日志位于同一个 XFS 文件系统内部;
  • log=external 表示日志位于单独的块设备上;
  • sunitswidth 用于描述底层 RAID 条带几何;
  • reflink=1bigtime=1 等能力是否存在取决于创建时的格式和内核、xfsprogs 支持。

XFS 的空间分配单位不是“每个文件都连续占用一段磁盘”,而是 extent。一个 extent 表示一段连续的文件逻辑块映射到一段连续的物理块。

例如,一个文件的逻辑空间可能是:

逻辑块 0..1023    -> 物理块 100000..101023
逻辑块 1024..2047 -> 物理块 500000..501023

这表示文件包含两个 extent,而不是要求整个文件在物理上连续。


二、Allocation Group:XFS 如何把一个大文件系统拆成多个并行区域

2.1 AG 的定义

Allocation Group 是 XFS 在空间管理上的独立区域。每个 AG 通常包含:

  • AG superblock;
  • 空闲空间管理结构;
  • inode 分配管理结构;
  • 可选的反向映射和引用计数结构;
  • 该 AG 范围内的 inode 和数据块。

可以把整个文件系统抽象为:

整个 XFS 文件系统
├── AG 0
│   ├── AG superblock
│   ├── 空闲空间 B+ 树
│   ├── inode B+ 树
│   └── 数据块与 inode
├── AG 1
│   ├── AG superblock
│   ├── 空闲空间 B+ 树
│   ├── inode B+ 树
│   └── 数据块与 inode
└── ...

AG 是逻辑划分,不一定对应物理磁盘、分区或 LVM extent。一个 AG 可以跨越底层设备的很多区域,而一个物理设备也可以包含多个 AG。

2.2 AG 内部的核心索引

XFS 不通过扫描整块磁盘寻找空闲空间,而是维护索引结构。

空闲空间索引:按地址和按长度

一个 AG 至少需要回答两个问题:

  1. 某个物理地址附近有哪些空闲块?
  2. 是否存在一段至少为 N 个块的连续空闲空间?

因此 XFS 维护两类空闲空间 B+ 树:

  • 按空闲 extent 起始地址排序;
  • 按空闲 extent 长度排序。

设某个 AG 的空闲 extent 为:

[100, 20]   // 起始块 100,长度 20
[200, 80]
[400, 10]

按地址索引便于合并相邻空闲 extent;按长度索引便于快速寻找满足请求的空间。

如果一个文件追加写入需要 50 个连续块,分配器可以直接找到 [200, 80],而不必逐块扫描。

inode 索引

inode 也不是从固定表格中简单顺序分配。XFS 会在 AG 内维护 inode 分配信息,并通过 inode B+ 树索引已分配 inode。现代 XFS 还可能使用 finobt 等结构帮助寻找仍有空闲 inode 的 inode chunk。

因此,创建文件通常涉及两种资源:

  • 一个新的 inode;
  • 一个或多个数据 extent。

如果某个 AG 有很多空闲数据块但 inode 分配紧张,创建新文件仍可能失败。反过来,inode 很充足而数据空间不足,也不能写入文件。

2.3 为什么要有多个 AG

多个 AG 的主要作用是并行化,而不是简单“增加容量”。

假设一个文件系统只有一个全局空闲空间索引。大量线程同时创建文件或追加写入时,它们会竞争同一组元数据锁。多个 AG 可以让不同线程在不同 AG 中执行分配:

线程 A -> AG 2 的 inode 和空闲空间
线程 B -> AG 5 的 inode 和空闲空间
线程 C -> AG 1 的 inode 和空闲空间

这会降低单个全局锁结构的竞争。

但 AG 并不是越多越好:

  • AG 数量过多会增加元数据规模;
  • 每个 AG 过小会限制大 extent 的分配;
  • 高并发创建文件时,分配器仍可能集中使用少数 AG;
  • 底层设备若本身是 RAID,AG 的几何和 RAID 条带并不自动完全匹配。

2.4 文件如何选择 AG

XFS 的分配器会综合考虑:

  • 目录所在位置;
  • 当前 AG 的空闲空间;
  • 文件大小和预分配请求;
  • 并发负载;
  • inode 与数据是否适合局部放置;
  • RAID 条带信息;
  • 文件是否启用 realtime、reflink 等能力。

常见情况下,同一个目录中的文件可能倾向于在相近区域分配,但这不是“目录决定固定 AG”的规范保证。空间压力、并发和分配策略都会改变结果。

对于大文件,XFS 通常会努力分配较大的 extent;对于小文件,则需要在 inode、目录和数据块之间平衡局部性。

2.5 一个 AG 容量算例

假设:

  • 文件系统块大小为 4 KiB;
  • 有 4 个 AG;
  • 每个 AG 的逻辑数据容量约为 1 TiB;
  • 文件系统总容量约为 4 TiB。

一个 2.5 TiB 文件不要求完整地落在一个 AG 中。它可以被拆成多个 extent,分布在多个 AG:

文件逻辑空间
├── extent 0:AG 0,700 GiB
├── extent 1:AG 1,900 GiB
├── extent 2:AG 2,600 GiB
└── extent 3:AG 3,300 GiB

这解释了两个容易混淆的事实:

  1. 单个文件可以大于单个 AG;
  2. 单个 AG 的空闲空间不能简单相加成“保证能分配一个同样大小的连续 extent”。

如果文件需要一个很大的连续物理 extent,而每个 AG 都只有零散空闲空间,即使 df 显示总剩余空间很多,也可能无法满足某些大规模预分配请求。


三、XFS Journal:日志保证什么,不保证什么

3.1 Journal 的目标是元数据一致性

XFS 的 Journal 主要记录元数据变更,例如:

  • inode 状态;
  • 目录项;
  • 空闲空间索引;
  • inode 索引;
  • extent 映射;
  • 配额和其他文件系统元数据。

一次创建文件可能改变:

  1. 目录 inode;
  2. 目录数据块;
  3. 新文件 inode;
  4. inode 分配结构;
  5. 空闲空间 B+ 树;
  6. 目录索引。

如果这些更新分别写入磁盘时发生断电,文件系统可能出现“目录已经指向 inode,但 inode 分配表认为该 inode 仍未使用”等不一致。

日志的核心思想是:先把足够的信息写入稳定存储,再允许相关元数据以较复杂的顺序写回。崩溃后,XFS 可以重放已提交的日志事务,恢复元数据的一致状态。

3.2 事务提交的抽象过程

可以把一次元数据更新抽象成以下状态:

内存中的修改
    │
    ▼
形成日志事务
    │
    ▼
日志记录写入设备
    │
    ▼
日志提交(commit)
    │
    ▼
元数据块写回文件系统区域

崩溃时存在两类典型情况:

日志事务尚未提交

日志中只有不完整记录,恢复时通常不会把它当作已经完成的事务。

日志事务已经提交,但元数据尚未写回

挂载时的日志恢复会重放该事务,使元数据回到提交后的状态。

这就是日志恢复与普通“扫描全盘修复”的区别:日志首先解决最近一次运行期间的已知事务状态。

3.3 内部日志和外部日志

内部日志

/dev/sdb1
└── XFS
    ├── 数据区域
    └── 日志区域

日志占用同一个文件系统设备的部分空间,部署简单。

外部日志

/dev/nvme0n1p1 -> XFS 数据区域
/dev/nvme1n1p1 -> XFS 日志设备

外部日志可以把日志 I/O 与数据区域分离,是否有收益取决于设备延迟、写入模式和故障域设计。

外部日志有一个重要风险:数据设备和日志设备必须同时可用。日志设备丢失时,不能把它当作普通可丢弃缓存;其中可能包含尚未写回的关键元数据事务。

可以通过 xfs_info 查看日志位置:

xfs_info /data | grep -E 'log|meta-data'

3.4 XFS 日志不是数据日志

XFS 主要提供元数据日志,并不等价于 ext4 的 data=journal 模式。普通文件写入时:

  1. 用户进程把数据写入 Page Cache;
  2. 文件大小、extent、时间戳等元数据可能进入 XFS 日志;
  3. 文件数据稍后由 writeback 写入数据区域;
  4. fsync() 或相关同步操作要求内核把相应数据和元数据推进到稳定存储。

因此,下面两个事实必须分开:

  • “文件系统重启后目录结构可恢复”;
  • “最近写入的文件内容一定已经持久化”。

它们不是同一个保证。

例如:

write(fd, buf, len);

成功只表示数据已经被内核接受,通常不能单独证明断电后数据仍存在。

而:

write(fd, buf, len);
fsync(fd);

才是在应用层明确要求该文件相关数据和元数据完成同步的常见方式。是否真正满足持久性,还取决于设备是否正确处理 flush/FUA、控制器缓存是否有保护,以及应用是否需要同步父目录。

新建文件并执行 fsync(fd) 后,如果要求断电后目录项也一定存在,通常还需要对父目录执行:

fsync(dirfd);

这属于应用持久性协议,而不是 XFS 日志自动替应用完成的业务语义。

3.5 日志大小与性能

日志太小,事务提交可能更容易受到空间回收和提交节奏影响;日志太大,也会占用文件系统空间,并不自动产生线性性能收益。

日志位置和大小应结合:

  • 设备延迟;
  • 元数据更新频率;
  • 崩溃恢复时间要求;
  • 是否使用外部日志设备;
  • 底层 RAID 条带。

不要仅根据“日志越大越好”调整生产文件系统。先使用 xfs_info、I/O 监控和实际工作负载确认瓶颈。


四、写入、延迟分配与日志恢复的故障路径

XFS 通常使用延迟分配。进程写入文件时,系统可能先只记录:

文件逻辑块 0..N 已有脏数据

但暂时不立即决定这些逻辑块对应的物理块。等到回写时,XFS 可以看到更大的连续写入范围,从而选择更大的 extent,减少碎片。

典型路径是:

write()
  │
  ▼
Page Cache 中产生脏页
  │
  ▼
XFS 延迟分配记录逻辑空间
  │
  ▼
writeback 触发
  │
  ├── 分配物理 extent
  ├── 写入文件数据
  ├── 更新 inode 和 extent 元数据
  └── 提交必要的日志事务

如果在“脏页尚未写回”时断电,文件内容可能丢失或保留旧版本;如果元数据事务已经提交,重启后目录和 inode 结构仍应保持一致。

这也是为什么 dfdu 和应用看到的文件大小可能在一段时间内表现不同:

  • df 反映文件系统块使用;
  • du 根据文件的实际 extent 统计;
  • 稀疏文件的逻辑大小可以远大于实际占用;
  • 延迟分配和预留空间会影响短时间内的统计结果。

五、在线扩容:设备变大不等于 XFS 已变大

在线扩容必须经过完整链路:

底层存储容量增加
    │
    ▼
分区、LVM 或虚拟块设备识别新空间
    │
    ▼
块设备大小变大
    │
    ▼
xfs_growfs 修改 XFS 数据区域
    │
    ▼
df、xfs_info 验证文件系统容量

只完成前几步而不执行 xfs_growfs,XFS 仍然只能使用原来的空间。

5.1 LVM 场景

假设 XFS 挂载在 /data,逻辑卷为 /dev/vg0/lv_data

findmnt -no SOURCE,FSTYPE,TARGET /data
sudo lvs
sudo vgs
df -hT /data

扩大逻辑卷,例如增加 100 GiB:

sudo lvextend -L +100G /dev/vg0/lv_data

然后让 XFS 使用新增空间:

sudo xfs_growfs /data

验证:

df -hT /data
xfs_info /data

xfs_growfs 的参数是挂载点或设备相关对象;生产环境中建议使用当前实际挂载点,并先通过 findmnt 核对文件系统类型和源设备。

可以先查看计划而不执行修改:

sudo xfs_growfs -n /data

-n 是 dry run,用于显示计算结果;它不能替代真正扩容后的验证。

5.2 分区场景

如果 XFS 位于分区中,先扩大分区,再扩大文件系统。例如设备是 /dev/sdb1

lsblk -f /dev/sdb
sudo parted /dev/sdb print

扩大分区可以使用发行版提供的 growpart

sudo growpart /dev/sdb 1

然后确认内核已经看到新的分区大小:

lsblk -b /dev/sdb
sudo blockdev --getsize64 /dev/sdb1

最后:

sudo xfs_growfs /data
df -hT /data

这里有两个独立风险:

  • 分区边界操作错误可能破坏相邻分区;
  • 文件系统扩容不会修复底层设备、分区表或 LVM 配置错误。

任何分区调整前都应确认设备路径、分区号、备份和恢复方案。不要根据 /dev/sdX 的字母猜测设备身份,应结合 lsblk -f、WWN、序列号和挂载关系核对。

5.3 直接扩大的块设备

在虚拟机或云环境中,块设备可能已经由平台扩容:

lsblk
blockdev --getsize64 /dev/vdb
findmnt /data

如果文件系统直接位于 /dev/vdb,且块设备已经变大:

sudo xfs_growfs /data

如果块设备大小尚未更新,需先按云平台或虚拟化环境要求重新扫描设备。不能把 xfs_growfs 当作块设备扩容工具。

5.4 在线扩容到底改变了什么

XFS 在线扩容不会把已有文件重新复制到新区域。它主要完成:

  1. 读取新的设备边界;
  2. 计算可新增的 XFS 数据块;
  3. 为新增区域建立 AG 元数据;
  4. 更新文件系统总体数据范围;
  5. 使分配器可以从新增 AG 或新增 AG 尾部获取空间。

已有文件的 extent 通常保持不变,新增文件和追加写入才会逐步使用新空间。

扩容后可看到 agcount 增加,或者最后一个 AG 的可用范围变化。具体布局取决于原始文件系统几何、设备大小和当前 xfsprogs 实现,不应假设每次扩容都产生固定数量的 AG。

5.5 XFS 通常不能缩容

XFS 的常规在线操作支持扩容,但不支持把已有文件系统直接在线缩小。原因不是简单修改一个容量字段,而是需要:

  • 移动位于尾部区域的所有数据和元数据;
  • 重建 AG;
  • 处理日志、inode、空闲空间和引用关系;
  • 在不中断访问的情况下保持一致性。

生产中如果必须缩小容量,通常流程是:

  1. 创建新的较小 XFS 文件系统;
  2. 使用 xfsdump/xfsrestore 或其他经过验证的迁移方法复制数据;
  3. 校验权限、ACL、xattr、配额和应用一致性;
  4. 切换挂载点或设备;
  5. 回收旧文件系统。

直接对分区做缩小操作而不迁移文件系统,会截断 XFS 的有效区域,属于高风险破坏操作。


六、XFS 的挂载恢复和 xfs_repair

6.1 挂载时会先尝试日志恢复

如果系统上次崩溃时 XFS 处于挂载状态,重新挂载时内核通常会读取日志并重放已提交事务。常见现象是:

XFS (sdb1): Mounting V5 Filesystem
XFS (sdb1): Starting recovery (logdev: internal)
XFS (sdb1): Ending recovery (logdev: internal)

这类日志恢复是正常启动路径,不等于文件系统已经发生不可修复损坏。

如果日志设备、底层存储或元数据本身出现严重错误,挂载可能失败,并出现 I/O error、日志错误或 metadata corruption 等信息。

6.2 xfs_repair 解决什么问题

xfs_repair 是离线文件系统修复工具,用于检查和修复 XFS 元数据结构,例如:

  • AG superblock;
  • inode 分配信息;
  • 空闲空间 B+ 树;
  • inode B+ 树;
  • 目录和属性结构;
  • 文件 extent 引用;
  • 链接计数和孤儿 inode;
  • 配额相关元数据。

它不是“恢复所有丢失文件内容”的工具。文件数据块已经被覆盖、设备返回错误或应用本身写入了错误内容时,修复工具无法凭空还原原始数据。

6.3 正确的离线检查流程

假设文件系统设备为 /dev/mapper/vg0-lv_data,挂载点为 /data

先确认挂载状态:

findmnt /data
lsblk -f

卸载:

sudo umount /data

如果提示 busy,不要直接强制操作。先查找打开文件:

sudo fuser -vm /data
sudo lsof +f -- /data

先做只读检查:

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

-n 表示不修改文件系统。输出中的 “would” 或类似措辞表示工具认为需要修改,但实际没有写入。

如果确认需要修复,再执行:

sudo xfs_repair /dev/mapper/vg0-lv_data

修复完成后重新挂载:

sudo mount /data
dmesg -T | tail -n 100
df -hT /data

还应检查应用层数据、文件数量、权限、ACL 和关键校验和。文件系统结构恢复正常,不等于业务数据一定完整。

6.4 为什么不能对已挂载文件系统运行修复

xfs_repair 需要对 inode、空闲空间树和目录结构做全局判断。文件系统仍被内核和应用使用时:

  • inode 可能继续变化;
  • 脏页可能继续写回;
  • 日志可能继续提交;
  • 修复工具看到的状态不是稳定快照。

因此,对生产挂载点直接运行修复可能造成进一步损坏。应从救援系统、单用户环境或离线维护环境启动,并确保目标文件系统未挂载。

6.5 xfs_repair -L 的风险

当日志损坏或无法回放时,某些场景下可以使用:

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

-L 会清空或强制重建日志,使修复流程能够继续。它的代价是丢弃日志中尚未完成恢复的元数据事务。

因此:

  • -L 不是普通修复的加速选项;
  • 它可能导致最近创建的文件、目录项或元数据更新丢失;
  • 使用前应先保留块设备级快照或镜像;
  • 应记录内核错误、设备错误和修复输出;
  • 如果数据重要,优先复制故障现场并在副本上尝试。

如果底层设备仍有读错误,先处理存储故障。对一个持续返回 I/O error 的设备反复运行修复,可能把原本可恢复的数据变成不可恢复。

6.6 xfs_scrub 与离线修复的区别

现代 XFS 可以使用 xfs_scrub 对在线文件系统进行一致性检查。它会通过文件系统接口检查部分元数据,并在支持的情况下验证数据和元数据校验信息。

示例:

sudo xfs_scrub -n /data

这里的 -n 表示只检查、不进行修复动作;具体可用选项应以本机 man xfs_scrub 为准,因为发行版版本差异较明显。

xfs_scrubxfs_repair 的边界不同:

  • xfs_scrub 面向在线检查,适合发现潜在问题;
  • xfs_repair 面向离线全局修复;
  • xfs_scrub 不能替代设备级备份;
  • 某些深度损坏仍需要卸载后运行 xfs_repair

6.7 修复后的 lost+found

修复过程中,如果工具发现 inode 存在但无法通过目录树找到,可能会把它们连接到 lost+found。这些条目可能缺少原始路径信息。

恢复时可以检查:

find /data/lost+found -maxdepth 1 -ls
file /data/lost+found/*

但不要仅凭文件名判断业务意义。应结合:

  • inode 号;
  • 文件大小和时间;
  • 内容校验和;
  • 应用数据库;
  • 备份;
  • 文件内部格式。

七、大文件:逻辑大小、物理占用和 extent 树

7.1 XFS 的大文件能力来自多层设计

XFS 支持大文件,不是因为 inode 中存放了一个“超大连续地址”。大文件能力来自:

  • 64 位文件偏移和块编号;
  • extent 描述连续块范围;
  • extent B+ 树扩展映射数量;
  • 多 AG 提供更大的地址空间;
  • 延迟分配提高大写入形成大 extent 的机会;
  • 稀疏文件避免为未写区域分配物理块。

实际最大文件大小受多个条件共同限制:

文件最大大小
≤ min(
    XFS 格式和内核支持的上限,
    文件系统总地址空间限制,
    文件系统块大小对应的寻址范围,
    应用和系统调用使用的偏移类型限制,
    配额和可用空间限制
)

因此不能脱离具体文件系统几何、内核、架构和工具版本,简单宣称一个对所有系统都适用的最大值。

7.2 普通大文件的 extent 映射

假设一个 10 GiB 文件由三个 extent 组成:

逻辑偏移 0 GiB       -> 物理 extent A,长度 4 GiB
逻辑偏移 4 GiB       -> 物理 extent B,长度 3 GiB
逻辑偏移 7 GiB       -> 物理 extent C,长度 3 GiB

inode 可以直接保存少量 extent;当 extent 数量增加到超过 inode 内部空间时,XFS 会使用 extent B+ 树存放映射。

这带来两个边界:

  • 文件大小很大,不代表 extent 数量很多;
  • 文件大小不大,也可能因为频繁随机写造成大量碎片和 extent。

7.3 稀疏文件:lsdu 为什么不同

创建一个逻辑大小为 1 TiB、但只实际写入 4 KiB 的稀疏文件:

truncate -s 1T /data/sparse.img
printf x | dd of=/data/sparse.img bs=1 count=1 conv=notrunc status=none

查看:

ls -lh /data/sparse.img
du -h /data/sparse.img
du -h --apparent-size /data/sparse.img

预期关系是:

  • ls -lh 显示接近 1 TiB 的逻辑大小;
  • du -h 显示接近实际已分配块的大小;
  • du --apparent-size 再次显示接近 1 TiB。

原因是文件的逻辑地址空间包含一个巨大空洞,但空洞没有对应的物理 extent。读取空洞时,文件系统返回零;写入空洞中的位置时,XFS 才需要分配实际块。

复制时还要注意工具行为:

cp --sparse=always /data/sparse.img /backup/sparse.img

不同复制方式可能把空洞变成真实零块,使目标文件占用大量空间。备份工具是否保留 sparse、xattr、ACL、reflink 和项目配额信息,必须单独验证。

7.4 预分配与 fallocate

对大文件进行顺序写入前,可以预分配空间:

fallocate -l 100G /data/test.bin

这通常会让文件逻辑大小和已分配空间同时增加,但不同文件系统特性下可能使用预分配或 unwritten extent。所谓 unwritten extent 表示物理空间已经保留,但在真正写入前读取仍应返回零。

预分配的作用包括:

  • 提前暴露空间不足;
  • 降低运行期间频繁分配的压力;
  • 改善大文件连续性。

风险也很明确:

  • 预分配会立即消耗可用空间;
  • 业务若最终不写满,空间可能长期被占用;
  • 与配额、项目配额和保留空间共同作用;
  • 文件系统空间足够不代表底层阵列性能足够。

7.5 reflink 和写时复制

支持 reflink 的 XFS 可以让两个文件共享同一组数据块:

cp --reflink=always source.img clone.img

初始复制主要建立共享引用关系,而不是立即复制所有数据。之后修改其中一个文件时,XFS 执行写时复制:

共享 extent
   │
   ├── 文件 A 修改
   │
   ▼
分配新 extent
   │
   ▼
写入新数据
   │
   ▼
更新 A 的映射和引用计数

这依赖引用计数等元数据。它的好处是节省复制初期的 I/O 和空间,代价是:

  • 后续写入可能触发额外 COW;
  • 快照或克隆过多时,引用关系更复杂;
  • 备份工具必须明确支持 reflink 语义;
  • 不能把两个 reflink 文件当成完全独立的物理副本。

可以检查文件系统是否启用了 reflink:

xfs_info /data | grep -o 'reflink=[01]'

具体输出形式依发行版而异;若没有该字段,应查看完整 xfs_info 输出和本机 mkfs.xfs 文档。


八、目录、配额与大文件的资源边界

大文件运行失败不一定是 df 显示空间不足。还可能受到:

  • 用户配额;
  • 项目配额;
  • inode 配额;
  • 文件系统保留空间;
  • 单进程打开文件限制;
  • 应用使用 32 位文件偏移接口;
  • 目录项、xattr 或 reflink 元数据压力。

查看文件系统基本状态:

df -hT /data
df -i /data
xfs_info /data

查看挂载选项:

findmnt -no TARGET,SOURCE,FSTYPE,OPTIONS /data

对于 XFS 配额,应以实际挂载选项和本机工具为准,例如检查:

mount | grep ' /data '
sudo xfs_quota -x -c 'state' /data

如果启用了用户、组或项目配额,一个文件可能在全局空间仍充足时写入失败。诊断时应同时记录:

df -h       -> 全局数据块
df -i       -> inode
配额状态    -> 用户/组/项目限制
应用错误    -> errno,例如 EDQUOT、ENOSPC、EIO
内核日志    -> 文件系统或设备错误

ENOSPC 也不只意味着“磁盘百分比达到 100%”。它可能表示:

  • 数据块耗尽;
  • inode 耗尽;
  • 配额耗尽;
  • 某类元数据无法分配;
  • 日志或保留空间相关限制;
  • 底层设备无法继续提供可用块。

九、故障诊断:先区分容量、元数据、设备和应用问题

9.1 第一步:确认对象关系

findmnt -T /data
lsblk -f
xfs_info /data
df -hT /data
df -i /data

这组命令分别回答:

  • /data 实际挂载了什么设备;
  • 设备上是什么文件系统;
  • XFS 的 AG、日志和块大小如何;
  • 数据块和 inode 使用率如何。

9.2 第二步:检查内核和设备错误

dmesg -T | grep -Ei 'xfs|I/O error|blk_update|nvme|scsi|corrupt'
journalctl -k -b

如果同时出现:

I/O error
XFS metadata corruption
reset controller
medium error

优先怀疑底层设备、控制器、链路或 RAID,而不是立即运行修复。

设备健康信息应使用对应工具,例如 NVMe:

sudo nvme smart-log /dev/nvme0

SATA/SAS 设备可能使用:

sudo smartctl -a /dev/sdb

工具是否可用、设备路径如何识别,取决于硬件和发行版。

9.3 第三步:区分逻辑损坏和内容损坏

  • 目录无法遍历、无法挂载、日志无法回放:偏向文件系统元数据问题;
  • 文件能打开但内容校验不对:可能是应用、设备或数据本身问题;
  • 读文件返回 EIO:优先检查底层设备;
  • 写入返回 ENOSPC:检查全局空间、inode、配额和元数据;
  • 重启后最近文件丢失:检查应用是否调用 fsync,不要简单归因于 XFS 日志失败。

十、生产操作中的验证和恢复原则

扩容前

findmnt -T /data
lsblk -f
sudo lvs
df -hT /data

确认:

  • 操作的是正确 LV、分区或虚拟磁盘;
  • 目标挂载点确实是 XFS;
  • 底层设备已经完成扩容;
  • 监控和回滚方案可用。

扩容后

df -hT /data
xfs_info /data
sudo touch /data/.grow-test
sudo rm /data/.grow-test

touch 只能验证基本读写,并不能证明所有数据区域都可安全写入。生产环境还应观察内核日志、LVM 状态和设备健康。

修复前

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

同时保留:

  • 原始设备快照或块级副本;
  • dmesgjournalctl -k 输出;
  • xfs_repair -n 的完整输出;
  • 业务备份和恢复记录。

修复后

不要只看命令返回码。应检查:

sudo mount /data
findmnt /data
df -hT /data
sudo journalctl -k -b | grep -i xfs

再根据业务检查文件数量、权限、ACL、xattr、数据库一致性和关键文件校验和。


十一、几个必须避免的误解

误解一:XFS 有 Journal,所以 write() 成功就等于断电不丢数据

错误。Journal 主要保护元数据一致性;普通文件数据通常由 Page Cache 和 writeback 管理。需要持久化语义时,应用必须设计并调用适当的同步操作。

误解二:AG 就是物理磁盘分区

错误。AG 是 XFS 内部的空间管理区域,可能跨越底层设备范围,也不直接对应分区或 LVM extent。

误解三:设备扩容后 df 自动变大

错误。设备、分区/LVM 和 XFS 文件系统是不同层。XFS 必须显式执行 xfs_growfs

误解四:xfs_repair -L 是普通修复命令

错误。它会丢弃无法恢复的日志事务,可能造成最近元数据更新丢失。应在副本或快照上优先尝试。

误解五:df 有很多剩余空间,就一定能创建超大文件

错误。还要考虑 extent 连续性、配额、inode、元数据、保留空间和底层 I/O 错误。稀疏文件和预分配文件对空间统计的表现也不同。

误解六:XFS 可以像某些文件系统一样直接缩小

通常不能。需要迁移到新的较小文件系统,而不是直接缩分区或修改设备末端。


XFS 的核心不是某个单独命令,而是 AG、extent、日志、延迟分配和在线几何变化之间的协作。AG 把空间管理拆成可并行区域;Journal 让元数据更新具备崩溃恢复路径;延迟分配改善大写入的空间布局;xfs_growfs 让新增设备空间进入 AG 管理范围;xfs_repair 则在离线状态下重建和校正这些元数据关系。

当问题发生时,正确的诊断顺序应当是:先确认层次和挂载关系,再区分容量、配额、日志、元数据和设备错误,最后选择扩容、日志恢复、在线检查、离线修复或数据迁移。这样才能避免把“文件系统空间不足”误判成“磁盘坏了”,也避免用一次高风险修复覆盖原本仍可恢复的数据。


系列导航与关联阅读

官方资料

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