将 ZFS 池从 RAIDZ1 迁移到 RAIDZ2
我最近升级了我的家用 TrueNAS 服务器,把 18 TB 的数据从一个由 4 块磁盘组成的 RAIDZ1 ZFS 池迁移到了一个新的 RAIDZ2 池。
妙处在于,我只额外用了三块 8 TB 磁盘就完成了迁移,而且从未把数据转移到外部存储。
在不把数据移到外部存储的情况下从 RAIDZ1 升级到 RAIDZ2 之所以棘手,是因为:
- 你无法把 RAIDZ1 池原地转换为 RAIDZ2 池。
- 你无法把 ZFS 池缩减到更少的磁盘。
- 18 TB 的数据装不进一个由 3 块 8 TB 磁盘组成的 RAIDZ2 池(也就是那三块新磁盘)。
我是怎么做到的
第 0 步:初始状态
一开始,我有一个由 4x8TB 磁盘组成的 RAIDZ1 ZFS 池,23 TB 的总容量中已用掉 18 TB。我为这次迁移购买了三块翻新的 8 TB 磁盘。
第 1 步:借出一块磁盘来创建 RAIDZ2 池
首先,我从原来的 RAIDZ1 池中移出一块磁盘,使该池进入降级(degraded)状态。
然后我用以下内容创建一个新的 5x8TB RAIDZ2 池:
- 我的三块新磁盘
- 从我的 RAIDZ1 池借来的一块磁盘
- 一个充当假磁盘的 8 TB 稀疏文件(sparse file)
这个稀疏文件放在我的 /tmp 目录里,尽管那里的文件系统实际上装不下 8 TB 数据。没关系,因为这个稀疏文件只是临时的。它是一个让我能创建 5 磁盘池的取巧手段,但我不会向它写入任何数据。
第 2 步:将假磁盘离线
接下来,我把假磁盘(那个 8 TB 稀疏文件)从 RAIDZ2 池中离线。
假磁盘离线后,池仍然是健康的,因为 RAIDZ2 可以在缺少两块磁盘的情况下运行。
第 3 步:把 RAIDZ1 池的快照迁移到 RAIDZ2 池
我为 RAIDZ1 池中的所有数据集创建快照,然后用 zfs send 把快照迁移到新的 RAIDZ2 池。
第 4 步:销毁旧池
确认数据已成功迁移到 RAIDZ2 池后,我销毁旧的 RAIDZ1 池,这样就多出三块磁盘。
第 5 步:用旧磁盘替换假磁盘
我用旧 RAIDZ1 池中的一块磁盘替换 RAIDZ2 池中的假磁盘,这样它就不再缺磁盘了。
第 6 步:用旧磁盘扩展新池
最后,我使用 ZFS 扩容功能把旧 RAIDZ1 池剩下的两块磁盘加入新的 RAIDZ2 池,最终得到一个健康的 7x8TB RAIDZ2 池,可用容量共 33 TB。
更新:一种更安全的策略
读者们对这一策略提出了一种变体,我个人更喜欢:
- 用三块新磁盘和两个假磁盘(稀疏文件)创建一个 5x8TB RAIDZ2 池。
- 把假磁盘从 RAIDZ2 池中离线。
- 把数据从旧的 RAIDZ1 池迁移到新的 RAIDZ2 池。
- 从 RAIDZ1 池离线一块磁盘,用它替换 RAIDZ2 池中的一个假磁盘。
- 销毁 RAIDZ1 池。
- 把被销毁的 RAIDZ1 池剩下的三块磁盘迁移到 RAIDZ2 池。
我喜欢这个策略,因为它对迁移过程中的任何单块磁盘故障都有容错能力。在我最初的流程中,迁移可以承受 RAIDZ2 池中任何一块磁盘的故障,但如果 RAIDZ1 池的任何一块磁盘在迁移期间挂掉,RAIDZ1 池就会失败。
新策略唯一的缺点是我没有像测试我最初的策略那样实际测试过它。
为什么要从 RAIDZ1 换到 RAIDZ2?
我于 2022 年搭建了家用 TrueNAS 服务器,采用 4x8TB 的 RAIDZ1 池。当时我觉得 RAIDZ1 没问题,因为它能容忍一块磁盘故障,而我认为四块磁盘中有两块同时故障的可能性不大。
我没有考虑到的是,当我向池中添加磁盘时,每一块新磁盘都会增加我丢数据的风险。我不担心四块磁盘坏两块,但如果我有六块或八块磁盘,同时故障的可能性就开始显得更高了。
每往 RAIDZ1 池中加一块磁盘,向 RAIDZ2 迁移就更难一步,因为这意味着新池必须更大,而留给新池的物理磁盘插槽也更少。
RAIDZ2 可以在不丢数据的情况下容忍两块磁盘故障。即使我扩展到 10 块磁盘(当前服务器机箱的最大容量),三块磁盘同时故障的概率也低到让我不担心。
为什么从 RAIDZ1 换到 RAIDZ2 很难?
ZFS 不支持从 RAIDZ1 切换到 RAIDZ2。RAIDZ 模式在创建池时就已确定,永远无法更改。
最直白的办法是再买五块磁盘,组建一个 RAIDZ2 池,然后把所有数据搬过去。但这样一来我就有 4 块旧盘 + 5 块新盘 = 9 块磁盘,而我只想要 6-7 块。
如果你在网上搜如何把 RAIDZ1 池转换为 RAIDZ2,所有人都会说要么按最直白的方式做,要么把所有数据搬到一台备用 ZFS 服务器上,销毁 RAIDZ1 池,在原位置创建 RAIDZ2 池,再把数据迁回来。可万一你和我一样,游艇里没有闲置的 18 TB ZFS 服务器呢?
网上之所以没有多少关于更高效转换方法的指引,是因为我的方案依赖于 RAIDZ 扩容功能,而它六个月前才在 ZFS 中可用。
从 RAIDZ1 迁移到 RAIDZ2:实战
理论上,我的 RAIDZ1 到 RAIDZ2 迁移计划很直接,但实际执行时遇到了几个小波折。
我把确切的命令和输出的详细记录分享出来,供想重复这一过程的人参考。
第 1 步:用 U 盘演练迁移
我的迁移计划包含一些高风险操作。在把它用到存有全部数据的主 TrueNAS 服务器上之前,我先在一台测试服务器上试了一遍。
我在一台旧迷你 PC 上安装了 TrueNAS Core 25.04,然后接上了 7 个 U 盘。

