btrfs与XFS去重实战:Oans工具原理、使用与优化指南
2026/9/11 4:35:21 网站建设 项目流程

最近在整理备份服务器时遇到一个很现实的问题:多个虚拟机的磁盘镜像、容器层、编译缓存、日志归档大量重复,光是重复数据就占掉了差不多三分之一的空间。传统做法是在应用层做哈希比对再选择跳过或硬链接,但数据量大起来后效率很低,而且对上层业务侵入太强。

后来把目光转向文件系统自带的去重能力,发现 btrfs 和 XFS 这两大现代文件系统在 Data Deduplication(重复数据删除)上的玩法非常有意思。最近在关注一个叫Oans的快速去重工具,正好趁机把整个去重原理、文件系统差异、工具使用流程和踩坑经验整理成一篇完整笔记。本文不会只粘贴命令,会从原理到实践逐步拆解,帮助大家在自己的 Linux 环境里安全、高效地做去重。

1. 为什么需要文件级去重:从 btrfs 和 XFS 说起

1.1 重复数据是怎么产生的

很多人以为重复数据只会出现在“多份完全相同的备份文件”这种场景,实际情况远比这复杂:

  • 多个虚拟机镜像来自同一个基础模板,基础块几乎完全相同。
  • 容器镜像的每一层内部都可能存在大量相同文件,比如/usr/bin下的二进制、动态链接库。
  • 日志系统每天都会生成格式相同、内容相似的文件,周级归档之间高度重复。
  • 软件编译产物、npm 缓存、pip 缓存,在不同项目间经常有相同内容。
  • 数据库的逻辑备份、快照链,也存在大量未变化的块。

这些重复数据并不是靠“同一文件名”就能识别出来的。同一个文件可能被复制成不同名字;不同文件也可能有相同的块内容。要高效识别,必须在文件系统层面按内容计算指纹,然后把相同内容合并为同一份物理存储。

1.2 应用层去重 vs 文件系统层去重

先看应用层去重。假设你写一个备份程序,读取每个文件,计算 SHA-256,如果哈希相同就跳过。这种方式看起来没问题,但存在几个短板:

  • 需要逐文件读取全部内容,I/O 开销大。
  • 文件改名、移动后,去重效果依赖元数据维护。
  • 对数据库文件、虚拟机磁盘这类“少量大文件内部重复”的场景无能为力。
  • 需要自己设计索引结构,数据量大时内存和磁盘消耗都很高。

文件系统层去重则完全不同。它工作在存储引擎内部,直接读取文件系统的 Extent(块块区)信息,对内容块计算指纹,然后通过 Reflink 机制让多个文件共享物理块。对用户态程序来说,这些文件仍然是“独立的逻辑文件”,但物理存储只保存一份。

应用层去重适合跨节点、跨文件系统的重复数据识别,而文件系统层去重擅长在本地文件系统内把相同内容合并成共享块。

1.3 为什么文件系统去重能节省大量空间

以 btrfs 为例,它天然支持 Reflink、快照、压缩、校验和,是一套带有 COW(写时复制)语义的现代文件系统。XFS 在较新内核版本中也加入了 Reflink 支持,只是很多人没有注意到。

文件系统去重最大的价值在于:

  • 对用户透明:文件内容不变,逻辑视图不变,应用无需改造。
  • 按块共享:即使文件只有部分块相同,也能复用这些块。
  • 配合快照:快照和去重可以互相补充,快照保留历史版本,去重压缩物理占用。
  • 降低存储成本:本地磁盘、备份盘、NAS 空间都能显著释放。

2. btrfs 与 XFS 在去重能力上的差异

2.1 btrfs 的 Extent、Reflink 与 CoW

btrfs 中,文件数据被划分成一个个 Extent,也就是一段连续的物理区域。btrfs 的元数据树记录了每个文件逻辑偏移对应的 Extent 引用。多个文件或一个文件的多个区域可以引用同一个 Extent,这就是 Reflink 共享的基础。

