[PERFORMANCE]INTERMEDIATE

容量は足りているのに遅い、というやつ

以前、新しいVM基盤の容量見積もりだけ済ませて本番投入し、稼働後に「遅い」と言われて青ざめたことがあります。原因は単純で、IOPS要件を誰も聞いていませんでした。この記事では、ワークロードのIOPS要件からZFSのvdevトポロジーを逆算する実務プロセスを、具体的な数値と自宅VMでの検証ログとともに解説します。

以前、新しいVM基盤の容量見積もりだけを済ませて本番投入したことがあります。「usable 150TB、余裕あり」という数字だけを確認して進めたところ、稼働後しばらくして「VMの動きがもっさりする」という報告が上がってきました。調べてみると、そもそも秒間何回の読み書きが発生するかを誰も見積もっていませんでした。容量とIOPS(性能)は別のリソースで、片方を満たしても、もう片方が足りているとは限りません。当たり前のことなのに、見積もりの質問が「何TB必要ですか」で止まっていると、案外見落とされます。

この記事では、ワークロードの要件からIOPS/スループットを見積もり、そこからZFSのvdevトポロジー(ミラーかRAIDZか、本数はいくつか)を逆算するプロセスを、具体的な数値とともに解説します。数値はあくまで目安であり、実際のディスク型番のデータシートで検証することを前提にしてください。

ワークロードを分解する

サイジングの第一歩は、ワークロードを「ランダムかシーケンシャルか」「I/Oサイズ」「同期/非同期」「並列度」「ワーキングセットサイズ」の5軸で整理することです。この軸で見ると、必要な構成は自然に絞れてきます。

VM datastoreOLTPデータベースバックアップ/メディア
ランダム/シーケンシャルランダム多めランダム(R/W混在)シーケンシャル多め
I/Oサイズ4-8K前後8-16K128K以上
並列度(キューデプス)高い中〜高低い(ストリーム単位)
レイテンシ要求中程度厳しい緩い
ワーキングセット一部が頻繁アクセス全体的に高頻度ほぼ全域が低頻度

この3種類は性質が正反対に近く、同じ「容量ベース」の見積もりでも最適な構成はまったく違います。以降はVM datastoreを例に、実際の逆算プロセスを追います。

ビジネス要件をIOPSに変換する

正確な値はアプリのI/O統計(iostat、ハイパーバイザーの性能モニタ等)から取るのが本筋ですが、初期見積もり段階では業界目安値で概算し、後工程で実測に置き換えるのが現場の割り切りです。

VM datastoreの例で計算してみます。

  • VM 300台、1台あたり平均Write IOPS目安 10 IOPS(中程度の利用パターン、業界の目安値)
  • 合計必要Write IOPS = 300 × 10 = 3,000 IOPS

計算の起点をWrite IOPSに絞っているのは、後述のとおりWriteのほうがサイジング上の律速要因になりやすいためです。Readは後述のARC/L2ARCやミラーの複製効果で伸びる余地がありますが、Writeはディスクの物理限界に近いところで頭打ちになります。

ZFSのvdevトポロジーとIOPSの関係

ここが本記事で最も伝えたい部分です。同じディスク本数でも、RAIDZは「vdev1本あたり単体ディスク1台分」、ミラーは「vdev1本あたりほぼ単体ディスク1台分がvdev数だけ積み上がる」という違いがあります。これがRAIDZの「IOPSペナルティ」と呼ばれる、顧客に最も誤解されやすいポイントです。

  • ミラー(striped mirrors): 1回の書き込みはミラーペア内で完結し、他のミラーvdevには波及しません。トップレベルvdev間でストライピングされるため、プールのランダムIOPS ≈ ミラーvdev数 × 単体ディスクのIOPS。vdev数を増やすほどほぼ線形に伸びます。Readはミラー内の複製にロードバランスされ、複製数倍まで伸びる場合があります。
  • RAIDZ(RAIDZ1/2/3): 1回の論理I/Oがvdev内の複数ディスクにまたがるため、1つのRAIDZ vdevのランダムIOPSは、ディスク本数に関わらず単体ディスク1台分に近くなります。プール全体のIOPS ≈ RAIDZ vdev数 × 単体ディスクのIOPS。

