Oans:btrfs/XFS块级去重工具原理与实战,回收磁盘空间
2026/8/30 2:33:34 网站建设 项目流程

Docker 镜像同步了好几份、备份轮转历史堆积了几十个目录、虚拟机快照越来越大——翻遍服务器却发现每一个文件“看起来都该留着”。这种空间焦虑,经历过 Linux 存储运维的人应该都不陌生。真正的问题往往不是文件数量多,而是同一份数据在磁盘上被物理复制了很多次。

btrfs 和 XFS 用户此前想解决这个问题,选择并不多:老牌工具duperemove能做块级去重,但大规模扫描耗时很长;bees专注 btrfs,对 XFS 用户帮不上忙;而rmlintjdupes这类工具更多是查找整个文件的重复项,能处理“文件级重复”,却处理不了“文件内部只有部分块重复”的情况。

最近 Hacker News 上出现了一个新项目Oans,定位非常直接:btrfs 和 XFS 上的快速去重工具。它的核心思路不是帮你删除重复文件,而是在文件系统底层把相同的数据块合并为一份引用。文件路径、目录结构、打开方式全部保持不变,真实磁盘占用却会明显下降。

这篇文章会讲清楚三件事:Oans 这类工具背后的文件系统去重原理是什么;在 btrfs 和 XFS 上怎么搭建测试环境、制造重复数据、跑通一次完整去重;以及去重之后如何验证块共享确实发生,有哪些坑需要避开。

1. Oans 是什么:不是又一次“重复文件清理”

1.1 三种容易混淆的“去重”

很多人一听到 deduplication,第一反应是“找出重复文件然后删掉一个”。这个概念在文件系统层面并不准确,至少可以分成三个层级:

维度文件级去重块级去重压缩
比较粒度整个文件文件内部的数据块单个文件数据流
是否透明可能改变文件链接关系对用户完全透明对用户完全透明
典型工具/机制rmlintjdupesduperemovebees、Oansbtrfscompress选项
典型场景完全相同的照片、文档容器镜像层、备份快照、相似虚拟机文本、日志、代码

文件级去重通常把多个完全相同文件的 inode 合并,改成硬链接。比如jdupes -L就是把重复文件替换为硬链接。问题是它只能识别“整个文件内容完全一致”的情况,而且一旦某些程序按 inode 判断文件身份,可能产生副作用。

块级去重则是把文件拆成多个数据块,计算哈希后找到内容相同的块,让这些块在文件系统里共享同一个物理 extent。它不改变文件名、路径和 inode 语义,只是底层数据块被复用了。这比文件级去重更彻底,覆盖场景也更广。

压缩和去重也不能混淆。压缩是在单个文件内部消除冗余模式,对单份数据有效;去重是在多个文件之间消除完全相同的数据块,即使每个文件本身已经很小,只要份数够多,收益依然明显。

Oans 做的事情是第三种:块级去重。它调用的是文件系统内核接口,而不是简单地在用户空间比较文件之后删除副本。

1.2 Oans 与已有工具链的差异

在 btrfs 和 XFS 生态里,块级去重工具并不是空白市场。duperemove是老牌选择,支持 btrfs 和 XFS,原理是扫描文件、分块、计算哈希、找出相同块并调用去重接口。它的问题在于全量扫描时 CPU 和内存开销比较大,目录一多,等待时间会显著拉长。

bees是 btrfs 场景下很受欢迎的后台去重守护进程,思路是持续监控文件系统变化、在空闲时段做增量去重。但它基本绑定 btrfs,XFS 用户没法直接使用。

Oans 从项目定位上要解决的,正是这两个痛点:在 btrfs 和 XFS 上都提供可用的快速去重,同时把扫描和匹配效率放在核心位置。具体使用 Rust、C 还是 Go 实现,哈希算法选择什么,是否支持增量扫描,这些细节要以 Oans 项目仓库的 README 和源码为准,但“面向多文件系统 + 更快”的方向是明确的。

需要提醒的是,Oans 从 Show HN 上出现意味着项目还比较年轻。新工具往往迭代快、API 不稳定,生产环境引入前,务必先看维护活跃度、License、issue 反馈和近期提交记录。

1.3 为什么 btrfs 和 XFS 能成为“同一类工具”的目标

块级去重不能脱离文件系统单独存在。内核里有一个名为FIDEDUPERANGE的 ioctl,它允许用户程序告诉文件系统:“这两个文件的这一段数据是相同的,请让它们共享物理块。”不是所有文件系统都实现了这个接口。

