记录一下把 Oracle Cloud 上的 Ubuntu 26.04 ARM VM 从 ext4 原地转换为 Btrfs 的过程。

WARNING: 本文操作涉及文件系统级别的原地转换,一旦出错数据将无法恢复。我的 VM 是一台空白机器,没有任何重要数据,所以才敢这么操作。如果你的机器上有任何你不想丢的东西,请务必先做好完整备份(Oracle Cloud 的 Boot Volume Backup、rsync 到远端、dd 镜像等),确认备份可恢复之后再动手。文件系统转换失败 + 没有备份 = 数据全灭,没有后悔药。

背景

Oracle Cloud 免费 ARM 实例,150G 磁盘,系统盘只有一个 ext4 根分区。想用上 Btrfs 的透明压缩、快照和子卷管理,但不想重装系统。

方案:initramfs 钩子 + btrfs-convert

核心思路:根分区在挂载状态下无法转换,所以利用 initramfs 的 local-premount 阶段——此时根分区尚未挂载——执行 btrfs-convert

1. 安装 btrfs-progs

sudo apt install btrfs-progs

2. 创建 initramfs 钩子

btrfs-convertbtrfs 二进制打包进 initramfs:

# /etc/initramfs-tools/hooks/btrfs-convert
#!/bin/sh
set -e
PREREQ=""
prereqs() { echo "$PREREQ"; }
case $1 in prereqs) prereqs; exit 0;; esac
. /usr/share/initramfs-tools/hook-functions
copy_exec /usr/bin/btrfs-convert /usr/bin
copy_exec /usr/bin/btrfs /usr/bin

在 root 挂载前执行转换。注意 btrfs-convert 会生成新的 UUID,所以脚本里需要同时更新 GRUB 配置,否则重启后内核找不到 root 会掉进 initramfs shell:

# /etc/initramfs-tools/scripts/local-premount/btrfs-convert
#!/bin/sh
set -e
PREREQ=""
prereqs() { echo "$PREREQ"; }
case $1 in prereqs) prereqs; exit 0;; esac
ROOT_DEV="/dev/sda1"
FSTYPE=$(blkid -s TYPE -o value "$ROOT_DEV" 2>/dev/null || true)
if [ "$FSTYPE" = "ext4" ]; then
    OLD_UUID=$(blkid -s UUID -o value "$ROOT_DEV")
    echo ">>> Converting $ROOT_DEV from ext4 to btrfs..."
    btrfs-convert "$ROOT_DEV"
    btrfs filesystem label "$ROOT_DEV" cloudimg-rootfs
    NEW_UUID=$(blkid -s UUID -o value "$ROOT_DEV")
    # 更新 GRUB 配置中的 UUID,/boot 是独立分区需要先挂载
    if [ "$OLD_UUID" != "$NEW_UUID" ]; then
        echo ">>> UUID changed: $OLD_UUID -> $NEW_UUID, updating GRUB..."
        mkdir -p /tmp/boot
        mount /dev/sda16 /tmp/boot
        sed -i "s/$OLD_UUID/$NEW_UUID/g" /tmp/boot/grub/grub.cfg
        umount /tmp/boot
    fi
    echo ">>> Conversion complete."
fi

3. 更新 fstab 和 GRUB 默认配置

fstab 改用 LABEL= 挂载(不受 UUID 变更影响):

LABEL=cloudimg-rootfs  /  btrfs  defaults,noatime,compress=zstd  0 0

同时在 /etc/default/grub 里也建议改用 LABEL,避免将来 update-grub 又写回旧 UUID。

4. 重建 initramfs 并重启

sudo update-initramfs -u -k all
sudo reboot

子卷重排

转换后所有数据在顶层子卷 (subvolid=5)。参照 Arch 的常见布局,创建子卷并迁移数据:

子卷挂载点用途
@/根,快照主体
@home/home独立于根快照
@log/var/log排除日志
@cache/var/cache排除缓存
@tmp/var/tmp排除临时文件
# 挂载顶层子卷
mount -t btrfs -o subvolid=5 /dev/sda1 /mnt/btrfs-top

# 创建子卷
for sv in @ @home @log @cache @tmp; do
    btrfs subvolume create /mnt/btrfs-top/$sv
done

# rsync 迁移数据
rsync -aAX --exclude='/home/*' --exclude='/var/log/*' \
    --exclude='/var/cache/*' --exclude='/var/tmp/*' \
    --exclude='/proc/*' --exclude='/sys/*' --exclude='/dev/*' \
    --exclude='/run/*' --exclude='/tmp/*' \
    /mnt/btrfs-top/ /mnt/btrfs-top/@/

