TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

学生 VM 启动直到 5:寿命与回收——清理什么、按什么顺序清理,以及如何确保失败时不停止

状态:草稿(2026-10-16),我们的代码 + 事故记录 + 部分实际测量。 行为遵循了 TT Lab 后端的 reap_once·delete_vm_session 以及设计与事故记录。配额和上限在同一天通过只读方式进行了查询,以确认其与存储库中的值一致。 编写规则:以实测数据为基础,无法测量的标记为「未实测」,在文档中未找到的标记为「未确认」。

学生 VM 启动直到 5:寿命与回收

目录

TT Lab 的一句话原则是「给学员真正的 root shell,但阻止其逃逸, 使用过的资源必须被回收」。在 VM 场景下,最后这句话尤为沉重。剩下一个实验 Pod 只会浪费几百 MB,但剩下一台 VM 会整整锁住 4GiB。

세션의 수명과 지우는 순서 ① 시간표 (기본값) 시작 60분 만료 180분 연장 상한 연장(한 번에 60분까지) 회수기는 60초마다 돈다: 만료 · 고아(DB 에 없는 VM) · 유령(VM 없는 DB 행) 한 세션을 못 지우면 그 행만 건너뛰고 다음 바퀴에 다시 본다 ② 백엔드가 지우는 것은 둘뿐 VirtualMachine 삭제 cloud-init Secret 삭제 VMI 정지 KubeVirt virt-launcher 파드 종료 게스트 종료 신호 emptyDir 쓰기 층 삭제 쿠버네티스(파드와 함께) DataVolume 의 PVC VM 이 소유 → 가비지 컬렉션 (B 방식만) 골든 PVC 소유자 없음 · 세션 라벨 없음 → 지워지지 않는다 PVC 는 쓰는 파드가 있는 동안 지워지지 않는다(사용 중 보호). 그래서 VM → 파드 → 볼륨 순서가 자연스럽게 지켜진다.
图:会话的寿命与删除顺序

1. 寿命:60 分钟,最多可延长至三倍

2. 回收器:每 60 秒三个方向

后端每 60 秒执行一轮回收。管理员界面上的「立即回收」按钮也使用相同的函数。观察的方向有三个。

方向 内容 为什么需要
过期 时间到期的会话 → 清理资源并将记录标记为 expired 结束 默认路径
孤儿 集群中存在但数据库中没有活跃会话的 VM → 删除 当后端在创建 VM 后立即崩溃等原因导致记录与资源不一致时
幽灵 数据库显示运行中但实际没有 VM 的会话 → 将记录标记为 failed 结束 节点重启或 OOM 导致 VM 消失时。如果没有这个,由于一人一会话的限制,学生将永远无法开启新的实验

单独扫描 VM 孤儿也是有原因的。回收器最初只查看实验 Pod 列表,而 VM 位于 不同的命名空间中,因此在该列表中永远不可见。数据库行消失的 VM 可能会在占用 4GiB 内存的同时处于谁也看不见的状态。

在删除孤儿之前,会再检查一次数据库。这是为了防止回收器在读取列表后,将刚刚创建的会话的 VM 误认为孤儿。

事故 1:其他环境的回收器删除了学生的 VM

2026-09-12,在生产环境中,通过学生 API 打开的 VM 会话未能完成准备而失败。核对记录后发现,在 VMI 启动后 26 秒 VM 被删除了。并非由测试工具删除。

原因是共享。开发环境和生产环境的后端使用不同的数据库,但 共用同一个 VM 命名空间 。当时的回收器读取了整个 VM 命名空间,并把「不在我的数据库中」的判定为孤儿。因此,在开发环境回收器的眼中,生产环境学生的 VM 就成了孤儿。在给定相同条件的测试中,重现了删除生产环境 VM 的调用(未从 Kubernetes 审计日志中提取实际删除请求者的身份)。

修复方法是一个标签。后端在创建 VM 和 Secret 时, 为其附加自身环境名称的标签,孤儿回收仅以标签与自身环境相同的 VM 为目标。不推测没有标签的旧 VM 的所有者。列表本身继续查看全体,因此不会将数据库中注册的旧 VM 误认为是「不存在的资源」。修复后通过学生 API 重新打开的会话在 90.991 秒时准备就绪,并确认在结束后 VM、VMI、Secret 都已消失(1 次)。

事故 2:单行失败导致整个回收过程停滞

这是在 2026-10-14 网站安全检查中发现的问题。在清理过期会话的循环中,如果某一次删除抛出了异常, 整个这一轮循环就会终止。 当同一行在每分钟的同一位置失败时,其后面其他的过期会话、孤儿、幽灵也都在无人收理的情况下堆积起来。

现在,失败被限制在每一行内部。

for r in rows:
    try:
        delete_session_resource(r)
    except Exception as exc:
        failed += 1            # 실패를 세고
        print(f"[reaper] 만료 세션 회수 실패 {str(r['id'])[:8]}: {type(exc).__name__}", flush=True)
        continue               # 기록은 그대로 두어 다음 바퀴에 다시 본다
    con.execute("update sessions set status='expired', finished_at=now() where id=%s", (r["id"],))

