Skip to main content Skip to navigation Skip to footer
Limited time: Design Partner Program — BUSINESS plan free for life

Your Fragmentation Number Looks Terrifying. I Benchmarked What It Costs.

Our storage-health report flags repositories at nearly 20,000 extents per gigabyte. That number sounds alarming. I ran sequential and random read benchmarks on the worst offenders and the penalty was zero. Here is the data, and why Rediacc ships no defragment command.

TL;DR. Rediacc’s rdc machine query --storage-health reports a fragmentation figure per repository. On one production machine it flags GitLab at roughly 19,650 extents per gigabyte. The instinct is to defragment. I measured it instead.

  • A repository fragmented 16x more than its neighbor read at 149 MB/s versus 143 MB/s sequentially, and was faster on random 4K reads (719 vs 957 microseconds).
  • The device is flash. Fragmentation hurts spinning disks through seek time. On an SSD there is almost no mechanism left for it to hurt.
  • Running btrfs filesystem defragment here would unshare about 250 GB of reflinked forks and snapshots into a pool with 4.4 GB free. That is the real risk, and the benchmark says there is no benefit to weigh against it.

The storage-health report exists to answer one question: where is my disk going. It shows each repository’s size, how much data it shares with its forks, and a fragmentation figure. That last number can look frightening. On the machine I run, GitLab’s repository image reports 268,771 extents across 14.6 GB. That is about 19,650 extents per gigabyte, and the tool labels it “high”.

The reflex that follows is automatic. High fragmentation, so defragment. I have written that reflex into shell scripts on spinning disks for fifteen years. Before I added a defrag button to Rediacc, I wanted to know what the number actually costs on the hardware we run. So I benchmarked it on the live machine.

What the number actually counts

A Rediacc repository is a single LUKS image file living on a btrfs pool. The fragmentation figure comes from running filefrag on that image file. It counts the extents of the encrypted container, not the files your application reads inside it.

This matters because of how the data is stacked. From the bottom up: a physical SSD, then the host ext4 root filesystem, then a loop-backed pool file, then loop0, then the btrfs pool, then the LUKS image, then a device-mapper crypt device, then the inner ext4 your containers see. btrfs is copy-on-write. Every random write inside a repository writes a fresh extent in the image. Databases and container overlay layers write randomly all day, so the image accumulates extents by design.

The files inside the volume are a different story. I checked the GitLab repository: its gitaly binary is in 10 extents, a git pack file in 17. The inner filesystem is not fragmented. The 19,650-per-gigabyte figure describes the copy-on-write container, which is exactly what you would expect it to look like and tells you nothing about whether reads are slow.

The benchmark

I picked two repositories at opposite ends of the fragmentation scale and read from them with direct IO, which bypasses the page cache and forces a physical read.

RepositoryAverage extentExtents per GBSequential read
GitLab54 KB~19,650149 MB/s
Stack Overflow demo880 KB~1,190143 MB/s

A 16x difference in fragmentation produced no throughput penalty. The heavily fragmented file was marginally faster. Then the pattern that actually worries people, small random reads, the shape of database traffic:

RepositoryRandom 4K latencyIOPS
GitLab (fragmented)719 us1,390
Stack Overflow demo (less fragmented)957 us1,045

Again the more fragmented file is faster. The small gap tracks file size and backend caching, not extent layout. On flash, a random read is one lookup and one read regardless of where the surrounding extents sit. There is no head to move.

The throughput is modest in absolute terms, around 145 MB/s and 1,000 IOPS, because the device is a virtualized disk on a shared host and the data path is deep. That ceiling is set by the virtualization and crypt layers, above and below btrfs. Defragmenting the image cannot lift it.

The one thing fragmentation does cost

Honesty requires the other side. Fragmentation has exactly one measurable cost here, and it is not read speed. It is the time to enumerate the extent map:

  • filefrag on GitLab (268,771 extents): 3.19 s
  • filefrag on the Stack Overflow demo (152,364 extents): 0.74 s

Operations that walk every extent pay this. That includes the storage-health scan itself, backup synchronization, and delta tooling. It is seconds, it scales roughly with extent count, and it touches background jobs rather than your application. If extent-walk time ever becomes a real bottleneck, that is a narrow problem with narrow fixes. It is not a reason to rewrite live data.

Why Rediacc ships no defragment command

btrfs filesystem defragment has not preserved reflinks since around kernel 3.9. The manual page says so plainly: defragmenting breaks up the reflinks of copy-on-write data and can cause a considerable increase in space usage. Rewriting a file contiguously copies every shared extent into a private one.

On this machine almost everything is shared. Forks share their parent’s data through reflinks, and the backup timer adds read-only snapshots that share too. The pool is 99% full with 4.4 GB free. GitLab is 97% shared, so defragmenting it would try to copy up roughly 14 GB into 4.4 GB and fail partway. The Stack Overflow demo is 137 GB with 26 MB of unique data, so defragmenting it would attempt to materialize 137 GB that does not physically exist. Across all repositories about 250 GB is reflinked. A defrag pass is a space bomb, not a tune-up.

Even where it fit, it would not last. These images re-fragment within minutes under the same random-write workload. You would unshare your forks, briefly, for a read speed the benchmark says you already have.

What to read instead of fragmentation

The column worth your attention in the same report is divergence. It is the percentage of a repository’s image that is unique to it rather than shared with forks and snapshots. A fresh fork sits near 0%, because it shares almost everything. A repository that has been written heavily since it was forked climbs toward 100%.

Divergence answers the question fragmentation cannot: how much real, reclaimable disk does this repository cost. When a pool is tight, a low-divergence repository is a poor cleanup target, because its bytes are shared and deleting it frees little. The bytes live where divergence is high.

The takeaway

The fragmentation number is real, and on a copy-on-write image under random writes it will always look high. On flash it is informational. I measured a 16x spread and found no read penalty, a faster random profile on the more fragmented file, and a single small cost in background scan time. The tool that would “fix” the number would instead unshare a quarter of a terabyte of forks into a pool that has no room for them.

So Rediacc reports fragmentation and explains it, and offers no button to act on it. The honest engineering answer was to benchmark the assumption rather than automate the reflex.