Expanding btrfs filesystems

How to replace disks in a mirror and expand available space

A few months ago, I migrated a bunch of media from a ZFS mirror on my Mac to my home server VM. I used mismatched drives that I happened to have on hand, and while it works fine, I might as well replace the disks with the ones from the ZFS pool that I’m not using any more.

While ZFS works reasonably well, it carries a ton of technical and political debt, particularly on Linux. Since btrfs has matured significantly in the last few years, I’m using it on my Linux servers now. I like btrfs a lot. It has good performance, and offers advanced features like data and metadata checksumming, snapshots, RAID, and compression.

What follows is mostly for my own notes, but maybe it’ll help someone.

Note that /var/lib/plex/data is just one of the subvolumes that belongs to the btrfs filesystem I’m working on.

First, let’s identify the existing devices in the filesystem:

root@proteus:~# btrfs filesystem show /var/lib/plex/data
Label: 'Media'  uuid: 290c5b77-9bff-47d5-9f82-3d7576f134f5
        Total devices 2 FS bytes used 1.62TiB
        devid    1 size 5.46TiB used 1.62TiB path /dev/sdd2
        devid    2 size 3.64TiB used 1.62TiB path /dev/sdc2


/dev/sdd2, which btrfs regards as devid 2, is the disk I’ll replace first. I’ve already partitioned a replacement disk, /dev/sde2

Let’s start the replacement:

root@proteus:~# btrfs replace start -B 2 /dev/sde2 /var/lib/plex/data

The -B option makes the command block until it’s completed, so it’s completely optional. 2 is the devid we’re replacing. The filesystem will remain online and read/write during the migration. Btrfs doesn’t appear to prioritize regular filesystem I/O during the migration, so expect performance to suffer until the migration is done.

It’s also possible to monitor the status in a blocking way:

root@proteus:~# btrfs replace status  /var/lib/plex/data
7.4% done, 0 write errs, 0 uncorr. read errs

With the migration in progress, you can also see that the mirror has three devices attached:

root@proteus:~# btrfs filesystem show /var/lib/plex/data/
Label: 'Media'  uuid: 290c5b77-9bff-47d5-9f82-3d7576f134f5
	Total devices 3 FS bytes used 1.63TiB
	devid    0 size 3.64TiB used 1.62TiB path /dev/sde2
	devid    1 size 5.46TiB used 1.64TiB path /dev/sdd2
	devid    2 size 3.64TiB used 1.64TiB path /dev/sdc2

The new disk, devid 0, is only reporting 3.64TiB, even though it’s an 8TB disk. We’ll fix that once the migration is done.

Hours later, the process is done:

root@proteus:~# btrfs filesystem show /var/lib/plex/data/
Label: 'Media'  uuid: 290c5b77-9bff-47d5-9f82-3d7576f134f5
        Total devices 2 FS bytes used 1.62TiB
        devid    1 size 5.46TiB used 1.63TiB path /dev/sdd2
        devid    2 size 3.64TiB used 1.63TiB path /dev/sde2

Note how devid 2 is the new disk on /dev/sde2, but it still shows the wrong capacity. It’s unclear why resizing is not automatic, but it can be done manually:

root@proteus:~# btrfs filesystem resize 2:max /var/lib/plex/data
Resize '/var/lib/plex/data' of '2:max'

The resize operation completes almost instantly, and now we have:

root@proteus:~# btrfs filesystem show /var/lib/plex/data/
Label: 'Media'  uuid: 290c5b77-9bff-47d5-9f82-3d7576f134f5
	Total devices 2 FS bytes used 1.62TiB
	devid    1 size 5.46TiB used 1.63TiB path /dev/sdd2
	devid    2 size 7.28TiB used 1.63TiB path /dev/sde2

Finally, we can see that full redundancy is maintained:

root@proteus:~# btrfs device usage /var/lib/plex/data
/dev/sdd2, ID: 1
   Device size:             5.46TiB
   Device slack:              0.00B
   Data,RAID1:              1.63TiB
   Metadata,RAID1:          3.00GiB
   System,RAID1:           64.00MiB
   Unallocated:             3.83TiB

/dev/sde2, ID: 2
   Device size:             7.28TiB
   Device slack:            3.50KiB
   Data,RAID1:              1.63TiB
   Metadata,RAID1:          3.00GiB
   System,RAID1:           64.00MiB
   Unallocated:             5.65TiB

There are some relevant messages in the kernel log:

[1987714.818696] BTRFS info (device sdd2): dev_replace from /dev/sdc2 (devid 2) to /dev/sde2 started
[1998011.747252] BTRFS info (device sdd2): dev_replace from /dev/sdc2 (devid 2) to /dev/sde2 finished
[1999100.104284] BTRFS info (device sdd2): resizing devid 2
[1999100.104289] BTRFS info (device sdd2): new size for /dev/sde2 is 8001352437760

Now /dev/sdc can be removed from the system.

root@proteus:~# lsblk -S /dev/sdc
NAME HCTL       TYPE VENDOR   MODEL             REV TRAN
sdc  4:0:0:0    disk Mercury  Elite Pro Quad C 0    usb

The above command shows that the disk occupies bay C in the disk enclosure.

After removing the disk and replacing it with another 8TB disk, the entire process was repeated to replace /dev/sdd2.