跳到主要内容

扩展面:Template / WarmPool / Claim 与冷热启动、热池领取

30 秒导读: 核心的 Sandbox CRD 能把一个 Pod 包装成「远程沙箱」,但从零建一个沙箱要等 Pod 调度 + 拉镜像 + 启动,动辄几十秒。这一章讲的三个扩展类型——SandboxTemplate(蓝图)、SandboxWarmPool(预热池)、SandboxClaim(领取单)——就是为「秒级拿到一个能用的沙箱」而生:提前按模板批量造好一池沙箱晾着,用户要用时不新建、而是从池里领走一个已经 Ready 的。难点全在「领取」这一下:并发领、缓存滞后、跨命名空间安全、什么时候必须放弃热池回落冷启动。

本章是全项目工程含量最高的一章。为避免糊成一坨,分层来讲:

  1. 先把三类型的数据模型摆成表(它们各自装什么、有哪些安全开关);
  2. 再走 WarmPool 补池控制器(池子怎么维持在目标数量、模板变了怎么判旧);
  3. 再走 Claim 领取控制器(冷/热怎么判、怎么从池里 adopt、缓存滞后怎么不踩坑);
  4. 最后点一句 Template 控制器的职责(物化 NetworkPolicy)。

阅读前建议先看同组的 01-sandbox-api-model.md(Sandbox 的 Spec/Status 与 SandboxBlueprint)和 02-sandbox-controller.md(核心 reconcile),本章大量复用其中的 SandboxBlueprintSandbox 所有权模型。全景与阅读地图见 index.md


1. 这是什么(零基础也能懂)

一句话定义: 在「一个 CRD = 一个沙箱」的核心之上,再加一套「先备货、后领用」的资源,让「拿到一个沙箱」这件事从「现造」变成「现领」。

用一个类比建立直觉——共享充电宝柜机:

  • SandboxTemplate = 充电宝的型号规格(容量、接口、外壳)。它不是任何一个实体,只是「按这个规格造」的蓝图,还顺带规定了安全策略(默认怎么锁网络)。
  • SandboxWarmPool = 柜机。它盯着「我要常备 N 个满电充电宝」,少了就按型号造新的塞进去,多了就拿走。
  • SandboxClaim = 你的借用请求。你不关心哪一个,你只想「立刻拿一个满电、能用的走」。柜机里有现成的就直接给你(热领取,秒级);柜机空了才现造一个(冷启动,慢)。

解决谁的什么问题: 平台方要给成百上千个 AI agent 会话分配隔离沙箱。如果每来一个会话就现建一个 Pod,用户要干等 Pod 冷启动。热池让平台用闲时算力提前把 Pod 拉起来晾着,用户请求到达时几乎零等待。

它能做什么:

  • 用一份模板批量、可版本化地生产沙箱(改模板 → 池子自动换新)。
  • 维持一个「随时有货」的就绪沙箱池,支持 HPA 自动伸缩池大小。
  • 领取时优先给「已 Ready」的沙箱;池空了自动回落到冷启动。
  • 给沙箱注入按需的环境变量 / 存储卷 / 标签,并按模板策略校验这些注入是否被允许。
  • 到点自动过期回收(按绝对时间,或「结束后 TTL」)。

一句话直觉: 把「造一个 Pod」和「用一个 Pod」在时间上解耦——造在前(WarmPool 平时慢慢造),用在后(Claim 要用时快速领)。

本节不出现代码;下面开始进入结构与实现。


2. 顶层全景(它大概怎么转)

三个自定义资源 + 三个控制器,外加一个内存热池队列(informer 之外的快路)。怎么读下图:实线是「拥有 / 创建」,虚线是「参照 / 领取」,方向统一从上到下。

┌──────────────────┐ 参照蓝图 ┌──────────────────┐
│ SandboxTemplate │◀············· │ SandboxWarmPool │ 「常备 N 个」
│ (蓝图+安全策略) │ │ (replicas=N) │
└────────┬─────────┘ └────────┬─────────┘
│ Template 控制器 │ WarmPool 控制器
│ 物化出一条共享 │ 补池:少了就按蓝图造
▼ ▼
┌──────────────────┐ ┌──────────────────────────┐
│ NetworkPolicy │ │ 一池 Sandbox(warm 标签) │
│ (默认锁死内网) │ │ 晾在这里等人领 │
└──────────────────┘ └───────────┬──────────────┘
│ 就绪的 key 进内存队列