btrfs 天然是 CoW(写时复制)文件系统,reflink 和共享 extent 是底层能力的一部分,因此实现FIDEDUPERANGE水到渠成。

XFS 长期以来以高性能和高扩展性著称,传统上并不支持 reflink。从内核 4.9 开始,XFS 逐步引入 reflink 支持,配合新格式的 XFS 文件系统,也能支持共享 extent 和去重操作。

这就解释了为什么 Oans 能同时瞄准 btrfs 和 XFS:它并不需要自己发明“共享块”机制,只需要高效地完成“找相同块”这件事,然后把结果交给内核去合并。而 ext4 没有 reflink 支持,也没有实现完整的FIDEDUPERANGE,所以这类工具天然无法覆盖 ext4。

2. 核心原理:块级去重是如何工作的

2.1 CoW 与 reflink:去重的地基

要理解去重,先理解 reflink。假设你有一个 1GB 的文件,直接执行:

cp --reflink=auto original.bin copy.bin

在支持 reflink 的文件系统上,btrfs 和 XFS 会创建一个新的目录项,同时让两个文件指向同一组数据块,而不是真正复制 1GB 数据。磁盘占用几乎不变,两个文件看起来都完整可读。

关键差异在于写入行为:当你想修改copy.bin时,文件系统会先把将被覆盖的数据块复制一份,再执行写入。这就是“写时复制”。原本共享的 extent 在修改处发生分裂,其他未修改部分继续保持共享。

去重工具做的事,和cp --reflink方向相反。cp --reflink是在复制时直接建立共享;去重则是在文件已经物理存在多份冗余之后,扫描发现哪些块内容相同,再用FIDEDUPERANGE把它们合并。

因此,去重本质上是一种“事后 reflink”

2.2 一次完整去重的四个阶段

不管工具叫什么名字,块级去重流程基本一致:

  1. 扫描文件系统或指定目录,列出所有普通文件。
  2. 将文件按一定策略切成数据块。
  3. 计算每个块的哈希值,用哈希表查找相同内容。
  4. 对内容相同的块调用FIDEDUPERANGE合并,并保留一份物理数据。

分块策略很关键。如果整文件只算一个哈希,那么需要两个文件完全一致才能去重;如果按固定大小分块,则能识别部分相同的数据,但遇到文件内插入或删除数据导致块偏移时,后续块全对齐不上。更高级的工具会采用内容定义分块(CDC),让块边界随内容变化,代价是计算量更大。

Oans 选择哪种分块策略,直接决定它在大文件场景下的扫描速度和命中率。这也是技术选型时最值得研究的部分。

2.3 FIDEDUPERANGE 的工作方式

去重工具最终要通过系统调用把“哪些块需要合并”告诉内核。FIDEDUPERANGE接受一个源文件、一个目标文件,以及两者各自的区间范围。内核会比较区间内的数据是否一致,一致则建立共享 extent,不一致则返回错误。

这个过程涉及文件系统内部的 extents 管理。对用户空间工具来说,它不需要关心块在磁盘上的具体物理位置,只需要提供文件和偏移量,内核会完成共享关系的建立。

在实际工程中,工具通常会把待去重的块按文件聚合,尽量让同一个文件参与的 ioctl 调用次数更少、一次性合并更多区间,从而减少系统调用开销。这也是“快速去重”和“慢速去重”在工程实现上的一个主要差异点。

3. 适用场景与不适用场景

3.1 适合用去重解决的场景

  • 备份归档目录。每天轮转的备份里,大量未变化的数据块会反复出现,块级去重能把历史备份的真实磁盘占用压到很低。
  • 容器镜像与构建缓存。不同镜像层之间、不同构建产物之间存在大量相同内容,去重之后宿主机存储压力能明显缓解。
  • 虚拟机镜像库。多个虚拟机基于同一模板创建,模板内容在一段时间内基本不变,块级去重收益很高。
  • 测试环境快照。快照本身通过 CoW 共享已有数据,但把快照导出成独立文件、或复制到其他目录后,又会变成物理重复,此时去重可以高效回收空间。

