Migrating a ZFS pool from RAIDZ1 to RAIDZ2

Michael Lynch

將 ZFS 儲存池從 RAIDZ1 遷移至 RAIDZ2

原文由 Michael Lynch 發布,訂閱此部落格

最近我升級了家裡的 TrueNAS 伺服器,把 18 TB 的資料從原本由 4 顆硬碟組成的 RAIDZ1 ZFS 儲存池,遷移到了全新的 RAIDZ2 儲存池。

最巧妙的地方在於,我只多加了三顆 8 TB 硬碟,而且整個過程完全不需要把資料先搬到外部儲存裝置。

要在不把資料搬到外部儲存裝置的情況下從 RAIDZ1 升級到 RAIDZ2 之所以棘手,是因為:

  1. 無法直接原地把 RAIDZ1 儲存池轉換成 RAIDZ2 儲存池。
  2. 無法把 ZFS 儲存池縮減成更少顆硬碟。
  3. 18 TB 的資料根本塞不進由那三顆新硬碟組成的 3 顆 8TB RAIDZ2 儲存池。

我是怎麼做的

步驟 0:初始狀態

一開始,我有一個由 4 顆 8TB 硬碟組成的 RAIDZ1 ZFS 儲存池,總容量 23 TB 已用了 18 TB。為了這次遷移,我另外買了三顆整新品的 8 TB 硬碟。

步驟 1:借用一顆硬碟來建立 RAIDZ2 儲存池

首先,我從原本的 RAIDZ1 儲存池中拔掉一顆硬碟,讓它進入降級(degraded)狀態。

接著,我用以下組合建立一個新的 5x8TB RAIDZ2 儲存池:

  1. 三顆新硬碟
  2. 從原本 RAIDZ1 儲存池借來的一顆硬碟
  3. 一個 8 TB 的 sparse file(稀疏檔案),用來假裝成一顆硬碟

這個 sparse file 放在我的 /tmp 目錄下,雖然那裡的檔案系統實際上根本存不下 8 TB 的資料。但沒關係,因為這個 sparse file 只是暫時的。這是個權宜之計,讓我得以建立一個 5 顆硬碟的儲存池,但我不會真的寫入任何資料到它上面。

步驟 2:將假硬碟設為離線

接下來,我把那個假硬碟(8 TB 的 sparse file)從 RAIDZ2 儲存池中設為離線。

把假硬碟設為離線後,儲存池依然保持健康狀態,因為 RAIDZ2 即使少了兩顆硬碟也能正常運作。

步驟 3:將 RAIDZ1 儲存池的快照遷移到 RAIDZ2 儲存池

我為 RAIDZ1 儲存池中的所有資料集建立快照,並使用 zfs send 將快照遷移到新的 RAIDZ2 儲存池。

步驟 4:刪除舊儲存池

確認資料已成功遷移到 RAIDZ2 儲存池後,我刪除了舊的 RAIDZ1 儲存池,手邊因此多了三顆空閒的硬碟。

步驟 5:用舊硬碟替換假硬碟

我拿舊 RAIDZ1 儲存池中的一顆硬碟,來替換 RAIDZ2 儲存池中的假硬碟,讓它不再處於缺盤狀態。

步驟 6:用舊硬碟擴充新儲存池

最後,我透過 ZFS 的擴充功能,把舊 RAIDZ1 儲存池最後剩下的兩顆硬碟加入新的 RAIDZ2 儲存池,最終得到一個健康、由 7 顆 8TB 硬碟組成的 RAIDZ2 儲存池,可用空間為 33 TB。

更新:更安全的策略

更新(2025-07-25):感謝讀者們提供了另一個更能降低硬碟故障風險的替代策略。

讀者們建議的這個變化版本,我認為比我原本的做法更好:

  1. 用三顆新硬碟加上兩個假硬碟(sparse file)建立一個 5x8TB 的 RAIDZ2 儲存池。
  2. 將假硬碟從 RAIDZ2 儲存池設為離線。
  3. 將資料從舊的 RAIDZ1 儲存池遷移到新的 RAIDZ2 儲存池。
  4. 將舊 RAIDZ1 儲存池中的一顆硬碟設為離線,並用它來替換 RAIDZ2 儲存池上的一個假硬碟。
  5. 刪除 RAIDZ1 儲存池。
  6. 將已刪除的 RAIDZ1 儲存池中剩下的三顆硬碟遷移到 RAIDZ2 儲存池。

我喜歡這個策略,是因為它在遷移過程中能承受任何單一硬碟故障。在我原本的流程中,遷移可以承受 RAIDZ2 儲存池中任何一顆硬碟故障,但如果遷移期間舊 RAIDZ1 儲存池中有任何一顆硬碟故障,整個儲存池就會失效。