┌───────────────┐ 领取(adopt) ┌──────────────────────────────┐
│ SandboxClaim │ ───────────────▶ │ 内存热池队列 WarmSandboxQueue │
│ (我要一个) │ Claim 控制器 │ (快路:不走 informer List) │
└───────┬───────┘ └──────────────────────────────┘
│ 池空 / 带 env / 带卷 → 回落

┌────────────────────────────┐
│ 冷启动:直接按 Template 新建 │
│ 一个 Sandbox(cold 标签) │
└────────────────────────────┘

部件一句话职责:

部件干什么在哪
SandboxTemplate内联 SandboxBlueprint 蓝图 + 网络/环境/卷的注入策略extensions/api/v1beta1/sandboxtemplate_types.go:88
SandboxWarmPool声明「常备 N 个」,支持 scale 子资源(HPA)extensions/api/v1beta1/sandboxwarmpool_types.go:39
SandboxClaim「领取单」:引用某个热池,带生命周期/env/卷extensions/api/v1beta1/sandboxclaim_types.go:108
WarmPool 控制器补池:对齐目标数量、删旧造新、批量并发extensions/controllers/sandboxwarmpool_controller.go:68
Claim 控制器领取:找已有 → 从池 adopt → 回落冷启动extensions/controllers/sandboxclaim_controller.go:204
Template 控制器校验 + 物化那条共享 NetworkPolicyextensions/controllers/sandboxtemplate_controller.go:52
内存热池队列informer 之外的快路,秒级挑候选沙箱extensions/controllers/queue/

主线走一遍(高层): 管理员建 Template(定蓝图+锁网络)→ 建 WarmPool 指向该 Template → WarmPool 控制器按蓝图批量造出一池 warm 沙箱,就绪后其 key 进内存队列 → 用户建 Claim 指向该 WarmPool → Claim 控制器优先从内存队列挑一个 Ready 沙箱,把它的属主从 WarmPool 改成这个 Claim(adoption)→ 用户瞬间拿到一个能用的沙箱;若池空或 Claim 带了 env/卷,则回落到按 Template 冷启动新建。


3. 三类型数据模型(先把结构摆清)

三个类型都内联或复用核心的 SandboxBlueprint(见 01 章)。这里只讲扩展层新增的、决定行为的字段

3.1 SandboxTemplate:蓝图 + 默认安全策略

SandboxTemplateSpec 内联整份 SandboxBlueprint(podTemplate + volumeClaimTemplates + service),再叠四组「治理开关」。见 sandboxtemplate_types.go:88

字段取值默认作用
SandboxBlueprint(内联)沙箱长啥样:Pod 模板、卷模板、Service
networkPolicyManagementManaged / UnmanagedManaged是否由控制器托管一条 NetworkPolicy
networkPolicy受限的 ingress/egress 子集nil自定义网络规则;Managed 且为 nil → 套「安全默认」
envVarsInjectionPolicyAllowed / Overrides / DisallowedDisallowedClaim 能否注入/覆盖环境变量
volumeClaimTemplatesPolicyAllowed / Overrides / DisallowedDisallowedClaim 能否注入/覆盖存储卷模板

关键设计:安全是默认的,不是可选的。 三个策略字段默认值都指向「最紧」:网络托管、env/卷注入一律 Disallowed。要放开必须显式声明。

「安全默认」网络策略(Managed + networkPolicy=nil)长这样,注释见 sandboxtemplate_types.go:99-117,实现见 sandboxtemplate_controller.go:192buildDefaultNetworkPolicySpec:

  • Ingress: 只允许来自 agent-sandbox-system 命名空间里 app=sandbox-router 的流量(即只有数据面路由能进,见 04-sandbox-router.md)。
  • Egress: 允许访问公网(0.0.0.0/0),但显式 Except 掉全部内网段——RFC1918 私网(10/8172.16/12192.168/16)、链路本地/元数据服务器(169.254/16)、IPv6 ULA/链路本地。目的是防 agent 探测内网服务、连元数据服务器窃取云凭证。

