TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

学生虚拟机启动指南 1:从学生、后端、KubeVirt、存储四层看整体架构

状态:草稿(2026-10-16),基于我们的代码 + 官方文档 + 部分实测。 动作说明完全遵循 TT Lab 后端代码(backend/app/vms.py, backend/app/main.py)与设计文档。版本号为撰写本文当日读取并确认的值(Kubernetes v1.34.10、KubeVirt v1.9.0、CDI v1.66.0)。时间数据是在制作实验时测得的记录,并同时标注了样本量与日期。 编写规则:以实测数据为基础,未能测得的数据标记为“未实测”,在文档中未找到的内容标记为“未确认”。

学生虚拟机启动指南 1:从四层看整体架构

目录

1. 为什么不用 Pod 而用虚拟机

TT Lab 的实验大多数在单个 Kubernetes Pod 中运行。Pod 几秒钟即可启动,只需更换镜像环境即可改变,删除后也不会留下任何痕迹。然而,有一些实验是 Pod 无论如何也无法模拟的。

因此,只有这类实验才会通过 虚拟机来提供。学生在虚拟机内部拥有真正的 root 权限,可以随心所欲地操作 systemd、内核和磁盘。不过本系列的主题在于:防止这个 root 权限逃逸出虚拟机,并确保用完的虚拟机能够被确实回收。虚拟机是通过 Kubernetes 上的 KubeVirt 启动的,因为它们使用与实验 Pod 相同的集群、相同的调度器和相同的存储。

2. 四层架构

本系列故意只关注四个层级: 学生(浏览器)、后端(Web Pod)、KubeVirt、Kubernetes 存储。网关、镜像仓库、监控等夹在中间的组件已从图中剔除,因为“分配虚拟机”这一工作的核心骨架正是这四个部分。

실습 VM 이 뜨기까지 — 네 층, 열 걸음 번호 순서로 진행한다. 실선은 만드는 길, 점선은 읽거나 잇는 길. 학생 · 브라우저 ① 「실습 시작」 누름 ⑩ 웹 터미널 — root 셸 백엔드 · 웹 파드 ② 자리 확인 1인 1세션 · VM 8대 상한 ③ cloud-init Secret 채점 에이전트 · VM별 토큰 ④ 루트 디스크 고르기 골든 PVC 읽기, 안 되면 대체 ⑤ VirtualMachine 만들기 세션 라벨 · 환경 라벨 ⑨ 콘솔 중계 웹소켓 ↔ console 서브리소스 ⑧ 준비 확인 VMI Running → 에이전트 응답 VM 에는 exec 가 없다. 준비 확인·채점은 게스트 안 에이전트(8899 포트)로 한다. KubeVirt ⑥ 파드를 만들고 QEMU 를 띄운다 VM 원하는 상태 VMI 실행 하나 virt-launcher 파드 하나 QEMU·KVM 게스트 OS 8899 쿠버네티스 스토리지 ⑦ 볼륨 붙이기 골든 PVC OS 마다 하나 · 공유 스토리지 RWX · 읽기 전용으로 붙임 운영자가 한 번 만든다 쓰기 층 disk.qcow2 노드 로컬 emptyDir VM 마다 하나 VM 이 끝나면 함께 사라짐 골든이 없거나 못 쓰면 ④ 가 컨테이너 디스크로 대체한다(3·4편). 회수는 5편.
图:实验虚拟机启动前的四层架构与十个步骤

将每一层的作用概括为一句话如下:

层级 职责 在本系列中
学生 点击按钮,通过 Web 终端使用虚拟机内的 shell 第 1 篇
后端 检查空闲资源、准备注入虚拟机的内容、选择根磁盘、创建虚拟机对象。使用完毕后进行清理 第 1、5、6 篇
KubeVirt 根据 VM 对象创建 Pod,并在该 Pod 内通过 QEMU 启动 Guest 操作系统 第 2 篇
存储 挂载将成为 Guest 磁盘的卷。多个虚拟机共享读取一个 Golden PVC 第 3、4 篇

3. 十个步骤

① “开始实验”

当学生点击按钮时,浏览器发送 POST /api/sessions 。如果课程是 runtime: vm ,则此会话将创建虚拟机而不是 Pod。会话记录从一开始就会包含 runtime 。因为回收器稍后通过这一个值来决定“应该删除什么”。如果在此处错误判断了类型,系统会记录 Pod 已删除,而虚拟机则会继续占用 4GiB 的内存。

② 检查空闲资源

主要检查两个方面。首先是 一人一会话机制。如果已经打开了其他实验,系统会提示当前已开启的内容,而不会新建。由于在用户级锁内部同时进行了检查和记录,因此即使连续点击两次按钮也只会产生一个会话。

另一个是 **同时运行的虚拟机数量上限(默认 8 台)**。这个数字不是从数据库中获取,而是直接 向集群查询 得出的。因为数据库无法统计被回收器漏掉的“幽灵虚拟机”,而真正占用节点内存的是集群侧。如果触及上限,系统将返回提示:“当前已有 8 个虚拟机实验处于打开状态,没有空余名额”。为什么限制为 8 将在第 5 篇中探讨。

③ cloud-init Secret

虚拟机不会提前烘焙镜像,而是在公共云镜像的基础上,通过 首次启动时的 cloud-init 注入所需的内容。注入的内容主要有三项。

不把这些内容直接写入虚拟机清单而是放入 Secret 中,是有其实际测量依据的。KubeVirt 对内联 cloud-init 的大小限制为 2048 字节,但如果加上评分代理,则会超过 7000 字节。因此,所有实验虚拟机都统一走 Secret 这条路径。如果根据大小分流,就会产生只在加入代理那一瞬间才会崩溃的隐蔽路径。

④ 选择根磁盘