RAIDZ vdev(8D+2P構成の一部を簡略化)

論理I/O 1回

→ vdev内の全ディスク(データ+パリティ)に分散。プールのIOPSはvdev本数を増やしても、 1vdevあたりは単体ディスク1台分にとどまる。

ミラーvdev(2-way × 3vdev)

論理I/O 1回

→ 自分のペア(2本)だけで完結。他のミラーvdev(グレー)には影響しない。vdev数を増やす ほどIOPSはほぼ線形に伸びる。

同じディスク本数・同じ容量効率を狙っても、広いRAIDZ vdev1本より、狭いvdevを複数本(またはミラー)にするほうがランダムIOPSは大きく伸びます。容量効率とIOPSはトレードオフだと考えてください。

02,2504,5006,7509,0001102030vdev数 →
ミラー(2-way、Read) RAIDZ2(Read)

縦軸: 推定ランダムRead IOPS(単体ディスク 140 IOPSを仮定した目安値。実機のデータシートで要検証。ケーススタディのWrite IOPS目安と同じ数値を、Read/Writeがほぼ同程度という簡略化のもとで流用している)

上のチャートはReadIOPSの伸び方の目安です(単体ディスクのランダムRead/Writeがほぼ同程度という簡略化のもと、後述のケーススタディと同じ140 IOPSを使っています)。同じvdev数でも、ミラー構成はRAIDZ2構成のおよそ2倍の傾きで伸びていきます。この差はvdev数が増えるほど開いていきます。

シーケンシャルスループット重視の設計との対比

ここまでのランダムIOPSの話とは逆に、シーケンシャルワークロードでは設計思想がひっくり返ります。RAIDZ vdevのスループットは「データディスク本数 × 単体ディスクのシーケンシャル速度」に近づくためです。パリティディスクを除いた本数ぶん並列に読み書きできるので、バックアップやメディアリポジトリのような大容量シーケンシャル用途では、広いRAIDZ2/RAIDZ3が容量効率・スループットの両面で有利になります。

つまり、IOPS重視ならミラーか狭いRAIDZ、スループット重視なら広いRAIDZ2/RAIDZ3という対比になります。ワークロードの性質を取り違えると、この設計思想はそのまま逆転してしまいます。ランダムI/O主体のワークロードに広いRAIDZを当ててしまうのが、性能トラブルの典型パターンです。

キャッシュ層(ARC/L2ARC)が実効IOPSを変える

ここまでの計算は「キャッシュなしの物理ディスクIOPS」という最悪ケースの下限値にすぎません。実運用ではARCヒット率次第で実効性能は大きく改善します。

ワーキングセットがRAMに収まるかどうかが最初の分岐点です。収まらない場合、読み込み中心のランダムワークロードにはL2ARC(フラッシュ拡張キャッシュ)が有効です。ただし、L2ARCはRAM上にインデックス(ヘッダ)を持つため、RAM容量に対してL2ARCを過大に積むと、かえってARCそのものを圧迫するという実務上の落とし穴があります。

もう一つ注意したいのは、「キャッシュ前提で物理ディスク本数を削る」設計です。再起動直後やキャッシュがコールドな状態では、この節で説明した物理ディスクの下限値まで性能が落ち込みます。キャッシュはあくまで上振れ要素として見積もり、物理ディスク側は最悪ケースで要件を満たせるように設計するのが安全です。

同期書き込みとSLOG

NFS(sync)、iSCSI、DBなど同期書き込みが発生するワークロードでは、SLOGの有無がレイテンシを大きく左右します。

