状態: ドラフト (2026-10-16)、私たちのコード + 公式ドキュメント・ソース + トイコードの実測。ゴールデンVMの実測はまだありません。 ゴールデンディスクはコードとオペレーターツールまで導入されましたが、執筆時点のクラスターにはゴールデンPVCもそれに読み取る権限もまだ適用されていません(読み取り専用の照会で確認)。そのため、この記事におけるゴールデンVMの動作はKubeVirt v1.9.0・CDI v1.66.0のドキュメントとソースで確認したものであり、数値はノートPCで実行したトイコードとコンテナディスク(同じ原理)で測定したものです。作成ルール: 実測データが基本であり、測定できなかったものは「未計測」、ドキュメントから見つからなかったものは「未確認」と表示します。
学生VMが立ち上がるまで 4: PVC観点のオーバーレイ
目次
- 1. やりたいこと
- 2. 原理: ベースと書き込みレイヤー
- 3. KubeVirtのephemeralボリューム: これをPVC上で
- 4. PVCの目で見ると
- 5. なぜCSIクローンやスナップショットではないのか
- 6. セッションが終わったら: 書き込みレイヤーだけが消える
- 7. 代償と制限
- 参考
第3部は2つの方法の空白地帯で終わりました。コンテナディスクは20秒で立ち上がりますがルートが2.4GiBであり、DataVolumeはルートが大きいもののVMごとにイメージを新しく取り直すため初回起動が最大195秒かかります。この記事では、その間を埋める3番目の方法である、 ゴールデンPVC + 書き込みレイヤーを見ていきます。
1. やりたいこと
- 学生ごとに 大きなルート(10Giまたは20Gi)を与える。kubeadmのようにルートを多く消費する実習が容量不足で失敗しないように。
- 速く起動する。 VMを作成するたびに数GBを取得したりコピーしたりしない。
- 小さく消費する。 学生が30人いても、30人分の10Giを作成しない。
- セッションが終わったら痕跡が残らない。 次の学生にはクリーンなディスクが渡る。
これら4つを同時に実現する古くからの方法があります。 読み取り専用のベースの上に、人ごとの変更点だけを収める薄いレイヤーを重ねることです。コンテナイメージのレイヤー、仮想化のリンククローン、qcow2のバッキングファイルはすべて同じ考え方です。
2. 原理: ベースと書き込みレイヤー
qcow2ディスクイメージは バッキングファイルを指すことができます。 qemu-img create -f qcow2 -b <바탕> -F raw <쓰기 층> として作成すると、新しいイメージはベースと 異なるところだけ を記録します(qemu-imgドキュメント)。このときディスクは一定サイズのブロック(クラスター)に分かれ、ルールは2つです。
- 読み取り。 書き込みレイヤーにそのブロックがあれば書き込みレイヤーから、なければベースから読み取る(qcow2フォーマット仕様: 割り当てられていないクラスターはバッキングファイルから読み取る)。
- 書き込み。 最初に触れるブロックは、ベースからそのブロックを引っ張ってきて書き込みレイヤーに新しく配置し直して修正する。ベースは決して変更されない。
そのため、書き込みレイヤーは 空の状態で始まり、ゲストが書き込んだブロックの分だけ成長します。 ベースを変更しないため、複数のディスクが同じベースを一緒に指していても互いに混ざりません。
トイコードで測定してみる
ノートPCにqemu-imgがないため(新しくインストールしていません)、上記の2つのルールをPythonで模倣し、同じ条件下で測定しました。ベース512MiB(ランダムバイト)、ゲストが4KiBの書き込みをランダムな位置に1,000回行うタスクです。「丸ごとコピーしたあとに書き込み」と「空の書き込みレイヤーに書き込み」を5回ずつ実行した中央値です(0回目の試行はウォームアップとして除外しました)。両方式のディスク全体のハッシュが5回とも一致し、ゲストから見えるディスクが同じであることも確認しました。
| ブロック 64KiB | ブロック 2MiB | |
|---|---|---|
| 丸ごとコピー(512MiB) | 0.217秒 | 0.151秒 |
| 空の書き込みレイヤー作成 | 0.0002秒 | 0.0002秒 |
| 書き込み1,000回後の書き込みレイヤーサイズ | 59MiB | 504MiB(ベースの98%) |
環境: Apple M1 MaxノートPC、内蔵SSD、Python 3.12、標準ライブラリのみ。コードとローデータは docs/experiments/2026-10-16-cow-overlay-toy/ にあります。
数値から読み取るべきポイントは3つです。
- 作成は実質タダです。 空の書き込みレイヤーはディスクサイズに関係なく0.2ミリ秒です。コピーはディスクサイズに比例します。ここで0.2秒となっているのは同じSSD内のコピーであり、ベースがファイルキャッシュにあったためであるため、ネットワーク越しのストレージから10Giをコピーする時間をこの数値で語ることはできません(未計測)。計算上だけで言うと、1Gbps回線で10GiBは理論上約86秒です。
- 書き込みレイヤーは「書き込んだ量」ではなく「触れたブロック」の分だけ大きくなります。 実際に書き込んだのは3.9MiBですが、64KiBのブロックを945個触ったため59MiBになりました(約15倍)。分散した細かい書き込みはブロックサイズ単位で膨らみます。
- ブロックが大きく、書き込みが分散していると、レイヤーがベースと同じくらい大きくなります。 2MiBのブロックでは、512MiBのディスクが256ブロックしかなく、1,000回の書き込みがほぼすべてのブロックに触れました。書き込みレイヤーが薄く維持されるのは、 ゲストがディスクの小さな部分だけを修正するという前提があるからです。OSを丸ごとアップグレードするような実習であれば、この前提は崩れます。
実際のVMではどうでしょうか。同じ構造のコンテナディスクVM1つ(オペレーター用、9日目稼働中)を調べたところ、ベース(約596MiB)上の書き込みレイヤーは約1.10GiBでした(実測、2026-10-14)。9日間パッケージをインストールしてログを蓄積したVMであるため、1時間の実習セッションよりもはるかに多く書き込んだケースです。ゴールデンVMの起動直後・実習後の書き込みレイヤーサイズは 未計測です。
3. KubeVirtのephemeralボリューム: これをPVC上で
KubeVirtには、この構造をPVC上で実現するボリュームソースがすでに存在します。ユーザーガイドの ephemeral ボリュームです。要約すると次のとおりです。
- ネットワークボリューム(PVC)を 読み取り専用のベースとして使い、ベースは変更しない。
- すべての書き込みは ローカルの書き込みレイヤーイメージに向かう。
- 書き込みレイヤーはVMが起動するときに作成し、VMが停止または終了したら破棄する。
- ベースとしては現在のところPVCのみが使用可能。
TT Labバックエンドが作成するゴールデンVMのルートボリュームはこの1行です(backend/app/vms.py の vm_manifest).
volumes:
- name: root
ephemeral:
persistentVolumeClaim:
claimName: golden-ubuntu-24.04-10gi # OS 와 크기로 정해진 이름
readOnly: true # 빼지 않는다
KubeVirt v1.9.0のソースをたどると、この1行が以下の処理を行います。
- virt-controllerがvirt-launcherポッドを作成する際、このPVCをポッドボリュームとして そのまま(
readOnly転記し、書き込みレイヤー用の空ディレクトリ(emptyDir)をもう1つ追加します(pkg/virt-controller/services/rendervolumes.go). - ポッド内でPVCは
/var/run/kubevirt-private/vmi-disks/root/にアタッチされ、ベースはその中のdisk.img(raw)です。 - 起動時に
qemu-img create -f qcow2 -b <바탕 disk.img> -F raw <쓰기 층 disk.qcow2>で書き込みレイヤーを作成します(pkg/ephemeral-disk/ephemeral-disk.go)。書き込みレイヤーの仮想サイズはベースと同じです。 そのため、ベースのサイズがそのままゲストのルートのサイズになります。
コンテナディスクも同じ原理である点が興味深いところです。ベースがレジストリから取得したノードキャッシュなのか、ストレージクラスが提供したPVCなのかの違いだけです(第3部の図)。
4. PVCの目で見ると
「誰がどのPVCを持ち、何がいつ生成され消滅するのか」でまとめると次のようになります。
| ゴールデンPVC | 書き込みレイヤー | |
|---|---|---|
| 対象 | OS・サイズごとに1つ(名前は golden-<OS>-<크기>) |
VMごとに1つの disk.qcow2 ファイル |
| 場所 | 共有ストレージ(CSIドライバー)、RWX、ファイルシステムモード | virt-launcherポッドのemptyDir — ノードローカルであり、PVCではない |
| 作成者 | オペレーターがCDI DataVolumeで一度作成(レジストリ由来) | KubeVirtがVMを起動するたび |
| 所有者 | なし(VMは所有しない) | virt-launcherポッド |
| アタッチの形式 | すべてのVMが readOnly: true として |
そのVMのQEMUのみ |
| 削除のタイミング | オペレーターが削除するときのみ | VMが終了してポッドが削除されるとき |
いくつかの決定には理由があります。
- RWXで作成する。 学生VMは複数のノードに分散して配置され、同じゴールデンに 同時に アタッチする必要があります。RWOは1つのノードにしかアタッチできないため、バックエンドはRWOゴールデンを「使用不可」と判定します。ROXではなくRWXなのは、CDIが取得する際に書き込む必要があるからです。
- 本当の保護は
readOnly: trueです。 第3部で見たように、アクセスモードは書き込みを防ぎません。ベースがたった1回でも変更されると、その上のすべての書き込みレイヤーがおかしなディスクを参照するようになります。そのため、読み取り専用マウントを外さず、テストがマニフェストに読み書き可能としてアタッチされたゴールデンがないかチェックします。 - 一度も起動したことのないオリジナルでなければならない。 一度起動したディスクをゴールデンとして使用すると、SSHホストキー・machine-id・cloud-initの記録がすべての学生VMに同じように複製されてしまいます。そのため、ゴールデンはコンテナディスクと 同じイメージをそのまま取得し、PVCにオリジナルイメージのアドレスをアノテーションとして残してバックエンドが検証します。
- 即時バインドする。 第3部で見た「最初のユーザーまで遅延する」方式に従うと、最初の学生VMが到着したときに取得が始まります。ゴールデンはその前に取得し終わっている必要があるため、即時バインドを要求します。
- バックエンドは読み取りのみ行う。 バックエンドがゴールデンに対して行えることは、名前を知っているPVCを
getすることの1つだけです。作成や削除の権限がないため、回収コードのちょっとしたミスで全員のベースが消えてしまうことはありません(第6部)。
判定とフォールバック
バックエンドはVMを作成する前にゴールデンを読み取り、現在使用可能かどうかを判定します。存在しないPVCを指すVMは、KubeVirtがポッドを作成できず永遠に起動しないためです(v1.9.0の rendervolumes.go のPVC不在エラー)。
| 状況 | 結果 |
|---|---|
| ゴールデンPVCが存在しない・読み取り権限がない・APIエラー | コンテナディスクで起動する |
| 削除中である・まだバインドされていない | コンテナディスクで起動する |
| 1つのノードにしかアタッチされないボリュームである | コンテナディスクで起動する |
| CDIがまだ取得中である | コンテナディスクで起動する |
| 別のイメージから取得したゴールデンである | コンテナディスクで起動する |
| レッスンの値がタイポである | エラー(カリキュラムのテストが事前に検出する) |
フォールバックした際、学生の端末には「ルートが約2.4GiBと普段より小さい、 df -h / を確認せよ」という案内が表示されます。この案内は対話型シェルでのみ出力されます。採点エージェントもログインシェルでコマンドを実行するためであり、そうしないと案内が採点の出力に混ざってしまいます。なぜ DataVolume ではなくコンテナディスクに戻るのかというと、バックエンドに PVC を作成する権限を再び与えないためであり、195秒かかる最初の起動がゴールデン実習の準備時間の制限を超えてしまい、かえって失敗に終わってしまうからです。そのため ゴールデンは最適化であり、前提ではありません。 ゴールデンを有効にした実習も、コンテナディスクで合格しなければなりません。
5. なぜ CSI クローンやスナップショットではないのか
Kubernetes には、ボリュームを増殖させる公式な方法がすでに2つあります。
- CSI ボリュームクローン。 新しい PVC の
dataSourceに元の PVC を指定すると、空のボリュームの代わりに元のクローンが作成されます。クローンは元とは 独立したボリュームです。CSI ドライバがサポートしている必要があり、同じネームスペースであること、元とボリュームモードが同じであること、サイズが同じかそれ以上であることが条件です。ドライバがこの機能を実装しているかどうかはドライバによって異なります(Kubernetes ドキュメント)。 - ボリュームスナップショット。 特定時点のボリュームのスナップショットを取得し、そこから新しい PVC を作成します。CSI ドライバのスナップショット機能、スナップショットコントローラと CRD、および VolumeSnapshotClass が必要です(Kubernetes ドキュメント)。
CDI は DataVolume で PVC をクローンする際、これら2つを自動的に選択します。スナップショットが使用可能な場合は「スナップショット → リストア → スナップショット削除」で行い(スマートクローン)、CSI クローンが使用可能な場合はそれを使用し、両方とも使用できない場合は 元から対象へデータを流し込むホストコピーにフォールバックします(CDI クローン・StorageProfile ドキュメント)。この選択はストレージクラスの StorageProfile に cloneStrategy として残ります。
この記事を書いている日に TT Lab の共有ストレージクラスを確認したところ、スナップショット CRD はインストールされているものの VolumeSnapshotClass が一つもなく、StorageProfile のクローン戦略は copy(ホストコピー) でした(2026-10-16 取得)。そのため、このストレージで PVC クローンにより受講生ごとに 10Gi を作成すると、VM ごとに 10Gi がネットワーク経由で丸ごとコピーされることになります。スナップショットの道はありません。
ストレージが変わっても、書き込みレイヤーの利点は残ります。
| PVC クローン・スナップショットリストア | ゴールデン + 書き込みレイヤー | |
|---|---|---|
| VM ごとに作成される PVC | 1つ | なし |
| PVC クォータ(VM ネームスペース 12個)に対する | 同時 VM 数が PVC 数によって制限される | ゴールデンの数のみをカウントする |
| バックエンドに必要な権限 | PVC の作成・削除 | PVC の読み取り1つ |
| 作成にかかる時間 | ストレージのクローン実装に依存(ここでは完全コピー) | 空のファイル1つ |
| セッション終了時 | PVC を削除する必要がある(削除の失敗 = 容量リーク) | Pod とともに消滅する |
| 受講生の VM を停止して再起動した場合 | ディスクが残る | 書き込みレイヤーが消える |
最後の行が、この方式が払う代償です。実習セッションは一度開いて終了するため、これを許容しました。受講生が数日にわたって同じディスクを使い続けなければならない課題の場合、この方式は適していません。
6. セッション終了時:書き込みレイヤーのみが消える
ゴールデン VM マニフェストには dataVolumeTemplates がありません。VM はゴールデンを所有していないため、VM を削除してもゴールデンは残ります。ガベージコレクターが削除するのは VM と cloud-init Secret のみであり(第5回)、VM が削除されると virt-launcher Pod が終了し、その emptyDir、すなわち書き込みレイヤーが一緒に消えます。次の受講生に引き継がれるものは何もありません。
ゴールデン自体を変更する必要がある場合(イメージの更新)は、削除して同じ名前で再作成します。使用中の VM がある場合、Kubernetes の「使用中のストレージオブジェクトの保護」機能により、それらの VM が終了するまで PVC は残り、その間バックエンドは「削除中」を検知して新しい VM をコンテナディスクで起動します。名前にバージョンを付与して新しいゴールデンを隣に作成する方法(CDI の DataImportCron が行う方式)もありますが、そうするとバックエンドが名前を解決するために権限が1つ増え、受講生環境が検証されていないイメージに勝手に切り替わってしまいます。OSが6種類という規模では、手動で置き換える方がコストが低いと判断しました。
7. 代償と制限
- 書き込みレイヤーにサイズの上限はありません。 書き込みレイヤーが存在する emptyDir にサイズ制限がかけられていないため、上限はベースの仮想サイズのみです。受講生がルートをいっぱいまで使い果たすと、ノードのローカルディスクをその分消費します。ノードのディスク圧迫が発生すると、kubelet は多く使用している Pod から順に追い出しますが、それがまさに その受講生の VM です。そのため、サイズを 10Gi・20Gi のメニューとしてまとめています。VM の一時ストレージの要求・制限をスケジューラーに通知する方法はまだ確認していません。
- 使用していないブロックを読み込むたびに共有ストレージにアクセスします。 複数の VM が同じベースを同時に読み込む際の速度はストレージごとに異なり、 未計測です。
- ゴールデン VM が実際に起動するかどうかはまだ検証していません。 読み取り専用としてアタッチされたファイルシステム PVC をベースとして使用する書き込みパスにおいて、KubeVirt がディスクファイルの所有権を調整したりサイズを修正したりするためにベースへの書き込みを試みると、失敗する可能性があります。計測の最初の周回でこれを確認します。
- ゲストのルートサイズの計算に食い違いがあります。第3回で見たように、CDI ドキュメント通りであれば 10Gi ゴールデンのゲストディスクは 10Gi であり、設計ドキュメントでは約 9.4GiB が予想されていました。 未計測です。
計測項目と計測方法
計測スクリプト(scripts/measure_vm_disks.py)は、コンテナディスク・DataVolume・ゴールデンを同じノード上で1台ずつ作成、計測、削除します。開始前と各周回でノードの状態とメモリの空き状況を確認し、0周目はウォームアップとして別途記録し、1周目以降の中央値・最小値・最大値を算出します。計測する項目は、準備時間(リクエストからエージェントの準備完了まで)、VMI Running になるまでの時間、ゲストルートのサイズと空き容量、書き込みレイヤーの実際のサイズ(起動直後と 128MiB 書き込み後)、削除時間です。VM の作成と削除を伴うため、クラスターの書き込み権限を持つユーザーが実行します。
python3 deploy/vms/golden.py manifest --os ubuntu-24.04 --storage-class <공유 스토리지 클래스> # 골든 하나(적용은 사람)
python3 scripts/measure_vm_disks.py --preflight --node <노드> # 아무것도 만들지 않고 본다
python3 scripts/measure_vm_disks.py --node <노드> --rounds 4 # 예열 1 + 3바퀴
ゴールデンを実習で有効にするかどうかはこの結果に基づいて決定します。準備時間の中央値がコンテナディスクの1.5倍以内であり、ルートが 9GiB 以上であり、起動直後の書き込みレイヤーが数百 MB 以内であれば、kubeadm 系の実習から有効化します。
TT の一言。 30人に 10Gi ずつ割り当てていますが、実際に作成するのは 10Gi が1つと空ファイルが30個です。ただし「ベースは絶対に触らないこと」が命綱なので、
readOnly: trueは必ずアタッチしておき、書き込みはブロック単位で膨らむことも覚えておいてください!
参考
ドキュメントおよびソースはすべて 2026-10-16 に閲覧しました。
- KubeVirt ユーザーガイド、ディスクとボリューム(ephemeral): https://kubevirt.io/user-guide/storage/disks_and_volumes/
- KubeVirt v1.9.0 ソース:
pkg/ephemeral-disk/ephemeral-disk.go(ベースパスvmi-disks/<볼륨>/disk.img,qemu-img create -f qcow2 -b … -F raw) https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/ephemeral-disk/ephemeral-disk.go,pkg/virt-controller/services/rendervolumes.gohttps://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/virt-controller/services/rendervolumes.go - QEMU ドキュメント、qemu-img(バッキングファイル): https://www.qemu.org/docs/master/tools/qemu-img.html、qcow2 フォーマット仕様(割り当てられていないクラスターの読み取り): https://www.qemu.org/docs/master/interop/qcow2.html
- Kubernetes ドキュメント、CSI ボリュームクローン: https://kubernetes.io/docs/concepts/storage/volume-pvc-datasource/、ボリュームスナップショット: https://kubernetes.io/docs/concepts/storage/volume-snapshots/、永続ボリューム(使用中の保護): https://kubernetes.io/docs/concepts/storage/persistent-volumes/
- CDI ドキュメント、DataVolume クローン: https://github.com/kubevirt/containerized-data-importer/blob/main/doc/clone-datavolume.md、スマートクローン: https://github.com/kubevirt/containerized-data-importer/blob/main/doc/smart-clone.md、StorageProfile(
cloneStrategy): https://github.com/kubevirt/containerized-data-importer/blob/v1.66.0/doc/storageprofile.md - TT Lab のコードと記録:
backend/app/vms.py(golden_unusable_reason·choose_root_disk·vm_manifest),deploy/vms/golden.py,deploy/vms/README.md(ゴールデンディスクの章)、scripts/measure_vm_disks.py,docs/plans/2026-10-14-vm-golden-overlay.md - おもちゃの計測:
docs/experiments/2026-10-16-cow-overlay-toy/(cow_toy.py,output.jsonl)