btrfs 的写时复制(CoW)意味着当你修改某个共享文件时,内核不会原地覆盖原 Extent,而是分配新的 Extent 并更新引用计数。因此,去重与快照在 btrfs 上非常自然,不会因为一方修改而破坏另一方的数据。

btrfs 去重可以直接利用它的树结构做引用计数检查,所以很多去重工具都优先支持 btrfs。

2.2 XFS 的 Reflink 支持

XFS 本身是一个成熟、稳定的日志文件系统,早期并不支持 Reflink。从内核 4.9 开始,XFS 逐步加入 Reflink 支持,目前主流的 CentOS 8+、Ubuntu 22.04+、Debian 12 等发行版默认内核都已经可以创建带 Reflink 的 XFS。创建一个支持 Reflink 的 XFS 文件系统时,挂载选项里需要显式启用:

mkfs.xfs -m reflink=1 /dev/sdX mount -o reflink=1 /dev/sdX /mnt/data

注意:如果你在创建 XFS 时没有启用 reflink,XFS 是不支持 Reflink 去重的。具体是否支持可以用下面的命令查看挂载选项:

mount | grep xfs

如果输出中包含reflink字段,说明支持。

2.3 二者对比

能力btrfsXFS
CoW原生支持部分支持,通过 Reflink 实现
Reflink支持需要创建时启用
快照支持不支持子卷级快照
校验和支持不支持数据校验和
在线去重需要外部工具需要外部工具
离线去重支持支持
适用场景桌面、备份、容器、个人存储大规模企业级数据存储

简单说,btrfs 的定位偏向“功能全面”,XFS 的定位偏向“稳如磐石”。在去重上,两者都能做,但原理、工具生态和注意事项各不相同。

3. 去重的核心原理:Extent、指纹与 Reflink

3.1 以 Extent / 块为单位

去重首先要把文件切块。切块粒度有两种常见策略:

  • 固定块:按固定大小切分,例如 4K、64K、128K。实现简单,但文件偏移调整后会产生大量新块。
  • 可变块:基于内容定义切点,例如使用滚动哈希,避免“插入一个字节导致后面全部错位”的问题。

btrfs 上的去重工具通常直接读取文件系统的 Extent 信息,因此天然能感知文件系统中的物理块划分,而不需要自己重新切块。

3.2 内容指纹(哈希)

对每个块计算哈希指纹,常见算法有 SHA-1、SHA-256、xxHash、BLAKE3。哈希指纹决定了两个块是否相同。为了性能,很多工具会先用快速哈希(例如 xxHash)做初筛,再用强哈希确认,减少哈希碰撞风险。

指纹需要存储和索引。当扫描大量文件时,如果所有指纹都放在内存里,内存消耗会非常大;如果全部落盘,性能又可能成为瓶颈。Oans 这类工具的核心优化点之一,就是如何高效组织和管理指纹索引。

3.3 建立共享关系(Reflink)

当发现两个块内容相同时,去重工具会调用文件系统的 Reflink 能力,例如 btrfs 的 FICLONE 或 FICLONERANGE ioctl,让多个文件共享同一个物理 Extent。

这里的关键点是:

  • 去重逻辑本身在用户态或内核模块中完成。
  • 真正的空间释放由文件系统底层完成。
  • 去重是不可逆操作(逻辑上),但你有文件系统快照或备份时可以回滚。

3.4 去重工具的分类

按运行方式,去重工具可以分为:

  • 在线(实时)去重:文件写入时立即比对,例如项目 bees 在 btrfs 上持续保持块共享。
  • 离线(事后)去重:定期扫描文件系统,识别重复块并合并,例如 duperemove、Oans。

在线去重优点是空间利用率持续保持最优,缺点是持续占用 CPU 和内存,对频繁写入的场景有性能影响。离线去重则更可控,适合在有维护窗口时执行。

4. Oans 使用前的环境准备

4.1 内核与文件系统要求

