数据截至 (上游 commit ac864a6fe3bd)
核心控制器:reconcile 主循环、子资源与所有权
30 秒导读: 用户写下一个
Sandbox对象(见 01-sandbox-api-model), 谁来把它变成一个真正跑着的 Pod?就是本章的SandboxReconciler。它是整个项目的主线: 一个不断被触发的Reconcile函数,每次都把"用户想要的样子"和"集群里现在的样子"对齐—— 该建的建、该删的删、该领养的领养,并把结果写回.status。全部逻辑在controllers/sandbox_controller.go(约 1370 行,单文件)。
1. 这是什么(零基础也能懂)
一句话定义: 这是一个 Kubernetes 控制器(controller)——一段常驻程序,盯着 Sandbox
这种自定义资源,负责把它"落地"成真实的 Pod / Service / PVC。
它要解决的问题。 用户只想说一句"给我一个沙箱,用这个镜像"。但 Kubernetes 里一个能用的 沙箱其实是一堆东西:一个跑容器的 Pod、一个给它稳定网络名字的 Service、可能还有几块持久化 磁盘(PVC)。有人得把这句"想要"翻译成这一堆资源,还得在它们出问题时修好——这就是控制器。
控制器的核心心法:reconcile(调和)。 它不是"收到创建请求就建一次"的那种命令式代码。 它是一个幂等的对齐函数:不管当前是什么状态,每次运行都朝"用户声明的目标状态"推一步。
用户改了 Sandbox / Pod 挂了 / 定时到点
│ (事件触发)
▼
┌──────────────────────────────┐
│ Reconcile(req) │ ← 每次都问:现在长啥样?想要啥样?
│ 1. 读 Sandbox 对象 │
│ 2. 到期了吗? ── 是 ─► 清理 │
│ 3. 否则:拉齐 Pod/Svc/PVC │
│ 4. 算 conditions,写回 status │
└──────────────┬───────────────┘
▼
返回 RequeueAfter(什么时候再来看一眼)
一句话直觉/类比: 把它想成一个恒温器。你设定"我要 22℃"(Sandbox spec),恒温器不停地 测当前温度、开关空调,直到达到目标——你从不直接命令空调"开 3 分钟",你只声明目标,它负责收敛。
本章之后的部分,就是逐层拆开这个"恒温器"的内部。
2. 顶层全景(它大概怎么转)
2.1 一次 Reconcile 的骨架
真正的入口是 Reconcile(controllers/sandbox_controller.go:283-391,符号 SandboxReconciler.Reconcile)。
它的主流程可以一眼读完:
Reconcile(ctx, req)
│
├─① Get(Sandbox) 找不到? → 说明已删,直接返回,不报错 :158-164
│
├─② 起 trace span + 注入 traceID(可观测性,inline 不重排队) :166-195
│
├─③ DeletionTimestamp 非零? → "正在删除",啥也不做直接返回 :178-181
│ (真正的级联删除交给 K8s 的 ownerReference 垃圾回收)
│
├─④ 到期检查 checkSandboxExpiry(sandbox, now) :202
│ ├─ 已到期 & 还没标记 → 打 Expired 条件、写 status、1ms 后再来 :203-210
│ ├─ 已到期 & 已标记 → handleSandboxExpiry(删子资源+看关机策略) :212-213
│ └─ 没到期 → reconcileChildResources(拉齐三件套) :215
│ 再算一次到期,决定 RequeueAfter :216-221
│
└─⑤ 若 sandbox 未被删 → updateStatus(只在 status 变了才写) :224-230
return {RequeueAfter}, err
怎么读这张图: 从上往下是短路优先——先处理"不存在 / 正在删 / 已到期"这些特殊态,
把它们挡掉;只有"活着且没到期"的正常沙箱,才会走到④的 reconcileChildResources,那才是主戏。
2.2 谁调谁:核心函数地图
| 层级 | 函数 | 职责 | 位置(符号) |
|---|---|---|---|
| 入口 | Reconcile | 主循环,分派 | :154 |
| 主戏 | reconcileChildResources | 依次拉齐 PVC→Pod→Service,再算 conditions | :235 |
| 子资源 | reconcilePVCs | 建/领养持久卷声明 | :1139 |
| 子资源 | reconcilePod | 建/查/领养 Pod;Suspended 时删 Pod | :685 |
| 子资源 | reconcileService | 按需建/删/领养 headless Service | :513 |
| 元数据 | updatePodMetadata | 把标签/注解安全地同步到已存在的 Pod | :973 |
| 到期 | checkSandboxExpiry / handleSandboxExpiry | 判定到期 / 到期清理 | :1301 / :1220 |
| 条件 | computeConditions 等 | 算 Ready/Suspended/Finished 条件 | :278 |
| 装配 | SetupWithManager | 注册索引、watch、并发度 | :1346 |
2.3 三件套的顺序与 status 拼装
reconcileChildResources(:235-276)是把"想要"变"现有"的地方,顺序固定:
nameHash = NameHash(sandbox.Name) ← 全流程的"身份标签值",见 §7
│
├─ reconcilePVCs ── 建卷,失败不中断(errors.Join 累积) :242
├─ reconcilePod ── 返回 pod;pod==nil 则清空 PodIPs/NodeName :246-255
└─ reconcileService── 返回 svc :258
│
▼
computeConditions(sandbox, allErrors, svc, pod) → 写入 .status.Conditions :262-273
注意一个容错设计:三件套的错误用 errors.Join 累积而不是遇错即返(:243/247/259),
所以即使 PVC 建失败,Pod 逻辑仍会跑,status 也仍会算——控制器尽量多做事,再把所有错一起上报。
3. 子资源三件套(逐个走读)
这一节是本章的肉。三个 reconcileXxx 函数长得像,但各有各的坑。先建立一个共同心智模型:
每个函数都在回答同一组问题——它在不在?在的话归谁管?该建、该改、该删、还是该领养?
3.1 reconcilePVC:最简单的那个
reconcilePVCs(:1139-1217)遍历 spec.VolumeClaimTemplates,对每个模板算出名字
pvcName = <模板名>-<sandbox 名>(:1148),然后:
- 已存在 → 走所有权判定(见 §4),owned 就跳过、unowned 且授权就领养、别人的就报错。
- 不存在 → 用模板 Spec 建一个新 PVC,盖上
sandboxLabel=nameHash追踪标签,并SetControllerReference认领(:1196-1211)。
PVC 是最干净的样板:没有删除逻辑(卷通常要留住数据),只有"建"和"领养"。它是理解另外两个复杂函 数的起点。
3.2 reconcilePod:主角,冷/热两条路
reconcilePod(:685-971)是全文件最核心、也最长的函数。它做的第一件事很讲究——不是
直接按名字去 Get Pod,而是先用缓存字段索引列出所有带本沙箱追踪标签的 Pod:
// controllers/sandbox_controller.go:695-702 —— 靠 SetupWithManager 注册的索引做 O(1) 查找
podList := &corev1.PodList{}
r.List(ctx, podList,
client.InNamespace(sandbox.Namespace),
client.MatchingFields{podSandboxNameHashIndex: nameHash}, // 见 §7
)
接着确定"该找哪个 Pod 名"。这里是热池领养的关键:普通沙箱的 Pod 名就等于 sandbox.Name;
但如果这个沙箱是从热池(WarmPool)领来的,它领到的 Pod 名字是别的,记在注解里——由
resolvePodName 解出(:89-94,符号 resolvePodName):
// controllers/sandbox_controller.go:89-94 —— 有 pod-name 注解就用注解里的名字,否则用 sandbox 名
func resolvePodName(sandbox *sandboxv1beta1.Sandbox) string {
if name, ok := sandbox.Annotations[sandboxv1beta1.SandboxPodNameAnnotation]; ok && name != "" {
return name // = "agents.x-k8s.io/pod-name",指向领养来的热池 Pod
}
return sandbox.Name
}
冷启动(自己建 Pod)vs 热启动(领养预热好的 Pod)的完整机制,是 03-extensions-warmpool-claim 的主题;本章只讲控制器侧 "解析名字 → 找到 → 领养"这一半。
reconcilePod 的三条出口:
| 情形 | 行为 | 位置 |
|---|---|---|
OperatingMode == Suspended | 删掉 owned 的 Pod,清 pod-name 注解,返回 nil(见下) | :731-760 |
| 找到了 Pod(自己建的 / 领养的 / 已存在的) | reconcileExistingPod:判所有权、同步元数据、确保注解 | :789-854 |
| 没找到 Pod | 从 PodTemplate 构造并 Create;撞 AlreadyExists 就转去走"已存在"分支 | :856-970 |
Suspended = 优雅暂停。 挂起态下控制器主动删 Pod(:737-738)但保留 Sandbox 对象和 PVC——
相当于"关机但不销毁",磁盘还在,以后能再拉起。删之前照样要过所有权检查(:733-751):不是自己
owned 的 Pod 绝不删。
建新 Pod 的清洗步骤(安全关键,§5 详述): 从用户的 PodTemplate 拷标签/注解到新 Pod 时,
会逐个丢弃系统保留键(:861-869 用 isSystemLabel 过滤),再在合并之后盖上系统标签
podLabels[sandboxLabel] = nameHash(:871)——顺序很重要,保证用户永远盖不掉系统标签。
3.3 reconcileService:按需存在的 headless Service
reconcileService(:513-656)比 Pod 多一个维度:Service 可有可无,由 spec.Service(*bool)
决定。它是一个 headless Service(ClusterIP: "None",:535)——不做负载均衡,只提供一个稳定
DNS 名,靠 selector: {sandboxLabel: nameHash} 把流量指到那个 Pod。
三态真值表(desired = spec.Service):
| Service 现状 | desired=true | desired=false | desired=nil |
|---|---|---|---|
| 不存在 | 创建 headless Service :524-552 | 不建,清 status :555 | 不建,清 status :555 |
| 存在 · 自己 owned | 修正 selector/label 漂移 :627-651 | 删除 :566-571 | 保持 :627 |
| 存在 · unowned | 授权则领养 :586-625 | 清 status :573 | 不领养,清 status :587-591 |
| 存在 · 别人 owned | 报错拒用 :579-584 | 清 status :573 | 报错拒用 :579-584 |
领养 unowned Service 时还有一道额外闸门:ClusterIP 必须是 None 或空(:602-608)。因为
ClusterIP 是不可变字段,若领来一个普通(有 IP 的)Service 再改 selector 也没意义,直接拒绝。