Migrating a ZFS pool from RAIDZ1 to RAIDZ2

Michael Lynch

ZFSプールをRAIDZ1からRAIDZ2へ移行する

先日、自宅のTrueNASサーバーをアップグレードし、4台構成のRAIDZ1のZFSプールから新しいRAIDZ2プールへ18TB分のデータを移行しました。

面白いのは、追加したのは8TBディスク3台だけで、データを外部ストレージに退避することなく移行できた点です。

データを外部ストレージに移さずにRAIDZ1からRAIDZ2へアップグレードするのが難しいのは、次の理由からです。

  1. RAIDZ1プールをその場でRAIDZ2プールに変換することはできません。
  2. ZFSプールをディスク数の少ない構成に縮小することはできません。
  3. 18TBのデータを、3台の8TBディスクで構成したRAIDZ2プール(新規ディスク3台分)には収められません。

移行の手順

ステップ0:初期状態

開始時点では、8TB×4台のRAIDZ1のZFSプールがあり、容量23TBのうち18TBを使用していました。今回の移行のために、中古再生品の8TBディスクを3台購入しました。

ステップ1:1台を借用してRAIDZ2プールを作成する

まず、元のRAIDZ1プールからディスクを1台取り外し、プールを縮退(デグレード)状態にします。

次に、次の構成で8TB×5台の新しいRAIDZ2プールを作成します。

  1. 新しく購入した3台のディスク
  2. RAIDZ1プールから借用した1台のディスク
  3. 仮想ディスクとして使う8TBのスパースファイル1つ

スパースファイルは/tmpディレクトリに置きます。そこのファイルシステムは実際には8TBものデータを保存できませんが、問題ありません。スパースファイルは一時的なものです。5台構成のプールを作るためのハックで、実際にそこにデータを書き込むことはないからです。

ステップ2:仮想ディスクをオフラインにする

次に、RAIDZ2プールから仮想ディスク(8TBのスパースファイル)をオフラインにします。

仮想ディスクをオフラインにしても、プールは健全なままです。RAIDZ2は2台までのディスク欠損で動作できるためです。

ステップ3:RAIDZ1プールのスナップショットをRAIDZ2プールへ移行する

RAIDZ1プール内のすべてのデータセットのスナップショットを取得し、zfs sendを使って新しいRAIDZ2プールへ移行します。

ステップ4:古いプールを破棄する

データがRAIDZ2プールへ正常に移行されたことを確認したら、古いRAIDZ1プールを破棄します。これでディスクが3台余ります。

ステップ5:仮想ディスクを古いディスクで置き換える

古いRAIDZ1プールから1台を使って、RAIDZ2プール内の仮想ディスクを置き換えます。これで欠損ディスクがない状態になります。

ステップ6:古いディスクで新しいプールを拡張する

最後に、ZFSの拡張機能を使って、古いRAIDZ1プールから残った2台を新しいRAIDZ2プールに追加します。これで、健全な8TB×7台のRAIDZ2プールとして、合計33TBの使用可能な容量が確保できます。

追記:より安全な方法

追記(2025-07-25):ディスク障害のリスクをより適切に抑える代替手法を読者の方々にご提案いただきました。ありがとうございます。

読者の方から、私のオリジナルの方法よりも優れていると感じるバリエーションを提案いただきました。

  1. 新規ディスク3台と仮想ディスク(スパースファイル)2つを使って、8TB×5台のRAIDZ2プールを作成します。
  2. 仮想ディスクをRAIDZ2プールからオフラインにします。
  3. 古いRAIDZ1プールから新しいRAIDZ2プールへデータを移行します。
  4. RAIDZ1プールから1台をオフラインにし、RAIDZ2プール上の仮想ディスク1つと置き換えます。
  5. RAIDZ1プールを破棄します。
  6. 破棄したRAIDZ1プールから残った3台をRAIDZ2プールへ移行します。

この方法が気に入っているのは、移行中にどのディスクが1台故障しても耐えられる点です。私のオリジナルの手順では、RAIDZ2プール側のディスク故障には耐えられましたが、移行中にRAIDZ1プールのディスクが1台でも故障すると、プール全体が壊れてしまいました。

この新しい方法の唯一の欠点は、私のオリジナルの方法のように実際に検証できていないことです(オリジナルの方法をご覧ください)。

