Skip to content

fix(k8s): own sandbox pods with session Service so GC reclaims them on SIGKILL - #57

Open
qiuqionglin wants to merge 1 commit into
mainfrom
fix/k8s-owner-reference-gc
Open

fix(k8s): own sandbox pods with session Service so GC reclaims them on SIGKILL#57
qiuqionglin wants to merge 1 commit into
mainfrom
fix/k8s-owner-reference-gc

Conversation

@qiuqionglin

Copy link
Copy Markdown

问题

K8s sandbox provider 创建的每个 pod 都是裸 pod——没有 ownerReference、没有 controller。清理完全依赖 teardown() 的 Python finally 路径去删除 pod。

当进程被非优雅杀掉时——OOM、节点驱逐、force-delete、硬超时——teardown() 根本不执行,pod 会永久泄漏,堆积在节点上耗尽 per-node pod 槽位。

#56 加的 SIGTERM handler 只覆盖优雅关闭(cancel job 让 teardown() 有机会跑),对 SIGKILL 无能为力——按定义,SIGKILL 下任何用户态清理都无法执行。

生产上已观察到:数百个终止态、无 ownerReference 的 sandbox pod,谁都回收不了(裸 pod 没有 owner 可级联)。

修复

把每个 pod 的 ownerReference 指向 setup() 本就会创建的 session headless Service。这样 Service 一被删,Kubernetes 垃圾回收器就会级联回收所有 pod——与进程是否跑完自己的清理无关

  • setup() 接住创建 Service 的返回值,存下 metadata.uid
  • create() 给 pod 挂 ownerReference(kind: Service + 该 uid,controller=FalseblockOwnerDeletion=False——纯 GC 归属链,不是 controller 托管关系)。
  • teardown() 删 Service 时会级联删 pod;如果进程先死,pod 仍有主,等 Service 被清时照样被 GC。

本 PR 与 #56 正交互补:#56 优化优雅路径,本 PR 让非优雅路径的清理也稳。

 def setup(self) -> None:
     ...
-    self._v1.create_namespaced_service(namespace=self._namespace, body=svc)
+    created_svc = self._v1.create_namespaced_service(namespace=self._namespace, body=svc)
     ...
+    self._service_uid = created_svc.metadata.uid

 def create(self, spec: SandboxSpec) -> Sandbox:
     ...
+    owner_references = None
+    if self._service_uid is not None:
+        owner_references = [client.V1OwnerReference(
+            api_version="v1", kind="Service",
+            name=f"terrarium-{self._session_id}", uid=self._service_uid,
+            block_owner_deletion=False, controller=False,
+        )]
     pod = client.V1Pod(
         metadata=client.V1ObjectMeta(
             name=pod_name,
             labels={...},
+            owner_references=owner_references,
         ),

为什么安全

ownerReference 若指向一个不存在的 owner,K8s 会把 pod 当孤儿立即删除(有误删风险)。本修复永远不会走到这条路:Service 在 setup() 中创建,严格早于任何 create() 调用,且 uid 来自真实 API 返回——所以 pod 引用 owner 时,owner 一定已存在。

验证

单元测试全是 mock,只能证明 pod 对象被构造出带 ownerReference。为证明 Kubernetes 真的会据此级联 GC,我在本地 kind 集群(K8s v1.31.0)上做了端到端测试,精确复刻了本 PR 生成的 ownerReference 字段。

测试 A —— 级联 GC(owned)vs 泄漏(bare): 建 headless Service + 一个 owned pod(本 PR 行为)+ 一个裸 pod(现状),然后删 Service。

Pod ownerReference 删 Service 后
owned(本 PR) → Service 1s 内 Terminating,2s 内被完全 GC
bare(现状) 纹丝不动、永久留存 ✅(复现泄漏)

API server 接受并持久化了 ownerReference,字段与生成的完全一致:

{"apiVersion":"v1","kind":"Service","name":"terrarium-<session>",
 "uid":"bf713f89-...","blockOwnerDeletion":false,"controller":false}

测试 B —— 无误删:

  • owner Service 存在 → owned pod 稳定存活(不被误删)✅
  • owner uid 伪造/不存在 → pod 被立即当孤儿 GC

这同时证明了:owner 存在时级联 GC 生效,以及"owner 必须先存在"这个不变式(本修复恰好满足)正是保护存活 pod 不被误删的关键。

(kind 集群里 pod 显示 ErrImagePull(拉不到 busybox)是因节点离线,与 GC 无关——GC 只看 ownerReference,不看 pod phase。)

现有测试套件:tests/environment/ —— 371 passed,含新增的 test_create_owns_pod_with_session_service

…n SIGKILL

The K8s provider creates sandbox pods as bare pods (no ownerReference), so
cleanup relies entirely on teardown()'s Python finally path. When the process
is SIGKILLed — OOM, node eviction, force-delete, or a hard deadline — teardown
never runs and the pods leak permanently, exhausting per-node pod slots. The
SIGTERM handler added in #56 only covers graceful shutdown, not SIGKILL.

Set each pod's ownerReference to the per-session headless Service (already
created in setup()). Kubernetes' garbage collector then reclaims every pod when
the Service is deleted, independent of whether this process runs its own
cleanup. Deleting the Service in teardown() now cascades to the pods; if the
process dies first, the pods are still owned and get collected once the Service
is removed.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant