TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Student VM Launch Part 4: Overlay from a PVC Perspective — One Golden Disk, a Thin Write Layer per Student

Status: Draft (2026-10-16), our code + official docs & source + toy model measurements. Golden VM measurements not yet available. The golden disk includes code and operator tools, but on the day this post was written, neither the golden PVC nor the permission to read it had been applied to the cluster yet (verified via read-only inspection). Therefore, the golden VM behavior in this post is verified through KubeVirt v1.9.0 and CDI v1.66.0 documentation and source code, and the numbers were measured from a toy model and container disks (same principle) run on a laptop. Writing rules: measured data is the default, unmeasured items are marked as "Unmeasured", and items not found in the documentation are marked as "Unverified".

Student VM Launch Part 4: Overlay from a PVC Perspective

Table of Contents

Part 3ended with the void between two approaches. Container disks boot in 20 seconds but have a 2.4GiB root, while DataVolumes have a large root but fetch a new image for every VM, pushing the first boot up to 195 seconds. This post explores the third path bridging that gap: Golden PVC + Write Layer.

1. What We Want to Achieve

There is an old way to achieve all four at once: placing a thin layer containing only the changed parts per person on top of a single read-only base. Container image layers, virtualization linked clones, and qcow2 backing files all share the same idea.

2. Principle: Base and Write Layer

골든 PVC 하나 위에 학생마다 얇은 쓰기 층 ① 디스크 한 장은 두 겹이다 (칸 = 클러스터) 게스트가 보는 /dev/vda 쓰기 층 disk.qcow2 바탕(골든) disk.img 쓴 칸은 쓰기 층에서, 안 쓴 칸은 바탕에서 읽는다. 처음 쓰는 칸은 바탕에서 통째로 끌어올린 뒤 고친다. ② 여러 학생이 하나의 골든을 나눠 읽는다 노드 1 · virt-launcher 학생 A의 VM QEMU 쓰기 층 emptyDir 노드 1 · virt-launcher 학생 B의 VM QEMU 쓰기 층 emptyDir 노드 2 · virt-launcher 학생 C의 VM QEMU 쓰기 층 emptyDir 안 쓴 칸 읽기 골든 PVC — 공유 스토리지(CSI 드라이버) · RWX disk.img(raw) · 모든 VM 이 readOnly: true 로 붙는다 VM 이 소유하지 않는다 → VM 을 지워도 남는다 세션이 끝나면 파드가 사라지면서 쓰기 층(emptyDir)도 함께 간다.
Figure: A single golden PVC with a thin write layer per student

A qcow2 disk image can point to a backing file. When created with qemu-img create -f qcow2 -b <바탕> -F raw <쓰기 층> , the new image records only the differences from the base (qemu-img documentation). Here, the disk is divided into fixed-size blocks (clusters), and the rules are two lines:

Thus, the write layer starts empty and grows only by the blocks written by the guest. Because the base is not modified, multiple disks can point to the same base without interfering with each other.

Measuring with a Toy Model

