跳到主要内容

Btrfs快照方案

· 阅读需 6 分钟
Skyone
科技爱好者

去年从 ext4 文件系统切换到了 btrfs,当时主要看中的是快照功能。最开始使用的是 timeshift,能用,但是可定制性不够,遂尝试 直接手动创建快照。

自从用了快照,最显著的变化是:折腾各种危险的东西再也不需要使用虚拟机,直接host开搞,最坏不过回滚一下快照,重新生成一下 GRUB,一个健康的系统就又回来了。

当然,快照大多了,就开始考虑怎么自动化,于是简单写了个脚本,试了一下还不错,放在本文后面了。

我使用的 ArchLinux 使用了如下的分区方案:

╭────────────┬────────┬───────┬────────┬────────────────╮
│ Mounted on │ Size │ Type │ Volume | Filesystem │
├────────────┼────────┼───────┼────────┼────────────────┤
│ / │ 937.6G │ btrfs │ @ | /dev/nvme0n1p4 │
│ /boot │ 920.7M │ ext4 │ | /dev/nvme0n1p2 │
│ /boot/efi │ 475.1M │ vfat │ | /dev/nvme0n1p1 │
│ /home │ 937.6G │ btrfs │ @home | /dev/nvme0n1p4 │
╰────────────┴────────┴───────┴────────┴────────────────╯

我将快照保存在 Btrfs 根分区的 .snapshots 目录,就像这样:

[/]
├─ [@]
├─ [@home]
╰─ .snapshots
├─ [@-20250101] (ro)
╰─ [@home-20250101] (ro)

其中子卷使用 [] 表示。

日常使用不挂载 [/] 子卷,也就是说,无论怎么玩都不会破坏快照。

请注意!建议在 fstab 中不要指定 subvolid ,而是使用 subvol 直接指定子卷名称,这样恢复快照会更方便,下面的所有例子都以此为前提。

创建快照

当需要创建快照时,先挂载 [/] 子卷

mount -o compress=zstd:3,subvol=/ /dev/nvme0n1p3 /mnt

创建只读快照

btrfs subvolume snapshot -r /mnt/@ /mnt/.snapshots/@-`date +"%Y%m%d"`
btrfs subvolume snapshot -r /mnt/@ /mnt/.snapshots/@home-`date +"%Y%m%d"`

取消挂载 [/]

umount /mnt

恢复快照

可以通过修改 fstab 实现只重启两次即可恢复快照,不需要进入LiveCD模式。

具体思路是创建一个快照到 @-restore @home-restore

mount -o compress=zstd:3,subvol=/ /dev/nvme0n1p3 /mnt
btrfs subvolume snapshot /mnt/.snapshots/@-yyyymmdd /mnt/@-restore
btrfs subvolume snapshot /mnt/.snapshots/@home-yyyymmdd /mnt/@home-restore

编辑 /etc/fstab ,把 @ 子卷和 @home 子卷改成 @-restore@home-restore

这里修改 /etc/fstab 的作用是提示 grub 根分区的位置,而真正起作用的是 /mnt/@-restore/etc/fstab

cp /etc/fstab /mnt/@-restore/etc/fstab
grub-mkconfig -o /boot/grub/grub.cfg

重启电脑。

重启之后,再把两个子卷的名字改回去:

mount -o compress=zstd:3,subvol=/ /dev/nvme0n1p3 /mnt
btrfs subvolume delete /mnt/@
btrfs subvolume delete /mnt/@home
btrfs subvolume snapshot /mnt/.snapshots/@-yyyymmdd /mnt/@
btrfs subvolume snapshot /mnt/.snapshots/@-yyyymmdd /mnt/@home

编辑 /etc/fstab ,把 @-restore 子卷和 @home-restore 子卷改回 @@

grub-mkconfig -o /boot/grub/grub.cfg

再次重启电脑。

重启之后,把临时的 restore 分区删了

mount -o compress=zstd:3,subvol=/ /dev/nvme0n1p3 /mnt
btrfs subvolume delete /mnt/@-restore
btrfs subvolume delete /mnt/@home-restore
umount /mnt

结束

恢复快照(LiveCD)

