qcow2の構造

サブクラスタの話に入る前に、qcow2のデータ構造について簡単に説明する。 qcow2のデータはクラスタと呼ばれる小さなブロックから構成される。 ゲストから仮想ディスクに対して読み書きすると、 そのバックエンドのqcow2ファイルに対してクラスタ単位でI/Oが実施される。 例えば、クラスタサイズが64KB(QEMUのデフォルト)である環境でゲストから4KB(メジャーなブロックサイズ)単位で読み書きすると、qcow2ファイルへのI/Oは64KB単位になる。

qcow2ファイルはスパースなので、ゲストから見えるサイズと比べて実ディスクのサイズは小さくなる。 この状況では、原理的にゲストからみた仮想ディスク内のオフセットとホスト上のqcow2ファイルからみたそれは異なる。 したがって、仮想ディスクとqcow2ファイルの間でオフセットの変換が必要になる。 qcow2では2段の変換テーブル(L1テーブルおよびL2テーブル)を用いてオフセットの変換を実現している。 それぞれのテーブルを見ていく。

L1テーブルはqcow2ファイルに唯1つだけ存在する。 このテーブルは十分に小さくて、例えば 1TB の qcow2 ファイルであってもわずか 16KB 程度におさまる。 したがって、QEMUではキャッシュの意味で常にこれをRAM上に保持している 1。 このテーブルのエントリは64bitのポインタで、L2テーブルを指している。

L2テーブルはqcow2ファイルへの書き込みが進むにつれて動的に生成され、1つのqcow2ファイルに複数存在する。 エントリのサイズは64bitで、qcow2ファイル内において実データを格納するデータクラスタへのポインタを格納している。

L2テーブルは仮想ディスクサイズによっては巨大なものになり得るので、 そのすべてをRAMに乗せることは難しい。 そのため、古いQEMUでゲストから仮想ディスクへI/Oを発行すると、 L2テーブル参照のために実ディスクへ対して余計なI/Oが発行されてしまう。 そこで、一部のL2テーブルエントリのみをRAMに載せようというアイデアが生まれた。 このアイデアをL2キャッシュとよび、そのサイズをL2キャッシュサイズと呼ぶ。 もしL2キャッシュをL2テーブル全体を覆うようなサイズまで大きくできれば、 L2テーブルは完全にRAM上に乗るので、余計なI/Oを防ぐことができる。 では、そのようなL2キャッシュサイズをどうやって決めれば良いだろうか。

ここで冒頭で触れたクラスタサイズの話に戻る。 クラスタ数は単純に ディスクサイズ / クラスタサイズ で計算できる。 L2テーブルのエントリは64bitなので、L2テーブル全体のサイズは(ディスクサイズ / クラスタサイズ) x 64bit で求められる。 つまり、(L2テーブルを完全にRAMに乗せるための)L2キャッシュサイズもこの式によって計算すれば良い。

クラスタサイズとL2キャッシュサイズの間にどんな関係があるのか詳しくみていく。 クラスタサイズを小さくすると、細かな単位でqcow2ファイルへI/Oが実施されるため余計なI/Oを防ぐことができるが、 L2キャッシュサイズを大きくする必要があるのでRAM使用量が増大してしまう。 また、クラスタ数が増えるとデータサイズに対するメタデータの比率が大きくなるため、 いくらでも小さくすれば良いというわけでは無い。 一方、クラスタサイズを大きくすると、L2キャッシュサイズを小さくできるのでRAM使用量を抑えられるが、 余計なI/Oが発生してしまう。

出典:Subcluster allocation for qcow2 images

Libvirtの対応状況

L2キャッシュサイズはqcow2ファイルのI/Oに多大な影響を及ぼすことがわかった。 もちろんこれはQEMUでゲスト起動時にパラメータとして外部から与えることができる2が、 Libvirtではどうだろうか。

実は2021年3月時点では、まだLibvirtからこのパラメータを操作できない。 2016年から Feature Request 3 が存在していて、いくつかパッチ 4 5 も提出されているが、 まだマージへ至っていない。何故なのか。Feature Request のコメントを読んでみる。

