最近在整理备份服务器时遇到一个很现实的问题:多个虚拟机的磁盘镜像、容器层、编译缓存、日志归档大量重复,光是重复数据就占掉了差不多三分之一的空间。传统做法是在应用层做哈希比对再选择跳过或硬链接,但数据量大起来后效率很低,而且对上层业务侵入太强。
后来把目光转向文件系统自带的去重能力,发现 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 二者对比
| 能力 | btrfs | XFS |
|---|---|---|
| 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/data4.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-test5.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.bin和basefile虽然内容相同,但物理块是两份,占用了两倍空间。
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之类的选项。它的逻辑应当是:
- 读取每个文件的 extent map。
- 计算已分配块的指纹。
- 查找相同指纹的块。
- 调用 reflink ioctl 建立共享关系。
- 重新统计数据空间占用。
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-test6. 常见问题与排查思路
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。
排查步骤:
- 确认以普通用户运行,但用户对目标文件有读权限。
- 确认没有启用
nosuid或特权限制(通常与权限有关)。 - 尝试以 root 运行一次测试,确认是否是权限问题。
这里也强调一个安全原则:在正式环境执行去重前,务必在测试环境验证,并确保有备份。去重操作本质上修改了文件系统元数据,不可盲目对生产数据执行。
6.4 快照与去重冲突
btrfs 快照依赖 CoW,而去重会改变共享关系。如果你在去重后创建快照,然后再修改文件,一些之前共享的 extent 会被 CoW 复制,快照保留的版本并不一定保持去重状态。这是正常现象,不是 bug。
最佳实践是:
- 先创建快照,再执行去重。
- 或在维护窗口内统一执行“快照 + 去重 + 清理旧快照”流程。
7. 最佳实践与工程建议
7.1 明确去重边界
去重不是“万金油”,它最适合的场景是:
- 备份归档目录。
- 虚拟机模板镜像。
- 容器镜像仓库(本地文件系统缓存)。
- 日志和历史数据归档。
- 包缓存目录(npm、pip、maven)。
不适合的场景:
- 频繁随机写入的数据库数据目录。
- 高性能计算产生的临时文件。
- 需要极致低延迟的实时业务数据。
7.2 先扫描,再预览,最后执行
无论使用 Oans 还是其他去重工具,都建议遵循:
- 扫描并建立指纹索引。
- 预览重复块列表和预期节省空间。
- 抽样检查重复块内容是否确定相同。
- 执行去重。
- 验证空间与文件完整性。
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-test8. 总结与学习路线
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 上去重的踩坑经历。