rsync -aAX /mnt/btrfs-top/home/ /mnt/btrfs-top/@home/
rsync -aAX /mnt/btrfs-top/var/log/ /mnt/btrfs-top/@log/
rsync -aAX /mnt/btrfs-top/var/cache/ /mnt/btrfs-top/@cache/
rsync -aAX /mnt/btrfs-top/var/tmp/ /mnt/btrfs-top/@tmp/

# 设置默认子卷
btrfs subvolume set-default /mnt/btrfs-top/@

更新 fstab 为各子卷独立挂载,GRUB 加上 rootflags=subvol=/@,重启即可。

透明压缩效果

转换后对全盘做一次 btrfs filesystem defragment -r -czstd /,让旧文件也吃到压缩:

Type       Perc     Disk Usage   Uncompressed Referenced
TOTAL       55%      2.2G         4.0G         4.0G
none       100%      1.0G         1.0G         1.0G
zstd        40%      1.2G         2.9G         2.9G

4G 数据压到 2.2G,zstd 部分压缩率 40%。

定期维护

Ubuntu 的 btrfs-progs 不自带 systemd timer,需要额外安装:

sudo apt install btrfsmaintenance
sudo systemctl enable --now btrfs-scrub.timer btrfs-balance.timer

配置 /etc/default/btrfsmaintenance

  • scrub: 月度(校验数据完整性)
  • balance: 月度(回收碎片空间)
  • 挂载点都是“auto”,自动所有Btrfs挂载点

最终挂载参数

noatime,compress=zstd:3,discard=async,space_cache=v2

其中 discard=asyncspace_cache=v2 是内核默认启用的,不需要显式写在 fstab 里。

Podman + Btrfs

如果你在 Btrfs 上跑容器,推荐用 Podman——它原生支持 btrfs 作为 storage driver。在 /etc/containers/storage.conf 里设置:

[storage]
driver = "btrfs"

相比 Docker 默认的 overlay2,btrfs driver 的每一层镜像都是一个 btrfs 子卷,层与层之间通过原生 CoW 实现,不需要 overlayfs 的联合挂载。实际收益:

  • 去重更容易:btrfs 支持块级去重,用 beesduperemove 跑一遍,多个镜像之间共享的相同内容会自动合并为同一份物理数据,对镜像多的机器省空间效果显著
  • 可以单独快照:每个容器的子卷都可以独立做 btrfs 快照,方便备份或回滚特定容器的状态,粒度比整盘快照细得多
  • 与宿主文件系统统一:不需要额外的 loopback 设备或独立分区,容器存储和宿主共享同一个 btrfs 池,空间管理更灵活

bees 去重实测

Ubuntu 官方源里没有 bees,需要自己编译安装。我给 hash table 分配了 256MB,跑完之后效果:

Processed 156002 files, 76692 regular extents (93035 refs), 64930 inline.
Type       Perc     Disk Usage   Uncompressed Referenced
TOTAL       50%      1.8G         3.5G         5.1G
none       100%      911M         911M         1.1G
zstd        34%      941M         2.6G         3.9G

对比之前没有容器时的数据(4.0G referenced / 2.2G disk usage),加了容器镜像之后 referenced 涨到了 5.1G,但 disk usage 反而从 2.2G 降到了 1.8G——这就是去重的威力:多个容器镜像之间大量的共享层被合并成同一份物理数据,加上 zstd 压缩率从 40% 进一步降到 34%,实际占用比转换 Btrfs 之初还少。

注意事项:Btrfs 与数据库/VM 磁盘镜像

Btrfs 的 CoW 语义对虚拟机磁盘镜像和大多数数据库来说是个坑。VM 镜像(qcow2、raw)和数据库数据文件会产生大量随机小写入,CoW 导致碎片化严重、性能急剧下降。对于这类场景,建议:

  • VM 磁盘镜像 / 传统数据库(PostgreSQL、MySQL 等):将其放在单独的子卷上并禁用 CoW(chattr +C 或挂载选项 nodatacow),或者干脆用 ext4/XFS 的独立分区

  • SQLite:SQLite 默认的 rollback journal 模式在 CoW 文件系统上有双重写入放大的问题,但开启 WAL 模式后就没什么问题了——WAL 是追加写入,和 CoW 不冲突。开启方法:

    PRAGMA journal_mode=WAL;
    

    这条 PRAGMA 只需执行一次,设置会持久化到数据库文件中,后续连接自动使用 WAL 模式。也可以在命令行里一行搞定:

    sqlite3 your.db "PRAGMA journal_mode=WAL;"