這個新策略唯一的缺點是,我還沒有像我原本的策略那樣實際驗證過。

為什麼要從 RAIDZ1 換到 RAIDZ2?

我在 2022 年組了家用的 TrueNAS 伺服器,當時用的是 4 顆 8TB 硬碟組成的 RAIDZ1 儲存池。當時我覺得 RAIDZ1 能容忍一顆硬碟故障就夠了,4 顆硬碟中同時壞兩顆的機率應該很低。

我當時沒想到的是,每多加一顆硬碟,資料遺失的機率就會跟著提高。4 顆硬碟同時壞兩顆我不擔心,但如果擴充到 6 顆或 8 顆,同時故障的可能性就開始讓人不安了。

每往 RAIDZ1 儲存池多加一顆硬碟,之後要遷移到 RAIDZ2 就會變得更困難,因為新儲存池必須更大,而我能拿來建新儲存池的實體硬碟插槽卻更少了。

RAIDZ2 可以在不遺失資料的情況下容忍兩顆硬碟同時故障。即使我擴充到 10 顆硬碟(我這台機殼的上限),同時壞三顆的機率低到讓我不會擔心。

為什麼從 RAIDZ1 切換到 RAIDZ2 這麼困難?

ZFS 不支援把 RAIDZ1 切換成 RAIDZ2。RAIDZ 模式在建立儲存池時就定死了,之後就永遠無法更改。

最直覺的解法是再買五顆硬碟,建一個 RAIDZ2 儲存池,然後把所有資料搬過去。這樣一來,我最後會剩下原本的 4 顆加上新的 5 顆,總共 9 顆硬碟,但我其實只想要 6 到 7 顆。

如果你上網搜尋如何把 RAIDZ1 儲存池轉成 RAIDZ2,大家都會叫你用這種笨方法,或是把所有資料搬到另一台備用的 ZFS 伺服器上,把原本的 RAIDZ1 儲存池刪掉、原地重建一個 RAIDZ2 儲存池,再把資料搬回來。但如果你跟我一樣,手邊沒有一台閒置的 18 TB ZFS 伺服器躺在某艘遊艇上呢?

之所以很少有更有效率的 RAIDZ1 轉 RAIDZ2 方法,是因為我的解法依賴於RAIDZ 擴充功能,而這個功能是六個月前才在 ZFS 中推出的。

實際執行從 RAIDZ1 到 RAIDZ2 的遷移

理論上,我的 RAIDZ1 到 RAIDZ2 遷移計畫很單純,但在實際操作時還是遇到了一些小插曲。

我在此分享詳細的操作紀錄,包含實際執行的指令與輸出,給想重現這個流程的人參考。

步驟 1:用隨身碟做一次演練

我的遷移計畫包含一些高風險的操作。在拿存放所有資料的主力 TrueNAS 伺服器冒險之前,我先在一台測試用伺服器上試了一遍。

我在一台舊的迷你電腦上安裝了 TrueNAS Core 25.04,然後接上了 7 支 USB 隨身碟。

在備用的 TrueNAS 伺服器和七支 USB 隨身碟上測試我的遷移策略

在備用的 TrueNAS 伺服器和七支 USB 隨身碟上測試我的遷移策略

這不是個完美的實驗,因為隨身碟的大小都不一樣,但整個流程確實跑通了。這讓我有信心在正式環境的伺服器上嘗試遷移計畫。

步驟 2:備份資料

我前面說過我沒有把資料搬到外部儲存裝置,但嚴格來說不完全正確。我還是有用外部儲存空間做備份,只是最終沒有派上用場。

我本來就每天晚上用restic跑備份,但那是檔案系統層級的備份,而不是 ZFS 層級的。換句話說,如果我在遷移過程中毀了 ZFS 儲存池,就得重建每一個 ZFS 資料集,再從備份中還原檔案,而不是直接從支援 ZFS 的備份中還原所有東西。

即使我每天備份,還是有一個類型的資料我沒備份:影音媒體。我有囤積資料的習慣,所以會保留所有 DVD 和藍光光碟的原始映像檔。因為誰知道哪天我會不會突然想看 1998 年電影《哈啦瑪莉》DVD 裡的導演講評呢?

伺服器上大部分的空間都是我轉檔備份的 DVD 和藍光光碟。

伺服器上大部分的空間都是我轉檔備份的 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。好吧,沒關係,我就當作付個 30 美元左右的幾天備份費用。結果才發現,Wasabi 雖然支援按日計費,但任何備份的資料至少要付 90 天的費用。所以原本 30 美元的 Wasabi 帳單,瞬間變成了 300 美元。