注意坑(注释原文警告): 这是「Default Deny」ingress。如果 Pod 带 sidecar(Istio、监控 agent)监听自己的端口,默认策略会挡掉进 sidecar 的流量导致健康检查失败,必须自己在 networkPolicy.ingress 里放行。

3.2 SandboxWarmPool:预热池

SandboxWarmPoolSpec 很小(sandboxwarmpool_types.go:39),核心就三个字段:

字段说明
replicas池中期望的沙箱数,默认 1,Minimum=0;可被 HPA 接管
sandboxTemplateRef.name指向哪个 SandboxTemplate
updateStrategy.type模板变了怎么换旧沙箱:Recreate / OnReplenish(默认)

为什么能挂 HPA: 类型上打了 scale 子资源标注(sandboxwarmpool_types.go:98,specpath=.spec.replicas / statuspath=.status.replicas / selectorpath=.status.selector)。有了 /scale 子资源,HPA 就能像伸缩 Deployment 一样伸缩这个池——闲时缩、忙时扩。

两种更新策略(sandboxwarmpool_types.go:63) 决定「蓝图改了之后,池里那些用旧蓝图造的沙箱怎么办」:

策略行为适合
Recreate立刻删掉旧沙箱,保证池里只剩新蓝图的要求池子始终最新
OnReplenish(默认)旧的不主动删,等它被领走 / 被手动删,补新的时才用新蓝图平滑过渡、不浪费已就绪的旧沙箱

注意:只有 SandboxBlueprint 变化才算「旧」。改 annotation/label/模板级策略不触发 recreate(见 sandboxwarmpool_types.go:64-66 注释)。

TemplateRefField = ".spec.sandboxTemplateRef.name"(:29)是给 field indexer 用的索引路径——Template 变更时能反查「哪些 WarmPool 引用了它」并触发 reconcile。

3.3 SandboxClaim:领取单

SandboxClaimSpec(sandboxclaim_types.go:108)是用户侧的入口:

字段说明
warmPoolRef.name必填,从哪个热池领
lifecycle何时/如何过期(见下)
additionalPodMetadata要传播到沙箱 Pod 的 labels/annotations(有严格校验)
env要注入的环境变量;一旦指定就强制冷启动
volumeClaimTemplates要挂的存储卷;一旦指定就强制冷启动

冷启动的强制触发点(重要): envvolumeClaimTemplates 两个字段的注释都明说——热池里的通用沙箱不会预先带你这一份专属 env/卷,所以只要 Claim 填了它们,就不可能复用池里现成的,必须从模板现造(sandboxclaim_types.go:124-125:131)。

lifecycle(sandboxclaim_types.go:61)三件套:

  • shutdownTime:绝对过期时间点。
  • ttlSecondsAfterFinished:沙箱进入 Finished 状态后再保留多久,计时从镜像过来的 Finished 条件的 LastTransitionTime 起算。
  • shutdownPolicy:过期后怎么处置(默认 Retain)。

ShutdownPolicy 三态(sandboxclaim_types.go:41):

策略过期后用途
Delete删掉 Claim(级联删 Sandbox)用完即弃
DeleteForeground前台级联删:Claim 带 deletionTimestamp 留在 API 里,直到底层 Sandbox/Pod 真正终止才消失外部系统可据此观测关机进度
Retain(默认)删掉底层 Sandbox/Pod/Service 省资源,但保留 Claim 对象(Status 显示 Expired)留档、事后取状态

4. 控制器主线 A:WarmPool 补池

这节讲池子怎么维持在目标数量。入口 Reconcile(sandboxwarmpool_controller.go:68)很短:取对象 → 处理删除 → reconcilePool 对齐数量 → 回写 status。核心全在 reconcilePool(:106)。

4.1 补池主循环:数出来,再补齐或删多

