TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

学生 VM 启动之前 4:从 PVC 视角的叠加 — 一个黄金磁盘,每个学生一个轻量写层

状态:草稿(2026-10-16),基于我们的代码 + 官方文档/源码 + 小玩具实际测量。尚未对黄金 VM 进行实际测量。 黄金磁盘虽然已经融入了代码和运维工具中,但在撰写本文时,集群中既没有应用黄金 PVC,也没有应用读取它的权限(通过只读查询确认)。因此,本文中的黄金 VM 运行情况是通过查阅 KubeVirt v1.9.0 和 CDI v1.66.0 的文档与源码确认的,相关数据则是在笔记本电脑上运行的小玩具和容器磁盘(基于相同原理)中测量的。编写规则:实测数据为基础,未能测量的数据标记为“未实测”,在文档中未找到的内容标记为“未确认”。

学生 VM 启动之前 4:从 PVC 视角的叠加

目录

第 3 篇以两种途径的空白告终。容器磁盘虽然能在 20 秒内启动,但根目录有 2.4GiB;DataVolume 的根目录虽然很大,但每个 VM 都要重新拉取镜像,首次启动时间长达 195 秒。本文将探讨填补这一空白的第三条途径: 黄金 PVC + 写层。

1. 想要实现的目标

有一种历史悠久的方法可以同时实现这四个目标: 在一个只读基础之上,为每个人叠加一层只包含更改部分的轻量层。容器镜像的层、虚拟化的链接克隆(linked clone)以及 qcow2 的后备文件(backing file)都遵循相同的理念。

2. 原理:基础与写层

골든 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)도 함께 간다.
图:在一个黄金 PVC 之上,每个学生一个轻量写层

qcow2 磁盘镜像可以指向一个 后备文件。 qemu-img create -f qcow2 -b <바탕> -F raw <쓰기 층> 创建后,新镜像只会记录与基础 不同的部分 (qemu-img 文档)。此时,磁盘被划分为固定大小的块(cluster),规则只有两条:

因此,写层 从空开始,并且只随着客体(guest)写入的块的大小而增长。 由于基础不会被修改,因此即使多个磁盘同时指向同一个基础,它们也不会相互干扰。

通过小玩具进行测量

由于笔记本电脑上没有 qemu-img(没有重新下载),我们用 Python 模拟了上述两条规则,并在相同条件下进行了测量。基础为 512MiB(随机字节),客体对随机位置进行 1,000 次 4KiB 的写入。这是“整体复制后写入”与“写入空的写层”各运行五次的中位数(第 0 轮作为预热已排除)。通过确认五轮中两种方式的磁盘整体哈希值完全相同,也验证了客体看到的磁盘是完全一样的。

块大小 64KiB 块大小 2MiB
整体复制 (512MiB) 0.217秒 0.151秒
创建空写层 0.0002秒 0.0002秒
1000次写入后的写层大小 59MiB 504MiB (基础的 98%)

环境:Apple M1 Max 笔记本电脑,内置 SSD,Python 3.12,仅使用标准库。代码和原始数据位于 docs/experiments/2026-10-16-cow-overlay-toy/ 中。

从这些数字中可以得出三个结论:

  1. 创建实际上是免费的。 无论磁盘大小如何,空的写层只需 0.2 毫秒。复制则与磁盘大小成正比。这里的 0.2 秒是因为在同一个 SSD 内复制且基础在文件缓存中,因此无法用这个数字来代表从网络另一端的存储中复制 10Gi 的时间(未实测)。粗略计算一下,在 1Gbps 的线路上,10GiB 理论上大约需要 86 秒。
  2. 写层的增长取决于“触及的块数”,而不是“写入的量”。 实际写入的数据为 3.9MiB,但由于触及了 945 个 64KiB 的块,导致大小达到了 59MiB(约 15 倍)。分散的小写入会按块大小导致体积膨胀。
  3. 当块很大且写入分散时,写层会变得和基础一样大。 在 2MiB 的块大小下,512MiB 的磁盘只有 256 个块,因此 1,000 次写入触及了几乎所有的块。写层之所以能够保持轻量,是建立在 客体只修改磁盘的一小部分这一假设之上的。如果是升级整个操作系统之类的实验,这个假设就会被打破。

