↓ 跳过正文
  1. Posts/

祖传系统无损迁机:btrfs send/receive 实操记录

·4 分钟· · · #Linux #Btrfs #Arch #Migration
目录

我有一套从 ThinkPad 时代一路用下来的 Arch 系统,配置调了几年,包列表、zsh 环境、各种 dotfiles 都是攒出来的,重装一遍要命。2023 年先后做过两次整机迁移:一次同机换盘,一次从 SATA 盘迁到 NVMe。用的都是同一套办法:btrfs 只读快照 + send/receive。本文把当时的顺序整理出来,顺便补上"真迁新硬件"时还要注意的东西。

为什么不用 rsync
#

rsync 能搬文件,但搬不动 btrfs 的三样东西:

  • 子卷结构。我的布局是 @、@home、@var@cache、@var@log、@var@lib@docker 五个平级子卷,rsync 搬完就是一堆普通目录,子卷边界全丢。
  • 数据校验。btrfs 每个数据块都有 checksum,send/receive 传的是文件系统本身的指令流,到位即可信,不需要传完再比一轮。
  • 压缩属性。磁盘是 compress-force=zstd 挂载的,send/receive 保留文件系统语义,不像 rsync 那样要在目标端重新压一遍。

而快照是写时复制的,打一个只读快照几乎瞬时,系统继续跑,不影响任何服务——这就是在线迁移的基础。

第零步:先清垃圾再打快照
#

快照搬的是整个子卷,垃圾搬过去还是垃圾,只是让 send 更慢、目标盘更挤。打快照前值得过一遍:

  • 包管理缓存:pacman -Sc,以及 AUR 助手的构建缓存(比如 ~/.cache/yay,大户)
  • 孤儿包:pacman -Rns $(pacman -Qtdq)
  • 系统日志:journalctl --vacuum-size=100M
  • 用户态缓存(~/.cache 里的浏览器缓存等)和回收站

清完再打快照,传的数据量能小一个档次——我上次清掉的一千多 MB AUR 构建缓存,要是跟着快照走就白搬了。

完整顺序
#

flowchart LR
    A[挂载源盘/目标盘] --> B[逐子卷打只读快照]
    B --> C[send 对 receive]
    C --> D[快照转正为可写子卷]
    D --> E[替换 UUID: fstab/timeshift/grub]
    E --> F[挂 ESP 注册引导]
    F --> G[sync 卸载 重启切盘]

统一挂载参数(全程一致):

compress-force=zstd,noatime,ssd,space_cache=v2

第一步,挂上源盘和目标盘:

sudo mount -t btrfs -o compress-force=zstd,noatime,ssd,space_cache=v2 /dev/sda2 /tmp/src
sudo mount -t btrfs -o compress-force=zstd,noatime,ssd,space_cache=v2 /dev/nvme0n1p4 /tmp/dst

第二步,给每个子卷打只读快照(send 只认只读快照):

for i in $(ls -d @* | grep -v _ub); do sudo btrfs subvolume snapshot -r $i ${i}_ro; done

第三步,逐个发送:

sudo btrfs send @_ro | sudo btrfs receive /tmp/dst
sudo btrfs send @var@log_ro | sudo btrfs receive /tmp/dst
sudo btrfs send @var@cache_ro | sudo btrfs receive /tmp/dst
sudo btrfs send @var@lib@docker_ro | sudo btrfs receive /tmp/dst

第四步,快照转正。receive 收到的是 @_ro 这样的只读子卷,对它再打一个可写快照得到正式的 @,然后删掉 _ro 中间产物:

for i in $(ls -d *_ro); do
  sudo btrfs subvolume snapshot $i $(echo $i | cut -d '_' -f 1)
  sudo btrfs subvolume delete $i
done

第五步,替换 UUID,这是整个流程里最容易翻车的一步。新分区有新 UUID,blkid 查到后,sed 全局替换旧 UUID,落点有四个:

etc/fstab                     # 挂载表
etc/timeshift/timeshift.json  # 快照工具
boot/grub/grub.cfg            # 引导配置
boot/grub/grub-btrfs.cfg      # btrfs 快照引导菜单

第六步,处理 ESP 和引导项:

sudo mount /dev/nvme0n1p1 /tmp/dst/@/boot/efi
sudo efibootmgr -c -d /dev/nvme0n1 -p 1 -L "Arch" -l '\efi\boot\bootx64.efi'

第七步,收尾切盘:

sync
sudo umount /tmp/dst/@/boot/efi
sudo umount /tmp/dst
reboot
sudo efibootmgr -v   # 重启后确认引导项

这个过程是在线的
#

值得强调:整个迁移不需要停机。快照瞬时完成,send/receive 跑得再久系统也照常用,改 fstab、grub 改的都是目标盘上的副本,与运行中的系统无关。唯一的中断就是最后那次重启切盘——而换盘启动本来就要重启一次,这不算迁移的代价。

快照之后旧盘上的少量新写入(新日志之类)会有意留在旧盘上。家用机场景这点损失无所谓,所以我没有上增量 send(send -p)去补差量;如果要带着不能停的数据库在线搬,才需要"先全量、切盘前再补一次增量"的两段式。

真迁新硬件,还要补什么
#

上面两次都是同机换盘,所以少了最难啃的一节:硬件差异。ArchWiki 的迁移指南提醒得很到位,换机时逐项对一遍:

  • CPU 换厂商(Intel↔AMD):microcode 包要换(intel-ucode/amd-ucode)
  • GPU 换厂商:显卡驱动要换
  • 引导方式差异:目标机 UEFI 设置不同(如关闭 CSM、Secure Boot 状态),必要时新建 ESP 分区,引导项重新注册

另外两处我打算在下次迁移时改进:

  1. fstab 和 grub 配置重新生成,而不是手 sed。用 genfstab -U /mnt 生成挂载表,在目标盘 chroot 里跑 grub-mkconfig -o /boot/grub/grub.cfg。手改的 grub.cfg 总会有下次升级被覆盖生成的隐患。
  2. btrfs send --proto 2 --compressed-data(需要 btrfs-progs 6.0+ 和内核 6.0+,现在的 Arch 都满足)。它直接传输 zstd 压缩块,不做解压-再压缩,跨机传输能省不少时间。

还有一个容易踩的坑提前记一笔:btrfs 快照不递归。如果子卷里嵌套了别的子卷,快照里它只是个空目录。我的五个子卷是平级挂在顶层,天然没这个问题,但如果哪天在 @home 里再嵌子卷,send 前要单独处理。

相关文章