这里是本系列的核心。如果课程指定了 vm_disk: "golden" ,后端会读取运维人员预先创建的 Golden PVC (get 之一),并判定当前是否可用。如果可用,则将该 PVC 用作 只读底层,并为每个虚拟机叠加一个轻量的 写时复制层。如果不存在或不可用(缺失、无权限、正在获取中、单节点卷、不同镜像、正在删除),则不会报错,而是直接用默认磁盘(容器磁盘)启动。因为 Golden 是一种优化手段而非强制前提。磁盘的三种分支将在第 3 篇中介绍,Golden 与写时复制层将在第 4 篇中详细讲解。

⑤ 创建 VirtualMachine

后端向 Kubernetes API 创建一个 VirtualMachine 对象。该对象会附加标签:实验会话标识、会话 id,以及 由哪个环境(开发/生产)的后端创建。最后一个标签源于一次事故:开发和生产后端查看的是同一个虚拟机命名空间,结果某一侧的回收器将数据库中没有记录的虚拟机误判为孤儿,并删除了另一侧学生的虚拟机(第 5 篇)。

我们将此虚拟机设为 cloud-init Secret 的所有者,以便在虚拟机被删除时 Secret 也一并被清理。不过,由于所有者关联可能会失败,在删除时还会额外单独清理一次 Secret。

⑥ KubeVirt:VM → VMI → Pod → QEMU

从这里开始由 KubeVirt 接管。它会根据 VM 创建运行中的实例(VMI),并为每个 VMI 创建一个 virt-launcher Pod 。当 Kubernetes 调度器将该 Pod 安排到某个节点上后,该节点的 virt-handler 会指示 Pod 内的 libvirt “启动此虚拟机”。Guest 操作系统则作为该 Pod 内的 QEMU 进程运行。虚拟机为什么要在 Pod 内运行以及付出的代价是第 2 篇的主题。

⑦ 挂载卷

在创建 virt-launcher Pod 时,将作为 Guest 磁盘的卷也会一并挂载。如果是 Golden 实验,Golden PVC 会以 只读方式挂载,KubeVirt 会在其上方将写时复制层文件创建在 Pod 的临时目录(emptyDir)中。在 Guest 看来,这二者合并成了一个磁盘(/dev/vda)。如果没有可挂载的 PVC,KubeVirt 甚至无法创建 Pod,虚拟机也将永远无法启动。这就是为什么要在 ④ 中提前进行判定的原因。

⑧ 确认就绪状态

后端分两阶段进行等待。首先等待 VMI 变为 Running (每个实验设定的上限,默认 150 秒),然后等待 Guest 内的代理回应“已就绪”。

KubeVirt 并没有类似 Pod 的 exec 那样“在 Guest 内部执行命令”的机制。它的子资源仅限于 console、vnc、portforward 等,这是刻意为之的设计。因此,就绪状态的确认和评分都需要通过 ③ 中注入的代理来完成。

Running 我们曾实际踩过坑:仅仅看到 就放学生进去是不行的。代理在 cloud-init 前期启动,而搭建实验环境的准备脚本则在之后运行。如果在两者之间让学生进入,从第一步就会因为“找不到文件”而失败,评分也只能通过 8 个步骤中的 4 个。现在,只有当 cloud-init 的最后一行 打印出就绪标志,且准备脚本成功执行时才算完成。否则,后端会因超时而 坦诚地

失败。

⑨ 控制台转发与 ⑩ Web 终端 console 后端通过 WebSocket 连接到 VMI 的

在打开 WebSocket 时,后端会首先检查连接者是否为该会话的主人、会话是否正在运行。能够访问控制台子资源的 Kubernetes 身份只有后端服务账号(第 6 篇)。

4. 耗时情况 — 目前的实测数据

项目 数值 条件 样本
VMI 阶段:Pending → Scheduled → Running 0秒 → 10秒 → 20秒(以 10 秒为单位记录) Ubuntu 24.04 容器磁盘,2 vCPU·4Gi,2026-08-29 1次
Guest 内 cloud-init 完成 23.8秒 同上虚拟机 1次
使用 cloud-init 安装 Docker 的实践 约 76 秒(启动 25 + apt 索引 14 + 安装 37) 2026-08 实践开发中 1次
通过学生 API 打开的会话,从请求到准备就绪 90.991秒 单个 Istio 实践,2026-09-12 1次
含三台 VM 的会话:三台 VM 均处于 Running 状态 17~23秒 kubeadm 3节点实践,2026-09-23~24 多次测试的范围
相同会话:三个节点的准备脚本结束 约 300 秒 相同实践 相同
各项实践中设定的准备时间上限的依据 k3s 82秒 · Istio 94秒 · k0s 150秒 · Cilium 269秒 各项实践的答案验证 每次实践
为每个 VM 重新获取磁盘的方式(DataVolume)的首次启动 最多 195 秒 第 3 篇 仅记录最大值

表格说明了两件事。 VM 本身在 20 秒左右启动,其余时间是在 Guest 内部搭建实践环境的时间。而且,如果为每个 VM 重新获取磁盘,光是这一项就要花费几分钟。第 4 篇中的黄金磁盘(Golden Disk)正是为了解决这最后一行的问题。

局限性也很明显。大多数值都是测量了一次的数值,整个学生会话准备时间的分布(中位数和离散程度)属于 未测量。由于会话记录中同时保留了开始时间和准备时间,只要收集这些记录就可以计算出来。

5. 本图故意省略的内容

TT 的话。 按钮背后隐藏着十个步骤。然而,VM 启动只需 20 秒,其余大部分时间都在进行「在 Guest 中准备实践」和「准备磁盘」。因此,如果想加快速度,首先要看磁盘。第 4 篇见!

参考

所有文档均于 2026-10-16 查阅。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论