なぜRAIDZ1からRAIDZ2に切り替えるのか

2022年に自宅用TrueNASサーバーを構築した際は、8TB×4台のRAIDZ1プールで構築しました。RAIDZ1は1台のディスク故障に耐えられるため安心だと考えていましたし、4台中2台が同時に故障する可能性は低いと思っていました。

見落としていたのは、プールにディスクを追加するたびに、データ損失の確率が高まるという点です。4台中2台の故障は心配していませんでしたが、6台や8台になれば、同時故障はより現実的に感じられるようになります。

RAIDZ1プールにディスクを1台追加するごとに、RAIDZ2への移行は難しくなります。新しいプールをさらに大きくしなければならず、新しいプールに割り当てられる物理スロットも減ってしまうからです。

RAIDZ2はデータ損失なしで2台のディスク故障に耐えられます。仮に現在のサーバーケースの最大である10台まで拡張したとしても、3台が同時に故障する可能性は十分に低いと考えられるため、心配していません。

なぜRAIDZ1からRAIDZ2への切り替えは難しいのか

ZFSではRAIDZ1からRAIDZ2への切り替えはサポートされていません。RAIDZのモードはプール作成時に決まり、後から変更することはできません。

安直な解決策は、5台のディスクを追加で購入し、RAIDZ2プールを構築してから、すべてのデータを新しいプールへ移すことです。しかしこれでは、元の4台に新しい5台を加えて合計9台になってしまい、本来6〜7台で済ませたいところです。

オンラインでRAIDZ1プールをRAIDZ2に変換する方法を検索すると、誰もが安直な方法か、予備のZFSサーバーにすべてのデータを退避し、RAIDZ1プールを破棄してから同じ場所にRAIDZ2プールを作り、データを戻す方法を勧めています。でも私のように、ヨットの中に18TBの予備ZFSサーバーが転がっているわけではない場合はどうすればよいのでしょうか。

RAIDZ1からRAIDZ2へより効率的に切り替える方法についての情報が少ないのは、私の解決策がRAIDZ拡張に依存しており、この機能がZFSで利用可能になったのがわずか半年前だからです。

RAIDZ1からRAIDZ2への移行:実践編

理論上、私のRAIDZ1からRAIDZ2への移行計画はシンプルでしたが、実際に実行してみるといくつかのつまずきがありました。

この手順を再現したい方のために、実行したコマンドとその出力を詳細な記録として共有します。

ステップ1:USBメモリでの移行リハーサル

私の移行計画にはリスクの高い操作が含まれます。すべてのデータを抱えた本番のTrueNASサーバーで試す前に、まずテスト用サーバーで試してみました。

古いミニPCにTrueNAS Core 25.04をインストールし、USBメモリを7本接続しました。

予備のTrueNASサーバーとUSBメモリ7本で移行戦略をテストしているところ

ドライブのサイズがバラバラだったため完全な実験ではありませんでしたが、手順自体はうまくいきました。これで本番サーバーでも移行計画を試す自信が持てました。

ステップ2:データのバックアップ

先ほど、データを外部ストレージに移していないと言いましたが、厳密には正確ではありません。バックアップのために外部ストレージは使いましたが、結局使う必要はありませんでした。

すでにresticで毎晩バックアップを取っていますが、ZFSレベルではなくファイルシステムレベルでのバックアップです。つまり、移行中にZFSプールを破損してしまった場合、ZFS対応のバックアップから一括で復元するのではなく、ZFSデータセットを一つひとつ作り直して、バックアップからファイルを復元しなければなりません。

毎晩バックアップを取っているとはいえ、バックアップしていないデータが1種類あります。メディアです。私はデータを溜め込むタイプなので、所有するDVDやBlu-rayのディスクイメージをすべてそのまま保存しています。1998年公開の映画『メリーに首ったけ』のDVDで、ある日突然監督のコメンタリーが観たくなったらどうするのか、と考えてしまうからです。

ディスクサーバーの容量の大部分を占めているのは、リッピングしたDVDやBlu-rayです。

ディスクの生イメージとエンコード済みの動画ファイルを合わせると、TrueNASサーバーには約15TBの映画やテレビ番組があります。これらのファイルをクラウドストレージにバックアップしていないのは、WasabiやBackblaze B2のような低価格のプロバイダーでも、月額約90ドルかかってしまうからです。