同期書き込みは、ZIL(ZFS Intent Log)への書き込みが完了するまでアプリケーションに応答を返せません。SLOGは専用の低レイテンシデバイスにZILを逃がすことで、この待ち時間を短縮します。選定基準は容量ではなく、低レイテンシと電源断保護(PLP)対応です。非同期書き込み中心のワークロード(一般的なファイル共有等)では、SLOGの効果は限定的です。

recordsize/volblocksizeのミスアライメント

アプリのI/Oサイズとrecordsize/volblocksizeが不一致だと、読み書き増幅が発生し、無駄なIOPSを消費します。

たとえばDBのI/Oサイズが8Kなのに、ZFSのrecordsizeがデフォルトの128Kのままだと、8Kの書き込み1回のために128Kブロック全体を読み書きする増幅が起きます。冒頭の失敗をやらかしたときも、実はこれが二重で効いていました。IOPS要件を聞かなかっただけでなく、DBのI/OサイズもrecordsizeもデフォルトのままVMを載せていたので、増幅分がまるまる無駄なIOPSとして積み上がっていたわけです。

容量ヘッドルームと劣化時性能

性能サイジングは論理容量だけでなく、「性能を維持するための空き容量」と「劣化モード時の余力」も見込む必要があります。

プール使用率がおおむね80%を超えると、CoW(Copy on Write)による断片化で性能劣化が起きやすくなります。また、ディスク交換時のリシルバー(再構築)中は、プールのI/O余力の一部が再構築に使われます。ディスク本数が多い、あるいは大容量ディスクほどリシルバー時間が長くなり、性能が低下した状態のままさらされるリスク露出期間が延びます。性能設計は定常時だけでなく、縮退運転時も想定しておくべきです。

ケーススタディ:逆算の実例

同じIOPS要件でも、ミラーとRAIDZ2では必要なディスク本数が大きく異なり、どちらが最終的な律速要因になるかも構成によって変わります。実際に数字を追ってみます。

前提条件をまとめます。

  • ワークロード要件: VM 300台、必要合計Write IOPS 3,000(前述の計算より)
  • 容量要件: usable 150TB
  • 使用予定メディア: 10K SAS HDD、2TB、単体ランダムWrite IOPS目安 140(要実機検証)

ミラー構成(2-way)を検討する

IOPS基準で必要なvdev数 = 3,000 ÷ 140 ≈ 21.4 → 22vdev(44本)。この構成のIOPSは 22 × 140 = 3,080 で要件を満たします。しかし容量は、ミラーvdev1本あたりusable 2TBなので、22vdev では 44TB にしかなりません。150TB要件には遠く及ばないため、容量基準で必要なvdev数を再計算すると、150TB ÷ 2TB = 75vdev(150本)。この本数ならIOPSは 75 × 140 = 10,500 と大幅な余裕が生まれます。このケースでは容量が律速し、最終的に150本が必要です。

RAIDZ2構成(8D+2P、10本/vdev)を検討する

IOPS基準で必要なvdev数は同じ式で ≈ 21.4 → 22vdev(220本)。1vdevあたりのusableは8台分の2TB=16TBなので、22vdevのusableは 352TB。150TB要件を大幅に超過します。逆に容量基準だけで組むと 150TB ÷ 16TB ≈ 9.4 → 10vdev(100本)で足りてしまいますが、そのIOPSは 10 × 140 = 1,400 と要件の半分以下です。このケースではIOPSが律速し、最終的に220本が必要です。

最終比較

構成必要ディスク本数usable容量Write IOPS律速要因
ミラー(2-way)150本150TB約10,500容量
RAIDZ2(8D+2P)220本352TB(過剰)約3,080IOPS

本数だけを見るとRAIDZ2のほうが多いのに、容量効率(usable/raw)自体はRAIDZ2のほうが良いという、一見逆説的な結果になります。これは「IOPS要件が支配的なワークロードでは、容量効率の良いRAIDZが総ディスク本数でも有利とは限らない」という、この記事の核心を数字で裏づけています。今回のケースでは、総合的にミラー構成のほうがディスク本数を抑えられます。

