Btrfs快照方案
去年从 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