reconcilePool 的骨架就是经典的「声明式对齐」——数出当前活跃数,和期望数比,多退少补:

# 示意,非源码。对应 reconcilePool (sandboxwarmpool_controller.go:106)
sandboxes = list_by_label(warm_pool_label = hash(pool.name)) # 只看本池的
active = filter_active(sandboxes) # 删陈旧、认领孤儿、剔除卡住的
current = len(active)
desired = pool.spec.replicas or 1

if current < desired: # 少了 → 造
to_create = min(desired - current, max_batch) # 单轮上限 300
slow_start_batch(to_create, build_from_template)
elif current > desired: # 多了 → 删
to_delete = min(current - desired, max_batch)
sort_delete_priority(active) # 先删没就绪的、同组内删最新的
slow_start_batch(to_delete, delete)

几个工程细节值得单独点出:

  • 只看本池:warmPoolSandboxLabel = hash(pool.name) 做 label 选择器 List(:110-124),多池互不干扰。
  • 删多时的优先级排序(:208): 先删没 Ready 的(反正没价值),同一就绪状态内删最新的(老的更可能已被外部预热完毕)。这样缩容时优先牺牲「还没热好」的。
  • 剔除卡住的沙箱(:135-152): 若一个沙箱创建超过 warmPoolReadinessGracePeriod = 5 分钟还没 Ready,判为卡死,主动删掉重造——避免池子被僵尸占着名额。
  • 批量上限: sandboxCreateDeleteMaxBatchSize = 300(:50),单轮 reconcile 最多造/删 300 个,防止一次打爆 API server。

4.2 慢启动批处理:别一次性猛冲 API

造 / 删都不是串行 for 循环,而是走 slowStartBatch(:647)——从 1 个开始,每成功一批就把并发翻倍(1→2→4→8…),用 errgroup 并发跑每一批,任一失败就停、返回首个错误并汇报已成功数。

批次: [1] [2] [4] [8] ...
成功→ 成功→ 成功→ 成功→
并发翻倍,直到剩余为 0 或某批出错即止

妙在哪: 冷启动时若模板/配额本身有问题,第一批(1 个)就会失败,只浪费 1 次 API 调用而非 300 次;一切正常时又能快速指数放大到高并发,兼顾「探雷」和「吞吐」。这是从 Kubernetes ReplicaSet 借来的经典手法。

4.3 模板哈希与陈旧检测:蓝图变了怎么认出旧沙箱

改了 Template 的蓝图后,池里旧沙箱得被判为「陈旧(stale)」才能换新。逐个做深度比较太贵,所以用哈希短路 + 缓存

fetchTemplateAndHash(:349)一次性取模板并算两个哈希:

  • currentPodTemplateHash:仅为兼容旧标签(DeprecatedSandboxPodTemplateHashLabel)保留。
  • currentSandboxBlueprintHash:真正用于陈旧判定——对整份 SandboxBlueprint 做 JSON marshal 后 NameHash(computeSandboxBlueprintHash :340)。

isSandboxStale(:496)的判定阶梯(命中即返回):

① templateRefHash 标签对不上模板名 ─── 旧(指向了别的模板)
② 是孤儿沙箱? ─── 永远做完整语义比较(不信标签)
③ 沙箱的 blueprintHash == 当前哈希? ─── 新鲜,直接放行(便宜的快路)
④ 当前哈希算失败(空)? ─── 保守当「不旧」,避免误删全池
⑤ 这个旧哈希之前判过?(vettedHashes)─── 复用缓存结果,不再深比
⑥ 都不命中 → compareSandboxBlueprint 做一次语义 DeepEqual,结果写回缓存

第 ⑥ 步的 compareSandboxBlueprint(:586)不是裸 DeepEqual——它先把期望的 PodSpec 套上和创建时一样的 ApplySandboxSecureDefaults(comparePodSpecs :553),再比。因为控制器创建时会注入默认字段(关 SA token、改 DNS),不归一化就会「自己和自己不相等」而误判全池陈旧。卷模板比较是按下标顺序比的(compareVolumeClaimTemplates :569),刻意和哈希计算的 +listType=atomic 语义保持一致。