在一台备用的 TrueNAS 服务器和七个 U 盘上测试我的迁移策略
由于磁盘大小不一,这并不是完美的实验,但流程跑通了。这给了我信心,让我敢在生产服务器上执行迁移计划。
第 2 步:备份数据
我在上面说过我从未把数据移到外部存储,但这并不完全属实。我确实用外部存储做了备份,只是最终没有用上。
我已经在用 restic 做每夜备份,但那是文件系统层面的备份,而不是 ZFS 层面的。也就是说,如果迁移期间我的 ZFS 池损坏了,我就得重建每个 ZFS 数据集并从备份恢复文件,而不是从一个感知 ZFS 的备份中整体恢复。
虽然我每夜都备份,但有一类数据我不备份:媒体文件。我是个数据囤积者,保存了所有 DVD 和蓝光碟的原盘镜像。万一哪天我想看我 1998 年的 DVD 版《There’s Something About Mary》(《情色喜剧》)的导演评论音轨呢?

我的磁盘服务器上大部分空间都是我抓取的 DVD 和蓝光碟。
算上原盘镜像和转码后的视频文件,我的 TrueNAS 服务器上有大约 15 TB 的电影和电视剧。我不把这些文件备份到云存储,因为即使在 Wasabi 或 Backblaze B2 这类低价云存储服务商那里,每月也要花约 90 美元。
我告诉自己,如果媒体数据丢了,我可以从原盘重新抓取。但在这个流程开始时,一想到要把 500 多张盘一张张重新抓取和转码,我决定破例把媒体文件短期备份一下,以防迁移出什么问题。
哪里能备份 15 TB 三天?
我只需要备份 15 TB。如果能找到按天或按小时计费的服务商,应该相当划算。
我知道两家提供 ZFS 原生备份的云服务商:
- rsync.net:最低按一个月计费(15 TB 为 150 美元)。
- zfs.rent:需要我把自己的硬盘寄给他们。
所以这两个方案都不适合只备份几天数据。
有一些把 ZFS 备份到 S3 兼容存储的工具,但我不想试,感觉太复杂了。没有一台有 15 TB 空余容量的备用服务器,我没法验证备份是否可用。
我决定用我常用的 restic 备份脚本,把媒体文件备份到一家按天或更短粒度计费的云存储服务商。我找到的选项有:
- Amazon S3(Standard):$0.76/TB/天
- Google Cloud Storage(Standard):$0.66/TB/天
- Cloudflare R2:$0.50/TB/天
- Backblaze B2:$0.20/TB/天
- Hetzner storage boxes:$0.09/TB/天
- 可惜我是在迁移完成之后才发现这个选项的。
我选了 Backblaze B2,结果不知怎么,我的 15 TB 数据在 Backblaze 那边变成了 24.6 TB。
把所有东西上传到 Backblaze 花了我将近一周,所以数据存放时间比预期的长,但月底的账单只比平时多了几美元,我都不知道他们是怎么算的。
警告:查看云存储服务商的最低保留期政策
我最初在一次失败的迁移尝试中备份到了 Wasabi。好吧,没关系,我欠 Wasabi 大约 30 美元几天的备份费就行。结果发现 Wasabi 确实支持按天计费,但你必须为备份的任何数据支付至少 90 天的费用。于是,本来 30 美元的 Wasabi 账单变成了 300 美元。
我给 Wasabi 客服发邮件求情,他们给我的建议很奇怪:删掉你的账户。
apparently,删除整个 Wasabi 账户可以绕开 Wasabi 的最低存储期限要求。这不是什么黑技巧;这是 Wasabi 官方客服给出的指引。
但还没等我反驳,另一位 Wasabi 客服加入了工单,告诉我不用删账户。作为一次性的善意,他们会把我的超额费用抵扣掉(谢谢,Wasabi!)。
第 3 步:更新 TrueNAS
这次迁移需要 ZFS 的 RAIDZ 扩容功能,它包含在 OpenZFS 2.3.0 和 TrueNAS 的 24.10(Electric Eel)版本中。
我在 TrueNAS shell 中验证我的 ZFS 版本:
root@truenas:~# zfs --version
zfs-2.3.0-1
zfs-kmod-2.3.0-1陷阱:更新时注意你的更新通道。
我差点漏掉这一步,因为 TrueNAS 告诉我已经是最新版本。结果发现我是“我的更新通道”内的最新版本,而通道默认还停留在 24.x。所以,如果你看不到 ZFS 2.3.0 或更高版本,可能需要把 TrueNAS 更新到更高的主版本。
第 3 步:识别磁盘
开始迁移前,我需要识别所有磁盘,以便在 ZFS 命令行工具中引用它们。
最简单的查看方式是使用 fdisk 工具:
fdisk --list我觉得直接在 TrueNAS 的 Storage > Disks 仪表板里看更方便:
根据上面的截图,我的磁盘 ID 如下:
- 现有池中的磁盘:
sda、sdc、sde、sdf - 新磁盘:
sdb、sdg、sdh
第 4 步:找出最弱的磁盘
迁移中风险最高的时段,是从我从旧 RAIDZ1 池借出一块磁盘到我把数据迁移到新 RAIDZ2 池之间的时间。从 RAIDZ1 池中移出一块磁盘会让它进入降级状态。如果数据迁移期间旧池中任何其他磁盘挂掉,我就会丢数据。
正因为有这个风险窗口,我希望从旧池中借出最弱的磁盘去组建新池。RAIDZ2 池能容忍弱盘故障,但 RAIDZ1 池不能。
为了找出最弱的磁盘,我运行了下面这段简短的 bash 脚本来查询磁盘的 SMART 诊断数据:
for drive in /dev/sd?; do
[ -e "$drive" ] && echo -e "\n=== $drive ===" && smartctl -A $drive | \
grep -E '(Power_On_Hours|Wear_Leveling|Media_Wearout|Reallocated_Sector)'
done=== /dev/sda ===
5 Reallocated_Sector_Ct 0x0033 100 100 050 Pre-fail Always - 0
9 Power_On_Hours 0x0032 032 032 000 Old_age Always - 27228
=== /dev/sdb ===
5 Reallocated_Sector_Ct 0x0033 100 100 005 Pre-fail Always - 0
9 Power_On_Hours 0x0012 100 100 000 Old_age Always - 423
=== /dev/sdc ===
5 Reallocated_Sector_Ct 0x0033 100 100 010 Pre-fail Always - 0
9 Power_On_Hours 0x0032 077 077 000 Old_age Always - 20998
=== /dev/sdd ===
9 Power_On_Hours 0x0032 100 100 000 Old_age Always - 29606
=== /dev/sde ===
5 Reallocated_Sector_Ct 0x0033 100 100 010 Pre-fail Always - 0
9 Power_On_Hours 0x0032 067 067 000 Old_age Always - 29599
=== /dev/sdf ===
5 Reallocated_Sector_Ct 0x0033 100 100 010 Pre-fail Always - 0
9 Power_On_Hours 0x0032 067 067 000 Old_age Always - 29603
=== /dev/sdg ===
5 Reallocated_Sector_Ct 0x0033 100 100 005 Pre-fail Always - 0
9 Power_On_Hours 0x0012 100 100 000 Old_age Always - 141
=== /dev/sdh ===
5 Reallocated_Sector_Ct 0x0033 100 100 005 Pre-fail Always - 0
9 Power_On_Hours 0x0012 100 100 000 Old_age Always - 147从结果看,它们似乎都挺健康。唯一的差别指标是 Power-on hours(通电时长):
- 旧磁盘
sda:032sdc:077sde:067sdf:067
- 新磁盘
sdb:100sdg:100sdh:100
新磁盘都是 100%,这很合理,因为它们是刚翻新的。sda 最低,只有 32%,所以我把它定为最弱的磁盘,先把它移到新池。
第 5 步:创建稳定的磁盘标识符
/dev/sdX 路径在重启后并不稳定。这次是 /dev/sda 的磁盘,下次重启可能就变成 /dev/sdf。我不确定 ZFS 是否会把磁盘解析成更稳定的标识符,但我不想冒任何风险。
我写了这个 bash 函数,把 /dev/sdX 路径转换成该磁盘的稳定标识符:
get_disk_id() {
local dev=$1
local target="/dev/$dev"
for path in /dev/disk/by-id/*; do
if [ -L "$path" ] && [ "$(readlink -f "$path")" = "$target" ] &&
[[ "${path: -2:1}" != ":" ]]; then
echo "$path"
return 0
fi
done
echo "Disk ID not found for device: $dev" >&2
return 1
}使用我的 get_disk_id bash 函数时,传入 sdX 标识符,它就会返回该磁盘的稳定路径:
$ get_disk_id sda
/dev/disk/by-id/ata-HGST_HUS728T8TALE6L1_VGGGYUEG为了让流程可复用,我把磁盘 ID 存到环境变量里:
# Old disks
DISK_1="$(get_disk_id sda)"
DISK_2="$(get_disk_id sdc)"
DISK_3="$(get_disk_id sde)"
DISK_4="$(get_disk_id sdf)"并为新磁盘创建环境变量:
# New disks
DISK_5="$(get_disk_id sdb)"
DISK_6="$(get_disk_id sdg)"
DISK_7="$(get_disk_id sdh)"我还创建一个环境变量来指代现有的 RAIDZ1 池:
OLDPOOL='pool1'第 6 步:将弱盘离线
在动手之前,我先确认旧池是健康的,并且包含我预期的磁盘:
$ sudo zpool status ${OLDPOOL}
pool: pool1
state: ONLINE
scan: scrub repaired 68K in 10:40:08 with 0 errors on Wed May 14 06:25:26 2025
config:
NAME STATE READ WRITE CKSUM
pool1 ONLINE 0 0 0
raidz1-0 ONLINE 0 0 0
sde2 ONLINE 0 0 0
sdf2 ONLINE 0 0 0
sdc2 ONLINE 0 0 0
sda2 ONLINE 0 0 0
errors: No known data errorssda2(DISK_1)就是我要移到新 RAIDZ2 池的弱盘,所以我运行以下命令:
DISK_TO_OFFLINE='sda2'
sudo zpool offline "${OLDPOOL}" "${DISK_TO_OFFLINE}"检查 zpool status,可以看到离线该磁盘后池进入了降级状态,正如预期:
$ sudo zpool status ${OLDPOOL}
pool: pool1
state: DEGRADED
status: One or more devices has been taken offline by the administrator.
Sufficient replicas exist for the pool to continue functioning in a
degraded state.
action: Online the device using 'zpool online' or replace the device with
'zpool replace'.
scan: scrub repaired 68K in 10:40:08 with 0 errors on Wed May 14 06:25:26 2025
config:
NAME STATE READ WRITE CKSUM
pool1 DEGRADED 0 0 0
raidz1-0 DEGRADED 0 0 0
sde2 ONLINE 0 0 0
sdf2 ONLINE 0 0 0
sdc2 ONLINE 0 0 0
sda2 OFFLINE 0 0 0
errors: No known data errors第 7 步:擦除弱盘
创建 RAIDZ2 池时有个小麻烦。如果我尝试用弱盘(DISK_1)创建新池,ZFS 会告诉我它已经属于另一个池。即使我已经把磁盘离线,ZFS 仍坚持认为我的 RAIDZ1 池拥有这块磁盘。
为了阻止 RAIDZ1 池继续占用弱盘,我需要把它完全擦除。
我定义一个环境变量来跟踪要移动的磁盘:
MOVED_DISK="${DISK_1}"然后擦除磁盘上的所有数据:
wipefs 命令会不加任何确认地擦除整个磁盘,请谨慎运行。# Be careful! This command completely wipes the disk with no confirmation.
wipefs --all "${MOVED_DISK}"第 8 步:用稀疏文件创建假磁盘
我可以创建一个 4x8TB 的 RAIDZ2 池,但那样可用容量只有 16 TB,装不下我的 18 TB 数据。
于是,我创建一个 5x8TB 的 RAIDZ2 池,只是第五块磁盘并不真正存在。ZFS 允许我用一个稀疏文件作为磁盘来创建 ZFS 池。
要用稀疏文件创建假磁盘,我需要知道池中其他磁盘的确切大小,可以用 fdisk 查到:
$ fdisk --list | grep "^Disk.*bytes"
Disk /dev/sdf: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors
Disk /dev/sdd: 111.79 GiB, 120034123776 bytes, 234441648 sectors
Disk /dev/sda: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors
Disk /dev/sdc: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors
Disk /dev/sde: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors
Disk /dev/sdb: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors
Disk /dev/mapper/sdd3: 16 GiB, 17179869184 bytes, 33554432 sectors
Disk /dev/zd0: 10 GiB, 10737418240 bytes, 20971520 sectors
Disk /dev/sdg: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors
Disk /dev/sdh: 7.28 TiB, 8001563222016 bytes, 15628053168 sectors据此,我看到每块真实磁盘都是 8001563222016 字节,所以我用 truncate 命令创建一个假磁盘:
FAKE_DISK='/tmp/fake-drive.img'
truncate --size 8001563222016 "${FAKE_DISK}"最后,我还需要给新池起个名字。我以前用 pool1 这个名字时还不知道 ZFS 圈子里习惯把池命名为 tank。我以为这次是机会了,可以把新池命名为 tank,跟上潮流:
NEWPOOL='tank'可惜,我最终没能保住 tank 这个池名。TrueNAS 对池名有很多依赖,所以我选择改回 pool1,而不是把所有共享和 cron 任务都改成指向 tank(见下文)。
第 9 步:创建 RAIDZ2 池
终于到了创建我的 5x8TB RAIDZ2 池的时候:
zpool create \
-f \
${NEWPOOL} \
raidz2 \
-m "/mnt/${NEWPOOL}" \
"${DISK_5}" \
"${DISK_6}" \
"${DISK_7}" \
"${MOVED_DISK}" \
"${FAKE_DISK}"创建池成功了,我可以用 ZFS 工具查看它:
$ zpool status "${NEWPOOL}"
pool: tank
state: ONLINE
config:
NAME STATE READ WRITE CKSUM
tank ONLINE 0 0 0
raidz2-0 ONLINE 0 0 0
ata-HGST_HUS728T8TALE6L1_VGGGYUEG ONLINE 0 0 0
ata-HGST_HUS728T8TALE6L1_VRGMRVJK ONLINE 0 0 0
ata-HGST_HUS728T8TALE6L1_VRGNZU9K ONLINE 0 0 0
ata-TOSHIBA_HDWG480_71R0A14YFR0H ONLINE 0 0 0
/tmp/fake-drive.img ONLINE 0 0 0
errors: No known data errors
$ zpool list "${NEWPOOL}"
NAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
tank 36.4T 1.27M 36.4T - - 0% 0% 1.00x ONLINE -我也能在 TrueNAS 的 Web UI 里看到新的 RAIDZ2 池,它报告可用容量为 21.4 TiB(23.5 TB):

我的新 RAIDZ2 池在 TrueNAS Web UI 中可见。
第 10 步:移除假磁盘
假磁盘只是我 /tmp 目录里的一个文件,它根本存不下它声称的 8 TB。为了防止假文件耗尽 /tmp 文件系统空间时出现怪异行为,我立即把假磁盘从 RAIDZ2 池中移除:
zpool offline "${NEWPOOL}" "${FAKE_DISK}" && \
rm "${FAKE_DISK}"不出所料,ZFS 工具报告我的 RAIDZ2 池处于降级状态,但它可以容忍最多两块磁盘故障,所以一块磁盘离线没问题:
$ zpool status "${NEWPOOL}"
pool: tank
state: DEGRADED
status: One or more devices has been taken offline by the administrator.
Sufficient replicas exist for the pool to continue functioning in a
degraded state.
action: Online the device using 'zpool online' or replace the device with
'zpool replace'.
config:
NAME STATE READ WRITE CKSUM
tank DEGRADED 0 0 0
raidz2-0 DEGRADED 0 0 0
ata-HGST_HUS728T8TALE6L1_VGGGYUEG ONLINE 0 0 0
ata-HGST_HUS728T8TALE6L1_VRGMRVJK ONLINE 0 0 0
ata-HGST_HUS728T8TALE6L1_VRGNZU9K ONLINE 0 0 0
ata-TOSHIBA_HDWG480_71R0A14YFR0H ONLINE 0 0 0
/tmp/fake-drive.img OFFLINE 0 0 0
errors: No known data errors
$ zpool list "${NEWPOOL}"
NAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
tank 36.4T 1.41M 36.4T - - 0% 0% 1.00x DEGRADED -第 11 步:把数据迁移到新池
我的新 RAIDZ2 池已上线,容量 23.5 TB,于是我把 18 TB 数据从旧的 RAIDZ1 池搬过去。
为了传输所有数据,我为整个 RAIDZ1 池创建快照,然后把快照从旧池发送到新池:
SNAPSHOT_1="migrate1"
zfs snapshot -r "${OLDPOOL}@${SNAPSHOT_1}" && \
zfs send --verbose --raw --replicate "${OLDPOOL}@${SNAPSHOT_1}" \
| zfs receive -v -F "${NEWPOOL}"奇怪的是,命令完成时没有报任何错误,但我注意到 RAIDZ2 池上有一些数据集缺失。
排查迁移问题
为完成数据迁移,我逐个发送缺失的数据集:
DATASET='photos'zfs send --verbose --raw --replicate --skip-missing \
"${OLDPOOL}/${DATASET}@${SNAPSHOT_1}" \
| zfs receive -v -F -s "${NEWPOOL}/${DATASET}"当我把池挂载到 /mnt/tank/photos 时,目录仍然显示为空。
我尝试了 zpool export 和 zpool import,但没有任何变化。
最后,我重启了整个 TrueNAS 服务器,神奇地解决了问题。我能在 /mnt/tank/photos 里看到文件了。
恢复中断的传输
通过中断大传输,我发现 ZFS 默认发送的数据不支持断点续传。要恢复传输,我得做一套复杂的操作:先获取一个“resume token”,再把它加进 zfs send 命令里:
RESUME_TOKEN="$(zfs get -H -o value receive_resume_token "${NEWPOOL}/${DATASET}")"
zfs send -v -t "${RESUME_TOKEN}" | zfs receive -v -s "${NEWPOOL}/${DATASET}"输出声称成功解析了 resume token,但进度总是归零,所以我不知道这条命令到底有没有生效。
收尾迁移
由于迁移花了好几个小时,我创建一个新快照,并对自上个快照以来发生变化的所有数据做增量发送:
SNAPSHOT_2="migrate2"
zfs snapshot -r "${OLDPOOL}@${SNAPSHOT_2}"zfs send -v \
-i "${OLDPOOL}/${DATASET}@${SNAPSHOT_1}" \
"${OLDPOOL}/${DATASET}@${SNAPSHOT_2}" \
| zfs receive -v "${NEWPOOL}/${DATASET}"第 12 步:互换池名
我原本想把新池命名为 tank,但我意识到 TrueNAS 把网络共享和 cron 任务都绑定在池名上。如果我把主池从 pool1 改名为 tank,就得手动重建或修改一堆网络共享和 cron 任务。
更简单的办法是让新池接管旧池的名字,我就这么做了。
我先尝试导出池使其离线,但 TrueNAS 的各种系统服务阻止了我。于是,我强行停掉一堆服务:
sudo systemctl stop k3s
sudo umount -l /var/lib/kubelet
sudo systemctl stop smbd
sudo systemctl stop nmbd
sudo systemctl stop winbind
sudo systemctl stop middlewared
sudo systemctl stop netdata之后,我就可以把池离线来重命名了:
# Rename my old pool with the suffix `-old`.
zpool export -f "${OLDPOOL}" && \
zpool export -f "${NEWPOOL}" && \
zpool import "${OLDPOOL}" "${OLDPOOL}-old"
# Rename my new pool to my old pool's name.
zpool import ${NEWPOOL} ${OLDPOOL}
zfs set mountpoint="/mnt/${OLDPOOL}" "${OLDPOOL}"然后,我重启之前停掉的所有服务:
sudo systemctl start middlewared
sudo systemctl start smbd
sudo systemctl start nmbd
sudo systemctl start winbind
sudo systemctl start netdata
sudo systemctl start k3s但没起作用,所以我再次重启。
重启后,TrueNAS 把 pool1 识别为我新的 5x8TB RAIDZ2 池:
第 13 步:对新 RAIDZ2 池做 scrub
我不确定是否有必要,但在销毁旧池之前,我对新池跑了一次 scrub,为数据完整性多添一份信心。scrub 完成且没有错误:
第 14 步:销毁旧池
现在到了最吓人的一步:销毁旧池。我用 zpool destroy 把它干掉:
zpool destroy "${OLDPOOL}-old"第 15 步:用真实磁盘替换假磁盘
旧 RAIDZ1 池销毁后,我现在有三块磁盘要迁到新的 RAIDZ2 池。
第一块磁盘的迁移需要一个特殊流程,因为我要替换的是 RAIDZ2 池中的假磁盘(那个 8 TB 稀疏文件)。
我用 zpool replace 把假磁盘替换成真实磁盘:
FAKE_DISK='/tmp/fake-drive.img'
REPLACEMENT_DISK="$(get_disk_id sde)"
NEWPOOL='pool1'
zpool replace "${NEWPOOL}" "${FAKE_DISK}" "${REPLACEMENT_DISK}"检查 zpool status 显示 ZFS 正在把我刚换入的磁盘进行 resilver(数据重组):
$ zpool status "${NEWPOOL}"
pool: pool1
state: DEGRADED
status: One or more devices is currently being resilvered. The pool will
continue to function, possibly in a degraded state.
action: Wait for the resilver to complete.
scan: resilver in progress since Sat May 31 19:58:51 2025
2.04T / 28.5T scanned at 56.6G/s, 0B / 28.5T issued
0B resilvered, 0.00% done, no estimated completion time
config:
NAME STATE READ WRITE CKSUM
pool1 DEGRADED 0 0 0
raidz2-0 DEGRADED 0 0 0
ata-HGST_HUS728T8TALE6L1_VGGGYUEG ONLINE 0 0 0
ata-HGST_HUS728T8TALE6L1_VRGMRVJK ONLINE 0 0 0
ata-HGST_HUS728T8TALE6L1_VRGNZU9K ONLINE 0 0 0
ata-TOSHIBA_HDWG480_71R0A14YFR0H ONLINE 0 0 0
replacing-4 DEGRADED 0 0 0
/tmp/fake-drive.img OFFLINE 0 0 0
ata-ST8000VN004-2M2101_WSD5B9XY ONLINE 0 0 0
errors: No known data errorsTrueNAS UI 也显示我正在替换磁盘:
第 15 步:吸收剩下的两块磁盘
最后,是时候用旧 RAIDZ1 池剩下的两块磁盘来扩展新的 RAIDZ2 池了:
结果又碰到了一个问题。
$ NEWDISK=$(get_disk_id sdd)
$ zpool attach "${NEWPOOL}" raidz2-0 "${NEWDISK}"
cannot attach /dev/disk/by-id/ata-ST8000VN004-3CP101_WRQ02GX5 to raidz2-0: raidz_expansion feature must be enabled in order to attach a device to raidz原来 RAIDZ 扩容功能默认是关闭的。也许只有像我这样从旧版 TrueNAS 升级上来的情况下才是关闭的:
$ zpool get feature@raidz_expansion "${NEWPOOL}"
NAME PROPERTY VALUE SOURCE
pool1 feature@raidz_expansion disabled local我用 zpool upgrade 启用 RAIDZ 扩容功能:
zpool upgrade -o feature@raidz_expansion=enabled "${NEWPOOL}"然后,我确认 RAIDZ 扩容功能已启用:
$ zpool get feature@raidz_expansion "${NEWPOOL}"
NAME PROPERTY VALUE SOURCE
pool1 feature@raidz_expansion enabled local现在,我可以再试一次添加磁盘:
zpool attach "${NEWPOOL}" raidz2-0 "${NEWDISK}"zpool status 确认它正在添加磁盘:
$ zpool status "${NEWPOOL}" | grep --after-context=1 "expand:"
+ grep --after-context=1 expand:
+ zpool status "${NEWPOOL}"
expand: expansion of raidz2-0 in progress since Sun Jun 1 08:08:03 2025
17.1G / 28.5T copied at 501M/s, 0.06% done, 16:36:03 to go等 zpool status 显示扩展达到 100% 后,我对剩下那块磁盘重复这一过程,最终达到目标状态。
我完整的 7x8TB RAIDZ2 池
所有磁盘迁移完成后,我现在拥有一个健康快乐的 7x8TB RAIDZ2 池,可用容量 33 TB(30 TiB):
可选:强制数据重新条带化
读者 @intelfx 提醒我注意 RAIDZ 扩容功能的一个陷阱。在 RAIDZ 扩容之前就存在于磁盘上的数据不会自动从 3 份数据 + 2 份校验重新条带化(restripe)为 5 份数据 + 2 份校验,因此无法利用 7 磁盘阵列更高的存储效率。最终效果是,你无法用满 RAIDZ2 池的全部容量。
你可以通过强制重写迁移前的数据集来绕过这一点(例如,用 zfs send 把每个数据集发送到备份,再用 zfs receive 恢复)。OpenZFS 贡献者 Rob Norris 指出,OpenZFS 2.4.x 将引入一个 zfs rewrite 命令,能更优雅地实现同样的效果。
可选:对齐 TrueNAS 的功能配置
iXsystems 团队(维护 TrueNAS 的团队)的一位成员在 reddit 上评论说,TrueNAS 在技术上并不支持用户从命令行创建 ZFS 池。他建议我在 TrueNAS 里创建一个测试池,导出它的功能标志(feature flags),然后把这些标志应用到我通过命令行创建的新 RAIDZ2 池上。
我不清楚把 TrueNAS 的标志应用到命令行创建的池上到底有什么区别,但以下是我认为截至 25.04 版本 iXsystems 推荐的标志:
zpool set feature@lz4_compress=enabled "${NEWPOOL}"
zpool set feature@async_destroy=enabled "${NEWPOOL}"
zpool set feature@empty_bpobj=enabled "${NEWPOOL}"
zpool set feature@multi_vdev_crash_dump=enabled "${NEWPOOL}"
zpool set feature@spacemap_histogram=enabled "${NEWPOOL}"
zpool set feature@enabled_txg=enabled "${NEWPOOL}"
zpool set feature@hole_birth=enabled "${NEWPOOL}"
zpool set feature@extensible_dataset=enabled "${NEWPOOL}"
zpool set feature@embedded_data=enabled "${NEWPOOL}"
zpool set feature@bookmarks=enabled "${NEWPOOL}"
zpool set feature@filesystem_limits=enabled "${NEWPOOL}"
zpool set feature@large_blocks=enabled "${NEWPOOL}"
zpool set feature@large_dnode=enabled "${NEWPOOL}"
zpool set feature@sha512=enabled "${NEWPOOL}"
zpool set feature@skein=enabled "${NEWPOOL}"
zpool set feature@edonr=enabled "${NEWPOOL}"
zpool set feature@userobj_accounting=enabled "${NEWPOOL}"
zpool set feature@encryption=enabled "${NEWPOOL}"
zpool set feature@project_quota=enabled "${NEWPOOL}"
zpool set feature@device_removal=enabled "${NEWPOOL}"
zpool set feature@obsolete_counts=enabled "${NEWPOOL}"
zpool set feature@zpool_checkpoint=enabled "${NEWPOOL}"
zpool set feature@spacemap_v2=enabled "${NEWPOOL}"
zpool set feature@allocation_classes=enabled "${NEWPOOL}"
zpool set feature@resilver_defer=enabled "${NEWPOOL}"
zpool set feature@bookmark_v2=enabled "${NEWPOOL}"
zpool set feature@redaction_bookmarks=enabled "${NEWPOOL}"
zpool set feature@redacted_datasets=enabled "${NEWPOOL}"
zpool set feature@bookmark_written=enabled "${NEWPOOL}"
zpool set feature@log_spacemap=enabled "${NEWPOOL}"
zpool set feature@livelist=enabled "${NEWPOOL}"
zpool set feature@device_rebuild=enabled "${NEWPOOL}"
zpool set feature@zstd_compress=enabled "${NEWPOOL}"
zpool set feature@draid=enabled "${NEWPOOL}"
zpool set feature@zilsaxattr=enabled "${NEWPOOL}"
zpool set feature@head_errlog=enabled "${NEWPOOL}"
zpool set feature@blake3=enabled "${NEWPOOL}"
zpool set feature@block_cloning=enabled "${NEWPOOL}"
zpool set feature@vdev_zaps_v2=enabled "${NEWPOOL}"
zpool set feature@redaction_list_spill=enabled "${NEWPOOL}"
zpool set feature@raidz_expansion=enabled "${NEWPOOL}"
zpool set feature@fast_dedup=enabled "${NEWPOOL}"
zpool set feature@longname=enabled "${NEWPOOL}"
zpool set feature@large_microzap=enabled "${NEWPOOL}"感谢 TrueNAS 论坛上的 @NugentS 教我用稀疏文件的巧妙取巧方法来创建更大的 RAIDZ2 池。感谢 iXsystems 的 Chris 纠正了其中的一些细节。
随机一篇博客