メディアデータを失っても、元のディスクからすべてリッピングし直せばいいと自分に言い聞かせていました。しかし今回の作業を始めるにあたり、500枚以上のディスクを一枚ずつリッピングして再エンコードする事態を想像し、念のため例外的にメディアファイルを短期間だけバックアップすることにしました。移行中に何かあった場合に備えてです。

15TBを3日間だけバックアップするにはどこがよいか

必要なのは15TBをバックアップすることだけです。日割りや時間割りで課金してくれるベンダーが見つかれば、かなり安く済むはずでした。

ZFSネイティブなバックアップを提供しているクラウドベンダーは、私が知る限り2社あります。

  • rsync.net:最低課金期間は1か月(15TBで150ドル)です。
  • zfs.rent:自分のハードディスクを送付する必要があります。

そのため、どちらも数日間だけデータをバックアップするには適していませんでした。

ZFSをS3互換ストレージにバックアップするツールもありますが、複雑に感じたため試しませんでした。15TBの容量を持つ予備サーバーがなければ、バックアップが正常に機能しているか検証できないからです。

そこで、普段使っている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 Box:0.09ドル/TB/日
    • 残念ながら、この選択肢は移行が終わってから見つけました。

私はBackblaze B2を選びましたが、なぜか15TBのデータがBackblaze側では24.6TBになっていました。

Backblazeへのアップロードには1週間近くかかったため、想定より長くデータを保存することになりましたが、月末の請求額はいつもより数ドル高いだけでした。どのように料金が計算されたのかはよく分かりません。

警告:クラウドストレージベンダーの最低保管期間ポリシーを確認してください

最初は失敗した移行の際にWasabiにバックアップしました。まあいいか、数日分のバックアップでWasabiに30ドルほど払えば済むだろうと思っていました。ところがWasabiは日割り課金に対応しているものの、バックアップしたデータには最低90日分の料金がかかることが分かりました。結果として、30ドルの請求で済むはずが、300ドルの請求になりそうでした。

Wasabiのサポートにメールで寛大な対応をお願いしたところ、奇妙なアドバイスが返ってきました。アカウントを削除してください、というのです。

どうやら、Wasabiアカウント自体を削除すれば、最低保管期間の要件を回避できるようです。これはハックや裏技ではなく、Wasabiサポートからの公式な案内でした。

しかし私が反論する間もなく、別のWasabi担当者がチケットに参加し、アカウントを削除する必要はないと言ってくれました。今回一度限りの特別対応として、超過分の料金をクレジットしてくれるというのです(ありがとう、Wasabi!)。

ステップ3:TrueNASをアップデートする

今回の移行にはZFSのRAIDZ拡張機能が必要です。この機能はOpenZFS 2.3.0とTrueNASの24.10(Electric Eel)リリースで導入されました。

TrueNASのシェルから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は次のようになっていることが分かります。

  • 既存プール内のディスク:sdasdcsdesdf
  • 新規ディスク:sdbsdgsdh

ステップ4:最も弱っているディスクを見つける

移行で最もリスクが高いのは、古いRAIDZ1プールからディスクを借用してからデータを新しいRAIDZ2プールへ移行するまでの間です。RAIDZ1プールからディスクを1台取り外すとプールは縮退状態になり、データ移行中に残りのディスクのどれかが故障すると、データを失ってしまいます。

このリスクの高い期間があるため、古いプールの中で最も弱っているディスクを、新しいプールを作るために借用するディスクに選びたいと考えました。RAIDZ2プールならその弱いディスクが故障しても耐えられますが、RAIDZ1プールは耐えられないからです。

最も弱っているディスクを見つけるため、ディスクのSMART診断データを取得する次のbashスニペットを実行しました。

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

結果を見ると、どのディスクもおおむね健全に見えます。違いが出たのは通電時間だけでした。

  • 既存のディスク
    • 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がより安定した識別子に解決してくれるのかどうかは不明でしたが、念のため確実を期すことにしました。

そこで、/dev/sdXパスをディスクの安定した識別子に変換する次のbash関数を作成しました。

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関数を使うには、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:スパースファイルで仮想ディスクを作成する

8TB×4台のRAIDZ2プールを作ることもできますが、それでは使用可能な容量は16TBにしかならず、18TBのデータを収めるには足りません。

そこで、5台目のディスクは実際には存在しない、8TB×5台のRAIDZ2プールを作成します。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}"

最後に、新しいプールの名前を決める必要があります。以前はZFS文化圏でプール名をtankにする慣習を知らずにpool1という名前を使っていました。これを機にイケてる人たちの仲間入りをしようと、新しいプールをtankと名付けることにしました。

NEWPOOL='tank'

残念ながら、プール名tankは維持できませんでした。TrueNASではプール名に多くの依存関係があるため、すべての共有やcronジョブをtankに向けて更新するよりも、pool1に戻すことにしました(後述)。

ステップ9:RAIDZ2プールを作成する

いよいよ、8TB×5台の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)と表示されています。

TrueNASのWeb UIに表示された新しいRAIDZ2プール。

ステップ10:仮想ディスクを取り除く

仮想ディスクは/tmpディレクトリにある単なるファイルで、8TBものデータを実際に保存できるわけではありません。仮想ファイルが/tmpファイルシステムの空き容量を使い果たして奇妙な挙動を起こすのを防ぐため、すぐにRAIDZ2プールから仮想ディスクを取り除きます。

zpool offline "${NEWPOOL}" "${FAKE_DISK}" && \
  rm "${FAKE_DISK}"

想定どおり、ZFSユーティリティはRAIDZ2プールが縮退状態であると報告しますが、2台までのディスク故障に耐えられるため、1台がオフラインでも問題ありません。

$ 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.5TBの容量でオンラインになったので、古いRAIDZ1プールから18TBのデータを移します。

すべてを転送するために、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}"

出力上は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を新しい8TB×5台のRAIDZ2プールとして認識しました。

ステップ13:新しいRAIDZ2プールをスクラブする

必要かどうかは分かりませんが、古いプールを破棄する前にデータの整合性にさらに自信を持つため、新しいプールでスクラブを実行します。スクラブはエラーなしで完了しました。

ステップ14:古いプールを破棄する

いよいよ最も恐ろしい部分、古いプールの破棄です。zpool destroyで一気に削除します。

zpool destroy "${OLDPOOL}-old"

ステップ15:仮想ディスクを実際のディスクで置き換える

古いRAIDZ1プールを破棄したことで、新しいRAIDZ2プールへ移行できるディスクが3台手元にあります。

最初の1台の移行は、RAIDZ2プール上の仮想ディスク(8TBのスパースファイル)を置き換えるため、特別な手順が必要です。

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が交換したディスクへデータを再構築(リシルバリング)していることが分かります。

$ 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のUIにも、ディスクを置換中であることが表示されます。

ステップ15:残り2台のディスクを組み込む

最後に、破棄した古いRAIDZ1プールから残った2台で、新しい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%に達したことが確認できたら、残りのディスクでも同じ手順を繰り返し、最終的な状態に到達します。

完成した8TB×7台のRAIDZ2プール

すべてのディスクを移行し終え、健全で満足のいく8TB×7台のRAIDZ2プールが完成し、使用可能容量は33TB(30 TiB)になりました。

補足:データの再ストライピングを強制する

読者の@intelfxさんから、RAIDZ拡張機能の落とし穴について教えていただきました。RAIDZ拡張前にディスク上に存在していたデータは、3データ+2パリティから5データ+2パリティへ自動的に再ストライピングされないため、7台構成で向上したストレージ効率を活かせません。その結果、RAIDZ2プールの容量をすべて使い切ることができないのです。

これを回避するには、移行前のデータセットを強制的に書き直す必要があります(たとえば、各データセットをzfs sendバックアップし、zfs receiveで復元するなど)。OpenZFSコントリビューターのRob Norris氏が指摘しているように、OpenZFS 2.4.xでは同じことをよりエレガントに実現するzfs rewriteコマンドが登場予定です。

補足:TrueNASの機能設定に合わせる

TrueNASを開発するiXsystemsチームのメンバーがRedditでコメントし、TrueNASは技術的にはユーザーによるコマンドラインからのZFSプール作成をサポートしていないと述べていました。彼が提案したのは、TrueNAS上でテスト用プールを作成し、その機能フラグをダンプして、コマンドラインで作成した新しいRAIDZ2プールに適用する方法です。

CLIで作成したプールに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さんには、これらの詳細を正していただきました。ありがとうございます。

原文は Michael Lynch により に公開されました。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。