在实际的 VM 中情况会怎样呢?检查了一个具有相同结构的容器磁盘 VM(用于运维,已运行 9 天),发现基础(约 596MiB)之上的写层约为 1.10GiB(实测,2026-10-14)。由于这是一个运行了 9 天、安装了软件包并积累了日志的 VM,其写入量远大于一个小时的实验会话。黄金 VM 在启动后以及实验结束后的写层大小目前处于 未实测状态。

3. KubeVirt 的 ephemeral 卷:在 PVC 之上实现这一点

KubeVirt 中已经存在一个能够基于 PVC 实现此架构的卷源。即用户指南中的 ephemeral 卷。总结如下:

TT Lab 后端创建的黄金 VM 的根卷只有这一行(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 Pod 时,将此 PVC 原封不动地(readOnly 复制为 Pod 卷,并附加另一个用于存放写层的空目录 (emptyDir)(pkg/virt-controller/services/rendervolumes.go).
  2. 在 Pod 内部,PVC 挂载到 /var/run/kubevirt-private/vmi-disks/root/ ,基础是其中的 disk.img(raw)。
  3. 启动时使用 qemu-img create -f qcow2 -b <바탕 disk.img> -F raw <쓰기 층 disk.qcow2> 创建写层(pkg/ephemeral-disk/ephemeral-disk.go)。写层的虚拟大小与基础相同。 因此,基础的大小就是客体根目录的大小。

有趣的是,容器磁盘也遵循相同的原理。唯一的区别仅在于基础是来自镜像仓库的节点缓存,还是由存储类(StorageClass)提供的 PVC(第 3 篇图示)。

4. 从 PVC 的视角来看

如果按“谁拥有哪个 PVC、什么东西在何时创建和消失”来总结,如下所示:

黄金 PVC 写层
内容 每个 OS/大小一个(名称 golden-<OS>-<크기>) 每个 VM 一个 disk.qcow2 文件
位置 共享存储(CSI 驱动程序)、RWX、文件系统模式 virt-launcher Pod 的 emptyDir — 节点本地,不是 PVC
谁创建 运维人员通过 CDI DataVolume 创建一次(镜像仓库源) KubeVirt 每次启动 VM 时
谁拥有 无(VM 不拥有它) virt-launcher Pod
挂载形态 所有 VM 以 readOnly: true 挂载 仅该 VM 的 QEMU 挂载
何时消失 仅由运维人员删除时 当 VM 结束且 Pod 被删除时

做出某些决定是有原因的。

判定与替代

后端在创建 VM 之前会读取黄金镜像,以判定其当前是否可用。因为指向不存在的 PVC 的 VM 会导致 KubeVirt 无法创建 Pod 从而永远无法启动(v1.9.0 的 rendervolumes.go 中的 PVC 不存在错误)。

情况 结果
黄金 PVC 不存在 · 没有读取权限 · API 错误 使用容器磁盘打开
正在删除中 · 尚未绑定 使用容器磁盘打开
仅挂载到一个节点的卷 使用容器磁盘打开
CDI 仍在导入中 使用容器磁盘打开
从其他镜像导入的黄金镜像 使用容器磁盘打开
课程的值拼写错误 错误(课程测试会先捕获到这一点)

当被替代时,学生的终端显示:“根目录约为 2.4GiB,比平时小, df -h / 会提示“请通过 ... 进行确认”。该提示仅在交互式 Shell 中打印。因为评分代理也是通过登录 Shell 运行命令的,否则提示会混入评分输出中。之所以不使用 DataVolume 而是回退到容器磁盘,是为了不重新赋予后端创建 PVC 的权限,并且因为 195 秒的首次启动时间超出了黄金实训的准备时间上限,反而会导致失败。因此, 黄金镜像是优化项,而非前提条件。 开启黄金镜像的实训也必须能够在容器磁盘上通过。

5. 为什么不使用 CSI 克隆或快照

Kubernetes 中本来就有两种扩充卷的官方方法。

当 CDI 使用 DataVolume 复制 PVC 时,会自动在这两者之间进行选择。如果可以使用快照,则采用“快照 → 还原 → 删除快照”的方式(smart clone);如果可以使用 CSI 克隆,则采用该方式;如果两者都不行,则会回退到 将数据从源流向目标的主机复制(CDI 复制·StorageProfile 文档)。这种选择会作为 cloneStrategy 记录在存储类的 StorageProfile 中。

학생 셋에게 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 확인).
图:为三个学生提供 10Gi 根目录的三种方法

在撰写本文当天查看 TT Lab 的共享存储类时,发现虽然安装了快照 CRD,但 没有任何 VolumeSnapshotClass,且 StorageProfile 的复制策略是 copy(主机复制) (2026-10-16 查询)。因此,如果在此存储库中通过 PVC 克隆为每个学生创建 10Gi,则每个虚拟机都会通过网络完整复制 10Gi。根本没有快照这条路可走。

即使存储发生更改,写入层的优势依然存在。

PVC 克隆·快照还原 黄金镜像 + 写入层
每个虚拟机生成的 PVC 一个 无
对于 PVC 配额(每个虚拟机命名空间 12 个) 并发虚拟机数量受限于 PVC 数量 仅计算黄金镜像的数量
后端所需的权限 创建和删除 PVC 一个 PVC 读取权限
创建所需的时间 取决于存储的复制实现(此处为完整复制) 一个空文件
会话结束时 必须删除 PVC(删除失败 = 空间泄漏) 随 Pod 一起消失
关闭并重新启动学生虚拟机时 磁盘保留 写入层消失

最后一行是这种方法所付出的代价。由于实训会话只开启一次就会结束,所以我们接受了这一点。如果作业要求学生连续几天继续使用同一个磁盘,那么这种方法就不太合适。

6. 会话结束时:只有写入层消失

黄金虚拟机清单中没有 dataVolumeTemplates 。由于虚拟机不拥有黄金镜像,因此即使删除虚拟机,黄金镜像也会保留。回收器删除的只是虚拟机和 cloud-init Secret(第 5 部分),虚拟机删除后,virt-launcher Pod 随之结束,其 emptyDir(即写入层)也会一同消失。没有任何内容会传递给下一个学生。

当需要更换黄金镜像本身(更新镜像)时,将其删除并使用相同名称重新创建。如果有正在使用的虚拟机,由于 Kubernetes 的“正在使用的存储对象保护”机制,PVC 会一直保留到这些虚拟机结束运行,在此期间后端会看到“正在删除”并使用容器磁盘打开新的虚拟机。虽然也可以通过在名称中加入版本号在旁边创建新的黄金镜像(CDI 的 DataImportCron 所采用的方式),但这会增加后端解析名称所需的权限,并且学生环境会自动变成未经测试的镜像。考虑到只有六个操作系统规模,我们认为手动更换成本最低。

7. 代价与限制

测量对象与测量方法

测量脚本(scripts/measure_vm_disks.py)会在同一节点上一台接一台地创建容器磁盘、DataVolume 和黄金镜像,进行测量并将其删除。在开始前以及每轮测试中检查节点状态和剩余内存,第 0 轮作为预热单独记录,从第 1 轮开始计算中位数、最小值和最大值。测量的项目包括:准备时间(从请求到代理准备就绪)、达到 VMI Running 的时间、客户机根目录的大小与剩余空间、写入层的实际大小(启动后立即以及写入 128MiB 后)以及删除时间。由于涉及创建和删除虚拟机,因此由拥有集群写入权限的人员运行。

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 寄语。 为三十个人分别提供 10Gi,但实际创建的只有一个 10Gi 和三十个空文件。不过要记住,“绝对不要碰底层”是生死攸关的原则,因此 readOnly: true 务必附加好,同时也要记住写入会按块进行扩展!

参考资料

文档和源码均于 2026-10-16 查阅。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论