我寫信給 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

注意:更新時請確認你的發行分支(release train)。

我一開始不小心跳過了更新步驟,因為 TrueNAS 告訴我已經是最新版本。結果才發現,我是在目前的發行分支上是最新,但預設分支仍然停留在 24.x。所以,如果你沒看到 ZFS 2.3.0 或更新的版本,可能需要手動將 TrueNAS 更新到更高的主版本。

步驟 3:辨識硬碟

要開始這次遷移,我需要辨識出所有的硬碟,才能在 ZFS 命令列工具中指定它們。

顯示硬碟最簡單的方法是用 fdisk 工具:

fdisk --list

我覺得直接到 TrueNAS 的 Storage > Disks 儀表板查看更方便:

根據上面的截圖,我的硬碟 ID 如下:

  • 舊儲存池中的硬碟:sdasdcsdesdf
  • 新硬碟:sdbsdgsdh

步驟 4:找出最弱的硬碟

遷移過程中風險最高的階段,是從我從舊 RAIDZ1 儲存池借走一顆硬碟,到把資料遷移到新的 RAIDZ2 儲存池這段時間。把硬碟拔掉會讓舊儲存池進入降級狀態,如果這段期間舊儲存池中其他任何一顆硬碟故障,我就會遺失資料。

考量到這段高風險的空窗期,我希望用來建新儲存池的,是舊儲存池中最弱的那顆硬碟。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:032
    • sdc:077
    • sde:067
    • sdf:067
  • 新硬碟
    • sdb:100
    • sdg:100
    • sdh: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 errors

sda2DISK_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:用 sparse file 建立假硬碟

我當然可以建立一個 4x8TB 的 RAIDZ2 儲存池,但那樣只有 16 TB 的可用容量,根本不夠放我 18 TB 的資料。

相反地,我建立一個 5x8TB 的 RAIDZ2 儲存池,只不過第五顆硬碟其實並不存在。ZFS 允許我使用 sparse file 作為硬碟來建立 ZFS 儲存池。

要用 sparse file 建立假硬碟,我需要知道儲存池中其他硬碟的確切大小,這可以用 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 bytes,所以我用 truncate 指令建立一個同樣大小的假硬碟:

FAKE_DISK='/tmp/fake-drive.img'
truncate --size 8001563222016 "${FAKE_DISK}"

最後,我需要為新儲存池取個名字。之前我用的是 pool1,那時還不知道 ZFS 社群習慣把儲存池命名為 tank。我想說這次正好可以跟上潮流,把新儲存池命名為 tank

NEWPOOL='tank'

可惜,我最後沒能保留 tank 這個名稱。TrueNAS 有很多地方都依賴儲存池名稱,所以比起把所有共享和排程工作都改成指向 tank,我選擇讓儲存池名稱改回 pool1(詳見下方)。

步驟 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 的網頁介面上也能看到新的 RAIDZ2 儲存池,它顯示有 21.4 TiB(23.5 TB)的可用容量:

在 TrueNAS 網頁介面上可以看到我新建的 RAIDZ2 儲存池。

步驟 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 的容量,所以我開始把舊 RAIDZ1 儲存池中的 18 TB 資料搬過去。

為了完整轉移所有資料,我為整個 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 exportzpool 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}"

輸出顯示它已成功解析續傳權杖,但進度條每次都重設為零,所以我也不確定這個指令到底有沒有生效。

完成遷移的最後步驟

因為遷移花了好幾個小時,我又建立了一個新的快照,並以增量方式發送自上次快照以來有變動的所有資料:

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 的網路共享和排程工作都是跟儲存池名稱綁定的。如果我把主儲存池從 pool1 改名為 tank,就得手動重建或修改一大堆共享和排程工作。

更簡單的解法是讓新儲存池直接沿用舊儲存池的名稱,所以我決定這麼做。

我一開始嘗試匯出儲存池來讓它們離線,但各種 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 的 sparse file)。

我使用 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 errors

TrueNAS 介面也顯示我正在替換硬碟:

步驟 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):

選用:強制資料重新條帶化(restriping)

讀者@intelfx 提醒我 RAIDZ 擴充功能的一個陷阱。在 RAIDZ 擴充之前就已存在於硬碟上的資料,並不會自動從 3 份資料 + 2 份同位檢查碼重新條帶化為 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 的旗標套用到我從 CLI 建立的儲存池上到底會有什麼差別,但以下是我認為截至 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 教我這個巧妙的 sparse file 技巧,讓我得以建立更大的 RAIDZ2 儲存池。也要感謝來自 iXsystems 的 Chris 在 Reddit 上指正了這些細節

本文章由 muse-spark-1.2-contributor 進行翻譯

留言