3.2 不适合用去重解决的场景

  • 已经压缩或加密的数据。压缩后的数据本身熵很高,文件之间重复块极少;加密数据更是完全随机,去重基本没有收益。
  • 频繁随机写入的在线数据库文件。去重后块共享关系很容易在下次写入时被打散,反复去重反而增加碎片化和 CPU 消耗。
  • SSD 容量极度紧张且要求低延迟的在线业务。去重会增加元数据复杂度和可能的碎片化,对延迟敏感型业务不一定是好事。
  • 数据可靠性要求极高、但当前没有可靠备份的系统。任何批量修改文件系统元数据的操作都有风险,没有备份就不应该跑。

3.3 一个容易被忽略的判断

去重的价值不在于“马上多出多少空闲空间”,而在于它让“物理存储占用”和“逻辑数据量”解耦。归档类数据即使逻辑上保留了 10 份历史版本,物理上也可能只占 1 份多一点。这一点对备份保留策略特别有价值,因为你可以放心地把保留周期拉长,而不必担心磁盘迅速被填满。

4. 环境准备与安全前置

4.1 在测试机上创建 btrfs 和 XFS 测试文件系统

建议不要在正在使用的生产分区上直接实验。最简单的方式是创建两个 loop 镜像文件,分别格式化为 btrfs 和 XFS 来测试。整个过程需要 root 权限,请确认你操作的是测试环境。

# 创建两个 2GB 镜像 truncate -s 2G /tmp/oans-btrfs.img truncate -s 2G /tmp/oans-xfs.img # 绑定 loop 设备 losetup -fP /tmp/oans-btrfs.img losetup -fP /tmp/oans-xfs.img # 查看 loop 设备名 losetup -l

把输出中的/dev/loopX替换成实际设备名,然后格式化:

mkfs.btrfs /dev/loop0 mkfs.xfs /dev/loop1 mkdir -p /mnt/oans-btrfs /mnt/oans-xfs mount /dev/loop0 /mnt/oans-btrfs mount /dev/loop1 /mnt/oans-xfs

如果你的内核或系统版本较老,XFS 可能尚未开启 reflink 支持。可以通过xfs_info查看文件系统特性,确认输出中有reflink=1。btrfs 自内核 3.x 起基本都支持 reflink 相关能力,不需要额外配置。

4.2 安装 Oans

Oans 的具体安装方式以项目 README 为准。常见开源工具会提供预编译二进制,也可以从源码构建。拿到工具包之后,第一件事是执行帮助命令,确认当前版本的参数形式:

oans --help

如果输出里有--dry-run--scan--dedupe之类的选项,建议先用--dry-run之类的模拟模式观察工具会做什么,再真正去重。如果看到“usage”输出格式不同,就以工具自身说明为准,不要照搬其他工具的习惯。

4.3 安全边界:先备份,再小范围验证

去重操作会修改文件系统的 extent 共享关系。正常设计良好的工具不会丢数据,但任何批量修改元数据的操作都有风险,特别是当工具本身还处在早期迭代阶段时。运行前必须确认:

  • 测试环境与生产环境隔离。
  • 有可用的备份或快照。
  • 有明确的回滚方案,比如数据可以先复制到另一个磁盘。
  • 使用最小权限运行,不要用 root 在非必要范围内全盘扫描。

5. 完整示例:制造重复数据并跑通一次去重

5.1 制造典型重复场景

先在 btrfs 测试分区里生成一个 512MB 的随机文件,然后复制多份,模拟备份目录里的重复文件:

cd /mnt/oans-btrfs # 生成原始数据 dd if=/dev/urandom of=orig.bin bs=1M count=512 # 模拟多个备份副本 for i in 1 2 3 4 5; do cp orig.bin backup-$i.bin done # 再模拟一部分只有局部重复的文件 cp orig.bin partial.bin dd if=/dev/urandom of=partial.bin bs=1M count=1 conv=notrunc ls -lh

执行后,目录里会有orig.binbackup-1.binbackup-5.bin,以及一个开头 1MB 被改写的partial.bin。前六个文件内容完全相同,partial.bin有 511MB 与orig.bin相同。

这是非常典型的归档场景:多个历史版本之间大部分数据块相同,只有少量差异。

5.2 去重前记录空间占用

btrfs filesystem du来查看真实磁盘占用。这个命令会区分文件逻辑大小和实际独占的物理块大小。

btrfs filesystem du --summarize /mnt/oans-btrfs df -h /mnt/oans-btrfs

此时所有文件都是独立物理副本,df应该显示接近 3GB 的占用(512MB 原始文件加上 6 份 512MB 副本,其中partial.bin也接近 512MB)。

5.3 运行去重命令

