Migrating a ZFS pool from RAIDZ1 to RAIDZ2

Michael Lynch

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

原文は Michael Lynch により に公開されました。 このブログを購読する

最近、自宅の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プールにディスクを追加するたびに、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サーバーと7本のUSBメモリで移行戦略をテストしているところ

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

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

上で「データを外部ストレージに移さなかった」と書きましたが、厳密には正確ではありません。バックアップのために外部ストレージは使いましたが、結局それを必要とすることはありませんでした。

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

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

ディスクサーバーの容量のほとんどは、リッピングしたDVDとBlu-rayが占めています。

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

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

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 Boxes:$0.09/TB/日
    • 残念ながらこの選択肢は移行が終わった後に見つけました。

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

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

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

最初は失敗した移行の際に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台取り外すと、プールはデグレード状態になります。このデータ移行中に古いプールの他のディスクが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という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:スパースファイルでフェイクディスクを作成する

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

代わりに、8TB×5台のRAIDZ2プールを作成します。ただし5台目のディスクは実際には存在しません。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}"

最後に、新しいプールの名前を決める必要があります。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  -

新しいRAIDZ2プールはTrueNASのWeb UIからも確認でき、使用可能な容量は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プールがデグレード状態であると報告しますが、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プール上でいくつかのデータセットが欠けていることに気づきました。

移行のトラブルシューティング

データ移行を完了させるため、欠けていたデータセットを1つずつ送信します:

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ではネットワーク共有や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}"

より大きなRAIDZ2プールを作成するための巧妙なスパースファイルのハックを教えてくれたTrueNASフォーラムの@NugentSさんに感謝します。これらの詳細の一部を修正してくれたiXsystemsのChrisさんにも感謝します(詳細)。

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

コメント