Logo
“Btrfs快照方案”的封面

Btrfs快照方案

Avatar

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

隐私政策

Copyright © Skyone 2025