跨文件的隐形契约: compareSandboxBlueprint 必须覆盖 SandboxBlueprint 的每个字段,否则「漏掉的字段变了也不算旧」。核心类型文件 api/v1beta1/sandbox_types.go:157 就挂了一条注释提醒:改 SandboxBlueprint 字段时,记得同步改这个比较函数。这是一处典型的「类型定义 ↔ 控制器逻辑」耦合点。

4.4 孤儿认领与更新触发

filterActiveSandboxes(:259)顺带做两件事:

  • 认领孤儿(adoptSandbox :239): 没有 controllerRef 的 warm 沙箱,若不陈旧就 SetControllerReference 收为己有——覆盖控制器重启期间产生的游离沙箱。
  • Template 变更联动: SetupWithManager(:593)里 WatchesSandboxTemplate,靠 findWarmPoolsForTemplate(:619)用前面的 field index 反查受影响的 WarmPool 并入队。所以改一次模板,引用它的所有池都会被唤醒重算。

5. 控制器主线 B:Claim 领取

这是全章最难的部分。入口 Reconcile(sandboxclaim_controller.go:204)先算过期、按 ShutdownPolicy 处理过期路径,未过期则走 reconcileActive(:418)。

5.1 过期与生命周期

checkExpiration(:408)直接调用 internal/lifecycleTimeLeft(expiry.go:73),后者用 ExpireAt(:48)取「shutdownTimeFinished+TTL 两者中最早的那个」作为过期点,返回 (是否已过期, 剩余时长)。剩余时长还被 Reconcile 直接当作 RequeueAfter——到点自动醒来处理,不靠轮询。

过期后分流(Reconcile :265-300):Delete/DeleteForeground 立即删 Claim 并直接 return(继续走会去更新已删对象而崩);RetainreconcileExpired(:518)只删底层 Sandbox、保留 Claim,删前用 IsControlledBy 校验属主,不是自己的不动(:537)。

5.2 领取的三段式:fast-path 找已有 → 从池 adopt → 回落 template

reconcileActive(:418)先校验 additionalPodMetadata,再调 getOrCreateSandbox(:1516)。这个函数是「领取」的心脏,内部是一条优先级递降的查找链:

getOrCreateSandbox 的查找顺序(命中即返回):
1. claim.status.sandbox.name 记过一个? ─ Get 到且属主是我 → 直接用(最快)
2. claim 注解/旧标签记了 assigned 名? ─ 属主是我 → 用;还挂在 WarmPool 名下 → 去 completeAdoption
3. 按 claim.Name 名字查(冷启动用 claim.Name 造)─ 存在且属我 → 用
4. 带 env 或 volumeClaimTemplates? ─ 是 → return nil(强制冷启动,绕过队列)
5. 走内存热池队列 adoptSandboxFromCandidates ─ 领到一个 warm 的
6. 都没有 → return nil(交给上层冷启动)

回到 reconcileActive:若第 1-5 步拿到了沙箱(热路径),就顺带把模板的 metadata 合并进去、对齐 additionalPodMetadata(:447-502);若全落空(sandbox == nil),才进冷路径——getTemplate 取模板、createSandbox(:1262)从蓝图新建(:506-514)。这就是「先找已有/热池,回落 template」的分层。

冷 vs 热的判定,一句话: 带了专属 env/(第 4 步),或池子真空了(第 5 步领不到),就冷启动;否则尽量热领取。冷启动沙箱打 SandboxLaunchTypeCold 标签、名字用 claim.Name;热领取的打 warm 标签。

5.3 从内存队列挑候选:NodeSpread + Ready 优先

