生成目录!
序
目标:增量式备份整个系统。
怎么做到增量呢?rsync + btrfs 快照。其实只用 rsync 也是可以做到增量式的1,但是支持子卷的 btrfs 可以做得更好:
-
快速删除旧的备份
-
更简单的备份逻辑
-
子卷可以设置成只读(这是个很重要的优点哦~)
-
btrfs 支持压缩。系统里有好多文本文件的,遇到压缩效果不好的文件 btrfs 会自动放弃压缩
为什么要备份整个系统呢?——因为配置一个高度定制化的系统麻烦啊,只备份部分数据的话还可能漏掉需要的文件。另一个优点是可以直接启动到某个备份。
备份了整个系统,包括各种公开或者隐私的数据,一堆 cookies 和帐号配置,邮件和聊天记录等。难道就不需要加密一下下吗?于是,在 btrfs 之下再加一层 dm-crypt 加密。
介绍完毕,下边进入正题。
准备工作
首先,需要一个足够新的 Linux 内核,因为 btrfs 还是「实验特性」,每个版本都会有大量改进。如果用比较旧的内核就有可能出事。我用的是 3.12.6 版本。其次,安装 rsync 和 cryptsetup。
当然还要准备硬件:一块希捷 BackupPlus 1T USB 3.0 移动硬盘,以及一枚Express Card 34mm 转 USB 3.0 扩展卡,因为我的笔记本没有 3.0 的接口。注意使用 USB 3.0 扩展卡,内核需要载入 pciehp 模块,否则会出现不能识别 3.0 的设备或者后续接上去的设备的情况。Arch 官方内核将这个模块直接编译进内核了,而我自己编译的很不幸没有,只好重新编译了下内核。
PS: 这俩家伙一起配合工作,写入峰值能达到 110MiB/s,比我笔记本自身的硬盘还要快。
然后是分区、格式化。我使用了 GPT 分区表。为了安装 grub 以便启动,最好在开头分配 2M 空间给 grub 使用,不然会很麻烦2。记得给这个分区 bios_grub 标志(GParted「管理标志」里勾上即可)。下一个分区是 ext4 格式的启动分区。我会在这里放一个 Arch Linux Live 系统用于维护任务,以及用于启动到备份的内核和 initramfs。因为备份的分区会被加密,所以必须把内核和 initramfs 放在另外的地方。接下来的一个分区放加密过的备份数据用的。
像这样初始化加密分区:
cryptsetup luksFormat /dev/sdc3
密码要长,但也一定要记住密码,因为除了穷举外是没有办法恢复的。
初始化完毕之后就可以使用密码打开该设备了:
cryptsetup open /dev/sdc3 lilybackup
最后的参数是一个名字,它会是解密后的设备在 /dev/mapper
下的文件名。
如果一切完毕,要记得(在卸载文件系统之后)关闭该设备:
cryptsetup luksClose lilybackup
dm-crypt 是块设备级的加密。我们还要在其上建立文件系统:
mkfs.btrfs /dev/mapper/lilybackup
然后挂载之,并建立相应的目录和子卷结构。比如我的:
* backup (dir)
* home (dir)
* current (subvol, rw)
* 20131016_1423 (subvol, ro)
* 20131116_2012 (subvol, ro)
* ...
* root (dir)
* current (subvol, rw)
* 20131016_1821 (subvol, ro)
* 20131116_2128 (subvol, ro)
* ...
* run, for boot up directly, with edited /etc/fstab (dir)
* home (dir)
* 20131116 (subvol, rw)
* ...
* root (dir)
* 20131116 (subvol, rw)
* ...
* etc, store information and scripts (subvol, rw)
我把备份数据放到 backup 目录下,/ 和主目录 /home/lilydjwg 分开备份的。每个备份是使用日期和时间命名的快照子卷。除了用于每次同步的 current 目录外其它的子卷都是只读的,以免被意外修改。在 run 目录下是用于直接运行的,可写。这些可以按需建立。
开始备份
这是我备份 / 使用的脚本。是一个 zsh 脚本,这样可以避免 bash 中特殊字符可能带来的问题,虽然 bash 有 shellcheck 可以静态分析出可能有问题的地方。
这个脚本带两到三个参数。第一个是 / 的位置,因为我一般会直接从运行的系统执行备份,但也有可能使用另外的维护系统(比如系统滚挂掉的时候)。第三个参数是用于确认操作的。不加它的话会以 --dry-run
参数来运行 rsync。rsync 很复杂,所以最好先演习一遍以避免不小心手抖了做错事 =w=
为了避免日后对照着 rsync 手册来揣摸每个单字母选项的意义,我在这里全部使用了选项的完整形式。反正我是 zsh 用户,那么长的命令中大部分字符都是 zsh 给我补全出来的 =w=
#!/bin/zsh -e
cd $(dirname $0)
if [[ $# -lt 2 || $# -gt 3 ]]; then
echo "usage: $0 SRC_DIR DEST_DIR [-w]"
exit 1
fi
src=$1
dest=$2
doit=$3
if [[ $doit == -w ]]; then
dry=
else
dry='-n'
fi
rsync --archive --one-file-system --inplace --hard-links \
--human-readable --numeric-ids --delete --delete-excluded \
--acls --xattrs --sparse \
--itemize-changes --verbose --progress \
--exclude='*~' --exclude=__pycache__ \
--exclude-from=root.exclude \
$src $dest $dry
比较重要的几个 rsync 选项:
-
--archive
-
我们要备份,所以请保留所有信息
-
--one-file-system
-
只备份这个文件系统的内容,不要跑到 /sys 啊 /proc 啊 /dev 啊 /tmp 这类目录里去了。这也省得自己手动排除
-
--numeric-ids
-
文件的所有者信息使用数字而不要解析成用户名/组名。避免在跨系统使用时出差错
-
--exclude-from=root.exclude
-
从
root.exclude
文件中读取额外的排除列表
-
--acls --xattrs
-
保留文件 ACL 和扩展属性
我发现的 / 里需要排除的目录如下:
/var/cache/*/*
/var/tmp/
/var/abs/local/
/var/lib/mongodb/journal/
其中第一项写成那样是因为,我要保留 /var/cache
下的一级目录。
主目录的备份是类似的过程,只是更加复杂罢了。当我写我的主目录的备份脚本的时候,深切地体会到有圣人说过的一句话——过早的优化是万恶之源。因为我使用 eCryptfs 加密主目录时为了避免可以公开的文件被加密造成性能损失,做了一系列的软链接。它们一直在给我带来各种小麻烦和不爽……
注意这些脚本要以 root 的身份运行。待所有备份脚本跑完之后,对那个 current 子卷做一个只读快照就好了:
sudo btrfs subvolume snapshot -r current $(date +'%Y%m%d_%H%M')
下次要更新备份时是一样的步骤:跑同步脚本,创建新快照。第一次同步会比较慢,跑了一个多小时吧。后边的增量备份就比较快了,十几分钟就好。
从备份启动
要启动,首先把 grub 装过去。把 Arch Linux live 系统的配置写好,当然还有启动备份系统的配置,如下:
search --no-floppy --fs-uuid --set=root 090dcc64-2b6d-421c-8ef6-2ab3321aec62
menuentry "Archlinux-2013.12.01-dual.iso (x86_64)" {
load_video
set gfxpayload=keep
insmod gzio
insmod ext2
set isofile="/images/archlinux-2013.12.01-dual.iso"
echo "Setup loop device..."
loopback loop $isofile
echo "Loading kernel..."
linux (loop)/arch/boot/x86_64/vmlinuz archisolabel=ARCH_201312 img_dev=/dev/disk/by-label/lilyboot img_loop=$isofile earlymodules=loop
echo "Loading initrd..."
initrd (loop)/arch/boot/x86_64/archiso.img
}
menuentry "Archlinux-2013.12.01-dual.iso (i686)" {
load_video
set gfxpayload=keep
insmod gzio
insmod ext2
set isofile="/images/archlinux-2013.12.01-dual.iso"
echo "Setup loop device..."
loopback loop $isofile
echo "Loading kernel..."
linux (loop)/arch/boot/i686/vmlinuz archisolabel=ARCH_201312 img_dev=/dev/disk/by-label/lilyboot img_loop=$isofile earlymodules=loop
echo "Loading initrd..."
initrd (loop)/arch/boot/i686/archiso.img
}
set ver=3.12.6
menuentry "Arch Linux $ver backup" {
load_video
set gfxpayload=keep
insmod gzio
insmod ext2
echo 'Loading Linux kernel ...'
linux /boot/vmlinuz-linux-lily-$ver root=/dev/mapper/lilybackup rw cryptdevice=/dev/disk/by-uuid/815d01ea-6390-460f-8c82-84c9e9497423:lilybackup rootflags=compress=lzo,subvolid=0 break=postmount
echo 'Loading initramfs...'
initrd /boot/initramfs-$ver-backup.img
}
各个设备的卷标、UUID 和文件路径自己调整。最后一项需要的文件在后边准备,先介绍一下几个参数:
-
root
-
根分区所在的设备。是解密后的设备路径或者用 UUID 也可以。不过没关系的,这里不会有冲突的
-
cryptdevice
-
如果使用密码(而不是密钥文件)加密的话,这里是冒号分隔的两个参数:你的加密设备是哪个文件,以及它解密之后叫什么名字。
-
rootflags
-
这个是给我们的 btrfs 用的。指定要启用 lzo 算法压缩,使用根子卷。实际上根子卷里不是 Linux 系统的根。这里我只是让脚本把它挂载到
/new_root
上而已,脚本会因为找不到/sbin/init
而进入一个 shell 的
-
break=postmount
-
即使根没有问题,也请在挂载好它之后给我一个 shell,我可能需要做一些调整
内核很简单,直接 cp 过去就好了。initramfs 要另外生成。这是我用来生成的 mkinitcpio.conf.crypt 文件:
MODULES="btrfs"
BINARIES="/usr/bin/btrfs"
FILES=""
HOOKS="base udev autodetect modconf block encrypt filesystems keyboard fsck shutdown"
COMPRESSION="xz"
重要的地方:内核模块 btrfs
一定是要的,另外我还需要 btrfs
程序来操作子卷。在 HOOKS
数组的 filesystems
前添加了 encrypt
,用于在启动时询问密码并解码根分区。
我还准备添加 vi 程序来着,但是它说终端类型不认识,还说 /var/tmp
目录不存在。于是索性把自己静态链接的 vim 扔到内核一块去了。Vim 内建常见终端类型的数据,不那么挑剔的。zsh 所需要的文件太多,也放弃了。
然后使用 mkinitcpio 命令生成 initramfs 镜像:
sudo mkinitcpio -c mkinitcpio.conf.crypt -g initramfs-backup.img
然后把生成的文件复制到启动分区的相应路径下。
注意:如果你在 Arch Linux live 系统中为另外的内核生成该 initramfs,要指定内核和内核模块路径的根。一定不要将内核模块所在的目录软链接到/lib/modules
下,那样 mkinitcpio 不会添加任何块设备的内核模块的。我使用的命令如下:
mkinitcpio --kernel /run/archiso/img_dev/boot/vmlinuz-linux-lily-3.12.6 -r /mnt/backup/root/20131227_2044 --config mkinitcpio.conf -g /run/archiso/img_dev/boot/initramfs-3.12.6-backup.img
文件准备完毕,就可以启动过去了。注意我没有在备份分区的 run 目录下建立子卷,因为我准备进入 initramfs 之后再建立它们。
PS: 因为 BIOS 不支持,所以必须从 USB 2.0 来启动。在 Linux 内核启动的时候,可以将移动硬盘接到 USB 3.0 扩展卡上。具体时机是,initramfs 载入完毕,内核开始打印日志的时候。
进入备份系统
启动之后,会进入 initramfs 的 shell。在这个没有任务管理的 ash 中,使用 btrfs 命令在 /new_root/run
目录下建立新的子卷,注意不要加 -r
这个表示只读的选项了:
btrfs subvolume snapshot ../backup/root/20131016_1423 root/20131016
btrfs subvolume snapshot ../backup/home/20131016_1423 home/20131016
因为文件系统树的挂载结构变了,所以得拿准备好的 vim(或者 vi,如果你没准备 vim 的话)去编辑 root/20131016/etc/fstab
文件,将那些不会成功的挂载项都去掉,添加新的正确的项。PS: 如果使用 vim 的话,记得进去先set nocp
一下,不然会是兼容模式,和 vi 一样只能撒消一步的。
/dev/mapper/lilybackup / btrfs rw,relatime,compress=lzo,subvol=run/root/xxx 0 0
/dev/mapper/lilybackup /home/lilydjwg btrfs rw,relatime,compress=lzo,subvol=run/home/xxx 0 0
然后 cd /
,卸载 /new_root
并重新以子卷挂载之:
cd /
umount /new_root
mount -o compress=lzo,subvol=run/root/20131016 /dev/mapper/lilybackup /new_root
如果是因为找不到 /sbin/init
而进来这个 shell 的,那么就tail /init
,最后那行是需要执行的命令(当然有些修改):
exec env -i "TERM=$TERM" /usr/bin/switch_root /new_root /sbin/init
如果是因为break=postmount
参数而进来的,直接按Ctrl-D退出 shell 即可。
启动会继续进行,systemd 启动了,各种服务陆续启动中~~
如果很不幸地,忘记修改/etc/fstab
了,或者有错,那么 systemd 会毫不留情地「Welcome to emergency shell」。不过现在更正也为时未晚。在编辑完 fstab 之后,要先执行下systemctl daemon-reload
再退出那个 emergency shell。
接下来应该能一路顺利地到达指定时间的系统啦 =w=
时间机器打造完成哦耶~~
参考资料