
最近在整理备份服务器时遇到一个很现实的问题多个虚拟机的磁盘镜像、容器层、编译缓存、日志归档大量重复光是重复数据就占掉了差不多三分之一的空间。传统做法是在应用层做哈希比对再选择跳过或硬链接但数据量大起来后效率很低而且对上层业务侵入太强。后来把目光转向文件系统自带的去重能力发现 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 与 CoWbtrfs 中文件数据被划分成一个个 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 reflink1 /dev/sdX mount -o reflink1 /dev/sdX /mnt/data注意如果你在创建 XFS 时没有启用 reflinkXFS 是不支持 Reflink 去重的。具体是否支持可以用下面的命令查看挂载选项mount | grep xfs如果输出中包含reflink字段说明支持。2.3 二者对比能力btrfsXFSCoW原生支持部分支持通过 Reflink 实现Reflink支持需要创建时启用快照支持不支持子卷级快照校验和支持不支持数据校验和在线去重需要外部工具需要外部工具离线去重支持支持适用场景桌面、备份、容器、个人存储大规模企业级数据存储简单说btrfs 的定位偏向“功能全面”XFS 的定位偏向“稳如磐石”。在去重上两者都能做但原理、工具生态和注意事项各不相同。3. 去重的核心原理Extent、指纹与 Reflink3.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 安装 OansOans 的安装方式取决于项目发布形式。如果它发布在 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 --reflinkalways /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 bs1M count2048 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 bs1M count2048 mkfs.xfs -m reflink1 /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 ofbasefile bs1M count64 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对于 XFSdf -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 reflink1核心思路去重效果不仅取决于文件内容重复率还取决于文件的 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 上去重的踩坑经历。