第 5 步不走 informer 的 List(那要遍历、还可能读到陈旧缓存),而是走 WarmSandboxQueue 这个内存快路getCandidate(:780)从队列里挑,策略 pickSmart(:802):

  • NodeSpread: 统计候选在各节点的剩余数,优先挑「剩得最多的节点」上的——等价于把领取压力轮转铺开到各节点,避免都从同一节点领导致热点。
  • FIFO 兜底: 未调度的候选按最老优先。
  • Ready 优先 + fallback: 挑到就绪的立即返回;遇到未就绪的先记为 fallbackSandbox 暂存,继续找下一个 Ready 的;实在没有 Ready 的才用这个 fallback。挑剩的 key 用 defer 原样塞回队列(:791-799),不浪费。
  • Ghost Pod: 队列里有、集群里 Get 已 NotFound 的,直接跳过并弹下一个(:868)。

5.4 领取的一致性:adoption 与 informer 缓存滞后

真正把一个 warm 沙箱「过户」给 Claim 的是 adoptSandboxFromCandidates(:906)→ completeAdoption(:995)。过户动作(completeAdoption)做的事:

  1. 删掉 warm 相关标签(让它从热池查询里消失);
  2. 摘掉 WarmPool 的 ownerRef,SetControllerReference 改成这个 Claim;
  3. 摘掉「可被 autoscaler 驱逐」注解(现在它承载了活跃状态,不能被随便回收);
  4. 打上 claim 身份标签、合并模板 + Claim 的 metadata,一次 Patch 落地。

