[PERFORMANCE]ADVANCED

レンダーキャッシュを消した直後だけタイムラインがカクつく

`homenas`で4K素材を編集していて、DaVinci Resolveのキャッシュを掃除した直後だけタイムラインのスクラブがカクつくことに気づきました。犯人はasync destroyのバックログと、キャッシュ/プロキシファイルの高頻度な作成・削除によるプールの空き容量断片化でした。動画編集ワークロード特有のデータセット分離、recordsize設計、async destroyとの競合を避けるチューニングを整理します。

homenasで子どもの発表会の4K素材を編集していたときのことです。DaVinci Resolveのディスクキャッシュを溜め込みすぎたので「キャッシュをクリア」を押して数十GB分のキャッシュファイルを一括削除したところ、その直後だけタイムラインのスクラブ(ドラッグしてのコマ送り再生)が明らかにカクつくようになりました。数分待つと元に戻ります。最初はディスクの寿命でも疑いましたが、原因はもっと地味なところにありました。

動画編集は、大きな素材ファイルへのほぼシーケンシャルな読み込みと、プロキシ/レンダーキャッシュという大量の小さいファイルの激しい生成・削除が同居する、少し特殊なI/Oパターンです。この記事では、なぜキャッシュクリア直後にだけ再生が乱れるのか、そして動画編集用のZFSプールで意識しておくべきチューニングを整理します。

動画編集のI/Oは二重人格

編集作業中のI/Oは、性質の異なる2種類が同じプール上に同居しています。

対象I/Oパターン
元素材(カメラの4K/8Kファイル)ほぼシーケンシャル、大きいI/Oサイズ、書き込みは稀(読み込み中心)
プロキシ/レンダーキャッシュ/サムネイル小さいファイルが大量に頻繁に生成・削除される、ランダムI/O寄り

多くの人は元素材の置き場所しか意識せず、プロキシやキャッシュも同じデータセット、同じrecordsizeで運用しがちです。しかしこの2つは要求される特性が正反対で、同居させると片方の激しい churn がもう片方の再生体験を巻き込みます。

キャッシュクリアが引き金になる理由

数十GB分のキャッシュファイルを一括削除すると、zfs destroyrmそのものはすぐ終わりますが、実際のブロック解放処理は非同期解放(async destroy)としてバックグラウンドで継続します。これは以前書いた記事の通り、zpool get freeingで残量を確認できるあの仕組みです。

zpool get freeing,leaked homenas

このバックグラウンド処理は、space map(空き領域の管理構造)の更新という形でディスクへの書き込みを発生させます。タイムラインのスクラブは、プレビュー用のフレームをディスクから随時読み込む処理なので、このsync処理と読み込みが同じディスクI/O帯域を奪い合った瞬間に、体感できるレベルのカクつきとして表面化します。しかもキャッシュファイルは小さいファイルが大量にあるため、削除対象のブロック数そのものが多く、freeingのバックログが積み上がりやすいという性質もあります。

一括クリアの直後に編集を再開せず、zpool wait -t freeで解放の完了を待ってからスクラブに戻るだけでも、体感はかなり変わります。

# キャッシュクリアスクリプトの末尾に1行足すだけ
zpool wait -t free homenas

プールの空き容量が「虫食い」になる

もう一つの要因が、プールの空き容量断片化です。zpool listFRAG列で確認できます。

zpool list homenas

キャッシュ/プロキシファイルは生成と削除を高頻度で繰り返すため、ZFSのCopy-on-Write特性上、空き領域が虫食い状態になりやすいワークロードです。断片化が進むと、新しい書き込み(たとえば新しいプロキシファイルの生成)がまとまった連続領域を確保できず、複数の小さな空き領域に分散して書かれるようになります。HDDプールではこれがシーク時間の増加として直接効いてきて、再生のたびに参照するプロキシファイルの読み込みも遅くなりがちです。

FRAGが上がってきたら、キャッシュ/プロキシ用のデータセットだけでも定期的に作り直す(新しいデータセットを作ってプロキシを再生成する)運用が効きます。元素材のデータセットはほぼ読み込み専用なので、断片化の影響をあまり受けません。

データセットを分ける: recordsizeとキャッシュ設定を用途別に

homenasでは今、編集プロジェクトの下を用途別に3つのデータセットに分けています。

zfs create -o recordsize=1M tank/video/footage        # 元素材。大きいシーケンシャルI/O向け
zfs create -o recordsize=32K tank/video/proxy          # プロキシ/レンダーキャッシュ。小さいランダムI/O向け
zfs create -o recordsize=128K tank/video/project        # プロジェクトファイル・データベース

footageデータセットは書き込みがほぼ発生しないため、recordsizeを大きめ(1M程度)にしてシーケンシャル読み込み効率を優先します。proxyデータセットは逆に、小さいファイルが高頻度で生成・削除される前提でrecordsizeを小さくし、read-modify-writeの影響を抑えます。分けておくメリットは、proxy側の激しいchurnがfootage側のブロック配置やARCキャッシュの状態に影響しにくくなることです。

さらに、footageデータセットにはsecondarycache=all(通常のキャッシュ挙動)、頻繁に使い捨てるproxyデータセットにはsecondarycache=metadataを設定し、ディレクトリ探索(どのプロキシファイルがあるか)だけをキャッシュしてデータ本体でARCを圧迫しないようにしています。

zfs set secondarycache=metadata tank/video/proxy

メタデータ側の負荷にも目を向ける

プロキシ/キャッシュファイルが大量に生成・削除されるということは、そのぶんメタデータ(ブロックポインタなど)の生成・解放も大量に発生するということです。special vdevやARC/L2ARCの記事で触れた「メタデータ量はファイル数に強く依存する」という話がそのまま当てはまります。small vdevやL2ARCを導入している環境では、arc_summaryでメタデータのヒット率を定期的に確認し、キャッシュ用の小ファイルの奔流にメタデータキャッシュが振り回されていないか見ておくとよいと思います。

今の運用

  • 大きな一括削除(キャッシュクリア、古いプロジェクトの掃除)の直後はzpool wait -t freeを挟んでから作業を再開する
  • 元素材とプロキシ/キャッシュは別データセットに分け、recordsizeを用途別に設定する
  • キャッシュ/プロキシ用データセットはsecondarycache=metadataにして、データ本体のARC専有を防ぐ
  • zpool listFRAGを定期的に見て、上がってきたらキャッシュ用データセットだけ作り直す
  • メタデータ負荷はarc_summaryで継続的に確認する

homenasのスクラブは、これらを入れてからキャッシュクリア直後でも気にならないレベルになりました。動画編集は「読み込み中心の大きいファイル」と「書き捨ての大量の小さいファイル」が同じプール上でぶつかる、意外と特殊なワークロードだと実感しています。