Since qemu-img was not available on the laptop (we didn't install it), we simulated the two rules above in Python and measured them under the same conditions. The base is 512MiB (random bytes), and the guest performs 1,000 random 4KiB writes. These are the medians of five runs each for "Copy entirely then write" and "Write to an empty write layer" (run 0 was excluded as warmup). The total disk hashes for both methods matched across all five runs, confirming that the disk seen by the guest is identical.

64KiB Block 2MiB Block
Full Copy (512MiB) 0.217s 0.151s
Create Empty Write Layer 0.0002s 0.0002s
Write Layer Size After 1,000 Writes 59MiB 504MiB (98% of base)

Environment: Apple M1 Max laptop, internal SSD, Python 3.12, standard library only. Code and raw data are available in docs/experiments/2026-10-16-cow-overlay-toy/ .

There are three takeaways from the numbers.

  1. Creation is practically free. An empty write layer takes 0.2 milliseconds regardless of disk size. Copying is proportional to disk size. The 0.2-second copy time here occurred because it was within the same SSD and the base was in the file cache, so the time to copy 10Gi from storage across the network cannot be represented by this figure (unmeasured). A simple calculation shows that 10GiB over a 1Gbps line theoretically takes about 86 seconds.
  2. The write layer grows by the touched blocks
  3. , not the written amount. Although only 3.9MiB was actually written, 945 blocks of 64KiB were touched, resulting in 59MiB (about 15x). Scattered small writes get bloated to the block size. If blocks are large and writes are scattered, the layer grows as large as the base. With 2MiB blocks, a 512MiB disk has only 256 blocks, so 1,000 writes touched almost every block. Maintaining a thin write layer relies on the assumption thatthe guest modifies only a small part of the disk

. This assumption breaks down during labs that involve upgrading the entire OS. What about in an actual VM? Inspecting a container disk VM with the same structure (for operators, running for 9 days), the write layer on top of the base (approx. 596MiB) was about 1.10GiB (measured, 2026-10-14). Since this VM had packages installed and logs accumulated over 9 days, it represents much heavier usage than a one-hour lab session. The write layer sizes immediately after boot and after the lab for a golden VM are unmeasured.

3. KubeVirt's Ephemeral Volume: Doing This on Top of a PVC

KubeVirt already has a volume source that provides this structure on top of a PVC. It is the ephemeral volume in the user guide. In summary:

The root volume of the golden VM created by the TT Lab backend is just this single line (backend/app/vms.py of vm_manifest).

volumes:
  - name: root
    ephemeral:
      persistentVolumeClaim:
        claimName: golden-ubuntu-24.04-10gi   # OS 와 크기로 정해진 이름
        readOnly: true                        # 빼지 않는다

Tracing the KubeVirt v1.9.0 source code, this single line performs the following:

  1. When virt-controller creates the virt-launcher pod, it includes this PVC as a pod volume as is(readOnly , and attaches an additional empty directory (emptyDir) for the write layer (pkg/virt-controller/services/rendervolumes.go).
  2. Inside the pod, the PVC is mounted at /var/run/kubevirt-private/vmi-disks/root/ , and the base within it is disk.img(raw).
  3. At startup, qemu-img create -f qcow2 -b <바탕 disk.img> -F raw <쓰기 층 disk.qcow2> creates the write layer (pkg/ephemeral-disk/ephemeral-disk.go). The virtual size of the write layer is the same as the base. Therefore, the size of the base becomes the size of the guest root.

It is interesting that container disks follow the same principle. The only difference is whether the base is a node cache pulled from a registry or a PVC provisioned by a StorageClass (Figure in Part 3).

4. From the perspective of PVCs

Summarized as "who holds which PVC, and what is created and destroyed when," it looks like this:

Golden PVC Write Layer
What One per OS/size (name golden-<OS>-<크기>) One disk.qcow2 file per VM
Where Shared storage (CSI driver), RWX, filesystem mode emptyDir of virt-launcher pod — Node-local, not a PVC
Who creates it Administrator once via CDI DataVolume (registry source) KubeVirt every time a VM is powered on
Who owns it None (not owned by the VM) virt-launcher pod
Attachment style All VMs readOnly: true as When does only the QEMU of that VM disappear?
Only when the operator deletes it. Only when the operator deletes it. When the VM finishes and the Pod is deleted.

There are reasons behind certain decisions.

Evaluation and Fallback

Before creating a VM, the backend reads the golden image to evaluate whether it can be used right now. This is because a VM pointing to a non-existent PVC will never start as KubeVirt fails to create the Pod (v1.9.0 rendervolumes.go 's missing PVC error).

Situation Result
Golden PVC does not exist · No read permission · API error Open as container disk
Being deleted · Not bound yet Open as container disk
Volume attaches to only one node Open as container disk
CDI is still importing Open as container disk
Golden imported from a different image Open as container disk
Lesson value is a typo Error(Curriculum exam catches this first)

When fallback occurs, the student terminal displays "root is about 2.4GiB, smaller than usual, df -h / is displayed. The prompt is printed only in the interactive shell. Since the grading agent also runs commands via a login shell, otherwise the prompt would get mixed into the grading output. The reason it falls back to a container disk instead of a DataVolume is to avoid granting the backend permissions to create PVCs again, and because the first boot taking 195 seconds would exceed the preparation time limit of the golden lab, resulting in a failure instead. Therefore, golden is an optimization, not a prerequisite. Labs with golden enabled must also pass on a container disk.

5. Why Not CSI Cloning or Snapshots

Kubernetes already has two official ways to scale up volumes.

CDI automatically chooses between these two when cloning a PVC via a DataVolume. If snapshots are available, it uses "snapshot → restore → delete snapshot" (smart clone); if CSI cloning is available, it uses that; if neither is available, it falls back to host copy, which streams data from the source to the target(CDI cloning and StorageProfile documentation). This choice is recorded in the StorageProfile of the storage class as cloneStrategy .

학생 셋에게 10Gi 루트를 주는 세 방법 가. PVC 복제(CSI 클론 · 호스트 복사) 골든 A 10Gi B 10Gi C 10Gi 만들 때: 저장소가 클론을 하면 빠르고, 못 하면 10Gi × 3 을 복사 공간: 저장소 구현에 따라 0 ~ 30Gi · VM 마다 PVC 하나 나. 스냅샷에서 복원 골든 스냅샷 A 10Gi B 10Gi C 10Gi 필요: CSI 드라이버의 스냅샷 기능 + VolumeSnapshotClass 공간·시간은 저장소가 복원을 어떻게 하느냐에 달렸다 다. 골든 + 쓰기 층(이 시리즈의 방법) 골든 A 쓴 만큼 B 쓴 만큼 C 쓴 만큼 만들 때: 빈 파일 하나 · 공간: 골든 10Gi + 학생마다 쓴 만큼 대가: 쓰기 층이 노드 로컬이라 VM 을 끄면 사라진다 글 쓰는 날 TT Lab 의 공유 스토리지: 스냅샷 클래스 없음, CDI 복제 전략 copy. 나는 쓸 수 없고, 가는 네트워크로 통째로 복사한다(2026-10-16 확인).
Figure: Three ways to give three students a 10Gi root

Checking TT Lab's shared storage class on the day of writing, I found that the snapshot CRD was installed, but there was not a single VolumeSnapshotClass, and the cloning strategy in the StorageProfile was copy(host copy) (checked on 2026-10-16). Therefore, if 10Gi per student were created using PVC cloning in this storage backend, each VM would copy a full 10Gi over the network. The snapshot route is completely unavailable.

Even if the storage changes, the writing layer retains its advantages.

PVC Clone / Snapshot Restore Golden + Writing Layer
PVCs generated per VM One None
Regarding PVC quotas (12 VM namespaces) Concurrent VM count is bound to the number of PVCs Counts only the number of goldens
Required backend permissions Create and delete PVCs Read a single PVC
Time taken to create Depends on the storage clone implementation (full copy here) A single empty file
When the session ends Must delete the PVC (deletion failure = space leak) Disappears along with the Pod
When student VMs are turned off and on again Disk persists Writing layer disappears

The last row is the price paid by this approach. I accepted it because a lab session runs once and then ends. This approach is not suitable for assignments where students need to continue using the same disk over several days.

6. When the Session Ends: Only the Writing Layer Disappears

The golden VM manifest does not contain dataVolumeTemplates . Since the VM does not own the golden, deleting the VM leaves the golden intact. The garbage collector only deletes the VM and the cloud-init Secret (Part 5), and when the VM is deleted, the virt-launcher Pod terminates along with its emptyDir—that is, the writing layer. Nothing is passed on to the next student.

When the golden itself needs to be changed (updating the image), it is deleted and recreated with the same name. If there are VMs using it, Kubernetes' "Storage Object in Use Protection" keeps the PVCs until those VMs finish, during which time the backend sees "Terminating" and opens new VMs with container disks. There is also a method of putting a version in the name to create a new golden alongside the old one (the way CDI's DataImportCron works), but that requires the backend to resolve names with an extra permission, and automatically changes the student environment to an untested image. With a scale of six OSes, we decided that manual replacement was cheaper.

7. Costs and Limitations

What to Measure and How

The measurement script (scripts/measure_vm_disks.py) creates, measures, and deletes container disks, DataVolumes, and goldens one by one on the same node. It checks node status and free memory before starting and in each round, records round 0 separately as a warmup, and calculates the median, minimum, and maximum starting from round 1. It measures preparation time (from request to agent readiness), time until VMI Running, guest root size and free space, actual size of the writing layer (immediately after boot and after writing 128MiB), and deletion time. Since this involves creating and deleting VMs, it is run by someone with cluster write permissions.

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바퀴

Whether to enable golden for labs will be decided based on these results. If the median preparation time is within 1.5 times that of the container disk, the root is 9GiB or more, and the writing layer immediately after boot is within a few hundred MB, we will enable it for labs like kubeadm.

A Word from TT. We distribute 10Gi to thirty people, but what we actually create is a single 10Gi and thirty empty files. Instead, remember that "never touching the base" is vital, so make sure to attach readOnly: true , and also keep in mind that writes expand block by block!

References

All documents and sources were accessed on 2026-10-16.

Sign in to like

Comments

No comments yet.

Sign in to leave a comment