如果你的系统已经没办法引导了,也可以使用LiveCD恢复快照,这个过程甚至比上面的要简单。

进入LiveCD环境,修改 @@home 就结束了,什么配置都不用改。

mount -o compress=zstd:3,subvol=/ /dev/nvme0n1p3 /mnt
btrfs subvolume delete /mnt/@
btrfs subvolume delete /mnt/@home
btrfs subvolume snapshot /mnt/.snapshots/@-yyyymmdd /mnt/@
btrfs subvolume snapshot /mnt/.snapshots/@-yyyymmdd /mnt/@home
umount /mnt

重启即可。

快照脚本

自动创建快照,创建快照前会删除 Pacman 缓存,如果你不使用ArchLinux,请自行修改 make_snapshot 函数第2行为对应的包管理器

#!/usr/bin/env sh

set -e

run() {
if [ -z "$is_fake" ] || [ "$is_fake" = false ]; then
echo -e "sh: \033[1;32m$*\033[0m"
sudo "$@" > /dev/null
else
echo -e "> \033[1;32m$*\033[0m"
fi
}

make_snapshot() {
NOW_DATE=$(date +"%Y%m%d")
run pacman -Sc --noconfirm
run mount -o compress=zstd:3,subvol=/ "$DEVICE" /mnt

SNAPSHOT_FROM="/mnt/@"
SNAPSHOT_TO="/mnt/.snapshots/@-$NOW_DATE"
SNAPSHOT_NAME=".snapshots/@-$NOW_DATE"
if [ "$is_fake" = false ] && sudo btrfs subvolume list /mnt -o | grep -q "$SNAPSHOT_NAME"; then
run btrfs subvolume delete "$SNAPSHOT_TO"
fi
run btrfs subvolume snapshot -r "$SNAPSHOT_FROM" "$SNAPSHOT_TO"

SNAPSHOT_FROM="/mnt/@home"
SNAPSHOT_TO="/mnt/.snapshots/@home-$NOW_DATE"
SNAPSHOT_NAME=".snapshots/@home-$NOW_DATE"
if [ "$is_fake" = false ] && sudo btrfs subvolume list /mnt -o | grep -q "$SNAPSHOT_NAME"; then
run btrfs subvolume delete "$SNAPSHOT_TO"
fi
run btrfs subvolume snapshot -r "$SNAPSHOT_FROM" "$SNAPSHOT_TO"

run umount /mnt
}

DEVICE=$(findmnt -n -o SOURCE / | sed 's/\[.*\]//')

echo "List all devices"
df -h | grep nvme
echo -e "Selected device: \033[1;32m$DEVICE\033[0m"
echo
echo "Following commands will be executed:"
is_fake=true
make_snapshot

read -r -p "Are you sure? [y/N] " input
input=${input,,}
echo

if [ "$input" = "y" ]; then
is_fake=false
make_snapshot
fi

列出全部快照,会自动检查 @@home 的快照是否成对出现

#!/usr/bin/env sh

set -e

run() {
echo -e "sh: \033[1;32m$*\033[0m"
sudo "$@"
}

list_snapshots() {
run mount -o compress=zstd:3,subvol=/ "$DEVICE" /mnt
snapshot_dates=$(ls /mnt/.snapshots | grep '@-' | sed 's/@-//' | sort -u)

if [ -z "$snapshot_dates" ]; then
echo -e "\033[1;31mNo snapshots found.\033[0m"
run umount /mnt
return
fi

# Check if each date has a corresponding home snapshot
for date in $snapshot_dates; do
if ! ls /mnt/.snapshots | grep -q "@home-$date"; then
echo -e "\033[1;33mWarning: No home snapshot for date $date!\033[0m"
fi
done

# Display the list of dates
echo -e "\n\033[1;32mSnapshots found for the following dates:\033[0m"
for date in $snapshot_dates; do
echo -e "\033[1;36m$date\033[0m"
done

run umount /mnt
}

DEVICE=$(findmnt -n -o SOURCE /home | sed 's/\[.*\]//')

echo "List all devices"
df -h | grep nvme
echo -e "Selected device: \033[1;32m$DEVICE\033[0m"
echo

list_snapshots