在确认工具帮助信息后,对指定目录执行扫描和去重。由于不同版本命令可能不同,下面以常见的“扫描 → 去重”两步形态作为参考,实际参数请以oans --help输出为准:

# 先扫描,查看工具会识别出多少重复块(如果有 dry-run 模式务必使用) oans --scan /mnt/oans-btrfs # 确认无误后执行去重 oans --dedupe /mnt/oans-btrfs

如果工具支持只显示统计信息而不实际合并,请优先执行这一步。观察输出的重复块数量、可节省空间和整体耗时,再决定是否真正执行去重。

6. 运行结果与效果验证

6.1 用 btrfs filesystem du 验证共享

去重完成之后,再次执行:

btrfs filesystem du --summarize /mnt/oans-btrfs df -h /mnt/oans-btrfs

预期结果:文件逻辑大小没变,但df显示的真实空间占用应显著下降。6 份 512MB 文件加 1 个 512MB 局部重复文件,在理想情况下,物理占用只会比“原始 512MB + 少数差异块”略多。

btrfs filesystem du输出的exclusive列尤其值得看,它表示该文件独占的物理块大小。如果backup-1.bin的 exclusive 接近 0,说明它几乎完全和其他文件共享物理块。

6.2 用 filefrag 查看共享 extent

filefrag是 e2fsprogs 自带工具,也可以用来查看文件物理 extent 分布:

filefrag -v /mnt/oans-btrfs/backup-1.bin filefrag -v /mnt/oans-btrfs/partial.bin

共享 extent 通常会被标记为shared。如果看到大量 extent 带有 shared 标记,说明块级去重确实生效了。这一输出在 btrfs 和 XFS 上都能提供参考。

6.3 XFS 上如何验证

在 XFS 测试分区上重复同样步骤,可以采用类似命令:

filefrag -v /mnt/oans-xfs/backup-1.bin

另外,也可以使用xfs_io查看文件映射:

xfs_io -r -c "fiemap -v" /mnt/oans-xfs/backup-1.bin

如果 extent 信息中显示被多个文件共享,说明 XFS 的 reflink 去重已经生效。不同版本输出格式会有差异,重点关注“共享”或“shared”相关标记。

6.4 验证失败时的检查顺序

如果跑完去重后df占用没有任何变化,按以下顺序排查:

  1. 是否真的存在重复数据块?可以用sha256sum orig.bin backup-1.bin确认文件内容一致。
  2. 是否使用了正确的文件系统和挂载选项?ext4 不支持这类去重。
  3. 工具是否真正执行了去重,还是只输出统计就退出了?
  4. 是否有其他进程打开着这些文件,导致共享关系无法建立?
  5. 是否因为权限不足,ioctl 调用返回失败?

表格汇总如下:

问题现象可能原因排查方式解决方案
空间占用无变化文件内容本身不重复sha256sum对比使用真正重复的数据测试
工具显示扫描到重复块,但空间未释放文件被进程持续占用lsof查看打开句柄关闭进程后重试
操作返回权限错误权限不足查看命令输出日志使用授权账号或 sudo 执行
XFS 上去重无效果文件系统未启用 reflinkxfs_info查看特性重新创建支持 reflink 的 XFS 文件系统
工具帮助命令不识别参数版本差异或依赖缺失查看项目 README 和--help按实际版本参数调整

7. 常见问题与坑位分析

7.1 去重后空间为什么没有立即释放

块级去重建立的共享关系是文件系统内部的元数据变化,不是删除操作。对于已经打开的旧文件句柄,内核可能仍然保持对旧数据块的引用,直到所有 fd 关闭。如果去重后空间没有立刻减少,可以先等待一段时间,或检查是否有进程正在读写相关目录。

7.2 去重与文件碎片化

去重会改变文件物理块的分布,可能增加碎片化程度。对机械硬盘来说,碎片化会影响顺序读取性能;对 SSD 来说,影响相对较小。高频更新的大文件不适合反复去重,否则每次写入都会把共享块分裂出来,去重带来的空间收益很快被元数据开销和碎片化抵消。

7.3 去重与 btrfs 压缩的先后关系

btrfs 支持透明压缩,开启compress=zstd后,文件数据在写入时会被压缩。压缩后的数据和未压缩数据在去重时属于不同内容,因此不要期待压缩和去重能直接叠加。更合理的策略是:要么依赖压缩处理单文件冗余度,要么依赖去重处理多文件之间的重复块,根据实际数据特征选择适合的主手段。

