OpenZFSの重複排除機能が改善、ただし利用自制を推奨
(despairlabs.com)- OpenZFS 2.3.0のFast Dedupは従来のdedupより大きく改善されたが、一般ユーザーがデフォルトのように有効化するには、依然としてコストと条件を見極める必要がある機能である
- dedupは、すでに保存済みのブロックを再書き込みせず参照だけを増やす方式のため、すべての書き込み・解放パスにdedup tableの参照と更新コストが発生する
- 従来方式では、ZAPベースのテーブルにおけるread-modify-write増幅、トランザクション中に回収できないlive entry list、利益を生まないunique entryによって、メモリとIOの負担が大きかった
- Fast Dedupはlive entryを424バイトから216バイトへ削減し、dedup log、段階的flush、
zpool ddtprune、dedup_table_quota、DDT prefetch、kstatsによって、運用者がコストをより細かく制御できるようにした - 一般的なワークロードでは実際に重複するブロックは少ない可能性があり、OpenZFS 2.2のBRT/block cloningがより低コストで同様の節約を提供するため、dedupは大量の重複データがあり、かつzero-copyの代替手段がない場合に限って慎重に使う価値がある
OpenZFS dedupの基本動作
- 重複排除(dedup) は、OpenZFSがデータをディスクへ書き込む前に、同じデータがすでに存在すれば新しい書き込みを省略し、既存コピーへの参照を追加する機能である
- 難しいのは、「すでにディスク上にあるか」と「どこにあるか」を素早く把握するための情報を継続的に保存・参照し続けなければならない点である
- この情報を保持する構造がdedup tableである
- 概念的には、データチェックサムをキー、ディスク位置と参照数(refcount)を値に持つハッシュテーブルである
- ユーザーデータではなく、pool metadataの一部として保存される構造的なpoolデータである
書き込み・解放パスに伴うコスト
- dedupが無効なら、OpenZFSはmetaslab allocatorから領域を割り当て、返されたDVAをblock pointerに入れてデータを書き込む
- dedupが有効なら、まずチェックサムをdedup tableで参照する
- 項目がなければ新しい領域を割り当ててデータを書き込み、その後refcount 1の新しいdedup entryを作成する
- 項目があれば既存のDVAをblock pointerへコピーし、書き込みIOを完了扱いにしたうえでrefcountを増やす
- dedupで割り当てられたブロックには、block pointerに
Dフラグが設定される- 解放時に
Dフラグがあればdedup tableを再度参照し、refcountを減らす - refcountが0になればdedup entryを削除し、実際の領域を解放する
- 解放時に
- すべての書き込みと解放がdedup tableの参照・更新を経由するため、dedupが有効であるには、テーブル管理オーバーヘッドより実際の容量・IO削減効果のほうが大きくなければならない
従来のdedupが悪かった理由
-
ZAPベースdedup tableの増幅
- 従来のdedup tableは、OpenZFS標準のオンディスクハッシュテーブルオブジェクトであるZAPを使用している
- ZAPはディレクトリ、属性リスト、内部管理にも使われる汎用構造だが、dedup entry保存には適していない
- 一般的なdedup entryは40バイトのキーと、圧縮後で約64バイトの値から構成され、32KのZAP block 1個に約188個の通常entryが入る
- OpenZFSは部分ブロック書き込みやインプレース上書きを行わないため、entry 1個を更新するだけでもZAP block全体を読み、修正し、新しいブロックとして再書き込みしなければならない
- チェックサムキーは衝突耐性が強くなければならないため、任意の2つのentryが同じZAP block付近に配置される可能性は低く、1トランザクション内の複数更新が同じブロックに集まる可能性も低い
- RAMが十分でARCがdedup tableを保持し続けられれば読み取りコストは下がるが、そのためdedupには大量のメモリが必要だという古くからの推奨が生まれた
dedupvdev classは、十分に大きく高速な専用dedup vdevを追加することでメモリ要求を多少下げられるが、dedupが意味を持つ規模ではテーブル全体を収容できる大きさと十分な速度が必要である
-
live entry listのメモリ使用
- OpenZFSはトランザクション中に作成・変更されたdedup entryを、メモリ上のlive entry listに保持する
- これは、同じデータが同時に複数回書き込まれたとき、各書き込みスレッドがdedup tableにまだ存在しないと判断して全て新規書き込みしてしまう事態を防ぐための仕組みである
- lookupはまずlive entry listを確認する
- 対象entryがあればrefcountを増やす
- なければ“in progress”状態のlive entryを作成し、ZAPから実際のentryを読み込んでから“ready”に変える
- 同時にアクセスした他の書き込みスレッドはreadyになるまで待機する
- トランザクション終了時にlive entry listを走査し、関連内容をdedup ZAPへ反映する
- 従来のlive entryは1個あたり424バイトで、このメモリはARCではなくkernel slab memoryのため、システムメモリ逼迫時にも回収できない
- live entry listはトランザクションごとに空になるが、1トランザクション内で異なるデータを大量に書き込むとピークが大きくなる
-
unique entryがテーブルを膨らませる
- dedupはディスク上に保存されたすべてのブロックを追跡するが、実際に利益があるのはrefcountが1より大きい場合だけである
- refcountが1のunique entryは、将来同じデータが再度書き込まれるのを待つためのコストに近い
- dedupは暗号化と圧縮の後、ブロック単位で実行される
- 同じ元データでも、圧縮方式、暗号化キー、ファイル内のアラインメントまで一致しなければ同一ブロックとして扱われない
- 汎用ワークロードでは「本当に同じ」ブロックが少なく、dedupのコストが利益を上回りやすい
Fast Dedupの改善点
-
live entryの縮小
- Fast Dedupはまずlive entry listのメモリフットプリントを削減する
ddt_entry_tの大きな数値型フラグをbitfieldへ変更し、同期フィールドも単純化した- dedup済みデータブロックの初回書き込み時やrepair writeが必要なときだけ使う40バイトの状態を、別のIO stateオブジェクトへ分離した
- 従来のdedup entry値は4つのphysical entryを含み256バイトだった
- 各physical entryには3つの128ビットDVA、refcount、birth transaction idが含まれていた
- 4番目のentryは過去の
dedupditto機能の名残であり、現代のOpenZFSでは読み取りのみサポートし、新規書き込みには使わない - Fast Dedupでは、
copies=が変更されてより多くのDVAが必要になった場合でも、従来variantを別entryのように保持せず、必要な追加コピーだけを割り当てて既存dedup entryへ加える - 新しいFast Dedup tableのentry値は、従来の256バイトから72バイトへ縮小された
- live listのentry 1個は従来の424バイトから216バイトへ減少した
-
dedup logの導入
- 従来方式ではトランザクション終了時にlive entry listをdedup ZAPへ直接反映しており、entry周辺の187項目の大半が無関係でもブロック単位更新コストが発生していた
- Fast Dedupは、最近作成・重複したブロックほど再び重複したり解放されたりする可能性が高いという観察に基づき、dedup logを追加した
- トランザクションの終わりにlive entry変更をZAPへ直接書かず、ログへ記録する
- on-disk logはcrash safetyのために必要である
- in-memory logは高速lookupのために維持される
- lookup順序はlive entry list、in-memory log、dedup ZAPの順になる
- on-disk logはpool import時にin-memory logを復元するために使われる
-
段階的log flush
- 初期方式のように、ログが大きくなりすぎたときに一度にZAPへflushすると、数千entryだけでも長いpauseが発生し得た
- Fast Dedupは各トランザクションごとにログの一部をZAPへ反映するincremental flushingを使う
- flush量は実際のIOに費やした時間と比較して調整される
- 忙しい時期には少なく書き、静かな時期には多く書く
- in-memory logが大きくなってメモリ圧迫を招く場合はflushを加速できる
- on-disk logをappend-onlyのまま保ちつつ、全体停止なしに空にするため2つのlogを使う
- 1つはactive logとして新しい変更を受け取る
- もう1つはflushing logとしてZAPへ反映される
- flushing logが空になればon-disk logをzero処理し、2つのlogの役割を入れ替える
- scrubやresilverのようなpool scanでは、dedup logに安定した位置という概念がないため、scan要求時にlog flushingを加速してdedup ZAPへ全て反映してから従来方式でscanする
unique entry管理と運用機能
zpool ddtpruneはpoolのdedup tableから一部のunique entryを削除する- ageまたはpercentage基準で指定できる
- age基準は、最近使われたデータが再び重複しやすいワークロードに特によく合う
- pruningでdedup entryが削除されたブロックが後でコピーされても、既存ブロックとは重複排除されず、新しいブロックとして割り当てられる
- ただし、古いunique blockが突然何度もコピーされれば、新しい1つのブロックに対して複数参照が生じる可能性はある
dedup_table_quotapool propertyはdedup tableの最大サイズを制限する- 新規entry作成が上限を超える場合、そのentryは作成せず、通常のnon-dedup writeとして処理する
- 専用dedup deviceが満杯になったときにmain deviceへあふれないようにする用途でも使える
zpool prefetch -t ddtはdedup tableをARCへ事前読み込みする- pool import直後の性能改善に役立つ可能性がある
- Fast Dedupでも、logにないentryの参照やflush時のZAPアクセスが必要なため有効である
- 新しいkstatsとtuneableも追加された
- Linux:
/proc/spl/kstat/zfs/<pool>/ddt_stats_<checksum> - FreeBSD:
kstat.zfs.<pool>.misc.ddt_stats_<checksum> - Linux tuneable:
/sys/modules/zfs/parameters/zfs_dedup_log_* - FreeBSD tuneable:
vfs.zfs.dedup.log_*
- Linux:
zpool status -D、zdb -D、zdb -Sのような既存のdedup対応ツールも、新しい構造を理解できるよう更新された
従来dedup tableとの互換性
- Fast Dedupの大部分はon-disk formatの変更を必要とするため、従来のdedup tableにはそのまま適用できない
- 既存tableでも、オンディスク形式の変更を必要としない機能は動作可能である
dedup_table_quotazpool prefetch -t ddtddt_stats_*lookupおよびhit count- ZAP shrink
- dedup logを従来tableでも動作させること自体は比較的straightforwardな作業として残っている
- ただし、より小さいlive/log entryの利点は得られない
zpool ddtpruneは従来tableでも“percentage of uniques”モードだけなら追加しやすい- ageモードは新しいentry formatのデータが必要なため、従来formatでは不可能である
- 既存tableを新formatへ変換する機能は現在存在しない
copies=が変更されたことのない単純なケースなら、新しいZAPを作り、既存entryを変換・コピーする方法は可能である- online変換はold/new ZAPの同時参照または同時書き込みが必要になり複雑である
- offline変換はより簡単だが、poolをofflineにする必要がある
copies=変更により複数variantがrefcountを持つ場合、完全変換が不可能なことがある
- deduplicated datasetを、新しいdedupが可能な別poolへ送る方法は動作する
「良くなったのに、なぜ有効化すべきでないのか」
- Fast Dedupは従来よりoverheadが減り、より多くの境界的な状況で有用になり得る
- それでもdedupは依然としてIO処理量、メモリ使用量、dedup tableサイズのバランス問題である
- 汎用ワークロードでは、重複可能なブロックは非常にまれなことがある
- 例として、あるlaptop poolの
zdb -Ssimulated DDT結果では、dedupの利益はほとんどなかった- 11.7M entryの大半はrefcount 1のunique entryだった
- 実際にdedup可能なentry数は全体の中で丸め誤差レベルだった
dedup = 1.00と表示された
- このような場合、dedupを有効にしてもほとんど何も得られず、IOとメモリ圧迫だけが追加される
BRT/block cloningのほうが優れている場合
- OpenZFS 2.2以降にはBRT、すなわちblock cloningまたはreflinksがある
- dedup tableは、「このデータがすでにディスク上にあるか」を文脈なしに判定するための構造である
- 現代のシステムでは、コピー処理がコピーであることをstorage stackへ伝えられる場合がある
- LinuxとFreeBSDファイルシステムの
copy_file_range() - macOSの
copyfile() - Windowsの
FSCTL_SRV_COPYCHUNK - NFS、CIFS、OS block device driver、SCSI EXTENDED COPY、NVMe Copyなどにも類似機能がある
- LinuxとFreeBSDファイルシステムの
- クライアントプログラムと中間層がcopy offloadシグナルを伝えられれば、OpenZFSはBRTでrefcountだけを増やせる
- BRTはブロックがcloneされなければコストがかからず、cloneされた場合でもentryは16バイトで済む
- 例のpoolでは、BRTは
used 292M; saved 309M; ratio 2.05xを示した - dedup simulationよりraw savingはやや少なく同程度だが、cloneされていない全ブロックを追跡する大きなコストはない
実務的な判断基準
- Fast Dedupは、従来dedupの3軸であるIO throughput、memory usage、dedup table sizeをすべて改善する
- 失敗時の壊滅的コストも減り、運用者がtableを制限・整理するためのツールも得られた
- それでも利益を得るには条件が明確である
- データ規模が非常に大きいこと
- 同じデータが大量にコピーされること
- block cloningやsnapshot cloneのようなOpenZFSの他のzero-copy選択肢を使えないこと
- クライアントが「コピーしてほしい」という明確なシグナルを出せるワークロードなら、block cloningのほうがより低コストで大きな利益を提供し得る
1件のコメント
Hacker Newsのコメント
オフライン重複排除や、プールを完全に停止しなくてもよいが即座には実行されない遅延重複排除があるとよさそう
重複排除を有効にすると、すべての書き込みと解放操作ごとに重複排除テーブルの参照と書き込みが必要になる点は、ほとんどの場合で間違ったアプローチに見える。データを書き込むときは、ディスク容量を多めに使ってでもできるだけ早く終わってほしいので、作業中のファイルを7zipアーカイブの中に保存したりはしない。後でシステムが空いているときにZFSが重複データを見つけ、BRTのようなもので領域を回収してくれるとよいし、通常のスクラブ処理の一部として行うこともできそう
物理位置がMerkleハッシュツリーにハッシュされて組み込まれているため、物理位置を移動するには、変更したいノードまでの内部ノードをすべて書き直す必要があり、コストが高すぎる。よりよい設計は、ブロックポインタを持つすべてのノードを、論理ブロックポインタだけを持ちツリーにハッシュされる部分と、その論理ポインタに対応する物理位置だけをキャッシュのように保持するがMerkleツリーにはハッシュされない部分に分けることだったはず。そうすればBP書き換えは、Merkleツリーに属さないブロックだけを書き直せばよい。現在の構造では、望む機能をZFSで得るのは難しいが、読み取り時にハッシュ不一致が起きたら、ポインタのハッシュを使って重複排除テーブルからブロックを探し、重複排除済みブロックを再割り当てするような回避策は可能かもしれない。その代償は無駄な読み取りが1回増える程度なのでそれほど悪くはないが、BP書き換えができないと、たいていこうした継ぎはぎが生まれる
RAMと入出力をかなり消費するが、“groveler”はスケジュール設定や制限ができる。ただしWindows 2012 R2時代のバグのせいで、データを食う破損を経験したことがある
たとえばjdupesやduperemoveがある。必要なシステムコールをサポートするよう、ZFS側とduperemove側にPRを送った。ZFS側はレビューに時間がかかり、仕上げを完全に忘れていたことに気づいたので、また対応しないといけない
さまざまな状況で書き込み入出力を大きく減らせる。重複排除ストレージアレイでは、2つのVM間でファイルをコピーすると、実データはコピーせず元のブロックの参照カウンタを増やすだけ、という拡張もある。OSからはとんでもないTB/sの書き込み速度に見えて、なかなか格好いい
「従来型の重複排除の根本的な問題は、このオーバーヘッドが大きすぎて、まれで特定のワークロードでなければ回収しにくいことだ」という主張は、かなり奇妙に聞こえる
PureとDell/EMCのアレイを扱ったことがあり、VMWareワークロードでは通常、重複排除/圧縮で少なくとも3:1の削減が出た。基本VMイメージのコピーを1つだけ保存する方式は非常にうまく機能する。syslogサーバーでも重複排除/圧縮がよく効き、6:1の削減を見たことがある。重複排除の効果は、ハッシュされるブロックサイズに大きく左右され、小さいほどよい。ブロックが小さくなるほど一致するブロックが生じる確率は急速に高まり、経験上好ましいブロックサイズは4KB
また、圧縮と重複排除という別々の機能を混同しているようにも見える。ZFSではプールに圧縮を有効化でき、ほぼ常に価値があるが、重複排除は無効にしておける
ただしZFSファイルシステム上でVMをホストしているなら、ブロックまたはファイルシステムのクローンのような、よりよい戦略を使っている可能性が高い。そうしないのは、その環境でZFSの主要な差別化要素の1つを捨てるようなもの。汎用ファイルサーバーや個人のデスクトップ/ノートPC用途では、通常、重複ブロックは非常に少なく、オーバーヘッドが割に合わない。バックアップは、実装方法やファイルシステム層の前に暗号化されるかどうかによって、効く場合もあれば効かない場合もある。圧縮はまったく別の話で、現在のZFSのベストプラクティスでは、ほぼすべてのワークロードでデフォルトで有効にすること。最近ではCPUコストは言及する価値も小さく、保存容量の削減とは別に入出力の削減がかなり大きいこともある。一般的なログストアなら、経験上6:1よりはるかによい削減も可能
現在、ディスク1TBが約15ドル、サーバーRAM 5GBが約25ドルと見ると、損益分岐点に届かせるだけでも3:1の重複排除率が必要になる。データがうまくはまればTBあたり1GBでも耐えられるかもしれないが、運が悪ければ5GBでも十分でない可能性がある。だから記事では、ZFSの重複排除にはデータがぴったり合っていなければならない小さなsweet spotがあると言っており、ほとんどの人が気にしない理由でもある。他のファイルシステムは通常、経済性のよいオフライン重複排除を好む
syslogで重複排除/圧縮がよく効くという表現は、細かく見ると重複排除と圧縮は同じものではない。エンタープライズストレージの世界では両者がセットにされることが多いが、ログはおそらく重複排除ではなく圧縮の恩恵を受けているのであり、ZFSにはもともと圧縮があった
代わりに基本イメージをクローンすれば、変更前にはほとんど容量を消費しない。これはZFSのコピーオンライト特性のおかげ。ZFSの重複排除は、ボリュームに書き込まれるデータの既存コピーを探す機能。コンテナイメージストアのような一部の用途にはかなり意味があるかもしれないが、あるデータセットが最初から別のもののクローンだとすでに分かっているなら、非常に非効率的
以前は ZFS の重複排除を広範囲に使っていて、大きなメリットがあった。具体的な用途は VMWare クラスタ用ストレージで、中身がだいたい同じ Linux と Windows VM が数百台あった。Docker 以前の話
高速重複排除にはとても期待している。ここ数年 ArchiveBox のデータに ZFS の重複排除を使いたいと思っていたが、高速重複排除のおかげで、数百万 URL を 1 つのコレクションにアーカイブし、ファイルシステムに全体の圧縮を任せる方式がついに可能になりそうだ
アーカイブデータには jquery.min.js、bootstrap.min.css、ロゴ画像のようなものが、数千のスナップショットに繰り返し入っている。他のツールは 1 回のクロール内で圧縮して wacz や warc.gz ファイルを作るが、これまで撮ったすべてのスナップショットのデータベース全体をまたぐ圧縮を試みたツールは、今のところなさそうだ。重複排除ハッシュテーブル全体をそのまま保存しないように、Bloom filter のようなものを使う確率的重複排除アプローチを試した人がいるのかも気になる。ブロックハッシュを約 100 個ずつバケットにまとめ、Bloom filter に超圧縮表現を保存する。書き込み時に書き込むブロックのハッシュを Bloom filter で照会し、潜在的な重複排除ヒットが検出されたら、そのバケットの 100 個のブロックを直接走査して同一ハッシュを探す、という具合だ。理論的には解像度の異なる Bloom filter の階層を用意し、メモリ圧迫が高いときに高解像度フィルターを動的にディスクへ降ろすこともできる。Bloom filter の精度を調整可能なパラメータにすれば、CPU 時間/オーバーヘッドと節約できるバイト比率についての好みを選べる
dm-vdo も同じ動作をする。代わりに長い範囲を見る solid 圧縮を使うか、WARC ファイルをディレクトリ形式に近い構造へ展開するか、コンテンツ定義チャンキングベースの FUSE システムがあるならそちらのほうがよいかもしれない。Seafile がそうかもしれない
WARC 標準を見ると、すでにハッシュベースの重複排除を行い、最初に保存した後はポインタを使う方式がある。だからこれは、ファイルシステム層の重複排除があまり適していない典型的な例だ
RAM 使用量を抑えたままこれをきちんと動かすのに、なぜそんなに苦労しているのか気になる。商用ストレージアプライアンスは、接続されたディスク容量に比べて RAM が「少ない」システムでも、少なくとも 10 年ほど前から実現していた
単にフィンガープリントをデータベースに保存して、夜間に走査しながらブロックポインタを直せばよいのではないかと思う
長らく要望されている機能で、可能になればデフラグもできる。仮想メモリのようにブロックポインタの間接化を使えば、多少の速度コストで解決できるのではないかと考えたことはあるが、自分は ZFS 開発者ではないので、きっと見落としている点があるのだろう。http://eworldproblems.mbaynton.com/posts/2014/zfs-block-poin... / https://github.com/openzfs/zfs/issues/3582
大きな欠点は、RDMA を使うファイル転送プロトコルがないことだ。Hammer2 を FreeBSD で動かせるようにする実験ブランチもあると聞いた。ただし FreeBSD も RDMA サポートがない。FreeBSD 15 では Chelsio が NVMe-oF ターゲットとイニシエータのサポートを後援したが、TCP のみのようだ
cp --reflink=autoを使えばよいファイルレベル重複排除を得られる。このコマンドは軽量コピーを行い、ファイルレベルの ZFS クローンのように、データブロックは変更時にのみコピーされる。ハードリンクではなくコピーだ。reflink をサポートする他のコピーオンライト・トランザクションファイルシステムでも同じ方式が動作するはずだ
ZFS を本当に使いたかったが、すべてのデータは当然暗号化されるべきだ。ところが使い方が予想よりずっと複雑になり、面倒になると多くの人が単にデータを暗号化しないことに驚いた
Proxmox でさえ Web サイトに “Enterprise” と付いているので、デフォルトインストールで暗号化をサポートしていると思っていたが、暗号化と一緒に使おうとすると重要な機能を失う。Issue tracker も必ず見ておくべきだ。本番用ファイルシステムにあるとは予想しなかった驚くようなものがいくつかある
ファイルシステムとして、まったく別の API があればいいのにと思う。どの OS のファイルシステム API サーフェスも、後方互換性のために閉じ込められた完全な混乱状態だ
発表を探そうとしたが見つけられなかった。Developer Summit で見たと思ったのだが、違ったのかもしれない
重複排除のことは忘れて、ZFS 圧縮を使うほうがはるかに費用対効果が高い
一般的に rsync 作業中でも大きな動画ファイルには圧縮を切ることが多い。圧縮はストレージ容量や転送には効果が薄いか、むしろまったくない一方で、RAM と CPU を消費するからだ。重複排除は Virtual Machine OS イメージに向いている。保存コストの大部分が繰り返し使われるベースイメージだからだ
汎用的な重複排除は理論上は良さそうに見えるものの、実際にはうまくいかないことが多い。IPFS は rsync のようにデータを重複排除しようとして 可変サイズのチャンクとローリングハッシュ を使うが、実際には差を生まず、無用に複雑さを増すだけ