跳至主要内容 跳至导航 跳至页脚
限时:设计合作伙伴计划 — 永久享有 BUSINESS 套餐

你的碎片化数值看起来触目惊心。我测了一下它到底有多大代价。

我们的存储健康报告会把某些仓库标记为接近 20,000 个 extent/GB。这个数字听起来很吓人。我对最严重的几个仓库做了顺序读和随机读基准测试,结果惩罚为零。以下是数据,以及 Rediacc 为什么不提供碎片整理命令。

太长不看。 Rediacc 的 rdc machine query --storage-health 会报告每个仓库的碎片化指标。在某台生产机器上,它把 GitLab 标记为约 19,650 个 extent/GB。直觉是去做碎片整理。我改为实际测了一下。

  • 碎片化程度比邻近仓库高 16 倍的仓库,顺序读速度为 149 MB/s,而对比仓库是 143 MB/s;随机 4K 读也更快(719 us 对 957 us)。
  • 该设备是闪存。碎片化通过寻道时间损伤旋转磁盘。在 SSD 上,几乎不存在它造成伤害的机制。
  • 在这台机器上运行 btrfs filesystem defragment 会将约 250 GB 的 reflink fork 和快照解除共享,而存储池只剩 4.4 GB 空间。这才是真正的风险,而基准测试显示没有任何收益可以与之权衡。

存储健康报告的目的是回答一个问题:我的磁盘空间去哪了。它显示每个仓库的大小、与 fork 共享了多少数据,以及碎片化指标。最后这个数字看起来可能很吓人。在我运行的这台机器上,GitLab 的仓库镜像报告在 14.6 GB 里有 268,771 个 extent,约为 19,650 个 extent/GB,工具将其标注为”高”。

随之而来的反应是自动的。碎片化高,那就做碎片整理。我在旋转磁盘上把这个反应写进 shell 脚本已有十五年。在给 Rediacc 加入碎片整理按钮之前,我想知道这个数字在我们实际运行的硬件上到底意味着什么代价。于是我在这台生产机器上做了基准测试。

这个数字实际上在统计什么

Rediacc 的仓库是一个单独的 LUKS 镜像文件,存放在 btrfs 存储池上。碎片化指标来自对该镜像文件运行 filefrag,统计的是加密容器的 extent,而不是应用程序在其中读取的文件。

这一点之所以重要,是因为数据的堆叠方式。从底层往上:物理 SSD,然后是宿主机 ext4 根文件系统,然后是 loop-backed 存储池文件,然后是 loop0,然后是 btrfs 存储池,然后是 LUKS 镜像,然后是 device-mapper 加密设备,最后是容器所见的内层 ext4。btrfs 是写时复制(copy-on-write)的。仓库内的每次随机写入都会在镜像中写入一个新 extent。数据库和容器 overlay 层整天都在随机写,所以镜像按设计会不断积累 extent。

卷内部的文件是另一回事。我检查了 GitLab 仓库:其 gitaly 二进制文件在 10 个 extent 中,一个 git pack 文件在 17 个 extent 中。内层文件系统并不碎片化。19,650/GB 这个数字描述的是写时复制容器本身,这正是你预期它应有的样子,并不能告诉你读取是否慢。

基准测试

我挑选了碎片化程度相差最大的两个仓库,用直接 IO 读取它们(这绕过页面缓存,强制进行物理读取)。

仓库平均 extent 大小每 GB extent 数顺序读速度
GitLab54 KB~19,650149 MB/s
Stack Overflow 演示880 KB~1,190143 MB/s

16 倍的碎片化差距没有产生任何吞吐量损失。碎片化更严重的文件反而略快。然后是人们真正担心的场景(小随机读,也就是数据库流量的形态):

仓库随机 4K 延迟IOPS
GitLab(碎片化严重)719 us1,390
Stack Overflow 演示(碎片化较轻)957 us1,045

同样,碎片化更严重的文件更快。这个小差距源于文件大小和后端缓存,而非 extent 布局。在闪存上,无论周围的 extent 在哪里,一次随机读就是一次查找加一次读取,没有磁头需要移动。

吞吐量以绝对值衡量是适中的,约为 145 MB/s 和 1,000 IOPS,这是因为设备是共享宿主机上的虚拟化磁盘,数据路径很深。这个上限由 btrfs 上下的虚拟化层和加密层决定,对镜像做碎片整理无法突破它。

碎片化唯一真实的代价

诚实要求说出另一面。碎片化在这里确实有一项可测量的代价,但不是读取速度,而是遍历 extent 映射所花的时间:

  • GitLab 上的 filefrag(268,771 个 extent):3.19 秒
  • Stack Overflow 演示上的 filefrag(152,364 个 extent):0.74 秒

遍历每个 extent 的操作都要付这个代价,包括存储健康扫描本身、备份同步和增量工具。这只是秒级别的时间,与 extent 数量大致成正比,且只涉及后台任务而非你的应用程序。如果 extent 遍历时间真的成为实际瓶颈,那是一个范围窄小的问题,有范围窄小的解决方案,而不是重写实时数据的理由。

Rediacc 为什么不提供碎片整理命令

btrfs filesystem defragment 从大约内核 3.9 起就不再保留 reflink。手册页明确写道:碎片整理会拆散写时复制数据的 reflink,可能导致空间使用量大幅增加。将文件连续重写会将每个共享 extent 复制为私有 extent。

在这台机器上几乎所有数据都是共享的。fork 通过 reflink 共享父仓库的数据,备份定时器还额外添加了也进行共享的只读快照。存储池已 99% 满,只剩 4.4 GB 空间。GitLab 有 97% 是共享的,所以对它做碎片整理会尝试将约 14 GB 复制到 4.4 GB 的空间里,中途就会失败。Stack Overflow 演示仓库有 137 GB,其中只有 26 MB 是唯一数据,对它做碎片整理会尝试实体化 137 GB 实际上并不存在的数据。所有仓库合计约有 250 GB 是通过 reflink 共享的。碎片整理不是一次调优,而是一颗空间炸弹。

即便空间够用,也不会持久。这些镜像在同样的随机写工作负载下几分钟内就会重新碎片化。你会短暂地解除 fork 的共享,却换来一个基准测试已经证明你本来就有的读取速度。

应该关注什么而不是碎片化

在同一份报告里真正值得关注的列是”分歧率”(Divergence)。它是仓库镜像中属于该仓库独有、而非与 fork 和快照共享的数据百分比。一个新鲜 fork 的分歧率接近 0%,因为它几乎共享所有内容。一个自 fork 以来被大量写入的仓库会向 100% 攀升。

分歧率能回答碎片化无法回答的问题:这个仓库实际上占用了多少真实的、可回收的磁盘空间。当存储池空间紧张时,低分歧率的仓库是很差的清理目标,因为它的字节是共享的,删除它几乎不释放空间。字节真正所在的地方是分歧率高的仓库。

结论

碎片化数字是真实存在的,在随机写入下的写时复制镜像上它总是看起来很高。在闪存上,它只是参考信息。我测量了 16 倍的差距,没有发现读取惩罚,碎片化更严重的文件随机读性能反而更好,唯一的小代价是后台扫描时间。那个本来可以”修复”这个数字的工具,反而会将四分之一 TB 的 fork 解除共享,塞进一个根本没有空间容纳它们的存储池。

所以 Rediacc 报告碎片化并加以解释,不提供任何按钮来处理它。诚实的工程答案是对假设进行基准测试,而不是将反射动作自动化。