7.4 快照与去重的相互影响

btrfs 子卷快照本质上就会共享数据块,因此快照本身不是“需要去重”的场景。真正的问题通常出在:把快照内容导出成独立文件、或通过其他方式复制到别处后,重复数据又变回了物理独立。此时去重能帮助收回空间,但要注意快照机制和去重机制同时存在时,文件系统的共享关系会变得更复杂,监控和排查也要更细致。

7.5 数据安全与去重风险

理论上,FIDEDUPERANGE会先校验数据块内容一致,再建立共享,因此去重过程本身不应导致数据损坏。但工具存在 bug、内核版本存在缺陷、文件系统在去重过程中崩溃等小概率事件仍然存在。生产环境操作前,必须验证备份可用,并且最好先在测试分区完整跑一遍流程,确认工具行为符合预期。

8. 最佳实践与工程建议

8.1 先文件级去重,再块级去重

对于明显是整文件重复的场景,先用rmlintjdupes这类文件级工具处理,代价低、速度快。文件级去重完成后再用 Oans 做块级去重,可以大幅减少需要哈希的数据量,缩短整体耗时。这个顺序在归档类目录中尤其有效。

8.2 控制扫描范围,不要一上来就扫全盘

去重工具的扫描范围和目录深度直接影响执行时间。建议按子卷或顶层目录逐个处理,先从收益最高的备份目录、容器镜像目录开始,观察扫描速度和空间回收比,再决定是否扩大到更大范围。全盘扫描遇到活跃文件的可能性更高,产生碎片的概率也更大。

8.3 建立固定节奏,而不是每天都跑

归档数据通常是低频变化的。每月或每季度对归档目录做一次块级去重,比每天全量扫描更合理。高频去重不仅增加系统负载,还会把本该留给 CoW 机制的优化空间破坏掉。可以结合 crontab 在低峰时段运行,并保留日志用于追踪空间变化。

8.4 记录去重前、去重后的关键指标

每次运行前记录以下指标,方便判断工具是否真的带来了收益:

  • 逻辑数据总量(各文件大小之和)。
  • 物理磁盘占用(dfbtrfs filesystem du)。
  • 去重吞吐和耗时。
  • 文件系统碎片化情况。

长期记录这些数据,可以判断哪些目录适合去重、哪些目录去重后很快又膨胀,从而不断优化策略。

8.5 监控与告警

如果 Oans 计划部署到准生产或生产环境,建议在去重任务外部增加监控:

  • 任务是否成功结束,退出码是否正常。
  • 是否有FIDEDUPERANGE调用失败记录。
  • 去重前后空间变化是否符合预期。
  • 系统 I/O 和 CPU 在去重期间是否造成业务影响。

出现异常时,第一时间停止后续任务,优先保证业务数据安全。

8.6 对 Oans 项目的跟进建议

新项目处在快速迭代期,使用时锁定一个稳定版本,不要每次发布都无脑升级。关注项目仓库的 issue 反馈、提交频率和维护者响应速度。如果发现问题,优先在测试环境复现,再到项目仓库提交清晰的复现步骤和文件系统版本信息。

9. 总结与下一步实践

Oans 这类工具解决的核心问题,不是“帮忙删文件”,而是让 btrfs 和 XFS 上本来物理重复的数据,在文件系统层面变成共享块。它特别适合备份归档、容器镜像、虚拟机模板和测试快照这类静态数据居多的场景。而对于频繁写入的在线数据、已压缩文件、加密数据,块级去重的收益很小,甚至可能带来额外开销。

如果手头正好有 btrfs 或 XFS 服务器,建议先在一台测试机上用 loop 镜像搭一个独立文件系统,复制几百 MB 到几 GB 的重复数据,跑一遍 Oans 的扫描和去重,再用btrfs filesystem dufilefrag -v对比前后变化,亲自感受块级去重到底能回收多少空间。

值得深入的方向还包括:内容定义分块算法对命中率的影响、FIDEDUPERANGE在内核版本间的行为差异、btrfs 和 XFS 在共享 extent 管理上的异同。文件系统去重是个容易被低估的领域,表面看只是“找相同块”,实际做好扫描、哈希、合并和收敛,需要相当多的工程细节。

最后再强调一次安全底线:任何去重操作都建立在对数据有备份、可回滚、先在测试环境验证的前提下。数据安全永远优先于空间收益。

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

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

立即咨询