XFS 文件系统
一、起源:为什么需要一个新的文件系统
1990 年代初,Silicon Graphics(SGI)的 IRIX 系统需要应对大型多处理器工作站和专业图形工作站上不断增长的磁盘阵列容量。
彼时 SGI 使用的 EFS(Extent File System)已触及多项硬性上限:
| 限制项 | EFS | XFS 的目标 |
|---|---|---|
| 单文件系统最大容量 | 8 GB | 突破此限制 |
| 单文件最大尺寸 | 8 GB | 突破此限制 |
| I/O 带宽 | 无法充分利用磁盘阵列 | 线性扩展 |
XFS 的设计目标是从第一天起就面向大容量和高并发。
这份设计文档在 1996 年的 USENIX 会议上发表,标题为 “Scalability in the XFS File System”,
由 Adam Sweeney、Doug Doubley、Gregg H. Luddington、Bob Collins 和 David N. Cole 执笔。
2001 年 SGI 将 XFS 开源,2002 年进入 Linux 主线内核,
2014 年成为 Red Hat Enterprise Linux 7 和 CentOS 7 的默认文件系统。
至今 XFS 仍是处理 PB 级数据的头部选择。
二、核心设计哲学:三大支柱
纵观 XFS 的架构设计,可以提炼出三条贯穿始终的哲学线索:
2.1 无共享(Share-Nothing)并行化
传统单文件系统在全局锁下运作:所有 CPU 核心争抢同一把锁,所有磁盘 I/O 排队经过同一个瓶颈。
XFS 的解法是将磁盘划分为多个分配组(Allocation Group, AG),每个 AG 持有自己的锁、元数据结构和空闲空间管理。
线程 A 在 AG 0 创建文件时,线程 B 可以在 AG 3 同步分配块——两者互不阻塞。
2.2 B+ 树统治一切
XFS 几乎只用一种数据结构来组织所有元数据:
空闲空间追踪、inode 分配、目录索引、extent 映射、块反向映射、引用计数……全部由 B+ 树驱动。
这种极致复用降低了代码复杂度(整个 B+ 树实现在 xfs_btree.c 一个文件中),
同时借助 B+ 树的天然平衡性保证了各操作的确定性时间复杂度。
2.3 写时优化(Write-Time Optimization)
XFS 尽可能延迟具体的物理决策——写入时不立即分配块,而是将数据暂存在页缓存中,
等到后台刷盘时刻再以全局视野做出最优决策(延迟分配)。
这与传统文件系统的”写入即分配”形成了鲜明对比。
三、物理布局:分配组(AG)
3.1 AG 的结构
XFS 将 64 位的磁盘地址空间划分为若干等大小的区域,每个区域为一个 AG。
每个 AG 包含以下元数据头结构:
┌─────────────────────────────────────────────────────────┐
│ Superblock (SB) │ 块大小、AG 数量、UUID、根 inode │
├───────────────────────┼────────────────────────────────┤
│ AGF (Free Space) │ BNO/CNT 树根、RMAP 树根 │
├───────────────────────┼────────────────────────────────┤
│ AGI (Inode Header) │ INO/FINO 树根 │
├───────────────────────┼────────────────────────────────┤
│ AGFL (Free List) │ 元数据增长的预留块缓冲 │
├───────────────────────┴────────────────────────────────┤
│ Data Blocks │
└─────────────────────────────────────────────────────────┘
- AG 0 的超级块位于磁盘起始处(没有独立的 boot sector),其余 AG 的超级块为镜像副本
- 每个 AG 大小范围 16 MB ~ 1 TB;默认最小 4 个 AG
- 最后一个 AG 可能是不满的 “runt”(残余)AG
3.2 AG 独立性的意义
关键观察:AG 之间”不知道彼此的存在”。AG 3 的 AGF 不知道 AG 0 中有哪些块被占用。
这意味着:
- 分配 inode 和分配数据块可以完全并行
- 空闲空间搜索不需要遍历全局 bitmap
- 崩溃恢复可以按 AG 独立进行
SGI 论文中指出,这是 XFS 能够在 64 路 SGI Challenge 服务器上跑满磁盘阵列带宽的核心原因。
3.3 AG 大小与并行性的权衡
| AG 大小 | AG 数量 | 并行度 | 空闲空间池大小 |
|---|---|---|---|
| 较小(如 1 GB) | 多 | 高 | 碎片化风险 |
| 较大(如 64 GB) | 少 | 低 | 更易找到大连续空间 |
现代建议使用 1 GB 左右的 AG 大小作为平衡点。
四、B+ 树:无所不在的底层结构
4.1 XFS 中的八棵 B+ 树
| 树名 | 全称 | 索引键 | 用途 |
|---|---|---|---|
| BNO | Block NO | 块号 | 按起始块号追踪空闲区间 |
| CNT | CoUNT | 长度 | 按区间大小追踪空闲空间 |
| INO | INode | inode 号 | 追踪已分配的 inode chunk |
| FINO | Free INode | inode 号 | 仅追踪含空闲 inode 的 chunk |
| RMAP | Reverse MAP | 物理块号 | 块 → 所有者的反向映射 |
| REFCOUNT | Reference Count | 物理块号 | 共享块的引用计数 |
| 目录 B+ 树 | — | 文件名哈希 | 目录项按哈希值索引 |
| Extent B+ 树 | — | 文件内偏移 | 大文件的 extent 映射 |
4.2 BNO 与 CNT 的双索引策略
这是 XFS 设计中极为精妙的一笔:同一份空闲区间数据,由两棵 B+ 树以不同排序方式索引。
空闲区间: [start=1000, len=8] [start=2000, len=16] [start=500, len=4]
BNO 树排序: [500][1000][2000] ← 用于"精确分配"和"近邻分配"
CNT 树排序: [4][8][16] ← 用于"大小优先分配"
三种分配策略:
- Exact(精确):查找特定起始块附近的空间 → 查 BNO 树
- Near(近邻):扩展已有文件 → BNO + CNT 并行搜索,BNO 通常胜出
- Size(大小优先):新建文件,不在乎位置 → 查 CNT 树找第一个足够大的区间
4.3 B+ 树条目大小分析
不同功能存储的字段数量不同,导致条目大小差异显著:
| 特性 | 叶子节点最小/最大条目数 | 内部节点最小/最大条目数 |
|---|---|---|
| finobt | 126 / 252 | 252 / 505 |
| reflink | 168 / 336 | 252 / 505 |
| rmap | 84 / 168 | 45 / 91 |
这直接决定了各特性的元数据预留空间大小(详见第七节)。
五、inode 与目录的存储机制
5.1 inode 结构
每个 inode 包含:
- 标准元数据:大小、时间戳、UID/GID、权限
- Data Fork:主内容(extent 映射或目录项)
- Attribute Fork:ACL、扩展属性
一条重要规则:普通文件即使只有几字节内容,也永远使用 extent 映射,
绝不会将数据内联进 inode。小型目录和短符号链接则可以内联。
inode 以 64 个为一组(chunk)分配。现代 CRC XFS 每个 inode 512 字节,每 chunk 占用 8 个 4 KB 块(32 KB)。
5.2 大型目录的 B+ 树优化
| 目录规模 | 存储方式 |
|---|---|
| 少量条目 | 直接内联在 inode 中 |
| 中等规模 | 块级线性格式 |
| 大型目录(百万级条目) | 哈希索引的 B+ 树 |
B+ 树按文件名哈希值排序,查找百万级条目平均只需约 20 次比较(而非线性扫描百万次)。
六、延迟分配与推测式预分配
6.1 延迟分配(Delayed Allocation)
传统文件系统的写入路径:
write() → 分配块 → 写入数据 → 刷盘
XFS 的延迟分配路径:
write() → 数据进入页缓存 → 标记为 "delayed" → 预留空间计数
↓(后台刷盘时)
已知完整写入大小 → 选择最优块位置 → 一次性分配连续 extent
核心洞察:后台刷盘时,XFS 看见了完整的写入尺寸,
可以站在全局视野分配最优的连续空间,而非在写入进行中盲人摸象。
6.2 推测式预分配(Speculative Preallocation)
对于顺序追加工作负载(数据库日志、视频写入),XFS 主动分配超出当前文件大小的空间:
起始: 64 KB
每次翻倍: 64 KB → 128 KB → 256 KB → ... → 8 GB(上限)
5 分钟未增长: 后台扫描器回收未用空间
6.3 实际效果
这两项机制协同工作,使 XFS 能够:
- 极大减少碎片化
- 对顺序写入文件保持极佳的物理连续性
- 一个 1 GB 的顺序写入,最终可能只产生 1~2 个 extent 记录
七、日志系统:高效的写前日志
7.1 XFS 日志的设计原则
只记录元数据,不记录用户数据。
这一选择基于两个事实:
- 元数据体积远小于用户数据,日志可以很小
- 元数据损坏比数据损坏更危险(文件描述符断裂、引用计数错误),值得优先保证一致性
7.2 CIL + AIL 两阶段架构
Transaction A ─┐
Transaction B ─┼─→ [CIL (内存)] ──→ Journal (磁盘) ──→ [AIL] ──→ 最终位置
Transaction C ─┘ ↑ 积累到阈值后批量提交 ↑ 后台线程异步推送
日志空间回收: 仅在 AIL 项目到达最终位置后才能进行
CIL(Committed Item List):
- 内存中的暂存区,多个事务在此积累
- 达到阈值后,作为单一检查点一次性写入日志
- 效果:原本 1000 次小写入变成 1 次大写入,日志 I/O 次数大幅降低
AIL(Active Item List):
- 后台线程按时间顺序推动最旧的项目到达最终位置
- 确保崩溃恢复时只需重放少量操作
7.3 性能数据
原创论文的测试结果:
| 基准测试 | 成绩 |
|---|---|
| Datamation sort | 3.52 秒(当时顶尖) |
| MinuteSort | 1.6 GB |
| LADDIS NFS 基准 | 8806 SPECnfs ops/sec |
| 峰值吞吐量 | 300+ MB/s |
崩溃恢复时间:通常 < 15 秒,无论文件系统多大。
八、块空间开销
8.1 困惑的来源
空文件系统为何 df 显示已用?
运维人员常发现:新创建的 XFS 文件系统,du 显示 0 字节使用,但 df 显示 2~8% 已用。
这是正常且预期的行为。
8.2 固定元数据开销(每 AG)
| 组件 | 512 字节扇区 | 4096 字节扇区 |
|---|---|---|
| SB/AGI/AGF/AGFL 共享块 | 1 块 | 各占独立块(+3) |
| inode B+ 树根节点 | 1 块 | 1 块 |
| BNO/CNT 树根节点 | 2 块 | 2 块 |
| AGFL 块 | 4 块 | 4 块 |
| 小计(每 AG) | 8 块 | 11 块 |
8.3 特性开启后的元数据预留
XFS 为每种特性预留”最坏情况”的空间——假设整个 AG 都被该特性填满。
需要预留足够的 B+ 树节点来描述这种场景。
| 特性 | 预留键基础 | 10 GB AG 需块数 | 说明 |
|---|---|---|---|
| finobt | 每含空闲 inode 的 chunk 一条记录 | ~329 块/AG | 加速空闲 inode 搜索 |
| reflink | 每 4 KB 块一条记录 | ~1,960 块/AG | 追踪共享块(COW 克隆) |
| rmap | 每已分配块范围一条记录 | ~3,992 块/AG | 反向映射(在线修复) |
8.4 privileged 事务预留
| 文件系统大小 | 预留量 |
|---|---|
| < 640 MB | 总块数的 5% |
| ≥ 640 MB | 固定 8192 块 |
这确保了磁盘将满时,删除文件等元数据操作不会因无法分配事务日志空间而失败。
8.5 基准数据
8 AG、CRC 模式、全特性开启:
| 文件系统大小 | df 显示已用 | 百分比 |
|---|---|---|
| 512 MB | 9,153 块 | ~8% |
| 1 GB | 13,384 块 | ~6% |
| 10 GB | 58,568 块 | ~3% |
| 100 GB | 510,304 块 | ~2% |
| 1000 GB | 5,027,760 块 | ~2% |
结论:文件系统越大,元数据开销占比越小。
九、反向映射(RMAP)与 reflink
9.1 RMAP B+ 树
传统文件系统的映射:inode → 物理块
RMAP 提供的反向:物理块 → inode(所有者)
每条 RMAP 记录的结构:
[startblock, blockcount, owner inode#, offset]
这使 XFS 能够:
- 在线修复文件系统(无需卸载)
- 以文件名和路径报告错误(而非原始块号)
- 实现 reflink 和 COW 克隆
9.2 reflink:秒级文件克隆
cp --reflink=always huge_file.dat clone.dat
执行流程:
- 克隆文件的 extent 指针指向原始物理块(无数据复制)
- refcount B+ 树将受影响块的引用计数 +1
- 写入克隆文件时,检测到共享块(refcount > 1)
- 为修改部分分配新块,旧块 refcount -1
- 两份文件互不影响
实际效果:克隆 1 TB 文件在秒级完成,不消耗额外磁盘 I/O,不占用额外空间(直到任一文件被修改)。
十、完整文件写入路径追踪
以创建并写入一个 1 MB 文件为例,追踪 XFS 的完整执行路径:
┌─────────────────────────────────────────────────────────────┐
│ 步骤 1: inode 分配 │
│ │
│ 选择 AG(参考父目录位置) │
│ ↓ │
│ 查 FINO 树 → 找到含空闲槽位的 inode chunk │
│ ↓ │
│ 分配 inode → 添加目录项到父目录 B+ 树 │
│ ↓ │
│ 记录到 CIL(待提交日志) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 步骤 2: 数据写入 │
│ │
│ 数据写入页缓存(非磁盘) │
│ ↓ │
│ extent 标记为 "delayed"(延迟分配) │
│ ↓ │
│ 全局空闲空间计数器中预留 256 块 │
│ ⚠ 此时未分配任何具体磁盘块 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 步骤 3: 刷盘时的最优分配 │
│ │
│ 选 inode 所在 AG(优先近邻) │
│ ↓ │
│ BNO + CNT 树并行搜索 → 找到 inode 附近 256 个连续块 │
│ ↓ │
│ 更新 BNO 树和 CNT 树 │
│ 在 RMAP 树中记录所有权 │
│ 将 delayed extent 转为真实 extent │
│ ↓ │
│ 记录到 CIL │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 步骤 4: 日志提交 │
│ │
│ CIL 积累达到阈值 → 批量写入单一检查点到日志 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 步骤 5: 异步持久化 │
│ │
│ AIL 后台线程推动数据到最终磁盘位置 │
│ ↓ │
│ 到达最终位置后 → 释放对应日志空间 │
└─────────────────────────────────────────────────────────────┘
十一、可扩展性限制对比
| 限制项 | ext3 | ext4 | XFS |
|---|---|---|---|
| 单文件系统最大容量 | 16 TiB | 16 TiB | 16 EiB |
| 单文件最大尺寸 | 2 TiB | 16 TiB | 8 EiB |
| 单 extent 最大尺寸 | 4 KiB | 128 MiB | 8 GiB |
| 最大目录条目数 | ~10 万 | 约百万 | 数十亿 |
| 最大文件数 | 约 20 亿 | 约 40 亿 | 2^32 |
十二、设计权衡与适用场景
XFS 的优势场景
| 场景 | XFS 的优势 |
|---|---|
| 大文件顺序写入 | 延迟分配 + 推测式预分配 → 极佳物理连续性 |
| 高并发多线程写入 | AG 独立锁 → 近乎线性扩展 |
| PB 级文件系统 | 16 EiB 寻址能力,无结构性瓶颈 |
| 需要在线修复 | RMAP 支持,无需卸载即可自愈 |
| COW 克隆需求 | reflink 秒级克隆,零 I/O 开销 |
| 需要快速恢复 | CIL 优化 → 任意大小文件系统 <15 秒恢复 |
XFS 的劣势场景
| 场景 | 问题 |
|---|---|
| 大量小文件(< 4 KB) | B+ 树元数据开销相对较高 |
| 需要缩减文件系统 | 无官方 shrink 工具(需备份+重建) |
| 极简嵌入式环境 | ~20 万行内核代码,资源开销大 |
| 跨平台兼容性优先 | 原生 Linux/Open-source,Windows 不支持 |
十三、相关内核代码
Linux 内核中 XFS 代码位于 fs/xfs/:
| 文件 | 职责 |
|---|---|
xfs_alloc.c |
AG 分配策略(exact/near/size) |
xfs_bmap.c |
extent 映射管理 |
xfs_btree.c |
通用 B+ 树实现(所有树共用) |
xfs_log.c |
CIL/AIL/日志子系统 |
xfs_rmap.c |
反向映射 B+ 树 |
xfs_refcount.c |
reflink 引用计数 |
libxfs/ |
内核与用户空间共享代码 |
结语:二十余年的工程哲学
XFS 诞生于 1993 年的 SGI 实验室,跨越了三十年计算史:
从 IRIX 工作站到 Linux 数据中心,从 IDE 硬盘到 NVMe 全闪存阵列。
它之所以至今仍保持旺盛生命力,是因为其设计者做出了一个关键赌注:存储系统的未来在于并行性。
1996 年的论文标题 “Scalability in the XFS File System” 精准地预判了时代的走向。
当整个行业从单核走向多核、从 GB 级存储走向 PB 级数据湖时,XFS 的 AG 独立并行架构不仅没有过时,反而成了理所当然的选择。
最值得玩味的设计:
BNO 树和 CNT 树用两套排序方式索引同一份空闲空间数据——一个看似浪费的设计选择,却让三种分配策略都能以最优路径执行。
这种”冗余换效率”的思路,贯穿 XFS 的每一个子系统(CIL 批量提交、延迟分配、reflink COW),或许正是 XFS 工程哲学的缩影。
参考
- A. Sweeney et al., “Scalability in the XFS File System”, USENIX 1996
- C. Hellwig, “XFS: The Big Storage File System for Linux”, Linux Journal, 2009
- Mark Tinguely, “XFS Filesystem Block Usage”, Oracle Linux Blog
- internals-for-interns.com, “XFS Filesystem Internals”