记录一下把 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-convert 和 btrfs 二进制打包进 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=async 和 space_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 支持块级去重,用 bees 或 duperemove 跑一遍,多个镜像之间共享的相同内容会自动合并为同一份物理数据,对镜像多的机器省空间效果显著
- 可以单独快照:每个容器的子卷都可以独立做 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;"