ざっと読んだ感想では、L2キャッシュサイズを絶対値で記述する方法を Libvirt としては受け入れたく無いのかなと感じた。 コメントで提案されているとおり絶対値の代わりにパーセントで表記できると、 確かにユーザにとって分かりやすいと思う。 もちろんこのパラメータを早く使えるようにしたいという意見は多数ある。

時は進み QEMU3.1でL2キャッシュサイズがデフォルトで32MBとなったので、デフォルトのクラスタサイズ(64KB)の環境では、 何もしなくても256GBのqcow2ファイルのL2テーブル全体をRAMへ乗せられることになった。 さらにQEMU 5.2で新たにサブクラスタと呼ばれる実装が入ったことで、L2テーブルのサイズを1/16に削減できるようになった。 つまり、256GB x 16 = 4TB の qcow2ファイルをカバーできる。 ほとんどのユースケースで仮想ディスクサイズは4TB以下だろうから、 デフォルト設定のままでも十分適切な挙動になるのではと思う。

サブクラスタや1/16という数字は何処から来たのか。さらに調査を進める。

サブクラスタの導入

qcow2ファイルのクラスタサイズとL2キャッシュサイズをどのように扱えば良いか数年にわたって議論されてきた。 その一つの改善として、QEMU5.2でqcow2のL2テーブルエントリの拡張であるサブクラスタを導入した 6。 1個クラスタを32個のサブクラスタに分割するというのがアイデアのベースになる。

繰り返しになるが、これまでL2テーブルエントリは単純にデータクラスタを指すポインタを格納していた。 サブクラスタの導入によって、 既存実装のL2テーブルエントリの直後に32個のサブクラスタの状態をビットアップを持つことで、 1/32の粒度でI/Oを実施できるようになった。 このビットアップは 64bit (L2テーブルエントリのサイズと同じ)に収まるので、 L2テーブルエントリ数の観点では2倍になる。 I/Oの粒度が既存実装と同じ場合、L2テーブルエントリ数は2 x 1/32 = 1/16 まで劇的に減少する。 つまり、(L2テーブルを完全にRAMに乗せるための)L2キャッシュサイズを1/16まで削減することができる。

またパフォーマンスの改善について見てみると、バッキングファイルを使った場合などクラスタの新規割り当てが発生する状況において大幅にパフォーマンスが向上した。

40GBのファイルをバッキングファイルで作り4KB Rand Writeを実施

出典:Subcluster allocation for qcow2 images

サブクラスタを利用するには、qemu-img コマンドの引数でextended_l2=onとするだけで良い。 メジャーなファイルシステムではブロックサイズが4KBであることが多いので、 クラスタサイズを4KB x 32サブクラスタ = 128KB とすると不要なCopy-on-Write領域がなくなるので相性が良い。 ただし、過去のQEMUと互換性はなく、QEMU5.2以上で作ったqcow2ファイルは、QEMU5.2 未満で扱えないので注意する。

$ qemu-img create -f qcow2 -o extended_l2=on,cluster_size=128k img.qcow2 1T

クラスタサイズ、L2キャッシュサイズと仮想ディスクサイズの関係

QEMU5.2のデフォルト値はクラスタサイズが64KB、L2キャッシュサイズが32MBであるから、 サブクラスタを有効(extended_l2=on)にしている状況では、4TBのqcow2ファイルまでは L2テーブルが完全にRAM上に乗ると思う(要検証)。 逆にそれ以上の仮想ディスクを扱う場合には、L2キャッシュサイズやクラスタサイズについて 都度考慮する必要がある。

(仮想ディスクサイズ) / (クラスタサイズ) /(サブクラスタ導入による削減率)x(L2テーブルエントリのサイズ)<= L2キャッシュサイズ
(仮想ディスクサイズ) / 64KB / 16 x 8Byte <= 32MB
(仮想ディスクサイズ) <= 32MB x 64KB x 16 / 8Byte
(仮想ディスクサイズ) <= 4TB

まとめ

長々と書いてきたが結論をまとめると、QEMU5.2以降では extended_l2=on としておけば 4TB以下の仮想ディスクではI/Oパフォーマンスやオーバヘッドについて特に問題ならないと予想できる(要検証)。 手元に4TBのディスクが無くベンチマークなどは行っていないので、データが揃い次第追記する。