这里埋着本章最精妙的坑——informer 缓存滞后(#1107)。 控制器发出 adoption 的 Patch 后,本地 informer 缓存不会立刻反映「这个沙箱现在归我了」;下一轮 reconcile 可能还读到「它还归 WarmPool」的旧视图,于是又想再 adopt 一次。处理手法(sandboxclaim_controller.go:72-174 一带):

  • triggeredAdoptions 表(:151): 记「我已经为 claim(UID)+sandbox 发过 adoption 补丁了」。下一轮若又看到「还在 WarmPool 名下」,先查这张表——命中就知道「只是缓存没跟上」,不重发补丁(getOrCreateSandbox :1596-1600)。
  • errAdoptionTriggeredRetry 哨兵错误(:80): adoption 刚触发时不返回普通 error,而是返回这个哨兵。Reconcile(:363)把它转成一个有界的短 requeue(adoptionCacheLagRequeueDelay = 50ms,:87),而不是让它走「指数失败退避 + 全局限流桶」。

为什么不能返回普通 error? 普通 error 会进 controller-runtime 的指数退避限流器。而这个「等缓存收敛」的重试每一轮都会复发,直到缓存追上——退避会一轮轮翻倍(5ms*(2^k-1)),并发大量 Claim 时领取的尾延迟会被这退避炸上天。返回 nil error 让 workqueue Forget 掉这个 key、重置失败计数,只用固定 50ms 小延迟重排——这正是 50ms 这个数的用意:够覆盖典型 watch 延迟(多数一轮就收敛),又远小于它替代的多秒级退避。

  • 状态不提前落定: 缓存滞后这一轮 sandbox 还是 nil,computeAndSetStatus(:720)特意判断「若这是 adoption 重试且状态里已有沙箱名,就别把已记录的 Name/PodIPs 抹掉」(:725),避免状态被瞬间清空又填回、也保住了「重复领取保护」。

5.5 跨命名空间禁止 + 可领取性校验

领取前每个候选都过 verifySandboxCandidate(:2041):

  • 跨命名空间领养禁止: 候选与 Claim 不同命名空间 → ErrCrossNamespaceAdoption(:91:2043)。这是硬安全边界——绝不能把 A 命名空间的沙箱领给 B 命名空间的 Claim。
  • 可领取性(isAdoptable :2057): 没在删除中、带 warm/templateRefHash 标签、且属主确实是一个 SandboxWarmPool(APIVersion+Kind 双检),否则拒。
  • 热池匹配: 候选的 WarmPool 名必须等于 claim.spec.warmPoolRef.name

5.6 冷启动新建:env / 卷 / 策略校验

池空或强制冷启动时走 createSandbox(:1262),从模板深拷蓝图新建,并把 Claim 的注入项按模板策略落地校验:

  • 卷模板合并(mergeVolumeClaimTemplates :1422):volumeClaimTemplatesPolicy 分流——Disallowed(默认)直接拒(ErrVolumeClaimTemplatesDisallowed);Allowed 只许新增、重名即拒(ErrVolumeClaimTemplatesOverrideForbidden);Overrides 按名覆盖。
  • 环境变量注入(injectEnvs :1232): 模板策略非 Allowed/Overrides 直接拒;Allowed 下若容器用了 EnvFrom 也拒(无法安全防止覆盖);按 containerName 定位容器(默认第一个主容器),存在同名变量时只有 Overrides 才准覆盖。目标容器不存在会报错。
  • metadata 合并(mergePodMetadata :1194)与校验(validateAdditionalPodMetadata :1117): label 走域名白名单(默认 sandbox.users.io,须带域前缀),annotation 走黑名单(禁 kubernetes.io/k8s.io/agents.x-k8s.io 等系统域);还专门拦截伪造 app=sandbox-router 这类系统标签防冒充。与模板重名但值不同一律拒(不许覆盖)。

这些校验产生的错误都在 suppressErrors(:105)里——它们是用户配置错误,Reconcile 末尾 shouldSuppressError(:2138)会把它们「吞掉不返回 error」,只写进 Status 条件,避免因用户填错而陷入 crash-loop 式的无限重试。


6. 控制器主线 C:Template 控制器(点到为止)

SandboxTemplateReconciler.Reconcile(sandboxtemplate_controller.go:52)职责单一,就两件事:

  1. 打标签: ensureTemplateRefHashLabel(:169)确保模板自身带 sandboxTemplateRefHash 标签,供沙箱/NetworkPolicy 用选择器关联。
  2. 物化那条共享 NetworkPolicy:
    • Unmanaged → 删掉控制器建过的策略(交给 Cilium 等外部系统),不是自己建的不删(:85-99)。
    • Managed + networkPolicy=nilbuildDefaultNetworkPolicySpec(:192)套「安全默认」(见 §3.1)。
    • Managed + 自定义 → 用用户的 ingress/egress,但 PodSelector/PolicyTypes 由控制器强制(保证 default-deny 姿态)。
    • IsControlledBy + 语义 DeepEqual 做幂等对齐:一致就 O(1) 返回,漂移了才 Update,不存在才 Create(:124-166)。

关键点:一个 Template 一条共享 NetworkPolicy。 不是每个沙箱一条,而是模板级共享——靠 sandboxTemplateRefHash 标签选中该模板下所有沙箱。改一次规则,CNI 就对该模板下现存+未来的所有沙箱统一生效。这也是为什么策略字段是「模板级」而非「Claim 级」。


7. 巧妙之处(可带走的技术)

  • 内存热池队列做快路。 领取不走 informer List(慢、可能陈旧),而用独立内存队列 + NodeSpread/FIFO/Ready 优先挑候选(getCandidate :780)。这是把「秒级领取」落地的关键——informer 那条路留给正确性,内存队列这条路负责延迟。
  • 哈希短路 + vettedHashes 缓存做陈旧检测。 先比标签哈希,不行才做归一化后的语义 DeepEqual,且同一旧哈希只深比一次(isSandboxStale :496)。把「全池扫描是否陈旧」从 O(N 次深比) 压到 O(去重后的版本数)。
  • 归一化再比较,避免自己误判自己。 比较前对期望 PodSpec 套上和创建时相同的 ApplySandboxSecureDefaults(comparePodSpecs :553),消除控制器注入字段导致的假阳性——这是所有「声明式对齐」都会踩、但容易漏的坑。
  • 用哨兵错误绕开退避限流(#1107)。 errAdoptionTriggeredRetry + 50ms 有界 requeue 替代普通 error 的指数退避(Reconcile :363),把「等缓存收敛」这种必然复发的重试和「真失败」的退避彻底分开。领取尾延迟不再被退避炸掉。
  • 慢启动批处理探雷。 slowStartBatch(:647)从 1 开始指数放大,配置错时只赔 1 次 API 调用,正常时快速拉满并发。
  • 安全默认落到网络层。 egress 显式 Except 掉全部内网段 + 元数据服务器(buildDefaultNetworkPolicySpec :192),从 CNI 层堵住 agent 探内网 / 偷云凭证的路,而非只靠应用层自觉。
  • 用户错误 suppress 不 crash-loop。 配置类错误归入 suppressErrors 只写 Status 不重试(shouldSuppressError :2138),把「用户填错」和「系统故障」区分对待。

8. 边界与局限(诚实)

  • 领取有短暂尾延迟。 informer 缓存滞后时,一次领取需要额外 50ms 的 requeue 收敛(§5.4),不是严格「零延迟」。
  • 依赖缺失靠定时重试。 WarmPool/Template 还没建好时,Claim 走 1 分钟 requeue(Reconcile :344-350);代码里挂了 TODO 说这相对「即时 watch 触发」是延迟回退,尚未做 Template→claims 的 watch 映射。
  • 带 env/卷必冷启动,享受不到热池加速。 只要 Claim 填了 envvolumeClaimTemplates,一律绕过队列冷启动(getOrCreateSandbox :1674)——专属配置和「通用预热」天然互斥。
  • 卷模板比较对顺序敏感。 compareVolumeClaimTemplates(:569)按下标比,重排等价列表会被判为陈旧(为与 +listType=atomic 的哈希一致,刻意为之)。
  • 陈旧只认 SandboxBlueprint 改模板的 label/annotation/策略不触发 recreate(§3.2),依赖 compareSandboxBlueprint 覆盖全字段,漏字段就漏检测(靠 sandbox_types.go:157 的注释人肉约束)。

9. 横向对比

同 shelf 里「一个进程/容器即沙箱」的兄弟项目取舍不同:本项目把「造」与「用」在时间上解耦,用 Kubernetes 原生的声明式对齐 + informer + 自定义内存队列换来「秒级领取 + 可 HPA 伸缩的预热池」,代价是引入了 informer 缓存滞后这类分布式一致性难点(§5.4)。核心沙箱模型见 01/02,数据面领取后如何连上见 04-sandbox-router.md,客户端如何发起 Claim 见 05-client-sdks.md


10. 代码地图(导航索引)

主题文件关键符号
Template 数据模型 + 策略枚举extensions/api/v1beta1/sandboxtemplate_types.goSandboxTemplateSpecNetworkPolicyManagementEnvVarsInjectionPolicyVolumeClaimTemplatesPolicy
WarmPool 数据模型 + scale 子资源extensions/api/v1beta1/sandboxwarmpool_types.goSandboxWarmPoolSpecSandboxWarmPoolUpdateStrategyTypeTemplateRefField
Claim 数据模型 + 生命周期extensions/api/v1beta1/sandboxclaim_types.goSandboxClaimSpecLifecycleShutdownPolicyEnvVar
WarmPool 补池主循环extensions/controllers/sandboxwarmpool_controller.goReconcilereconcilePoolslowStartBatch
陈旧检测与模板哈希extensions/controllers/sandboxwarmpool_controller.gofetchTemplateAndHashisSandboxStalecompareSandboxBlueprintcomputeSandboxBlueprintHash
Claim 领取主循环extensions/controllers/sandboxclaim_controller.goReconcilereconcileActivegetOrCreateSandbox
从热池 adopt + 缓存滞后处理extensions/controllers/sandboxclaim_controller.goadoptSandboxFromCandidatescompleteAdoptionerrAdoptionTriggeredRetrytriggeredAdoptionMap
候选挑选策略extensions/controllers/sandboxclaim_controller.gogetCandidatepickSmartverifySandboxCandidateisAdoptable
冷启动新建 + 注入校验extensions/controllers/sandboxclaim_controller.gocreateSandboxmergeVolumeClaimTemplatesinjectEnvsvalidateAdditionalPodMetadata
过期/TTL 计算internal/lifecycle/expiry.goExpireAtTimeLeftFinishedCondition
安全默认extensions/controllers/utils.goApplySandboxSecureDefaultsSandboxTemplateRefHash
Template 控制器 + 默认网络策略extensions/controllers/sandboxtemplate_controller.goReconcilebuildDefaultNetworkPolicySpecensureTemplateRefHashLabel