dedupを切り忘れたVMストアが返ってこなくなった話
検証のつもりで`dedup=on`にしたVMストアをそのまま忘れて本番投入し、半年後に容量整理で`zfs destroy`を叩いたら丸一日プールが返ってこなくなったことがあります。DDT(dedupテーブル)がRAMから溢れた瞬間に何が起きるのか、事前にどう見積もればよいのか、そして多くの場合dedupより先に検討すべき代替手段を整理します。
以前、検証用にdedup=onにしたVMストアを、そのまま切り忘れて本番運用に持っていってしまったことがあります。しばらくは何も起きませんでした。問題が表面化したのは半年後、容量整理で古いVMイメージ数十個をまとめてzfs destroyしたときです。コマンドは返ってこず、zpool statusを叩いてもプールは生きているのに他の操作も詰まる。結局丸一日、DDT(Dedup Table)がひたすらランダムI/Oを処理し続けるのを見ているだけでした。
dedupは「容量が節約できるから」と圧縮(compression)と同列に語られがちですが、まったく別次元のリスクを伴います。圧縮は失敗してもCPU負荷が上がるだけですが、dedupは設計を誤るとプールそのものが実質的に応答不能になります。この記事では、DDTがなぜRAMを消費するのか、RAMから溢れると何が起きるのか、事前にどう見積もればよいのか、そして多くの場合dedupより先に検討すべき代替手段を整理します。
DDTという台帳
ZFSのdedupは、ブロック単位の重複排除です。データセットでdedup=onにすると、書き込まれる各ブロックのチェックサムがDDT(Dedup Table)に記録され、同じチェックサムを持つブロックが既に存在すれば、実データを新規に書き込まず参照カウントを1増やすだけで済ませます。
zfs set dedup=on tank/vmstore
一見、圧縮と同じ「1行で有効化できる最適化」に見えますが、決定的に違うのはDDTがプール全体で共有される台帳だという点です。書き込みのたびにDDTを引き(重複判定)、削除のたびにもDDTを引きます(参照カウントの減算、ゼロになれば実データを解放)。つまりdedupを有効にしている間、あらゆる書き込みと削除がDDT参照というひと手間を伴うようになります。
ARCに収まっている間は快適、それだけに怖い
DDTのエントリは、プール上のメタデータとして永続化されつつ、通常のメタデータと同様にARC(RAM上のキャッシュ)にも乗ります。DDT全体がARCに収まっている限り、参照はRAM上で完結するため高速です。問題が起きるのは、DDTのサイズがARCの容量を超えた瞬間です。
ブロック書き込み/削除のたびに発生するDDT参照
→ DDT全体がARCに収まっている限りは高速だが、収まりきらなくなった瞬間、書き込みだけでなくzfs destroyのような削除操作までブロック単位のランダムディスクI/Oに支配される。HDDプールでは特に致命的。
DDTエントリ1件あたりのRAM消費は、ZFSのバージョンやチェックサムアルゴリズムによって変動しますが、おおむね数百バイト程度(よく引用される目安として300バイト前後)とされています。ブロックサイズが既定の128Kであれば、1TBのユニークデータに対するDDTエントリ数はおおよそ800万件程度になり、これだけでも数百MB〜数GB規模のRAMを要求します。さらにブロックサイズが小さい(recordsizeを32K/16Kなどに下げている)データセットでは、同じ容量でもエントリ数が数倍に増えるため、RAM要求はその分だけ跳ね上がります。実務では「1TBのユニークデータあたり数GB程度のRAM」という粗い目安がよく使われますが、これはあくまで概算であり、実際の値はブロックサイズと重複率によって大きく変わるため、後述する方法で必ず自分のデータで見積もる必要があります。
RAMから溢れた瞬間に起きること
DDTがARCに収まらなくなると、参照のたびにディスクへのランダムI/Oが発生します。これが致命的なのは、次の2点が同時に起きるためです。
- 通常の書き込みが遅くなる。 128Kブロックを1つ書くたびにDDT参照が発生し、それがキャッシュミスであればランダムディスクI/Oを1回(場合によっては複数回)伴います。HDDプールでは、この1回のランダムI/Oのレイテンシが数ms〜十数msかかり、シーケンシャル書き込みのつもりだったワークロードが実質的にランダムI/O律速になります。
- 削除(
zfs destroy/rm)も同じコストを負う。 参照カウントを減らす操作も同じDDT参照を必要とするため、大量のブロックを解放するzfs destroyは、解放対象のブロック数ぶんだけランダムI/Oを発生させます。冒頭で書いた自分のケースがまさにこれで、プールが実際にハングしていたわけではなく、大量の小さなランダムI/Oを律儀に処理し続けていただけでした。体感としては「詰んだ」状態と変わりませんが。
厄介なのは、dedupをオフに戻しても、既にDDT参照済みの既存データはすぐには解放されないことです。dedup=offは新規書き込みに対してDDTへの登録を止めるだけで、既にDDTに登録されているブロックの参照カウント管理はそのまま残ります。RAM逼迫を今すぐ解消したい場合、対象データを書き直す(dedupオフの状態でコピーし直す)か、参照しているスナップショット/クローン/ファイルをすべて削除してブロックの参照カウントをゼロにするまで、DDTのサイズは実質的に減りません。
zdb -Sで有効化前に見積もる
dedupの最大の利点は、有効化する前に効果を試算できることです。zdb -S(dedup simulate)は、実際にdedupを有効化することなく、既存のプール上のデータに対してdedupを適用した場合の重複排除率を計算してくれます。
zdb -S tank
出力されるdedup = X.XX(重複排除率)が1.5〜2.0を大きく超えないようであれば、DDTのRAMコストに見合う効果が得られない可能性が高く、dedupを有効化しない判断も十分に合理的です。逆に、この時点で高い重複排除率が見込めるワークロード(後述)であれば、次にRAMコストの試算に進みます。
有効化した後にDDTの実際の状態を確認するには、次のコマンドを使います。
zpool status -D tank
zdb -D tank
これらはDDTのエントリ数、重複排除率、メモリ使用量の概算を表示します。dedupを運用しているプールでは、定期的にこの出力を確認し、DDTサイズがRAM容量に対してどの程度の余裕があるかを監視する必要があります。
緩和策: special vdev(dedup allocation class)
DDT全体をARC(RAM)だけに頼らせるのではなく、DDT専用の高速デバイス(special vdev、特にミラー構成のNVMe)に置くことで、RAMから溢れた場合でも「ランダムディスクI/O」の相手がHDDではなく低レイテンシなSSD/NVMeになり、最悪ケースの被害を大きく緩和できます。これはOpenZFSのバージョンによって対応状況(dedup専用のallocation classとしての細かい制御を含む)が異なるため、導入前に手元のzfs version/zpool versionで対応状況を確認してください。
zpool add tank special mirror /dev/nvme0n1 /dev/nvme1n1
ただし、special vdevはRAM不足そのものを解決する魔法ではなく、あくまで「RAMから溢れた場合の落ち込み幅」を緩和する手段である点には注意が必要です。RAM容量に対して適切なサイズのDDTに収める設計が、依然として第一の防衛線です。
dedupより先に検討すべき代替手段
多くの現場では、dedupを有効化する前に検討すべき選択肢があります。
| 状況 | 推奨される代替手段 |
|---|---|
| 一般的な圧縮可能データ全般 | compression=lz4(またはzstd)。CPU負荷は軽く、RAM上の追加台帳も不要 |
| VMテンプレートからのクローン展開など、既知の完全複製 | zfs clone。スナップショットのブロックをそのまま共有するため、DDTなしで実質的な重複排除が得られる |
| バックアップリポジトリでの重複排除 | バックアップソフトウェア側(Veeam/Bacula等)の重複排除機能。ZFS側でdedupを重ねる必要性は薄い |
| 真にブロック単位での偶発的重複が多いワークロード(重複排除率がzdb -Sで高く出る場合) | dedup有効化を検討。ただしRAM見積もりとDDT監視体制が前提 |
特にzfs cloneは、VMのゴールデンイメージから多数のインスタンスを展開するようなユースケースで、dedupが狙っている効果(同一ブロックの重複排除)をDDTなしで実現できるため、真っ先に検討すべき選択肢です。
結局、zdb -Sさえ叩いていれば
自分のケースを振り返ると、検証でdedup=onにした時点でzdb -Sを1回叩いてさえいれば、重複排除率がコストに見合わないことにその場で気づけたはずでした。「検証だから後で切る」は、切り忘れた瞬間から本番のリスクになります。dedupを触るなら、最低限次を確認してから決めてください。
zdb -Sで重複排除率を事前に試算し、コストに見合う効果があるか- 見込まれるユニークデータ量とrecordsizeから、DDTの概算RAM要求を試算したか(小さいrecordsizeほどエントリ数が増える)
- そのRAM要求が、ARCとして確保できる現実的な範囲に収まっているか
zfs cloneや圧縮、バックアップソフト側の重複排除で目的を達成できないか- dedupをオフに戻しても既存データのDDT参照はすぐには解消されない、という制約を理解した上での判断か
読み込み側のキャッシュ設計(ARC/L2ARC)と同じく、DDTもRAMを奪い合う存在です。うちのhomenasではあの一件以来、dedupは一切使わず、zfs cloneと圧縮だけで運用しています。