In-place conversion to BTRFS
Because Feep! Search runs on a server with a fairly small hard disk (and also to making huge copies all the time), I need a way to share files between my production and development environments, ideally without causing dev changes to accidentally mangle production data. I’m currently using hardlinks to read-only files, which is a strategy that can sort of emulate copy-on-write data but has some annoying limitations: in particular, it only works correctly for files that are deleted and replaced, so any files that are updated in place (like SQLite databases) had to be treated specially. To simplify this, I decided to switch out the underlying filesystem for one that has better support for the kind of things I want to do.
Choosing a new filesystem
I decided I needed a filesystem that would:
- Support some kind of copy-on-write for files, like snapshots or reflinks. This rules out my current filesystem (ext4).
- Come built in to my current kernel (Ubuntu 20.04), so I don’t have to deal with installing kernel modules; this rules out ZFS.
- Be stable, which rules out bcachefs.
This got the list down to BTRFS or XFS; in the end, I went with BTRFS for two reasons:
- XFS volumes can’t be shrunk, which would be annoying if I decided to switch again in the future and didn’t have a way to make room for a new filesystem.
- BTRFS has some extra features which aren’t strictly necessary but I can see being useful, like automatic compression and send/receive.
Another option I considered briefly was to take snapshots at the LVM (Linux Volume Manager) layer. I mostly decided not to do this because it would require managing mountpoints; BTRFS subvolumes use regular UNIX file permissions and don’t need root permissions to modify. I would also likely end up overprovisioning disk space, and I don’t know how it works to run out of physical space on the drive when the filesystem still thinks it has room. Finally, I already have tooling for post-hoc deduplication when two versions of a file turn out to be identical, and dealing with block-level deduplication using LVM-VDO seemed awkward. I didn’t dig into this too deeply though, and it might be a perfectly reasonable solution to my problem.
Converting to BTRFS
Now that I had a filesystem selected to migrate to, it was time to start running commands and hoping that none of them corrupted my data.
I poked around and determined that the existing filesystem was mounted from /dev/delta-vg/root
. Consulting the LVM howto I gather that delta-vg
is the name of the volume group (which in my case only has one physical volume—a 1TB hard disk), and root
is the name of a logical volume in that group. This drive started its life as the root filesystem for a host named delta
, so this all makes sense. Poking around with sudo lvs delta-vg
reveals I also have another logical volume named swap_1
which looks like it was the swapfile for that system; I’ll clean that up later so I can use the entire drive.
While poking around my LVM setup, I also discovered my current root partition is on a 500GB SSD of which only ~250GB is allocated. Apparently this is by design: the Ubuntu installer’s “use full disk” setting only uses half the disk, even if you’re only using LVM for encryption, not snapshots. This is particularly annoying because I regularly run out of /tmp
space, but thought I’d just misremembered what size drive I’d purchased.
Preparing
Rewriting filesystems in-place is always a slightly nerve-wracking process. Fortunately for me all of the data on this drive is recoverable (though it’d be annoying) if it gets lost somehow. Still, I prepared beforehand by taking checksums of all of my files (so I could at least know if anything had gotten corrupted) and also making a snapshot of the filesystem so if it went disastrously wrong I could probably roll back:
sudo find /media/fe2o3 -type f -print0 | sudo xargs -0 sha512sum > fe2o3.sha512sum
sudo umount /media/fe2o3
sudo lvcreate -L 900G -s -n snap_20250714 delta-vg/root
I have to admit I just copied this from a tutorial so I’m not entirely sure what the -L
is for; in hindsight I think it ought to have been -L 100%ORIGIN
.
This snapshot shares all of the underlying data, and btrfs-convert
says it keeps the original filesystem’s metadata as well (in a subvolume called ext2_saved
), so this should be very cheap to keep around while testing that the new filesystem is working OK. I could also have mounted the snapshot readonly to keep the data available, but I didn’t need that and so didn’t bother. (I bet if you were really clever you could do something with overlayfs
and bind mounts and make this a zero-downtime migration, but that seems messy.)
Conversion
Now that I had something resembling a rollback plan, I consulted the btrfs-convert
manpage and kicked off the conversion:
sudo e2fsck -fvy /dev/delta-vg/root
sudo btrfs-convert /dev/delta-vg/root
This took a couple of hours (I didn't monitor it closely, but I gather it computes checksums for every file and so has to read the entire disk) and reported success:
wolf@delta:~$ sudo btrfs-convert /dev/delta-vg/root
[sudo] password for wolf:
create btrfs filesystem:
blocksize: 4096
nodesize: 16384
features: extref, skinny-metadata (default)
checksum: crc32c
creating ext2 image file
creating btrfs metadata
copy inodes [o] [ 28982/ 12198]
conversion complete
After that I remounted the filesystem and ran sudo sha512sum -c --quiet fe2o3.sha512sum
to make sure all my data was still where I expected it to be; that took a bit over three hours and reported no problems.
As mentioned above, I had also noticed an old swap partition that I didn’t need any more, so I also cleared that up, removed the snapshot now that it looked like my data was OK, and gave my root partition the rest of the disk:
sudo lvremove delta-vg/snap_20250714
sudo lvremove delta-vg/swap_1
sudo lvextend -l +100%FREE /dev/delta-vg/root
sudo btrfs filesystem resize max /media/fe2o3
Subvolumes and snapshots
Now that I’m switched over to btrfs, I can set up a subvolume and copy the files into it (this is very quick because it just has to populate the metadata; the data is still shared by reflink):
mv /media/fe2o3/feepsearch/prod{,.old}
btrfs subvolume create /media/fe2o3/feepsearch/prod
cp --recursive --preserve=all --reflink=always /media/fe2o3/feepsearch/{prod.old,prod}
Finally, I switched from cp -r --link
to btrfs subvolume snapshot
and moved some SQLite databases into the partition, and now I can easily copy the production data into my development environment.