TL;DR. Rediacc’s
rdc machine query --storage-healthreports 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 defragmenthere 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.
| Repository | Average extent | Extents per GB | Sequential read |
|---|---|---|---|
| GitLab | 54 KB | ~19,650 | 149 MB/s |
| Stack Overflow demo | 880 KB | ~1,190 | 143 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:
| Repository | Random 4K latency | IOPS |
|---|---|---|
| GitLab (fragmented) | 719 us | 1,390 |
| Stack Overflow demo (less fragmented) | 957 us | 1,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:
filefragon GitLab (268,771 extents): 3.19 sfilefragon 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.