土法炼钢兴趣小组的算法知识备份

【存储工程】XFS 架构:大文件与高并发

文章导航

分类入口
storage
标签入口
#xfs#filesystem#allocation-groups#btree#reflink#cow

目录

一、XFS 设计哲学

1.1 起源:SGI IRIX 与大规模存储

XFS 诞生于 1993 年的硅谷图形公司(Silicon Graphics, Inc.),最初运行在 IRIX 操作系统上。 SGI 的核心业务是高性能计算和影视后期制作,客户需要处理的文件动辄几十 GB 甚至数 TB。 当时主流的 EFS(Extent File System)在面对这类工作负载时已经力不从心:元数据操作串行、单线程分配、文件大小受限。 XFS 就是为了解决这些问题而设计的——从第一天起,它的目标就是「大文件、高并发、可扩展」。

1994 年,XFS 在 IRIX 5.3 上正式发布。2001 年,SGI 将 XFS 移植到 Linux 内核(2.4 系列),并以 GPL 协议开源。 此后 XFS 在 Linux 社区持续演进,逐步成为企业级工作负载的首选文件系统之一。 2014 年起,Red Hat Enterprise Linux 7 将 XFS 设为默认文件系统(Default File System),这一决定延续至今。

1.2 核心设计原则

XFS 的设计围绕四个核心原则展开:

可扩展性(Scalability)。文件系统的元数据结构必须能随磁盘容量线性扩展,不能在容量翻倍时出现性能悬崖。 XFS 通过分配组(Allocation Group)实现元数据的水平分区,允许多个 CPU 同时操作不同区域的元数据。

大文件友好。XFS 从设计之初就使用 64 位地址空间,理论上支持的最大文件大小为 8 EiB,最大卷大小同样为 8 EiB。 作为对比,ext4 的最大卷大小为 1 EiB,最大文件大小为 16 TiB。

高效的元数据管理。所有元数据结构——inode 索引、空闲空间、区段映射——都使用 B+树(B+Tree)组织。 B+树 在有序查找、范围扫描和插入删除方面都有 O(log n) 的时间复杂度,适合管理数百万乃至数十亿条记录。

日志保护。XFS 使用元数据日志(Metadata Journal)保证崩溃一致性。 只有元数据写入日志,数据本身不写日志,这在保障一致性的同时避免了日志带来的写放大。

1.3 与 ext4 的定位差异

ext4 是一个通用文件系统,擅长处理大量小文件的日常工作负载,启动速度快,工具链成熟。 XFS 的设计侧重点不同:它在大文件顺序 I/O、高并发元数据操作、大容量卷管理方面有结构性优势。

下面这张表总结了两者的关键差异:

维度 ext4 XFS
最大卷大小 1 EiB 8 EiB
最大文件大小 16 TiB 8 EiB
元数据结构 H-Tree(目录)+ 区段树 全面 B+树
并行分配 单一块组锁 分配组级别并行
在线缩容 支持 不支持
默认日志模式 ordered ordered
reflink 支持 是(内核 4.9 起)

XFS 不支持在线缩容(Online Shrink),这是它的一个已知限制。 如果预计卷大小会频繁变化,需要在部署前考虑这一点。

1.4 XFS 在 Linux 内核中的位置

XFS 的内核代码位于 fs/xfs/ 目录下,代码量约 15 万行(Linux 6.5)。 它通过 VFS(Virtual File System)层向上提供标准的 POSIX 文件操作接口,向下通过块设备层(Block Layer)与存储设备交互。 XFS 在 VFS 和块设备层之间实现了自己的一套完整元数据管理和空间分配逻辑。

用户态应用程序
     │
     ▼
┌──────────────────────┐
│  VFS 层              │
│  open/read/write/... │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│  XFS 文件系统层       │
│  ├─ 分配组管理       │
│  ├─ B+树元数据       │
│  ├─ 日志子系统       │
│  └─ 延迟分配引擎     │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│  块设备层            │
│  bio → request       │
└──────────┬───────────┘
           │
           ▼
      存储设备

二、Allocation Groups 并行设计

2.1 分配组的基本概念

分配组(Allocation Group,AG)是 XFS 实现并行化的核心机制。 mkfs.xfs 在格式化时将整个卷(Volume)划分为若干等大的 AG,每个 AG 独立管理自己的 inode、空闲空间和区段映射。 默认情况下,AG 的大小由 mkfs.xfs 根据卷大小自动计算,通常在 256 MiB 到 1 TiB 之间。 AG 的数量通常在 4 到 数千个之间,取决于卷的总容量。

# 查看已有 XFS 文件系统的 AG 数量和大小
xfs_info /dev/sda1

典型输出如下:

meta-data=/dev/sda1        isize=512    agcount=4, agsize=65536 blks
         =                 sectsz=512   attr=2, projid32bit=1
         =                 crc=1        finobt=1, sparse=1, rmapbt=0
         =                 reflink=1    bigtime=1 inobtcount=1 nrext64=0
data     =                 bsize=4096   blocks=262144, imaxpct=25
         =                 sunit=0      swidth=0 blks
naming   =version 2        bsize=4096   ascii-ci=0, ftype=1
log      =internal         bsize=4096   blocks=16384, version=2
         =                 sectsz=512   sunit=0 blks, lazy-count=1
realtime =none             extsz=4096   blocks=0, rtextents=0

输出中 agcount=4 表示有 4 个分配组,agsize=65536 blks 表示每个 AG 包含 65536 个块。

2.2 AG 的内部结构

每个 AG 都有一套完整且独立的元数据结构,可以看作一个「迷你文件系统」。 AG 内部的布局从磁盘偏移 0 开始,依次排列如下:

AG 内部布局:
┌──────────────────────────────────────────────┐
│  超级块副本(Superblock Copy)                │  扇区 0
├──────────────────────────────────────────────┤
│  AGF(AG Free Space Header)                 │  扇区 1
├──────────────────────────────────────────────┤
│  AGI(AG Inode Header)                      │  扇区 2
├──────────────────────────────────────────────┤
│  AGFL(AG Free List)                        │  扇区 3
├──────────────────────────────────────────────┤
│  B+树根节点区域                               │
│  ├─ 按块号排序的空闲空间 B+树(bnobt)        │
│  ├─ 按大小排序的空闲空间 B+树(cntbt)        │
│  ├─ inode B+树(inobt)                      │
│  ├─ 空闲 inode B+树(finobt)                │
│  └─ 反向映射 B+树(rmapbt,可选)             │
├──────────────────────────────────────────────┤
│  数据区域                                     │
│  (inode 块、数据块、空闲块混合分布)          │
└──────────────────────────────────────────────┘

超级块副本。每个 AG 都保存一份超级块(Superblock)的副本。 如果 AG 0 的主超级块损坏,xfs_repair 可以从其他 AG 的副本中恢复。

AGF(AG Free Space Header)。记录该 AG 的空闲空间统计信息和两棵空闲空间 B+树的根节点位置。

AGI(AG Inode Header)。记录该 AG 的 inode 分配信息和 inode B+树的根节点位置。

AGFL(AG Free List)。保留一小部分预留块,供 B+树 在分裂或合并时使用,避免在元数据操作的关键路径上再去做空间分配。

2.3 并行分配机制

AG 设计的核心价值在于并行性。 每个 AG 有自己独立的锁(Per-AG Lock),当多个线程同时创建文件或分配磁盘空间时,只要它们落在不同的 AG 上,就不会产生锁竞争。

XFS 的 inode 分配器会尽量在目标目录所在的 AG 中分配新 inode,同时也会考虑各 AG 的负载均衡。 具体策略定义在内核源码 fs/xfs/xfs_ialloc.c 中的 xfs_ialloc_ag_select() 函数里。 简化后的选择逻辑如下:

inode 分配的 AG 选择逻辑:

1. 优先选择父目录所在的 AG
2. 如果该 AG 的空闲 inode 不足,则轮转到下一个 AG
3. 每个 AG 维护一个 inode 计数,避免单个 AG 过载
4. 最终选择空闲 inode 最多的 AG

这种设计在高并发场景下效果明显。 例如,一台多核服务器上运行着数十个进程同时写日志文件。 如果使用 ext4,所有进程在分配 inode 时会竞争同一把全局锁。 而在 XFS 上,这些进程的 inode 分配会分散到不同的 AG 上,锁竞争显著降低。

2.4 AG 数量的选择

AG 数量并非越多越好。过多的 AG 会导致:

mkfs.xfs 的默认策略已经经过仔细调优,大多数场景下不需要手动调整。 如果确实需要,可以通过 -d agcount=N-d agsize=N 参数指定:

# 手动指定 AG 大小为 1 TiB
mkfs.xfs -d agsize=1099511627776 /dev/sda1

# 手动指定 AG 数量为 8
mkfs.xfs -d agcount=8 /dev/sda1

三、B+Tree 元数据管理

3.1 为什么选择 B+树

文件系统的元数据管理本质上是一个索引问题:给定一个逻辑文件偏移,快速找到对应的物理块号;给定一段空闲空间需求,快速找到满足条件的连续区间。

B+树 是解决这类问题的经典数据结构。它的核心特性如下:

XFS 在元数据管理中全面使用 B+树,包括以下几种:

B+树类型 键值 用途
bnobt 起始块号 按块号排序的空闲空间索引
cntbt 区段大小 按大小排序的空闲空间索引
inobt inode 号 已分配 inode 的索引
finobt inode 号 有空闲槽位的 inode 块索引
bmbt 文件逻辑偏移 文件区段映射
rmapbt 物理块号 反向映射(物理块→owner)
refcntbt 物理块号 引用计数(reflink 使用)

3.2 空闲空间 B+树

每个 AG 维护两棵空闲空间 B+树(Free Space B+Tree),它们索引的是同一份数据,但排序方式不同:

这种双 B+树 设计的好处在于:无论是按位置查找还是按大小查找,都能在 O(log n) 时间内完成,不需要遍历整个空闲空间列表。

空闲空间双 B+树 结构示意:

bnobt(按块号排序)              cntbt(按大小排序)
┌────────────┐                  ┌────────────┐
│   根节点    │                  │   根节点    │
└─────┬──────┘                  └─────┬──────┘
      │                               │
  ┌───┴───┐                       ┌───┴───┐
  ▼       ▼                       ▼       ▼
┌────┐ ┌────┐                  ┌────┐ ┌────┐
│叶1 │→│叶2 │                  │叶1 │→│叶2 │
└────┘ └────┘                  └────┘ └────┘

叶节点内容:                    叶节点内容:
  块100, 长度5                    长度2, 块500
  块200, 长度8                    长度5, 块100
  块500, 长度2                    长度8, 块200

当需要分配一段连续空间时,XFS 的空间分配器(Space Allocator)根据分配策略选择使用哪棵树:

3.3 Inode B+树

每个 AG 维护一棵 inode B+树(inobt),记录该 AG 中所有已分配的 inode 块(Inode Chunk)。 XFS 将 inode 按 64 个一组分配,称为一个 inode 块。 inobt 的叶节点记录每个 inode 块的状态:块的起始位置和一个 64 位位图(Bitmap),指示哪些 inode 槽位已分配、哪些空闲。

从内核 3.16 起,XFS 引入了空闲 inode B+树(finobt,Free Inode B+Tree)。 finobt 只索引那些至少有一个空闲槽位的 inode 块。 它的作用是加速 inode 分配:当需要分配新 inode 时,不必遍历整棵 inobt 去寻找有空闲槽位的块,直接查 finobt 即可。

3.4 文件区段 B+树

每个文件的区段映射(Extent Map)存储在 inode 的数据叉(Data Fork)中。 XFS 使用一种分层策略来管理区段映射:

内联格式(Extent List)。当文件的区段数量较少(通常 ≤ 19 个区段,取决于 inode 大小)时,区段记录直接存储在 inode 内部。 每条区段记录(Extent Record)占 128 位,格式如下:

区段记录格式(128 位):
┌──────┬────────────────────┬────────────────────┬──────────────┐
│ flag │  逻辑偏移(54位)   │  物理块号(52位)   │ 块数(21位) │
│ 1位  │  文件内偏移         │  磁盘上的起始块     │ 区段长度     │
└──────┴────────────────────┴────────────────────┴──────────────┘

其中 flag 位标识该区段是否为「未写入」状态(Unwritten Extent),用于预分配。

B+树格式(BMap B+Tree)。当文件的区段数量超过 inode 能容纳的上限时,XFS 将区段映射转换为 B+树。 B+树 的根节点存储在 inode 内部,叶节点存储在外部磁盘块上。 每个叶节点包含多条区段记录,以文件逻辑偏移为键排序。

这种设计使得小文件(只有几个区段)不需要额外的磁盘 I/O 来读取区段映射——一切都在 inode 内部完成。 而大文件(可能有数百万个区段)则能通过 B+树 高效索引,避免线性扫描。

3.5 反向映射 B+树

反向映射 B+树(Reverse Mapping B+Tree,rmapbt)是 XFS v5 格式引入的可选特性。 它记录的是物理块到所属者(Owner)的映射:给定一个物理块号,rmapbt 能告诉你这个块属于哪个文件的哪个偏移,或者属于哪个元数据结构。

rmapbt 的主要用途包括:

启用 rmapbt 会增加约 1-2% 的空间开销和少量写入开销,但在数据保护方面的价值通常值得这个代价。

# 格式化时启用反向映射 B+树
mkfs.xfs -m rmapbt=1 /dev/sda1

3.6 引用计数 B+树

引用计数 B+树(Reference Count B+Tree,refcntbt)用于支持 reflink 特性。 当多个文件共享同一物理区段时,refcntbt 记录每个共享区段的引用计数。 只有当引用计数降为 0 时,物理空间才会被真正释放。

refcntbt 的叶节点格式如下:

refcntbt 叶节点记录:
┌────────────────┬──────────────┬──────────────┐
│ 起始物理块号    │ 块数          │ 引用计数      │
└────────────────┴──────────────┴──────────────┘

四、日志设计

4.1 元数据日志的基本原理

XFS 使用预写日志(Write-Ahead Log,WAL)来保证崩溃一致性(Crash Consistency)。 核心规则很简单:任何元数据修改,必须先写入日志,然后才能写入元数据的最终位置。 如果系统在中途崩溃,重新挂载时内核会重放(Replay)日志中的记录,将元数据恢复到一致状态。

XFS 默认只记录元数据的日志,不记录数据。这是 ordered 模式(也是 ext4 的默认模式)。 在这种模式下,内核保证:在元数据日志提交之前,相关的数据已经写入磁盘。 这样做的好处是避免了数据日志带来的写放大——对于大文件顺序写入场景,这一点尤为重要。

4.2 日志结构

XFS 的日志由连续的日志记录(Log Record)组成,每条日志记录包含一个或多个日志项(Log Item)。 日志记录的头部包含序列号(Log Sequence Number,LSN),用于在恢复时确定记录的先后顺序。

日志是一个环形缓冲区(Circular Buffer)。新的日志记录追加到尾部,已经提交并刷盘的记录可以被覆盖。 日志的头部位置(Head)和尾部位置(Tail)由超级块中的 LSN 字段追踪。

日志环形缓冲区:

        tail                    head
         │                       │
         ▼                       ▼
┌────────┬───────┬───────┬───────┬────────┐
│已释放  │ rec 5 │ rec 6 │ rec 7 │ 已释放  │
│(可覆盖)│      │       │       │(可覆盖)│
└────────┴───────┴───────┴───────┴────────┘

4.3 日志事务与预留

XFS 的日志系统使用事务(Transaction)模型。 一个事务包含一组逻辑上相关的元数据修改,要么全部提交,要么全部回滚。 在事务开始前,XFS 会在日志中预留(Reserve)足够的空间,确保事务在写入过程中不会因为日志空间不足而失败。

预留机制的一个重要细节是:XFS 使用两阶段提交(Two-Phase Commit)。 第一阶段将日志项写入内存中的日志缓冲区(In-Core Log Buffer,iclog)。 第二阶段将 iclog 刷写到磁盘上的日志设备。

事务提交流程:

1. 事务开始 → 预留日志空间
2. 修改内存中的元数据
3. 将修改记录写入 iclog(内存)
4. 事务提交 → iclog 标记为待刷写
5. iclog 满或显式 fsync → 刷写到磁盘日志
6. 日志落盘后 → 元数据可以写入最终位置
7. 元数据落盘后 → 日志空间可回收

4.4 日志条带单元

在使用 RAID 阵列(RAID Array)时,XFS 允许将日志的写入粒度对齐到 RAID 条带单元大小(Log Stripe Unit)。 这样可以避免日志写入跨越条带边界,减少 RAID 层面的读-改-写(Read-Modify-Write)操作。

# 格式化时指定日志条带单元,单位为字节
mkfs.xfs -l su=65536 /dev/sda1

# 挂载时指定日志写入大小
mount -o logbsize=256k /dev/sda1 /mnt/data

logbsize 挂载选项控制内存中日志缓冲区的大小。 较大的 logbsize 可以减少日志刷写的频率,但会增加崩溃后需要重放的数据量。 对于写入密集的工作负载,将 logbsize 设为 256k 通常是一个合理的起点。

4.5 外部日志设备

XFS 支持将日志放置在独立的块设备上(External Log Device),通常是一块高速 SSD 或 NVMe 设备。 这种配置将日志 I/O 和数据 I/O 分离到不同的设备上,避免两者之间的 I/O 竞争。

# 格式化时指定外部日志设备
mkfs.xfs -l logdev=/dev/nvme0n1 /dev/sda1

# 挂载时指定外部日志设备
mount -o logdev=/dev/nvme0n1 /dev/sda1 /mnt/data

外部日志设备在以下场景中特别有价值:

