Appearance
先让一份 ModelService 跑起来,再谈资源回收
把数据库暂时放在一边。你只需要回答:收到一份期望副本为 1 的声明,谁让它变成可用服务?第一轮先用专用 Kind 集群里的固定副本 chart-demo 做实验;它没有绑定 SQL 预留。能证明这条链以后,下一章才给它加预算。
CR 到 status 的实现路线
k8s/crd.yaml 规定 spec.image、replicas 等;Manager.For(ModelService).Owns(Deployment).Owns(Service) 把子对象漂移送回同一对象键。Reconcile 先登记 finalizer,再创建/更新可拥有的子对象,最后比较 observedGeneration、updatedReplicas 与 availableReplicas。只有已观察且副本数相符才报告 Running;重复 status 不写回。删除时 UID 前置条件保护同名新对象。
完整源码:modeldock/operator/main.go · 第 42–59 行
go
if !u.GetDeletionTimestamp().IsZero() {
for _, obj := range []client.Object{d, s} {
err := c.Get(ctx, req.NamespacedName, obj)
if client.IgnoreNotFound(err) != nil {
return done, err
}
if err == nil && meta.IsControlledBy(obj, u) {
uid := obj.GetUID()
if obj.GetDeletionTimestamp() == nil {
err = c.Delete(ctx, obj, client.Preconditions{UID: &uid}, client.PropagationPolicy(meta.DeletePropagationForeground))
}
return ctrl.Result{RequeueAfter: 1000000000}, client.IgnoreNotFound(err)
}
}
if cu.RemoveFinalizer(u, finalizer) {
return done, c.Update(ctx, u)
}
return done, nil进程重启不需要把旧事件按顺序重放,重新 List 就能从现状算出差异;MySQL 的额度回收仍需显式对账 worker。用控制器 envtest验证重复写,端到端测试验证删除后再次 release 不多还。
让控制器看到一次漂移
控制器实作对照两种方案:一次性的 API 调用足以响应“现在创建一个容器”,却不会在进程恢复后持续修复漂移;Operator 把意图存入 etcd,重复观察 CR 与子资源,在每次重启后继续收敛。成本是缓存滞后、幂等语义与 RBAC。通过专用实验集群观察其行为:
sh
export KUBECONFIG="$PWD/tmp/handbook.kubeconfig"
kubectl -n modeldock-lab get msvc,deploy,pods,svc
kubectl -n modeldock-lab scale deploy/chart-demo --replicas=0
kubectl -n modeldock-lab wait deploy/chart-demo --for=jsonpath='{.spec.replicas}'=1 --timeout=45s只在固定副本模式运行漂移实验。CPU Mock 在独立容器处理 SSE,内部只有两个执行位,其它请求排队,等待数暴露在 /metrics。模拟的目的是用相同 HTTP 协议和真实调度生命周期检验控制面。它既没有分词器,也没有权重和显存管理器。
再读:接入账本后,删除何时才算完成?
下面是读完资源预留后的回看路径。第一次阅读可先停在前面的漂移实验。
MySQL 提交不能和 K8s create 合并成一笔 ACID 事务:若 create 已被 API 接受,进程在标记 delivered 前崩溃,重试会遇到 AlreadyExists。这里用确定名称和 admission-id 标签回读确认,在专属实验 namespace 里重放;多写方环境要把请求摘要、资源 UID 和 fencing token 一起持久化,不能把可修改标签当鉴权。单步 tick 用于实验;serve 模式用 Gin 接收 POST /requests,并运行每秒 tick 的后台 worker。Authorization 决定租户,不从请求体接受 tenant;GET /requests/:id 同时约束 ID 与租户。配置 ADMISSION_KEYS_JSON 为租户到随机密钥的映射,每个密钥至少 16 字符。
投递确认时把实际 CR UID 和 outbox delivered 一起提交。reconcile ID 对账:对象缺失才尝试归还;UID 替换时停下;记住曾观察到的 Ready;初始 generation 超过十分钟仍从未观察到 Ready 时,用带 UID/resourceVersion 前置条件的 DeleteOptions 发起清退,下一轮确认消失后归还。它是显式运维命令,需定期调用才会持续补偿。
释放分两步:先删本次专用 CR,foreground GC 等子资源,确认目标 Pod(包括 Terminating)已不存在,再运行 release。重复 release 不二次归还额度。实际集群不能只看到 deletionTimestamp 就释放物理容量;网络分区时可能还有 GPU kernel 在旧节点运行。本实现保守地在任何受管 Pod 正终止时延迟释放;集群读期间事务持锁,慢 API 会延长锁等待。生产应拆成 draining → 已核对 UID/设备 → 条件提交的异步过程。
sequenceDiagram participant W as Worker participant D as MySQL participant K as Kubernetes W->>D: Commit quota and outbox W->>K: Create named ModelService K-->>W: Created or already exists W->>D: Acknowledge after identity check
看三处提交之间的空隙
- submit/admit 提交 SQL 后崩溃:outbox 仍在。2. 外部 Create 成功但确认丢失:确定名字回读对象身份,再确认 binding。3. 删除请求成功但 Pod 仍 Terminating:保持预留,等待真实状态后再归还。任何时候都不要用“过了多少秒”代替这三种证据。
完整源码:modeldock/admission/cluster.go · 第 95–113 行
go
func reconcile(db *sql.DB, id string) error {
var uid, state string
var seen bool
e := db.QueryRow("SELECT b.uid,b.ever_ready,r.state FROM binding b JOIN reservation r ON r.id=b.id WHERE b.id=?", id).Scan(&uid, &seen, &state)
if e != nil {
return e
}
if state == "released" {
return nil
}
cr, e := getCR(id)
if e != nil {
return e
}
if cr == nil {
return release(db, id)
}
if cr.Metadata.UID != uid {
return fmt.Errorf("replaced CR; operator investigation required")