I recently spent more time than I would like to admit trying to convert a Btrfs filesystem from free-space cache v1 to v2. The conversion appeared to fail silently, and the usual tools for diagnosing a busy device gave no useful output whatsoever.
Background
The filesystem in question was a two-device Btrfs RAID1 array:
/dev/sda— 1.82 TiB/dev/sdc— 2.73 TiB
Inspecting the superblock with btrfs inspect-internal dump-super showed:
1 2 | |
No FREE_SPACE_TREE in the flags. The filesystem was using the legacy free-space cache v1, which gets stored as regular files in each subvolume rather than as a dedicated tree structure. Converting to v2 is generally recommended as it is more robust and survives unclean shutdowns better.
First Attempt
Mounting with the conversion flags seemed straightforward enough:
1 | |
The filesystem mounted without errors. But checking the superblock again showed incompat_flags 0x161 — unchanged. No FREE_SPACE_TREE. Trying again without clear_cache made no difference.
btrfs-progs 6.17.1 ships a more direct tool for this:
1 | |
So I unmounted /mnt/btrfs and ran:
1 | |
The result:
1 2 | |
The Puzzling Part
This is where it gets frustrating. The error says the device is busy, but nothing confirmed that:
fuser /dev/sda /dev/sdc— nothinglsof /dev/sda /dev/sdc— nothingfindmnt—/mnt/btrfswas not listed- No device-mapper devices existed
- Both block devices were writable
I had also paused the Docker containers running on the host. Or so I thought.
Finding the Culprit
The key insight is that fuser, lsof and findmnt all operate within the context of the host's mount namespace. Docker containers run in their own separate mount namespaces. A filesystem mounted inside a container's namespace is invisible to these tools from the host's perspective, but the kernel still considers the underlying block device in use.
The way to look across all mount namespaces is to inspect /proc/*/mountinfo directly:
1 | |
This searches the mountinfo file of every running process and returns the paths of those that reference btrfs. In my case, it returned:
1 2 3 | |
Checking those PIDs:
- PID 5359:
tini -- /bin/bash -c start.sh - PID 5440: docker container
- PID 5520: docker container API process
The containers had /dev/sdc mounted at /data inside their namespace. Pausing the containers leaves them running with their mount namespaces intact. The Btrfs device set was still held open by the kernel on behalf of those containers, which is why btrfs rescue could not obtain exclusive access to both RAID1 members.
The Fix
Bringing the stack down properly rather than just pausing it:
1 | |
Afterwards, re-running the grep returned nothing:
1 2 | |
The filesystem was now genuinely unmounted from all namespaces. Retrying the cache clear:
1 | |
1 | |
Mounting with v2 afterwards:
1 | |
Confirmed via:
1 2 | |
And definitively via:
1 | |
which showed FREE_SPACE_INFO, FREE_SPACE_EXTENT and FREE_SPACE_BITMAP entries under a FREE_SPACE_TREE ROOT_ITEM — the v2 tree was created and active.
Takeaway
There are two things worth noting here. First, ERROR: cannot open device '/dev/sda': Device or resource busy is not a helpful error message when nothing on the host appears to be using the device. A message along the lines of "device held open in a non-host mount namespace" would save significant time. The existing tooling has a blind spot here.
Second, sudo grep -l btrfs /proc/*/mountinfo is a genuinely useful command in situations like this. It bypasses the limitations of fuser and lsof by going directly to the kernel's per-process mount information, which covers all namespaces. Worth keeping in your back pocket whenever a block device reports busy and nothing obvious explains why.