実機検証: ここまでの主張をVM上で確かめる

ここまでの説明は理屈の上では正しくても、実際にZFSがそう動くかは別問題です。VirtualBox上のUbuntu 22.04 VM(zfsutils-linux 2.1.5、ディスクはVM内のスパースファイルで作成したループバックデバイス)で、構造的に検証できる4項目を実際に動かして確認しました。使用したコマンドと実際の出力は、以下のパネルで全10ステップを追えます。

FLOWSCENEsizing-zfs-performance-by-workload
tankvdev-1vdev-2disk1disk2disk3disk4
$ truncate -s 300M /root/zfs-scenario-perf/disk1.img /root/zfs-scenario-perf/disk2.img /root/zfs-scenario-perf/disk3.img /root/zfs-scenario-perf/disk4.img && zpool create tank_mirror mirror /root/zfs-scenario-perf/disk1.img /root/zfs-scenario-perf/disk2.img mirror /root/zfs-scenario-perf/disk3.img /root/zfs-scenario-perf/disk4.img && zpool status tank_mirror
pool: tank_mirror
state: ONLINE
config:
NAME STATE READ WRITE CKSUM
tank_mirror ONLINE 0 0 0
mirror-0 ONLINE 0 0 0
/root/zfs-scenario-perf/disk1.img ONLINE 0 0 0

4本のディスクでstriped mirror(2 vdev)を作成する。トップレベルvdevが2本、互いに独立している

最初に検証の範囲を明確にしておきます。このVM環境で裏付けが取れるのは、ハードウェアに依存しない構造的・相対的な効果だけです。ケーススタディで使った「10K SAS HDD単体で140 IOPS」のような絶対値は、VirtualBoxの仮想ディスク(ホストディスク上のスパースファイル)では原理的に再現できません。これは記事中でも「要実機検証」と明記した通りで、ここでの検証対象はあくまで構成そのものが持つ性質に絞っています。

ミラー/RAIDZのトップレベルvdev構造(step-1, step-2)

同じ4本のディスクで、striped mirror(zpool create tank_mirror mirror disk1 disk2 mirror disk3 disk4)とRAIDZ2(zpool create tank_raidz raidz2 disk1 disk2 disk3 disk4)をそれぞれ作成し、zpool statusで構造を比較しました。以下はVM内で実行した際の生ログです。

$ zpool create tank_mirror mirror disk1.img disk2.img mirror disk3.img disk4.img && zpool status tank_mirror
  pool: tank_mirror
 state: ONLINE
config:

	NAME                                   STATE     READ WRITE CKSUM
	tank_mirror                            ONLINE       0     0     0
	  mirror-0                             ONLINE       0     0     0
	    /root/zfs-scenario-perf/disk1.img  ONLINE       0     0     0
	    /root/zfs-scenario-perf/disk2.img  ONLINE       0     0     0
	  mirror-1                             ONLINE       0     0     0
	    /root/zfs-scenario-perf/disk3.img  ONLINE       0     0     0
	    /root/zfs-scenario-perf/disk4.img  ONLINE       0     0     0

errors: No known data errors
$ zpool create tank_raidz raidz2 disk1.img disk2.img disk3.img disk4.img && zpool status tank_raidz
  pool: tank_raidz
 state: ONLINE
config:

	NAME                                   STATE     READ WRITE CKSUM
	tank_raidz                             ONLINE       0     0     0
	  raidz2-0                             ONLINE       0     0     0
	    /root/zfs-scenario-perf/disk1.img  ONLINE       0     0     0
	    /root/zfs-scenario-perf/disk2.img  ONLINE       0     0     0
	    /root/zfs-scenario-perf/disk3.img  ONLINE       0     0     0
	    /root/zfs-scenario-perf/disk4.img  ONLINE       0     0     0

errors: No known data errors

