为什么要做这件事
一台 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-backupcp -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。
# 批量正常关机所有 VMfor vmid in $(qm list | awk 'NR>1 {print $1}'); do echo "Shutting down VM $vmid ..." qm shutdown $vmiddone
# 批量正常关机所有 LXCfor ctid in $(pct list | awk 'NR>1 {print $1}'); do echo "Shutting down CT $ctid ..." pct shutdown $ctiddone
# 等全部停下来sleep 30qm listpct listqm list / pct list 里 STATUS 都应当为 stopped。如果有卡住的,用 qm stop <id> / pct stop <id> 强制停(相当于拔电,风险自担)。
4. 确认外置备份盘并创建临时备份池
4.1 查外置盘的 by-id 路径
为什么用 by-id 而不是 sdg:Linux 的设备名(sdb、sdg 等)在重启后可能变动(跟 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 | 块大小 | 适用场景 |
|---|---|---|
| 9 | 512 B | 老式 512n 硬盘(基本已绝迹) |
| 12 | 4 KB | 现代机械盘和多数 SSD(推荐默认) |
| 13 | 8 KB | 部分高端 NVMe、企业级 SSD |
| 14 | 16 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 statuszfs list5. 打 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 都有这个 snapshotzfs list -t snapshot -r ssd-r 表示递归——ssd 池下所有 dataset / ZVOL 都打上同名 snapshot(ssd@migrate-2026-04-22、ssd/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-1zfs send 参数说明
-w(--raw):按磁盘原样发送。如果数据集启用了压缩 / 加密 / 去重,raw 模式不解压不解密,直接发送压缩/加密后的块。传输最快、占用最小-L(--large-block):允许发送 >128K 的 record size。接收端池必须启用large_blocksfeature(OpenZFS 默认已开)-R(--replicate):递归复制整棵树(含子 dataset、snapshot、properties),常用于整池迁移-i(--incremental):只发增量(见 5.3 节)
zfs receive -F的-F是”强制覆盖接收端同名数据集”。接收端名字如果已存在但不是你想要的,-F会把它抹掉——确认无误再用。
5.3 进阶:用增量 send 降低停机窗口
上面的流程需要 VM 全程停机 → 全量传输完成才能启动。大数据量时可能停机好几个小时。
增量 send 的思路:VM 继续运行时先做一次全量,停机后只发增量。
# 第 1 步:VM 运行中(不停机),先做一次全量快照 + 全量 sendzfs snapshot -r ssd@full-before-stopzfs 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 空间对比
# 源池和备份池并排看 usedzfs list -r -o name,used,written ssdzfs list -r -o name,used,written temp_backup对应 dataset 的 used 应当接近(raw send 下可以精确匹配;非 raw 由于压缩差异可能略有出入,但数量级必须一致)。
6.2 snapshot 完整性
# 备份池里应该能看到全部迁移用的 snapshotzfs list -t snapshot -r temp_backup | grep migrate-2026-04-226.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 进一步挂载 partition7. 销毁原池并重建为 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 自己报盘上有残留 labelzpool 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 ssdzpool 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
# 循环恢复所有 ZVOLfor 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/$diskdone
# 确认zfs list -r ssd9. 恢复 PVE 配置并启动 VM
# 把之前备份的 .conf 文件放回去cp -a /root/pve-config-backup/*.conf /etc/pve/qemu-server/ 2>/dev/nullcp -a /root/pve-config-backup/*.conf /etc/pve/lxc/ 2>/dev/null
# 启动 VM/CT 做最终验证qm start 100qm start 101qm start 102pct 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 send 报 must specify a snapshot | 忘了先 zfs snapshot 或写的是 dataset 而不是 dataset@snap | 按 5.1 节先打 snapshot |
zpool create 报 invalid vdev specification | 磁盘上有旧 label/ 分区表残留 | 确认盘没错之后加 -f |
| VM 启动后提示文件系统不一致 / 需要 fsck | 当时没正常 shutdown 就 snapshot 了 | 进 VM 恢复模式跑 fsck;下次务必先 qm shutdown |
| 恢复后 VM 启动报”磁盘找不到” | 配置里磁盘指向的 storage 名对不上 | 检查 /etc/pve/qemu-server/<id>.conf 里 scsi0: 等行的 storage id,必要时 qm set 修正 |
zfs send 速度极慢 | 没加 -w / 两端都在压缩计算 / 网络/磁盘带宽瓶颈 | 加 -w raw send;大文件量时用 pv 观察吞吐 |
zpool status 报 DEGRADED 且有 FAULTED 盘 | 某块参与重建的磁盘本身故障 | 先 zpool replace 换盘;不要接着把数据 restore 上去 |
作者注本文基于作者一次真实的 PVE ZFS 池从条带升级为 RAIDZ1 的实操流程整理。涉及命令经过修正(如补上强制先打 snapshot),尽量贴近 OpenZFS 2.x + PVE 8.x 的现代实践。不同 ZFS / PVE 版本参数可能微调,执行前请对照
man zfs-send与man zpool-create确认。
如果这篇文章对你有帮助,欢迎分享给更多人!