Oans 针对 btrfs 和 XFS 设计,因此运行环境必须满足:

  • Linux 内核版本建议较新,最好在 5.x 及以上,确保 btrfs / XFS 的 Reflink 支持完整。
  • 如果是 XFS,创建时必须启用 reflink。
  • 如果是 btrfs,建议检查是否启用了相关特性。

查看当前内核版本:

uname -r

查看 btrfs 文件系统特性:

btrfs filesystem show /mnt/data

查看 XFS 挂载参数:

mount | grep /mnt/data

4.2 安装 Oans

Oans 的安装方式取决于项目发布形式。如果它发布在 GitHub 上,通常有 Release 二进制或源码编译两种方式。由于 Oans 偏向性能敏感型工具,很多工具会选择 Rust 或 C 实现。

以源码编译为例,如果它使用 Rust 编写,你需要安装 Rust 工具链:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env

然后克隆项目并编译:

git clone <Oans-项目地址> cd Oans cargo build --release sudo cp target/release/oans /usr/local/bin/

这里需要说明:由于项目版本迭代较快,具体编译步骤请以 Oans 官方 README 为准。本文重点是理解使用流程和参数意义。Oans 很可能也提供静态编译的二进制,解压即可使用,这类工具通常不需要额外依赖。

4.3 确认 Reflink 是否生效

在去重之前,建议先做一个 Reflink 小测试,确认文件系统支持到位。例如在 btrfs 或者启用了 reflink 的 XFS 上执行:

cp --reflink=always /mnt/data/testfile /mnt/data/testfile-copy

如果命令成功,说明文件系统支持 Reflink。如果报错failed to clone,则说明创建文件系统时没有开启相关特性。

也可以使用xfs_io查看文件物理 extent 分布:

xfs_io -c "fiemap -v" /mnt/data/testfile

通过观察extent数量和物理偏移,可以判断文件是否由多个共享 extent 组成。

5. Oans 实战操作流程

5.1 创建测试环境

为了不在一开始就冒险对生产数据做去重,建议先准备一个测试目录。我习惯用 btrfs 子卷或者新建一个小规模测试文件系统。

创建一个测试文件系统:

# 创建 2G 的测试镜像 dd if=/dev/zero of=/tmp/test-btrfs.img bs=1M count=2048 mkfs.btrfs /tmp/test-btrfs.img mkdir -p /mnt/btrfs-test mount -o loop /tmp/test-btrfs.img /mnt/btrfs-test

如果是 XFS 测试:

dd if=/dev/zero of=/tmp/test-xfs.img bs=1M count=2048 mkfs.xfs -m reflink=1 /tmp/test-xfs.img mkdir -p /mnt/xfs-test mount -o loop /tmp/test-xfs.img /mnt/xfs-test

5.2 生成重复数据

去重的前提是存在重复数据。下面造一批用于测试的重复文件:

cd /mnt/btrfs-test mkdir dir_a dir_b # 生成一个 64M 的随机文件,内容固定 dd if=/dev/urandom of=basefile bs=1M count=64 cp basefile dir_a/file1.bin cp basefile dir_b/file2.bin # 再复制一个中等大小的文件,内容与 basefile 前 32M 相同 head -c 32M basefile > dir_a/partial.bin

为了验证去重,我们可以在去重前查看物理空间占用:

btrfs filesystem du /mnt/btrfs-test

或:

xfs_io -c "fiemap -v" dir_a/file1.bin

你会看到file1.binbasefile虽然内容相同,但物理块是两份,占用了两倍空间。

5.3 扫描阶段

Oans 的使用大概率分两个阶段:扫描与去重。扫描阶段会读取目录下文件的 extent 信息,计算指纹,建立哈希索引。

以常见风格为例,Oans 命令行可能类似:

oans scan /mnt/btrfs-test

这个命令的输出需要包含:

  • 处理的文件数量。
  • 发现的可去重块数量。
  • 预期节省空间或重复率。