この出力が、「ミラーはvdev数を増やすほどIOPSが線形に伸び、RAIDZはディスク本数を増やしても1vdevあたりのIOPSは単体ディスク1台分に留まる」という記事冒頭の構造的主張の裏付けです。IOPSの絶対値ではなく、独立した書き込みパス(トップレベルvdev)が何本あるかという構造そのものが両者で違う、という点が実機で確認できました。

recordsize不整合による書き込み増幅(step-3〜step-5)

recordsize=128k(デフォルト)とrecordsize=8kの2つのデータセットに同じ8MBのファイルを置き、128Kレコード境界をまたがない8Kオフセット(819,200バイト)へ8Kの書き込みを1回当てて、スナップショット基準のwrittenプロパティで実際に書き直されたバイト数を比較しました。

セットアップ(2つのデータセットに同一のベースファイルを配置し、基準スナップショットを取る)の生ログです。

$ zfs create -o recordsize=128k tank/db128k && zfs create -o recordsize=8k tank/db8k && dd if=/dev/urandom of=base.bin bs=1M count=8 && cp base.bin /tank/db128k/data.bin && cp base.bin /tank/db8k/data.bin && zfs snapshot tank/db128k@before && zfs snapshot tank/db8k@before && zfs get recordsize tank/db128k tank/db8k
8+0 records in
8+0 records out
8388608 bytes (8.4 MB, 8.0 MiB) copied, 0.0453427 s, 185 MB/s
NAME         PROPERTY    VALUE    SOURCE
tank/db128k  recordsize  128K     local
tank/db8k    recordsize  8K       local

128Kレコード境界をまたがない8Kオフセットへの書き込みと、その直後のwritten@beforeの生ログです(zpool syncでtxgを確定させてから計測しています)。

$ dd if=/dev/urandom of=/tank/db128k/data.bin bs=8k count=1 seek=100 conv=notrunc && zpool sync tank && zfs get -Hp written@before tank/db128k
1+0 records in
1+0 records out
8192 bytes (8.2 kB, 8.0 KiB) copied, 0.000319096 s, 25.7 MB/s
tank/db128k	written@before	151552	local
$ dd if=/dev/urandom of=/tank/db8k/data.bin bs=8k count=1 seek=100 conv=notrunc && zpool sync tank && zfs get -Hp written@before tank/db8k
1+0 records in
1+0 records out
8192 bytes (8.2 kB, 8.0 KiB) copied, 0.000271489 s, 30.2 MB/s
tank/db8k	written@before	104448	local

結果は次の通りです。

データセットrecordsize8K書き込み1回あたりの実written
db128k128K151,552 バイト
db8k8K104,448 バイト

recordsize=128kのほうが約1.45倍多く書き直されていることが確認できました。単純な「128K÷8K=16倍」という理論値ほど差が開かなかったのは、この検証ではベースファイルが8MBと小さく、インダイレクトブロック(メタデータ)のCoW書き直しコストが両ケースで相対的に大きな比率を占めるためです。実運用規模のデータセットではメタデータのオーバーヘッド比率は下がり、レコード本体のサイズ差(128K対8K)がより支配的になります。つまり増幅の「方向」と「実在」は実機で確認できましたが、「何倍になるか」は規模に依存するため、この倍率をそのまま実運用の見積もりに使わないでください。

同期書き込み(sync)のレイテンシペナルティ(step-6, step-7)

同じデータセットにsync=standard(デフォルト)とsync=disabledのそれぞれで、4K単位・O_DSYNCの書き込みを200回行う所要時間を計測しました。

生ログです(sync=standard側はzfs get syncの出力も含めています)。

$ zfs create tank/syncds && zfs get sync tank/syncds && time dd if=/dev/zero of=/tank/syncds/syncfile bs=4k count=200 oflag=dsync
NAME         PROPERTY  VALUE     SOURCE
tank/syncds  sync      standard  default
200+0 records in
200+0 records out
819200 bytes (819 kB, 800 KiB) copied, 0.878054 s, 933 kB/s

