TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Until Student VMs Spin Up 5: Lifespan and Garbage Collection — What to delete, in what order, and ensuring it never stops even on failure

Status: Draft (2026-10-16), our code + incident log + partial measurements. The operation followed the TT Lab backend's reap_once·delete_vm_session and design/incident logs. Quotas and limits were queried on the same day as read-only to verify they match the stored values. Writing rules: Measured data is the default, unmeasured items are marked as [Unmeasured], and items not found in documentation are marked as [Unconfirmed].

Until Student VMs Spin Up 5: Lifespan and Garbage Collection

Table of Contents

TT Lab's single-sentence principle is "Give students a real root shell while preventing them from escaping, and unfailingly reclaim used resources." The last phrase weighs particularly heavy for VMs. If a practice pod is left behind, a few hundred MBs leak, but if a single VM is left behind, 4 GiB is tied up entirely.

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

1. Lifespan: 60 minutes, extensions up to three times

2. Garbage Collector: Three directions every 60 seconds

The backend runs a garbage collection cycle every 60 seconds. The "Collect Now" button on the administrator screen also uses this same function. It looks in three directions.

Direction Target Why it is needed
Expiration Expired session → Clear resources and finish with a expired log Default path
Orphan VMs that exist in the cluster but have no active session in the DB → Deleted When records and resources get out of sync, such as when the backend crashes right after creating a VM
Ghost Session where DB is running but VM does not exist → Record failed to close When a VM disappears due to a node restart or OOM. Without this, a 1-person-per-session limit prevents students from ever opening a new lab.

There is also a reason for scanning VM orphans separately. The reclaimer initially only looked at the list of lab pods, but the VMs were in another namespace, so they would never show up on that list. A VM whose DB row had disappeared could end up holding 4 GiB of memory while remaining completely invisible to anyone.

Right before deleting orphans, the DB is checked one more time. This is to prevent the reclaimer from mistaking a VM from a session created just after it read the list as an orphan.

Incident 1: A reclaimer in another environment deleted student VMs

2026-09-12, a VM session opened via the student API in the production environment failed to become ready. Upon checking the logs, the VM was deleted 26 seconds after the VMI started. It was not deleted by the test tool.

The cause was sharing. The development and production environments have separate backends, but they share a single VM namespace. At the time, the garbage collector read the entire VM namespace and judged any VM "not in my DB to be an orphan." Therefore, to the garbage collector in the development environment, the production student's VM appeared to be an orphan. Under identical conditions in a test, the call to delete the production VM was reproduced (we did not extract the actual identity of the requester from the Kubernetes audit logs).

The fix was a single label. When creating VMs and Secrets, the backend attaches its own environment name as a label, and orphan collection targets only VMs whose label matches its own environment. It does not guess the owner of old VMs without a label. Since the list itself still views the entirety, it does not mistake old VMs registered in the DB as "non-existent resources." After the fix, the session reopened via the student API became ready in 90.991 seconds, and we confirmed that the VM, VMI, and Secret all disappeared after completion (1 occurrence).

Incident 2: A Single Row's Failure Halted the Entire Collection

This issue was discovered during a site security audit on 2026-10-14. Within the loop that deletes expired sessions, if a single deletion threw an exception, the entire iteration terminated. While the same row failed at the same point every minute, other expired sessions, orphans, and ghosts behind it accumulated without being collected by anyone.

Now, failures are contained per row.

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"],))

Changing the record expired to occurs after a successful deletion. If the order is reversed, a VM that failed to be deleted would be recorded as a "completed session" and never become a collection target again. The failure count is also retained in the collection status displayed on the administrator screen.

In the same audit, a deadline ( pods= 60 mins × 3 + 15 mins) was also added toactiveDeadlineSeconds prevent expired root shells from living forever, even if the web and DB pods stop together and the reaper fails to run at all. Normal sessions end earlier than the extension limit (180 mins), so they never hit this deadline.

3. What Gets Deleted and in What Order

The backend directly deletes only two things.

  1. VirtualMachine Objects
  2. cloud-init Secret (containing the grading token)

Everything else is subsequently deleted by Kubernetes and KubeVirt. The order is as follows.

Order What Who Rationale
1 VM deletion Backend delete_vm_session
2 VMI stop and deletion KubeVirt VM owns VMI
3 virt-launcher pod termination KubeVirt & Kubernetes Pod passes termination signal to guest and waits for VM to shut down (30s grace period)
4 Write layer (emptyDir) deletion Kubernetes emptyDir shares the same lifespan as the pod
5 DataVolume and its PVC deletion Kubernetes Garbage Collector VM owns the DataVolume created with dataVolumeTemplates (persistent disk labs only)
— Golden PVC Nobody deletes it No owner, and no labels matched by the reaper

One of the reasons the backend does not need to manually coordinate this order is Kubernetes' Storage Object in Use Protection. According to Kubernetes documentation, if a PVC in use by a pod is requested for deletion, the actual deletion is deferred until the pod stops using it. Thus, the order of VM → pod → volume is naturally preserved, preventing disks still in use by the guest from disappearing prematurely. The same protection applies when deleting golden images, leaving golden PVCs as "Terminating" until all VMs using them have finished (Part 4).

For a three-node session, deleting a single representative VM causes the remaining VMs, Secrets, session-exclusive network policies, and services to be deleted together via labels. In actual measurements, 15 seconds after deleting one representative, all 3 VMs, 3 Secrets, services, and policies dropped to 0, and when a second worker creation was made to fail, even the already created ones were entirely reclaimed (2024-09-24).

Why Delete Secrets Separately Again?

Since the cloud-init Secret lists the VM as its owner, the garbage collector should delete it when the VM is deleted. However, that owner link can fail. Attaching the owner to the Secret after creating the VM is a separate modification request, and security audits indicated that because the backend service account lacks Secret modification permissions , this linking consistently fails. In fact, an orphaned cloud-init Secret was found lingering 32 days without a VM. Therefore, the backend explicitly deletes the Secret by name after deleting the VM. If left behind, the old cloud-init would revive if the same session name is reused. Cleaning up remaining Secrets and permissions is documented as a manual task.

4. Upper Limits: Bounding the Scale of Damage

No matter how robust reclamation is, failures can happen. Therefore, we bound "how much can leak at worst" using upper limits.

Limit Value Source
Concurrent lab VMs 8 Backend (queries the cluster to count)
VM Namespace Quota — VM count 8 ResourceQuota
Same Quota — CPU requests / Memory requests 16 / 48Gi ResourceQuota
Same Quota — PVC count 12 ResourceQuota (uses only golden and temporary scratch during provisioning)

The reason for having two layers is that if the backend's upper limit calculation fails (if KubeVirt stutters and cannot read the list, the backend does not impose an upper limit—because blocking here would halt all VM labs together), quotas act as a final safety net.

The reason the number 8 was not raised is also documented. When creating a multi-VM session (2024-09-24), the combined available memory request headroom of nodes actually providing KVM was only about 17 GiB, and during measurements, one node even entered NotReady status for about a minute due to system OOM. Raising the quota only changes rejections from "no capacity" to scheduling failures and node pressure. Node memory runs out first. The fact that VMs receive no discounts on memory requests (Part 2) and that when a node falls under memory pressure, it is the lab pods rather than the VMs that die first are also reasons why the upper limit was set conservatively.

5. Remaining Gaps

TT's Note. The shorter the cleanup code, the better. The backend only cleans up two things, leaving the rest to "who owns whom" and "do not delete while in use." Also, skip failed rows and retry on the next iteration! Everything must not halt because of a single failure.

References

All documentation was accessed on October 16, 2026.

Sign in to like

Comments

No comments yet.

Sign in to leave a comment