2951 字
8 分钟
数据无价:ZFS 存储池高可靠迁移与重建实战
2026-02-12

为什么要做这件事#

一台 Proxmox VE 上有一个名为 ssd 的 ZFS 池,跑着若干虚拟机和 LXC 容器。随着机器增加,想把原来的条带(无冗余)池改成 RAIDZ1(类似 RAID5):加一层磁盘冗余,一块盘坏了数据还在。

但 ZFS 池一旦建好,“由条带改 RAIDZ1” 是做不到在线改的——必须销毁原池重建。于是整套流程就变成:

原池 ssd (条带)
↓ 1. 备份配置 + 停机
↓ 2. snapshot + zfs send
外置盘 sdg (temp_backup)
↓ 3. 验证备份完整 ← ★ 不通过就回头
↓ 4. 销毁原池、重建为 RAIDZ1 ← ★ 不可逆分水岭
新池 ssd (RAIDZ1)
↓ 5. 反向 zfs send 恢复
↓ 6. 恢复 PVE 配置、启动 VM
↓ 7. 清理临时池
完成

核心思想:借一块外置盘 sdg 做中转站。原池 → 外置盘 → 新池。

这是不可逆操作,请先读完整篇再动手

本流程涉及 zpool export + zpool create 两个真正销毁原池的命令。一旦执行,原池 label 被覆盖,没有撤销键。中间任何一步出错,数据恢复只能靠外置备份盘。强烈建议做这套操作前额外准备一份独立备份(比如 PBS 或 rsync 到另一台 NAS),而不是只信任这里的 sdg 中转盘。

1. 前置条件与风险认知#

1.1 硬件前提#

  • 外置备份盘 sdg 容量 ≥ 原池已用量 × 1.2 先查原池实际占用:

    zpool list ssd
    # NAME SIZE ALLOC FREE ...
    # ↑ 这一列就是已用量

    如果 ALLOC 是 800G,备份盘至少需要 1T(留 20% 余量,避免 ZFS 在 95% 以上性能骤降)。

  • 原池所有 VM / LXC 镜像要能一起装下:用同一块备份盘,不要分散到多个盘。

1.2 PVE 架构前提#

本文流程针对的是数据池(比如名为 ssd 的 ZFS 池,挂载在 PVE 之外存放 VM 镜像)。

不要对 PVE 系统根池 rpool 使用本流程

如果你的 PVE 本身就是以 root-on-ZFS 的方式安装(PVE 安装时选了 ZFS 选项),那么 rpool 是系统根池。本文流程不适用于 rpool——重建 rpool 涉及 bootloader、initramfs、UEFI 条目等复杂操作,走本文步骤会让系统无法启动。

先确认一下:

zpool list
# 看是否有 rpool 以及它是不是本文要操作的池
mount | grep 'on / '
# 看根文件系统是否来自 ZFS

本文针对的是非根的数据池,比如 ssd 这种专门放 VM 镜像的池。