real	0m0.883s
user	0m0.018s
sys	0m0.036s
$ zfs set sync=disabled tank/syncds && rm -f /tank/syncds/syncfile && time dd if=/dev/zero of=/tank/syncds/syncfile bs=4k count=200 oflag=dsync
200+0 records in
200+0 records out
819200 bytes (819 kB, 800 KiB) copied, 0.00389297 s, 210 MB/s

real	0m0.009s
user	0m0.004s
sys	0m0.005s
sync設定所要時間
standard(デフォルト)0.883秒
disabled0.009秒

約98倍の差が出ました。この絶対値はこのVM環境(スパースファイル+ホストのファイルシステム)固有のものですが、「同期書き込みはZILへのコミット待ちが発生し、非同期書き込みより大幅に遅くなる」という構造そのものは、どのバックエンドディスクでも起きる普遍的な効果です。実機のHDD/SSDではこの倍率と一致しませんが、SLOGの有無がレイテンシを左右するという記事の主張の裏付けにはなります。

ARCキャッシュによる再読み込みの高速化(step-9, step-10)

128MBのファイルを書き込んだ後、zpool export/zpool importでARCの内容を破棄してから、同じファイルを2回連続で読み込み、コールド読み込みとウォーム読み込みの所要時間を比較しました。

セットアップ(128MB書き込み→export/import)の生ログです。この回はホスト側のディスクI/Oが一時的に詰まっており(zpool exportD状態でブロックしていたのをVM内で確認)、書き込み自体に50秒近くかかっています。ZFS側の問題ではなく、この検証環境固有のノイズとして記録しておきます。

$ zfs create tank/cachet && dd if=/dev/urandom of=/tank/cachet/bigfile bs=1M count=128 && zpool export tank && zpool import -d /root/zfs-scenario-perf tank
128+0 records in
128+0 records out
134217728 bytes (134 MB, 128 MiB) copied, 50.7161 s, 2.6 MB/s

コールド読み込み/ウォーム読み込みの生ログです。

$ time dd if=/tank/cachet/bigfile of=/dev/null bs=1M
128+0 records in
128+0 records out
134217728 bytes (134 MB, 128 MiB) copied, 0.204159 s, 657 MB/s

real	0m0.210s
user	0m0.005s
sys	0m0.153s
$ time dd if=/tank/cachet/bigfile of=/dev/null bs=1M
128+0 records in
128+0 records out
134217728 bytes (134 MB, 128 MiB) copied, 0.0658993 s, 2.0 GB/s

real	0m0.074s
user	0m0.004s
sys	0m0.069s
読み込み所要時間スループット
コールド(ARCなし)0.210秒657 MB/s
ウォーム(ARCヒット)0.074秒2.0 GB/s

約3倍の高速化です。この数字も実ディスクの速度とキャッシュ層のRAM速度の差によって変わりますが、「ワーキングセットがARCに乗ればディスクの物理限界を超えて速くなる」という効果自体は、このVM環境でも明確に確認できました。

最低限、これだけは聞く

あの一件以来、新しいワークロードを載せる前には最低限次を確認するようにしています。

  • I/Oパターン(ランダム/シーケンシャル、読み書き比率)
  • I/Oサイズ(アプリ側の実際の値。recordsize/volblocksize合わせのため)
  • 同期書き込みの有無(SLOGの要否)
  • ワーキングセットサイズ(ARC/L2ARCサイジングのため)
  • 容量の成長率(将来のディスク追加余地)
  • 許容レイテンシ(SLOGメディア・ネットワーク設計に影響)
  • 劣化運転を許容する期間(リシルバー時間とディスク本数のトレードオフ)

「何TB必要ですか」で終わらせず、この一言目を足すだけで、稼働後に呼び出される確率はだいぶ下がります。ここで説明した逆算プロセスを踏まえて、実際の数値を入れて試算できるツールもこのサイトで公開しているので、容量とあわせて性能側の目安も確認しながら構成を検討してみてください。