将记录更改为 expired 的时机是在 删除成功之后。如果顺序相反,未被删除的 VM 就会被记录为「已结束的会话」,从而再也不会成为回收的目标。失败次数会与其他回收状态一起保留在管理员界面上。

在同样的检查中,对于实操 Pod,我们还设置了截止时间(activeDeadlineSeconds = 60分钟 × 3 + 15分钟)。这是为了防止即使 Web Pod 和数据库同时停止导致回收器完全不工作,过期的 root shell 也不会永久存活。正常会话会在延长上限(180分钟)之前结束,因此不会触及这个截止时间。

3. 清理什么,按什么顺序清理

后端直接清理的只有 两个:

  1. VirtualMachine 对象
  2. cloud-init Secret(包含评分 Token)

其余部分由 Kubernetes 和 KubeVirt 依次清理。顺序如下:

顺序 对象 执行者 依据
1 删除 VM 后端 delete_vm_session
2 停止并删除 VMI KubeVirt VM 拥有 VMI
3 终止 virt-launcher Pod KubeVirt·Kubernetes Pod 向 Guest 发送终止信号并等待 VM 关闭(30秒宽限期)
4 删除写入层(emptyDir) Kubernetes emptyDir 的生命周期与 Pod 相同
5 删除 DataVolume 及其 PVC Kubernetes 垃圾回收器 VM 拥有由 dataVolumeTemplates 创建的 DataVolume(仅限持久盘实操)
— 黄金 PVC 无人删除 没有所有者,回收器筛选的标签中也没有它

后端无需直接手动调整顺序,Kubernetes 的 PVC 保护机制(Storage Object in Use Protection)在其中发挥了作用。根据 Kubernetes 文档,即使请求删除正在被 Pod 使用的 PVC,其实际删除也会延迟,直到该 Pod 不再使用它为止。因此,VM → Pod → 卷的顺序得以自然维持,不会发生 Guest 正在使用的磁盘先消失的情况。在删除黄金镜像时也会触发相同的保护,在所有正在使用它的 VM 结束之前,黄金镜像会一直保持「正在删除」状态(第 4 篇)。

对于包含三个节点的会话,如果删除代表性 VM,其余的 VM、Secret、会话专用网的策略以及服务都会通过标签被一同删除。实测显示,删除一个代表性节点 15 秒后,3 个 VM、3 个 Secret、服务和策略的数量全部变为 0;在人为制造第二个 Worker 创建失败的情况下,已经创建的部分也被全部回收(2026-09-24)。

为什么还要单独再删一次 Secret?

由于在 cloud-init Secret 中将 VM 记为所有者,因此当 VM 被删除时,垃圾回收器应该会将其一同删除。然而, 这种所有者关联可能会失败。 在创建 VM 后为 Secret 挂载所有者属于单独的修改请求,安全检查显示后端服务账号 没有 Secret 的修改权限 ,这导致该关联似乎总是失败,相关记录也有迹可循。实际上确实发现了一个在没有 VM 的情况下残留了 32 天的 cloud-init Secret。因此,后端在删除 VM 后,会再通过名称删除一次 Secret。因为如果不这样做,当再次出现相同的会话名称时,旧的 cloud-init 就会复活。记录显示,残留 Secret 的清理与权限整理需要由人工来处理。

4. 上限:限制故障的波及范围

无论回收机制多么健壮,都有可能失败。因此,我们将「最坏情况下会泄漏多少」通过上限约束起来。

上限 数值 来源
并发实操 VM 8 台 后端(通过查询集群来统计)
VM 命名空间配额 — VM 数量 8 ResourceQuota
同上配额 — CPU 请求 / 内存请求 16 / 48Gi ResourceQuota
同上配额 — PVC 数量 12 ResourceQuota(仅使用黄金镜像以及拉取过程中的临时存储)

之所以采用双重保险,是因为当后端上限无法正确计算时(当 KubeVirt 发生波动而无法读取列表时,后端不会设置上限——因为如果在此处拦截,所有 VM 实操都会同时停摆),配额(Quota)会在最后进行拦截。

记录中也说明了为什么没有将数字 8 调高的原因。在创建多 VM 会话时(2026-09-24),实际提供 KVM 的节点的可用内存请求余量加起来也大约只有 17GiB,在测试过程中甚至还出现了一个节点因系统 OOM 导致约 1 分钟内变为 NotReady 的情况。 提高配额只会将拒绝原因从「没有位置」变成调度失败和节点压力。节点的内存会首先耗尽。VM 的内存请求没有折扣(第 2 篇),并且当节点陷入内存压力时,首先死去的不是 VM,而是同一节点上的实操 Pod,这也是将上限设定得比较保守的原因。

5. 残留的漏洞

TT 总结。 清理代码越短越好。后端只需清理两项,其余的交给“谁拥有谁”以及“正在使用则不清理”。此外,失败的行直接跳过,在下一轮循环中重试!因为不能因为一个错误而让整个流程停滞。

参考

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

登录后即可点赞

评论

还没有评论。

登录后即可发表评论