4.6 日志大小规划

日志太小会导致事务频繁等待日志空间回收,影响写入吞吐量。 日志太大则浪费磁盘空间,并且崩溃恢复时间变长。

mkfs.xfs 的默认日志大小已经针对卷大小做了合理计算。 如果需要手动调整,可以参考以下经验值:

卷大小 推荐日志大小
< 1 TiB 默认即可(通常 64-128 MiB)
1-10 TiB 128-512 MiB
> 10 TiB 512 MiB - 2 GiB
# 手动指定日志大小为 512 MiB
mkfs.xfs -l size=512m /dev/sda1

五、延迟分配与预分配

5.1 延迟分配

延迟分配(Delayed Allocation,delalloc)是 XFS 的一个关键性能优化。 当应用程序调用 write() 写入数据时,XFS 并不立即分配物理磁盘块。 相反,它只是在内存中记录「这段数据需要 N 个块」,并将数据暂存在页缓存(Page Cache)中。 真正的物理块分配推迟到数据实际刷写(Writeback)到磁盘时才执行。

这种设计有三个好处:

减少碎片。延迟分配允许 XFS 在真正分配时看到完整的写入模式。 例如,应用程序可能通过多次小的 write() 调用写入一个大文件。 如果每次 write() 都立即分配,分配器只能看到每次请求的几个块,难以做出全局最优的分配决策。 延迟到 writeback 时,分配器能看到积累的全部待分配数据,从而分配更大的连续区段。

减少元数据操作。多次小写入合并为一次大分配,减少了 B+树 的插入操作和日志写入。

支持取消分配。如果数据在刷写前被删除或截断,就不需要分配和释放物理块,减少了不必要的 I/O。

5.2 预分配机制

XFS 的推测性预分配(Speculative Preallocation)在延迟分配的基础上更进一步。 当文件正在增长时,XFS 会预测文件的最终大小,并提前预留比当前需要更多的空间。

预分配的大小策略定义在内核源码 fs/xfs/xfs_iomap.c 中。 简化后的逻辑如下:

预分配大小计算(简化):

如果文件大小 < 64 KiB:
    预分配 = 文件当前大小的 2 倍
如果文件大小在 64 KiB 到 1 GiB 之间:
    预分配 = 逐步增加,最大到 64 MiB
如果文件大小 > 1 GiB:
    预分配 = 64 MiB(默认上限)

预分配的区段在 inode 中标记为「未写入」(Unwritten),这意味着:

5.3 allocsize 挂载选项

allocsize 挂载选项允许管理员覆盖 XFS 的默认预分配大小上限:

# 设置预分配大小上限为 1 GiB
mount -o allocsize=1g /dev/sda1 /mnt/data

较大的 allocsize 适用于:

较大的 allocsize 的代价:

从内核 4.13 起,XFS 的推测性预分配默认行为改为动态模式(Dynamic Speculative Preallocation),自动根据文件增长模式调整预分配大小,大多数场景下不再需要手动设置 allocsize

5.4 fallocate 与显式预分配

除了 XFS 的自动预分配之外,应用程序还可以通过 fallocate() 系统调用显式预分配空间:

#include <fcntl.h>

int fd = open("/mnt/data/bigfile", O_WRONLY | O_CREAT, 0644);

// 预分配 10 GiB 空间,不写入数据
fallocate(fd, 0, 0, 10LL * 1024 * 1024 * 1024);

fallocate() 在 XFS 上的实现非常高效:它直接操作区段 B+树,在元数据层面标记一段连续空间为「未写入」,不需要实际写入数据。 这在需要一次性预留大量空间的场景中(如数据库预扩展表空间文件)非常有用。

XFS 还支持 fallocate()FALLOC_FL_INSERT_RANGEFALLOC_FL_COLLAPSE_RANGE 标志,允许在文件中间插入或移除区段,无需复制数据。

引用链接(Reflink)是 XFS 从内核 4.9 开始支持的特性,允许多个文件共享同一物理数据块,而不需要实际复制数据。 这在概念上类似于硬链接(Hard Link),但硬链接共享的是 inode,而 reflink 共享的是数据区段。 两个通过 reflink 创建的文件拥有独立的 inode 和独立的元数据,只是它们的区段记录指向相同的物理块。

reflink 文件共享示意:

文件 A 的 inode              文件 B 的 inode
├─ 区段 [0, 100) → 块 1000  ├─ 区段 [0, 100) → 块 1000  ← 共享
├─ 区段 [100, 200) → 块 2000 ├─ 区段 [100, 200) → 块 2000 ← 共享
└─ 区段 [200, 300) → 块 3000 └─ 区段 [200, 250) → 块 5000 ← 独立

refcntbt 记录:
  块 1000, 长度 100, 引用计数 = 2
  块 2000, 长度 100, 引用计数 = 2
  块 3000, 长度 100, 引用计数 = 1
  块 5000, 长度 50,  引用计数 = 1

6.2 写时复制

当任何一方修改共享的数据块时,XFS 触发写时复制(Copy-on-Write,CoW)操作:

  1. 分配新的物理块;
  2. 将原始数据复制到新块中;
  3. 在新块上执行修改;
  4. 更新修改方的区段映射,指向新块;
  5. 将原始块的引用计数减 1。

这个过程对应用程序完全透明。CoW 操作只发生在实际写入时,读取共享数据不会触发复制。

GNU coreutils 从 8.24 版本起支持 --reflink 选项,可以利用文件系统的 reflink 能力创建文件的即时副本:

# 创建 reflink 副本(接近瞬时完成)
cp --reflink=always source.img dest.img

# 自动检测文件系统是否支持 reflink
cp --reflink=auto source.img dest.img

reflink 复制与普通复制的性能差异极其显著。 对于一个 100 GiB 的文件,普通 cp 需要数分钟完成数据复制。 cp --reflink 只需要复制元数据(区段映射和引用计数),通常在毫秒级别完成。

6.4 使用 duperemove 进行去重

duperemove 是一个用户态工具,能扫描文件系统中的重复数据块,并利用 reflink 将它们去重:

# 安装 duperemove
yum install duperemove  # RHEL/CentOS
apt install duperemove  # Debian/Ubuntu

# 扫描并去重指定目录
duperemove -dr /mnt/data/vm-images/

-d 参数表示执行去重操作(不仅仅是扫描),-r 表示递归处理子目录。

duperemove 的工作原理:

  1. 将文件分割为固定大小的块(默认 128 KiB);
  2. 计算每个块的校验和(Hash);
  3. 找到校验和相同的块对;
  4. 对校验和相同的块进行逐字节比较确认;
  5. 使用 FIDEDUPERANGE ioctl 调用让文件系统执行 reflink 去重。

去重后,重复数据块只保留一份物理副本,多个文件通过 reflink 共享。 后续写入任何共享块时,CoW 机制自动保证数据独立性。

reflink 可以实现文件级别的快照(Snapshot)语义,无需卷管理器(Volume Manager)的支持。 对于虚拟机镜像管理、容器存储等场景,这种文件级快照非常实用:

# 为虚拟机磁盘创建快照
cp --reflink=always vm-disk.qcow2 vm-disk-snap-$(date +%Y%m%d).qcow2

与 LVM 快照(LVM Snapshot)相比,reflink 快照有以下优势:

reflink 需要在 mkfs.xfs 格式化时启用。在 xfsprogs 4.17 及以后的版本中,reflink 默认启用。

# 显式启用 reflink(通常不需要,默认已启用)
mkfs.xfs -m reflink=1 /dev/sda1

# 检查现有文件系统是否启用了 reflink
xfs_info /dev/sda1 | grep reflink

注意:reflink 与 DAX(Direct Access)模式不兼容。 如果需要在持久内存(Persistent Memory)设备上使用 DAX 模式,不能同时启用 reflink。

七、实时子卷(Realtime Subvolume)

7.1 实时子卷的用途

实时子卷(Realtime Subvolume,RT)是 XFS 从 IRIX 时代继承的特性。 它允许将文件系统分为两个部分:常规数据区域和实时数据区域。 实时区域使用独立的存储设备(RT Device),采用固定大小的区段分配策略,为对带宽(Bandwidth)有严格要求的工作负载提供可预测的 I/O 性能。

在 SGI 的原始应用场景中,RT 子卷用于影视后期的视频流采集和回放。 这类工作负载要求在指定时间窗口内完成固定量的 I/O,不允许因元数据操作或空间碎片导致的延迟毛刺。

7.2 RT 子卷的工作方式

RT 子卷的空间管理与常规数据区域不同:

固定大小区段的好处是分配和释放都是 O(1) 操作,不涉及树的分裂或合并,延迟可预测。

# 格式化时指定 RT 设备和 RT 区段大小
mkfs.xfs -r rtdev=/dev/nvme1n1,extsize=1m /dev/sda1

# 挂载时指定 RT 设备
mount -o rtdev=/dev/nvme1n1 /dev/sda1 /mnt/data

7.3 将文件分配到 RT 子卷

要将文件分配到 RT 子卷,需要在文件的 inode 标志中设置 XFS_XFLAG_REALTIME。 可以通过 xfs_io 工具完成:

# 创建文件并标记为实时文件
xfs_io -f -c "chattr +rt" /mnt/data/video-stream.raw

# 查看文件的 RT 标志
xfs_io -c "lsattr" /mnt/data/video-stream.raw

也可以通过设置目录的继承属性(Inherit Attribute),让该目录下新创建的文件自动分配到 RT 子卷:

# 设置目录的 RT 继承标志
xfs_io -c "chattr +rt" /mnt/data/video/

7.4 RT 子卷的限制

RT 子卷在 Linux 上的使用有一些限制需要注意:

在现代 Linux 环境中,RT 子卷的使用场景已经比较小众。 大多数高带宽 I/O 需求可以通过 NVMe SSD、合理的预分配和 I/O 调度器调优来满足,不一定需要 RT 子卷。

八、XFS 诊断工具

8.1 xfs_info:查看文件系统参数

xfs_info 是最常用的 XFS 诊断工具,用于查看已挂载文件系统的格式化参数:

xfs_info /mnt/data

输出会显示块大小、AG 数量和大小、inode 大小、日志大小、是否启用了 reflink、rmapbt 等特性。 在排查问题时,xfs_info 通常是第一个要执行的命令。

8.2 xfs_db:低级元数据调试

xfs_db 是 XFS 的低级调试工具,可以直接读取和修改磁盘上的元数据结构。 它只能在未挂载的文件系统上使用(或以只读模式操作已挂载的文件系统)。

常见用法:

# 以只读模式打开文件系统
xfs_db -r /dev/sda1

# 查看超级块信息
xfs_db> sb 0
xfs_db> p

# 查看 AG 0 的空闲空间头
xfs_db> agf 0
xfs_db> p

# 查看指定 inode 的信息
xfs_db> inode 131
xfs_db> p

# 查看 inode 的区段映射
xfs_db> bmap

xfs_db 还可以用于统计文件系统的碎片程度:

# 统计空闲空间碎片
xfs_db -r -c "frag -f" /dev/sda1

8.3 xfs_repair:文件系统修复

xfs_repair 用于检查和修复不一致的 XFS 文件系统。它必须在文件系统未挂载的状态下运行。

# 标准修复
xfs_repair /dev/sda1

# 仅检查,不修复
xfs_repair -n /dev/sda1

# 如果日志损坏,先清零日志再修复
xfs_repair -L /dev/sda1

xfs_repair 的工作流程:

  1. 阶段 1:读取并验证超级块。如果主超级块损坏,尝试从其他 AG 的副本恢复。
  2. 阶段 2:扫描并验证所有 AG 的元数据结构——空闲空间 B+树、inode B+树等。
  3. 阶段 3:扫描所有 inode,验证区段映射、目录结构。
  4. 阶段 4:检查目录树的连通性,修复孤立 inode。
  5. 阶段 5:重建 AG 级别的空闲空间 B+树。
  6. 阶段 6:重建 AG 级别的 inode B+树。
  7. 阶段 7:最终验证和写回。