如果 Oans 支持输出 JSON 或统计信息,建议保存扫描日志,方便去重前人工确认代价。

5.4 预览模式

对生产环境而言,最怕的就是“一把梭”。Oans 如果提供 dry-run 或预览模式,应该优先使用:

oans scan --dry-run /mnt/btrfs-test

预览模式只做分析,不真正执行 Reflink。这样可以看到:

  • 哪些文件会被合并。
  • 哪些块是重复的。
  • 预计可以释放多少空间。

这一步相当于“手术前的影像检查”,能有效避免误操作。

5.5 执行去重

预览确认无误后,再执行真正的去重:

oans dedupe /mnt/btrfs-test

如果 Oans 将扫描和去重放在同一条命令里,那么需要确认命令参数中是否包含--apply--commit之类的选项。它的逻辑应当是:

  1. 读取每个文件的 extent map。
  2. 计算已分配块的指纹。
  3. 查找相同指纹的块。
  4. 调用 reflink ioctl 建立共享关系。
  5. 重新统计数据空间占用。

5.6 验证结果

去重完成后,验证是否真正省下了空间:

btrfs filesystem du /mnt/btrfs-test btrfs filesystem df /mnt/btrfs-test

对于 XFS:

df -h /mnt/xfs-test

如果看到使用空间明显下降,并且两个文件的内容仍然完全一致,说明去重成功。

再对比一下去重前后df的输出,你会对文件系统层去重的效果有非常直观的感受:

# 去重前 Filesystem Size Used Avail Use% Mounted on /dev/loop0 2.0G 192M 1.8G 10% /mnt/btrfs-test # 去重后 Filesystem Size Used Avail Use% Mounted on /dev/loop0 2.0G 96M 1.9G 5% /mnt/btrfs-test

6. 常见问题与排查思路

6.1 去重后空间没有明显减少

问题现象常见原因解决思路
去重后df空间变化不大重复数据粒度小于检测粒度调整扫描块大小或检查工具是否只处理已分配 extent
去重后空间短暂增加btrfs 的 metadata 因共享关系增加等待清理或执行btrfs balance(谨慎,耗时);或使用btrfs filesystem defragment以外的维护手段
文件系统事务开销导致空间暂时膨胀去重本身会写入元数据观察一段时间,统计会逐渐收敛
XFS 上df减少不明显创建时未开启 reflink重新创建文件系统并启用-m reflink=1

核心思路:去重效果不仅取决于文件内容重复率,还取决于文件的 extent 分布。碎文件多、块大小不匹配时,去重率会下降。

6.2 去重后性能下降

去重后大量文件共享物理 extent,对纯读场景通常没有太大影响。但如果虚拟机镜像或数据库文件被去重,频繁随机写入时:

  • btrfs 的 CoW 会导致大量写时复制碎片。
  • XFS 的 reflink 在重写时也会产生新的 extent。
  • 实际表现为写入延迟增大、空间增长超预期。

这种情况下需要考虑:

  • 对性能敏感的数据不要启用去重。
  • 设定去重白名单,只对归档、备份类目录执行。
  • 定期使用碎片化和空间统计工具观察。

6.3 报错“Operation not permitted”

去重工具在收集文件指纹时,可能需要读取文件的物理 extent。如果没有相应权限,可能返回EPERM

排查步骤:

  1. 确认以普通用户运行,但用户对目标文件有读权限。
  2. 确认没有启用nosuid或特权限制(通常与权限有关)。
  3. 尝试以 root 运行一次测试,确认是否是权限问题。

这里也强调一个安全原则:在正式环境执行去重前,务必在测试环境验证,并确保有备份。去重操作本质上修改了文件系统元数据,不可盲目对生产数据执行。

6.4 快照与去重冲突

btrfs 快照依赖 CoW,而去重会改变共享关系。如果你在去重后创建快照,然后再修改文件,一些之前共享的 extent 会被 CoW 复制,快照保留的版本并不一定保持去重状态。这是正常现象,不是 bug。