1.3 关键事实(建池后不可改)#

  • ashift 值一旦建池就锁死,以后不能改。选错了就是永久性的性能问题(见第 4.2 节原理)。
  • zpool create -f 一执行,原盘上的 ZFS label 立即被覆盖,原池再也 import 不回来。
  • PVE 的 VM 配置文件 /etc/pve/qemu-server/*.conf 不在 ZFS 池上(它们在 pmxcfs 的 SQLite 里),所以销毁 ssd 池不会直接丢配置。但 VM 磁盘镜像(ZVOL)在 ssd 池里,没备份就丢了。

2. 备份 PVE 配置文件(必做的第一步)#

这一步顺序非常关键——放在整个流程最前面,而不是恢复阶段。原因:虚拟机的 CPU / 内存 / 磁盘映射 / 网卡配置都在 *.conf 里,丢了就没法启动 VM(就算磁盘恢复了也不知道怎么拼回去)。

mkdir -p /root/pve-config-backup
cp -a /etc/pve/qemu-server/*.conf /root/pve-config-backup/
cp -a /etc/pve/lxc/*.conf /root/pve-config-backup/
# 确认备份有内容
ls -lh /root/pve-config-backup/
TIP

如果你不想只放在本机 /root/(万一根盘也出问题就一起丢了),可以顺手打包发到另一台机器:

tar czf pve-config-backup-$(date +%F).tar.gz /root/pve-config-backup/
scp pve-config-backup-$(date +%F).tar.gz user@backup-host:/path/

3. 停止所有虚拟机和容器#

为什么必须停机zfs snapshot 虽然在文件系统层是原子的,但 VM 内存里还没落盘的数据没法被 snapshot 到。从 VM 视角看,这相当于”突然断电”——重启后可能出现文件系统不一致,严重时 VM 无法启动。

正确做法是先让 VM 正常关机(shutdown),内存刷盘后再做 snapshot。

# 批量正常关机所有 VM
for vmid in $(qm list | awk 'NR>1 {print $1}'); do
echo "Shutting down VM $vmid ..."
qm shutdown $vmid
done
# 批量正常关机所有 LXC
for ctid in $(pct list | awk 'NR>1 {print $1}'); do
echo "Shutting down CT $ctid ..."
pct shutdown $ctid
done
# 等全部停下来
sleep 30
qm list
pct list

qm list / pct listSTATUS 都应当为 stopped。如果有卡住的,用 qm stop <id> / pct stop <id> 强制停(相当于拔电,风险自担)。

4. 确认外置备份盘并创建临时备份池#

4.1 查外置盘的 by-id 路径#

为什么用 by-id 而不是 sdg:Linux 的设备名(sdbsdg 等)在重启后可能变动(跟 BIOS 扫盘顺序、热插拔顺序有关)。用 by-id 路径的是磁盘序列号,永久稳定。

ls -l /dev/disk/by-id/ | grep sdg
# 找到类似:
# ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y5XXXXXX -> ../../sdg

记下完整的 ID 串(本例 ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y5XXXXXX)。

4.2 ashift=12 是什么,为什么必须加#

ashift 告诉 ZFS 磁盘的物理扇区大小是 2^ashift 字节:

ashift块大小适用场景
9512 B老式 512n 硬盘(基本已绝迹)
124 KB现代机械盘和多数 SSD(推荐默认)
138 KB部分高端 NVMe、企业级 SSD
1416 KB少数超高性能 NVMe

现代硬盘几乎全是 4K 物理扇区(Advanced Format / 512e / 4Kn),如果用 ashift=9(默认),ZFS 以为扇区是 512B,每次写其实要触发磁盘内部的”读-修改-写”一个 4K 扇区,性能直接砍到一半以下

ashift 一旦建池就锁死,不能改。所以宁可大不可小:现代硬盘建议至少 ashift=12;NVMe SSD 条件允许给 ashift=13

# 建临时备份池(单盘,不做冗余,只用来中转)
zpool create -o ashift=12 temp_backup \
/dev/disk/by-id/ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y5XXXXXX
# 确认
zpool status
zfs list

5. 打 snapshot 并 zfs send 到备份池#

5.1 为什么必须先 snapshot#

zfs send 只能发送 snapshot,不能直接发送活跃的数据集或 ZVOL。原因:活跃数据集一直在变,没有”时间点”概念,接收端不知道该停在哪里。

Snapshot 是某一时刻整个数据集的只读时间切片,ZFS send 基于它才能生成确定性的数据流。

# 递归对整个 ssd 池打一个 snapshot,名字统一好识别
zfs snapshot -r ssd@migrate-2026-04-22
# 查看确认所有 dataset/ZVOL 都有这个 snapshot
zfs list -t snapshot -r ssd

-r 表示递归——ssd 池下所有 dataset / ZVOL 都打上同名 snapshot(ssd@migrate-2026-04-22ssd/vm-100-disk-0@migrate-2026-04-22 等)。

5.2 传输到备份池#

# 先在备份池里建个容纳目录
zfs create temp_backup/ssd_backup
# 逐个 ZVOL 发送(基于刚才打的 snapshot)
zfs send -w -L ssd/vm-100-disk-0@migrate-2026-04-22 | \
zfs receive -F temp_backup/ssd_backup/vm-100-disk-0
zfs send -w -L ssd/vm-101-disk-0@migrate-2026-04-22 | \
zfs receive -F temp_backup/ssd_backup/vm-101-disk-0
zfs send -w -L ssd/vm-101-disk-1@migrate-2026-04-22 | \
zfs receive -F temp_backup/ssd_backup/vm-101-disk-1
zfs send -w -L ssd/vm-102-disk-0@migrate-2026-04-22 | \
zfs receive -F temp_backup/ssd_backup/vm-102-disk-0
zfs send -w -L ssd/vm-102-disk-1@migrate-2026-04-22 | \
zfs receive -F temp_backup/ssd_backup/vm-102-disk-1
zfs send 参数说明
  • -w (--raw):按磁盘原样发送。如果数据集启用了压缩 / 加密 / 去重,raw 模式不解压不解密,直接发送压缩/加密后的块。传输最快、占用最小
  • -L (--large-block):允许发送 >128K 的 record size。接收端池必须启用 large_blocks feature(OpenZFS 默认已开)
  • -R (--replicate):递归复制整棵树(含子 dataset、snapshot、properties),常用于整池迁移
  • -i (--incremental):只发增量(见 5.3 节)

zfs receive -F-F 是”强制覆盖接收端同名数据集”。接收端名字如果已存在但不是你想要的,-F 会把它抹掉——确认无误再用。

5.3 进阶:用增量 send 降低停机窗口#

上面的流程需要 VM 全程停机 → 全量传输完成才能启动。大数据量时可能停机好几个小时。

增量 send 的思路:VM 继续运行时先做一次全量,停机后只发增量。

# 第 1 步:VM 运行中(不停机),先做一次全量快照 + 全量 send
zfs snapshot -r ssd@full-before-stop
zfs send -R -w -L ssd@full-before-stop | zfs receive -F temp_backup/ssd_backup
# 第 2 步:这时才停机(见第 3 节)
# 第 3 步:停机后再打一次快照
zfs snapshot -r ssd@final-stopped
# 第 4 步:只发从 full-before-stop 到 final-stopped 的增量
zfs send -R -w -L -i @full-before-stop ssd@final-stopped | \
zfs receive -F temp_backup/ssd_backup

这样 VM 实际停机时间 = 增量传输时间(通常几分钟),远少于全量传输时间(几十分钟到几小时)。

6. 验证备份完整性(不通过就回头!)#

这一节通不过,千万不要执行下一节的销毁操作

备份有没有真的完整,是整个流程的生死线。任何一项校验不通过都应该先查问题、必要时重做备份,再进入第 7 节。

6.1 空间对比#

# 源池和备份池并排看 used
zfs list -r -o name,used,written ssd
zfs list -r -o name,used,written temp_backup

对应 dataset 的 used 应当接近(raw send 下可以精确匹配;非 raw 由于压缩差异可能略有出入,但数量级必须一致)。

6.2 snapshot 完整性#

# 备份池里应该能看到全部迁移用的 snapshot
zfs list -t snapshot -r temp_backup | grep migrate-2026-04-22

6.3 端到端启动测试(最可靠)#

最硬核的校验是:在备份池上启动一台 VM,确认系统能正常跑

# 举例:把 VM 100 临时指向备份池的 ZVOL
# 用 /root/pve-config-backup/100.conf 里的配置修改磁盘指向 temp_backup/ssd_backup/vm-100-disk-0
# 启动测试
qm start 100
# 进 VM 里确认系统、应用正常
# 确认无误后关机再改回来
qm stop 100

如果不想动配置,至少用 zfs clone 克隆一份出来挂载读取:

zfs clone temp_backup/ssd_backup/vm-100-disk-0@migrate-2026-04-22 \
temp_backup/ssd_backup/vm-100-disk-0-clone
# 映射为块设备(如果是 ZVOL)
ls -l /dev/zvol/temp_backup/ssd_backup/vm-100-disk-0-clone
# 或用 kpartx / parted 进一步挂载 partition

7. 销毁原池并重建为 RAIDZ1(不可逆分水岭)#

执行下面两条命令后,原池的数据就回不来了

这是整个流程的分水岭zpool export ssd 还能 zpool import ssd 回来;但 zpool create -f ssd ... 一旦执行,原盘的 ZFS label 被新池覆盖,再也 import 不回来

执行前最后确认:

  • VM / LXC 都已 stopped
  • 第 6 节所有校验都通过
  • PVE 配置文件已在 /root/pve-config-backup/
  • 备份盘 sdg 的 ID 和即将要格式化的 sdb-sde 的 ID 分清楚了

7.1 先人工核对磁盘 ID#

# 把所有要参与重建的盘的 by-id 路径打出来
ls -l /dev/disk/by-id/ | grep -E 'sd[b-e]$'

把输出复制保存,核对每个 ID 都是原池里的,sdg 的 ID 绝对不在其中

临时备份池 temp_backup 务必先导出

重建 ssd 池之前,把 temp_backup 先 export 掉(但不销毁),避免手误把 sdg 写进下一步命令:

zpool export temp_backup

7.2 导出原池(可回滚的最后一步)#

zpool export ssd

如果这一步之后反悔,还能 zpool import ssd 恢复。下一步 zpool create -f 才是真正的不可逆点。

7.3 重建为 RAIDZ1(先不加 -f 试运行)#

# 先不加 -f 试探,让 ZFS 自己报盘上有残留 label
zpool create -o ashift=12 ssd raidz1 \
/dev/disk/by-id/你的-sdb-设备ID \
/dev/disk/by-id/你的-sdc-设备ID \
/dev/disk/by-id/你的-sdd-设备ID \
/dev/disk/by-id/你的-sde-设备ID

期望输出:会报类似 invalid vdev specification: use '-f' to override 的错,提示磁盘上有旧池 label。这个报错是保护性的——让你确认盘没选错。

确认 4 块盘 ID 没错、没夹带 sdg 之后,加 -f 正式执行:

zpool create -f -o ashift=12 ssd raidz1 \
/dev/disk/by-id/你的-sdb-设备ID \
/dev/disk/by-id/你的-sdc-设备ID \
/dev/disk/by-id/你的-sdd-设备ID \
/dev/disk/by-id/你的-sde-设备ID
zpool status ssd

zpool status 应该显示 state: ONLINE 且 vdev 是 raidz1-0

8. 从备份池恢复数据到新池#

先把备份池 import 回来:

zpool import temp_backup

然后反向 send:

# 先对备份池的数据也打一个恢复用的 snapshot(保持一致性)
zfs snapshot -r temp_backup/ssd_backup@restore-2026-04-22
# 循环恢复所有 ZVOL
for disk in vm-100-disk-0 vm-101-disk-0 vm-101-disk-1 vm-102-disk-0 vm-102-disk-1; do
echo "Restoring $disk ..."
zfs send -w -L temp_backup/ssd_backup/$disk@restore-2026-04-22 | \
zfs receive -F ssd/$disk
done
# 确认
zfs list -r ssd

9. 恢复 PVE 配置并启动 VM#

# 把之前备份的 .conf 文件放回去
cp -a /root/pve-config-backup/*.conf /etc/pve/qemu-server/ 2>/dev/null
cp -a /root/pve-config-backup/*.conf /etc/pve/lxc/ 2>/dev/null
# 启动 VM/CT 做最终验证
qm start 100
qm start 101
qm start 102
pct start <ctid>

逐台进 VM 里确认:系统能进入、服务能起、数据文件存在。

10. 清理临时备份池#

建议:新池稳定运行 48 小时以上再清理

新池刚建好头几天仍有微概率因硬件 / 配置问题暴露。强烈建议保留备份池至少 48 小时并观察 zpool status、VM 稳定性后再清理——有问题还能从 temp_backup 再恢复一次。

确认一切 OK 后:

zpool export temp_backup
# 物理拔出 sdg,或者重新分区用于其他用途

11. 回滚与容灾思考#

本方案是单副本中转——数据只在 sdg 这一个备份盘上。现实中有若干失败场景:

失败场景影响能否救回
sdg 临时掉线,但数据盘 label 还没被覆盖重插 sdg 再 zpool import 即可
zpool export ssd 之后后悔zpool import ssd 即可
zpool create -f ssd 执行后备份完整按第 8 节流程恢复
zpool create -f ssd 执行后发现备份有损坏原池 label 已被覆盖数据全丢
sdg 本身故障 + 还没 create 新池zpool import ssd 回退
sdg 故障 + 已 create 新池两边都没了数据全丢

结论:sdg 这一份备份不够。负担得起的话:

  • 方案 A:Proxmox Backup Server (PBS) 做 VM 级别的定时备份,这是 PVE 官方方案
  • 方案 B:把 zfs send 的数据流同时发到两块独立备份盘
  • 方案 C:关键数据(如数据库)额外跑一份应用层备份(mysqldump / pg_dump 到另一台机器)

12. 常见故障排查#

现象可能原因对策
zfs sendmust specify a snapshot忘了先 zfs snapshot 或写的是 dataset 而不是 dataset@snap按 5.1 节先打 snapshot
zpool createinvalid vdev specification磁盘上有旧 label/ 分区表残留确认盘没错之后加 -f
VM 启动后提示文件系统不一致 / 需要 fsck当时没正常 shutdown 就 snapshot 了进 VM 恢复模式跑 fsck;下次务必先 qm shutdown
恢复后 VM 启动报”磁盘找不到”配置里磁盘指向的 storage 名对不上检查 /etc/pve/qemu-server/<id>.confscsi0: 等行的 storage id,必要时 qm set 修正
zfs send 速度极慢没加 -w / 两端都在压缩计算 / 网络/磁盘带宽瓶颈-w raw send;大文件量时用 pv 观察吞吐
zpool statusDEGRADED 且有 FAULTED某块参与重建的磁盘本身故障zpool replace 换盘;不要接着把数据 restore 上去
作者注

本文基于作者一次真实的 PVE ZFS 池从条带升级为 RAIDZ1 的实操流程整理。涉及命令经过修正(如补上强制先打 snapshot),尽量贴近 OpenZFS 2.x + PVE 8.x 的现代实践。不同 ZFS / PVE 版本参数可能微调,执行前请对照 man zfs-sendman zpool-create 确认。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

数据无价:ZFS 存储池高可靠迁移与重建实战
https://blog.olinl.com/posts/zfs-pool-migration-reconstruction-guide/
作者
顾拾柒
发布于
2026-02-12
许可协议
CC BY-NC-SA 4.0

目录