-L 参数(清零日志)应该作为最后手段使用。它会丢弃日志中未提交的事务,可能导致最近修改的元数据丢失。

8.4 xfs_fsr:在线碎片整理

xfs_fsr(XFS File System Reorganizer)是 XFS 的在线碎片整理(Online Defragmentation)工具。 它通过重新分配文件的物理区段来减少碎片,工作原理是:

  1. 创建一个临时文件;
  2. 将原文件的数据以连续方式写入临时文件;
  3. 交换原文件和临时文件的区段映射;
  4. 删除临时文件。
# 对整个文件系统执行碎片整理
xfs_fsr /mnt/data

# 只对指定文件执行碎片整理
xfs_fsr /mnt/data/database/data.ibd

# 设置碎片整理的运行时间限制(秒)
xfs_fsr -t 3600 /mnt/data

检查文件的碎片程度:

# 使用 filefrag 查看文件的区段分布
filefrag -v /mnt/data/database/data.ibd

输出示例:

Filesystem type is: 58465342
File size of /mnt/data/database/data.ibd is 10737418240 (2621440 blocks of 4096 bytes)
 ext:     logical_offset:        physical_offset: length:   expected: flags:
   0:        0..  524287:     262144..    786431: 524288:
   1:   524288.. 1048575:    1048576..   1572863: 524288:    786432:
   2:  1048576.. 1572863:    2097152..   2621439: 524288:   1572864:
   3:  1572864.. 2097151:    3145728..   3670015: 524288:   2621440:
   4:  2097152.. 2621439:    4194304..   4718591: 524288:   3670016: last,eof
/mnt/data/database/data.ibd: 5 extents found

5 个区段对于一个 10 GiB 的文件来说碎片程度极低,通常不需要整理。

8.5 xfs_scrub:在线一致性检查

xfs_scrub 是较新的工具(内核 4.15 起),可以在文件系统挂载状态下执行一致性检查:

# 对已挂载的文件系统执行在线检查
xfs_scrub /mnt/data

# 检查并尝试自动修复
xfs_scrub -y /mnt/data

xfs_scrub 利用反向映射 B+树(rmapbt)交叉验证元数据的一致性。 因此,在启用了 rmapbt 的文件系统上,xfs_scrub 的检查能力更强。

九、XFS 性能调优

9.1 mkfs.xfs 格式化参数

格式化参数在文件系统的整个生命周期中都无法更改,因此需要在格式化时仔细规划。

块大小

XFS 的默认块大小(Block Size)为 4096 字节,与大多数 Linux 发行版的页大小一致。 对于主要存储大文件的场景,可以考虑使用更大的块大小来减少元数据开销:

# 使用 64 KiB 块大小(仅适用于 64 KiB 页大小的内核,如 ARM64 某些配置)
mkfs.xfs -b size=65536 /dev/sda1

注意:块大小不能超过系统页大小。在 x86_64 的标准 4 KiB 页大小内核上,块大小最大为 4 KiB。

Inode 大小

XFS 的默认 inode 大小为 512 字节(ext4 为 256 字节)。 更大的 inode 可以存储更多的内联区段记录和扩展属性(Extended Attribute),减少间接块的使用:

# 使用 1024 字节的 inode
mkfs.xfs -i size=1024 /dev/sda1

对于扩展属性使用较多的场景(如 SELinux、ACL、Samba),较大的 inode 有助于将这些属性存储在 inode 内部,避免额外的磁盘 I/O。

RAID 对齐

在 RAID 阵列上格式化 XFS 时,正确设置条带单元(Stripe Unit)和条带宽度(Stripe Width)至关重要:

# 假设 RAID 10 阵列:4 块盘,每块盘条带大小 64 KiB
# su = 条带单元大小,sw = 数据盘数量
mkfs.xfs -d su=64k,sw=4 /dev/md0

su(Stripe Unit)是单块磁盘上的条带大小。 sw(Stripe Width)是数据盘的数量(不含校验盘)。 正确设置这两个参数后,XFS 的空间分配器会尽量按条带宽度的倍数分配区段,最大化顺序 I/O 性能。

目录块大小

目录块大小(Directory Block Size)影响目录操作的性能。 默认值等于文件系统块大小。对于包含大量小文件的目录(如邮件服务器的 Maildir),可以增大目录块大小:

# 设置目录块大小为 64 KiB
mkfs.xfs -n size=65536 /dev/sda1

9.2 挂载选项调优

inode64

inode64 挂载选项允许 XFS 在整个卷的所有 AG 中分配 inode,而不是仅在卷的前 1 TiB 范围内。 在大容量卷上,这可以更好地利用所有 AG 的并行能力:

mount -o inode64 /dev/sda1 /mnt/data

在 64 位系统上,inode64 从内核 3.7 起默认启用。 如果需要与某些 32 位 NFS 客户端兼容,可以使用 inode32 回退到旧行为。

noatime 与 relatime

noatime 禁止更新文件的访问时间戳(Access Time),减少不必要的元数据写入:

mount -o noatime /dev/sda1 /mnt/data

relatime(Relative Access Time)是一个折中方案:只有当访问时间早于修改时间时才更新。 大多数 Linux 发行版默认使用 relatime。 对于读密集的工作负载(如静态文件服务器),noatime 可以进一步减少 I/O。

logbsize

logbsize 控制内存中日志缓冲区的大小,影响日志写入效率:

# 设置日志缓冲区大小为 256 KiB
mount -o logbsize=256k /dev/sda1 /mnt/data

合法值包括 32k、64k、128k、256k。 较大的值可以减少日志刷写次数,适合写入密集的工作负载。 如果文件系统使用了日志条带单元(-l su=),logbsize 必须是条带单元的整数倍。

allocsize

前面在延迟分配章节已经讨论过。在内核 4.13 之前的版本上,对于大文件写入场景,显式设置 allocsize 可能有帮助:

mount -o allocsize=1g /dev/sda1 /mnt/data

9.3 数据库工作负载调优

XFS 是许多数据库系统推荐的底层文件系统。以下是针对数据库工作负载的具体调优建议。

MySQL / InnoDB

InnoDB 使用大文件存储表空间数据,XFS 的大文件支持和预分配能力与 InnoDB 配合良好。

推荐配置:

# mkfs 参数
mkfs.xfs -f -d su=64k,sw=4 -l su=64k,size=512m /dev/sda1

# mount 参数
mount -o noatime,logbsize=256k,allocsize=64m /dev/sda1 /mnt/mysql

InnoDB 的 innodb_flush_method 建议设为 O_DIRECT,绕过页缓存,避免双重缓冲。

PostgreSQL

PostgreSQL 使用大量 8 KiB 的页文件,每个表和索引都对应一个或多个文件。

推荐配置:

# mkfs 参数
mkfs.xfs -f -d su=64k,sw=4 /dev/sda1

# mount 参数
mount -o noatime,logbsize=256k /dev/sda1 /mnt/pgdata

PostgreSQL 的 full_page_writes 参数与 XFS 日志之间存在交互: XFS 的元数据日志不保护 PostgreSQL 的数据页,因此 full_page_writes 应保持为 on(默认值)。

通用建议

9.4 I/O 调度器配合

XFS 本身不涉及 I/O 调度,但正确选择 I/O 调度器(I/O Scheduler)可以进一步提升性能。

对于 NVMe SSD,推荐使用 none(无调度器),因为 NVMe 设备有自己的硬件队列管理:

echo none > /sys/block/nvme0n1/queue/scheduler

对于 HDD 或 SATA SSD,mq-deadline 通常是好的选择:

echo mq-deadline > /sys/block/sda/queue/scheduler

9.5 调优参数速查表

参数 作用 推荐值 适用场景
mkfs -d su=X,sw=Y RAID 条带对齐 与硬件一致 RAID 阵列
mkfs -l size=X 日志大小 128m-2g 写入密集
mkfs -i size=X inode 大小 512(默认) 通常无需调整
mount -o noatime 禁用访问时间 启用 读密集
mount -o logbsize=X 日志缓冲区 256k 写入密集
mount -o allocsize=X 预分配上限 默认 大文件顺序写
mount -o inode64 64 位 inode 默认启用 大容量卷

十、XFS 在生产环境

10.1 RHEL 默认文件系统

从 Red Hat Enterprise Linux 7 开始,XFS 取代 ext4 成为默认文件系统。 Red Hat 做出这一决定的理由在官方文档中有明确说明:

RHEL 上的 XFS 最大支持卷大小为 500 TiB(受限于 Red Hat 的支持策略,而非文件系统本身的技术限制)。

SUSE Linux Enterprise Server(SLES)默认使用 Btrfs,但也完整支持 XFS。 Debian 和 Ubuntu 默认使用 ext4,但 XFS 在这些发行版上同样是一等公民。

10.2 数据库工作负载

XFS 是 MySQL、PostgreSQL、MongoDB、Cassandra 等主流数据库的推荐文件系统之一。

数据库工作负载对文件系统的核心需求包括:

XFS 在这四个维度上都有良好的表现。 特别是延迟分配和预分配机制,使得数据库文件的区段连续性通常优于 ext4。

10.3 虚拟化与容器存储

在虚拟化环境中,XFS 的 reflink 能力为虚拟机镜像管理提供了有效支持。

典型用法:维护一个基础镜像(Golden Image),通过 cp --reflink 为每台虚拟机创建独立的磁盘文件。 这些文件在创建时几乎不占用额外空间,只有当虚拟机修改数据时才通过 CoW 分配新的物理块。

# 基础镜像
/mnt/vm-pool/base-centos8.qcow2   # 10 GiB

# 为 50 台虚拟机创建 reflink 副本
for i in $(seq 1 50); do
    cp --reflink=always base-centos8.qcow2 vm-${i}.qcow2
done

50 个 reflink 副本的元数据开销通常只有几 MiB,而普通复制需要 500 GiB 的磁盘空间。

在容器环境中,Podman 和 CRI-O 的 overlay 存储驱动(Overlay Storage Driver)在 XFS 上运行时可以利用 reflink 加速镜像层的复制操作。

10.4 大规模部署经验

监控指标

在生产环境中运行 XFS 时,建议监控以下指标:

# AG 级别的空闲空间统计
xfs_spaceman -c "freesp -s" /mnt/data

# 文件系统级别的计数器
cat /sys/fs/xfs/sda1/stats/stats

/sys/fs/xfs/<device>/stats/stats 文件提供丰富的运行时统计信息,包括:

指标 含义
xs_write_calls write 系统调用次数
xs_read_calls read 系统调用次数
xs_log_writes 日志写入次数
xs_log_blocks 日志写入的块数
xs_iflush_count inode 刷写次数
xs_abt_lookup 空闲空间 B+树 查找次数
xs_blk_alloc 块分配请求次数

这些指标可以导入 Prometheus 或 Grafana 进行长期监控。

容量规划

XFS 不支持在线缩容,因此容量规划需要前瞻性思考:

# 在线扩容(底层设备已经扩容后)
xfs_growfs /mnt/data

备份策略

XFS 提供专用的备份和恢复工具:

# 使用 xfsdump 进行完整备份
xfsdump -l 0 -f /backup/full.dump /mnt/data

# 使用 xfsdump 进行增量备份(级别 1)
xfsdump -l 1 -f /backup/incr.dump /mnt/data

# 从备份恢复
xfsrestore -f /backup/full.dump /mnt/restore

xfsdump 理解 XFS 的内部结构,能正确处理扩展属性、ACL 和多流数据叉。 相比通用的 tarrsyncxfsdump 在备份和恢复大量文件时效率更高。

10.5 XFS 与其他文件系统的选型