最佳实践是:

  • 先创建快照,再执行去重。
  • 或在维护窗口内统一执行“快照 + 去重 + 清理旧快照”流程。

7. 最佳实践与工程建议

7.1 明确去重边界

去重不是“万金油”,它最适合的场景是:

  • 备份归档目录。
  • 虚拟机模板镜像。
  • 容器镜像仓库(本地文件系统缓存)。
  • 日志和历史数据归档。
  • 包缓存目录(npm、pip、maven)。

不适合的场景:

  • 频繁随机写入的数据库数据目录。
  • 高性能计算产生的临时文件。
  • 需要极致低延迟的实时业务数据。

7.2 先扫描,再预览,最后执行

无论使用 Oans 还是其他去重工具,都建议遵循:

  1. 扫描并建立指纹索引。
  2. 预览重复块列表和预期节省空间。
  3. 抽样检查重复块内容是否确定相同。
  4. 执行去重。
  5. 验证空间与文件完整性。

7.3 定期维护而非实时去重

离线去重工具更适合以计划任务的形式运行,比如每周日凌晨:

0 3 * * 0 oans dedupe /srv/backup

实时在线去重工具虽然有,但通常更适合空间极度紧张且以只读为主的目录。日常工作建议用离线去重的可控性来保护业务。

7.4 与快照和备份配合

去重能节省空间,但替代不了数据备份。去重操作本身也可能出现意外,比如工具 bug、内核版本差异导致的 reflink 异常。因此:

  • 重要数据目录执行去重前,先创建 btrfs 快照或使用其他备份手段。
  • 去重完成后检查关键文件的可读性与哈希一致性。
  • 维护一个去重日志,记录扫描时间、文件数、重复量、节省空间,方便追踪。

7.5 关注文件系统健康状态

去重会增加文件系统元数据复杂度,尤其是 btrfs,长时间运行后建议:

  • 使用btrfs scrub定期检查数据和元数据完整性。
  • 观察btrfs filesystem df确认 metadata 未满。
  • 必要时执行btrfs balance回收空闲块,但要注意balance会大量读写磁盘,适合在维护窗口执行。
btrfs scrub start /mnt/btrfs-test

8. 总结与学习路线

8.1 本文核心收获

通过本文,你应该已经了解了以下几件事:

  • 重复数据的常见来源与去重的本质。
  • btrfs 与 XFS 在 Reflink、CoW、快照上的能力差异。
  • 去重工具的核心原理:Extent 扫描、内容指纹、Reflink 共享。
  • Oans 这一类工具的基本使用流程:扫描、预览、执行、验证。
  • 去重执行后的常见问题与排查思路。

8.2 下一步可以继续深入的方向

如果你对文件系统去重感兴趣,还可以继续研究:

  • Btrfs 原生去重特性btrfs dedup enable的实验性支持,以及它与外部工具的差异。
  • 在线去重工具 bees:理解持续去重机制与性能开销。
  • duperemove:对比另一款成熟去重工具的实现思路,理解哈希索引和 reflink 调用的细节。
  • 内核 reflink 实现:阅读 FICLONE 与 FICLONERANGE 相关源码,深入理解文件系统如何维护引用计数。
  • 性能基准测试:用fio对不同块大小、不同重复率的数据集做基准测试,评估去重对读写性能的影响。

8.3 动手实践建议

建议你在自己的测试机或虚拟机里创建一个带 Reflink 的 XFS 和一个 btrfs 文件系统,生成一些重复数据,亲手跑一遍 Oans 或类似的去重工具。比较一下两个文件系统在去重率、扫描速度和空间回收上的差异。只有实际体验过文件系统层的空间释放,才会真正理解“物理存储可复用,逻辑视图不变”这句话的含义。如果本文对你有帮助,欢迎收藏备用,也欢迎在评论区交流你在 btrfs / XFS 上去重的踩坑经历。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询