Docker 镜像同步了好几份、备份轮转历史堆积了几十个目录、虚拟机快照越来越大——翻遍服务器却发现每一个文件“看起来都该留着”。这种空间焦虑,经历过 Linux 存储运维的人应该都不陌生。真正的问题往往不是文件数量多,而是同一份数据在磁盘上被物理复制了很多次。
btrfs 和 XFS 用户此前想解决这个问题,选择并不多:老牌工具duperemove能做块级去重,但大规模扫描耗时很长;bees专注 btrfs,对 XFS 用户帮不上忙;而rmlint、jdupes这类工具更多是查找整个文件的重复项,能处理“文件级重复”,却处理不了“文件内部只有部分块重复”的情况。
最近 Hacker News 上出现了一个新项目Oans,定位非常直接:btrfs 和 XFS 上的快速去重工具。它的核心思路不是帮你删除重复文件,而是在文件系统底层把相同的数据块合并为一份引用。文件路径、目录结构、打开方式全部保持不变,真实磁盘占用却会明显下降。
这篇文章会讲清楚三件事:Oans 这类工具背后的文件系统去重原理是什么;在 btrfs 和 XFS 上怎么搭建测试环境、制造重复数据、跑通一次完整去重;以及去重之后如何验证块共享确实发生,有哪些坑需要避开。
1. Oans 是什么:不是又一次“重复文件清理”
1.1 三种容易混淆的“去重”
很多人一听到 deduplication,第一反应是“找出重复文件然后删掉一个”。这个概念在文件系统层面并不准确,至少可以分成三个层级:
| 维度 | 文件级去重 | 块级去重 | 压缩 |
|---|---|---|---|
| 比较粒度 | 整个文件 | 文件内部的数据块 | 单个文件数据流 |
| 是否透明 | 可能改变文件链接关系 | 对用户完全透明 | 对用户完全透明 |
| 典型工具/机制 | rmlint、jdupes | duperemove、bees、Oans | btrfscompress选项 |
| 典型场景 | 完全相同的照片、文档 | 容器镜像层、备份快照、相似虚拟机 | 文本、日志、代码 |
文件级去重通常把多个完全相同文件的 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 一次完整去重的四个阶段
不管工具叫什么名字,块级去重流程基本一致:
- 扫描文件系统或指定目录,列出所有普通文件。
- 将文件按一定策略切成数据块。
- 计算每个块的哈希值,用哈希表查找相同内容。
- 对内容相同的块调用
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.bin、backup-1.bin到backup-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占用没有任何变化,按以下顺序排查:
- 是否真的存在重复数据块?可以用
sha256sum orig.bin backup-1.bin确认文件内容一致。 - 是否使用了正确的文件系统和挂载选项?ext4 不支持这类去重。
- 工具是否真正执行了去重,还是只输出统计就退出了?
- 是否有其他进程打开着这些文件,导致共享关系无法建立?
- 是否因为权限不足,ioctl 调用返回失败?
表格汇总如下:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 空间占用无变化 | 文件内容本身不重复 | sha256sum对比 | 使用真正重复的数据测试 |
| 工具显示扫描到重复块,但空间未释放 | 文件被进程持续占用 | lsof查看打开句柄 | 关闭进程后重试 |
| 操作返回权限错误 | 权限不足 | 查看命令输出日志 | 使用授权账号或 sudo 执行 |
| XFS 上去重无效果 | 文件系统未启用 reflink | xfs_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 先文件级去重,再块级去重
对于明显是整文件重复的场景,先用rmlint、jdupes这类文件级工具处理,代价低、速度快。文件级去重完成后再用 Oans 做块级去重,可以大幅减少需要哈希的数据量,缩短整体耗时。这个顺序在归档类目录中尤其有效。
8.2 控制扫描范围,不要一上来就扫全盘
去重工具的扫描范围和目录深度直接影响执行时间。建议按子卷或顶层目录逐个处理,先从收益最高的备份目录、容器镜像目录开始,观察扫描速度和空间回收比,再决定是否扩大到更大范围。全盘扫描遇到活跃文件的可能性更高,产生碎片的概率也更大。
8.3 建立固定节奏,而不是每天都跑
归档数据通常是低频变化的。每月或每季度对归档目录做一次块级去重,比每天全量扫描更合理。高频去重不仅增加系统负载,还会把本该留给 CoW 机制的优化空间破坏掉。可以结合 crontab 在低峰时段运行,并保留日志用于追踪空间变化。
8.4 记录去重前、去重后的关键指标
每次运行前记录以下指标,方便判断工具是否真的带来了收益:
- 逻辑数据总量(各文件大小之和)。
- 物理磁盘占用(
df或btrfs 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 du和filefrag -v对比前后变化,亲自感受块级去重到底能回收多少空间。
值得深入的方向还包括:内容定义分块算法对命中率的影响、FIDEDUPERANGE在内核版本间的行为差异、btrfs 和 XFS 在共享 extent 管理上的异同。文件系统去重是个容易被低估的领域,表面看只是“找相同块”,实际做好扫描、哈希、合并和收敛,需要相当多的工程细节。
最后再强调一次安全底线:任何去重操作都建立在对数据有备份、可回滚、先在测试环境验证的前提下。数据安全永远优先于空间收益。