场景 推荐文件系统 理由
通用桌面 ext4 成熟稳定,工具链完善,支持在线缩容
大文件存储 XFS 大文件性能好,区段管理高效
数据库 XFS 或 ext4 两者都可以,XFS 在大文件和并发方面略优
需要文件级快照 XFS(reflink) 无需卷管理器,操作简单
需要数据校验和 Btrfs 或 ZFS XFS 不提供数据校验和
需要在线缩容 ext4 XFS 不支持在线缩容
NFS 服务器 XFS 大容量、高并发场景下表现好

10.6 XFS 的演进方向

XFS 社区在持续推进几个重要方向:

在线修复(Online Repair)。xfs_scrub 已经具备在线检查能力,社区正在开发在线修复功能,目标是在不卸载文件系统的情况下修复检测到的不一致。相关代码的主要维护者是 Darrick J. Wong。

大区段计数(Large Extent Counters)。传统 XFS inode 中的区段计数字段只有 31 位,限制了单个文件最多约 21 亿个区段。nrext64 特性将区段计数扩展到 64 位,消除了这一限制。该特性在内核 5.19 中合入。

父指针(Parent Pointers)。为每个 inode 记录其父目录的信息,使 xfs_repair 能在不扫描整棵目录树的情况下重建目录结构。该特性在内核 6.7 中合入。

原子文件交换(Atomic File Swap)。支持两个文件的区段映射原子性交换,为应用程序提供安全的文件替换机制,避免数据丢失窗口。

参考文献

规范与设计文档

  1. XFS 官方文档,“XFS Filesystem Disk Structures”,https://xfs.wiki.kernel.org/。XFS 磁盘格式的权威参考,详细描述了超级块、AG 结构、B+树 格式。

  2. Linux 内核文档,“XFS Filesystem”,https://docs.kernel.org/filesystems/xfs.html。包含挂载选项、sysfs 接口和管理操作的说明。

  3. Adam Sweeney 等,“Scalability in the XFS File System”,USENIX Annual Technical Conference,1996。XFS 的原始设计论文,阐述了 AG 并行设计和 B+树 元数据管理的动机与实现。

源码

  1. Linux 内核源码,fs/xfs/xfs_ialloc.c:inode 分配器实现,包括 AG 选择逻辑。可在 https://elixir.bootlin.com/linux/latest/source/fs/xfs/xfs_ialloc.c 查阅。

  2. Linux 内核源码,fs/xfs/xfs_iomap.c:I/O 映射和预分配逻辑。可在 https://elixir.bootlin.com/linux/latest/source/fs/xfs/xfs_iomap.c 查阅。

  3. Linux 内核源码,fs/xfs/xfs_alloc.c:空间分配器核心实现,包括 bnobt 和 cntbt 操作。可在 https://elixir.bootlin.com/linux/latest/source/fs/xfs/xfs_alloc.c 查阅。

  4. Linux 内核源码,fs/xfs/xfs_log.c:日志子系统核心实现。可在 https://elixir.bootlin.com/linux/latest/source/fs/xfs/xfs_log.c 查阅。

  5. Linux 内核源码,fs/xfs/xfs_reflink.c:reflink 和 CoW 实现。可在 https://elixir.bootlin.com/linux/latest/source/fs/xfs/xfs_reflink.c 查阅。

书籍与演讲

  1. Darrick J. Wong,“XFS: Recent and Future Adventures in Filesystem Evolution”,Linux Plumbers Conference,2019。介绍了 rmapbt、reflink 和在线修复的设计与进展。

  2. Christoph Hellwig,“XFS: The Big Storage File System for Linux”,Linux Foundation Collaboration Summit,2012。概述了 XFS 在 Linux 上的架构和性能特性。

  3. Red Hat,“Managing file systems”,RHEL 9 Documentation,https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/managing_file_systems/。包含 XFS 的管理和调优建议。

工具

  1. xfsprogs 项目,包含 mkfs.xfsxfs_infoxfs_dbxfs_repairxfs_fsr 等工具。源码仓库:https://git.kernel.org/pub/scm/fs/xfs/xfsprogs-dev.git/

  2. duperemove 项目,文件级去重工具,支持 XFS 和 Btrfs 的 reflink 去重。项目地址:https://github.com/markfasheh/duperemove

  3. xfsdump 项目,XFS 专用的备份和恢复工具。源码仓库:https://git.kernel.org/pub/scm/fs/xfs/xfsdump-dev.git/


上一篇: ext4 架构与调优 下一篇: Btrfs:写时复制文件系统

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · storage

存储工程索引

汇总本站存储工程系列文章,覆盖 HDD、SSD、NVMe、持久内存、索引结构、压缩、分布式存储与对象存储。

2025-09-02 · storage

【存储工程】存储引擎概览:堆文件到 LSM-Tree 的演化路径

数据库系统的架构可以划分为两大层:上层的查询处理层负责解析 SQL、生成执行计划、优化查询;下层的存储引擎(Storage Engine)负责把数据持久化到磁盘,并在需要时高效地把数据取回来。查询处理层决定"做什么",存储引擎决定"怎么存、怎么取"。同一个查询处理层可以对接不同的存储引擎——MySQL 的 InnoDB…

2025-09-03 · storage

【存储工程】B-Tree 与 B+Tree:页式存储引擎的工程实现

数据库要把数据存到磁盘上,又要以尽可能少的磁盘 I/O 把数据找回来。这个矛盾催生了一系列面向磁盘的索引结构(Disk-oriented Index),其中最成功的就是 B-Tree 家族。从 1970 年 Rudolf Bayer 和 Edward McCreight 在波音科学研究实验室提出 B-Tree 起,这个…

2025-09-07 · storage

【存储工程】索引结构:从 B+Tree 到倒排索引

数据库里存了一亿行数据,要找出 userid 42 的那一行。没有索引的做法是全表扫描(Full Table Scan)——从第一个数据页读到最后一个数据页,逐行比对。假设每个数据页 16 KB,一亿行占 20 GB,即使顺序读能跑到 500 MB/s,也需要 40 秒。加一个 B+Tree 索引,三次磁盘 I/O 就…


By .