调和循环:kubectl apply 之后发生了什么

KServe 源码拆解系列 · 第 3 篇

第 2 篇里,predictor、transformer、explainer 安静地躺在一个 YAML 里。可你 kubectl apply 一按回车,这几百行配置就要在几秒内变成 Deployment、Service、VirtualService------谁在动手?顺序是什么?中途失败怎么办?本篇跟一次完整的调和循环:起点是 pkg/controller/v1beta1/inferenceservice/controller.goReconcile(),spec 到 Deployment 的翻译细节留给第 4 篇。


kubectl apply 之后,谁在看?

答案是 controller-runtime 的 informer。第 1 篇见过 SetupWithManager 里的 ctrlBuilder,三种注册就是看门的:

go 复制代码
ctrl.NewControllerManagedBy(mgr).
	For(&v1beta1.InferenceService{}). // ← 主对象:isvc 增删改都触发调和
	Owns(&appsv1.Deployment{}).       // ← 子资源:我创建的,它变了也要调和
	Owns(&corev1.Service{})
...
ctrlBuilder = ctrlBuilder.
	Watches(&v1alpha1.ServingRuntime{}, handler.EnqueueRequestsFromMapFunc(r.servingRuntimeFunc),
		builder.WithPredicates(servingRuntimesPredicate())). // ← Runtime 变了,反查
	Watches(&corev1.Pod{}, ...)                             //   在用的 isvc 重调
return ctrlBuilder.Complete(r)

For 盯主对象;Owns 盯子资源------KServe 创建的每个子资源都盖 ownerReference 指向 isvc,子资源事件因此能映射回父对象;Watches 盯任意资源,靠 map 函数反查。Pod 的 watch 带 predicate:只放行带 serving.kserve.io/inferenceservice-name 标签、init 容器状态有变化的 Pod------storage-initializer 的下载进度就记在 init 容器状态里(第 5 篇伏笔)。

事件流向:watch → 限速 workqueue → Reconcile(ctx, req),req 里只有一个名字+命名空间,第一行永远是 r.Get 把对象捞出来。


Reconcile 主流程:9 步走完一次调和

markdown 复制代码
kubectl apply ─▶ API Server ─▶ informer 缓存 ─▶ 限速 workqueue ─▶ Reconcile(ctx, req)

Reconcile() 内部(controller.go:122-418):

 1. r.Get(isvc)                 ← NotFound 就返回,ownerRef 级联回收子资源
 2. 读 inferenceservice-config  ← 集群级配置:部署模式、ingress、镜像默认
 3. GetDeploymentMode()         ← Raw 还是 Serverless 在这拍板(下节)
 4. finalizer 检查              ← 首次调和补上 inferenceservice.finalizers
 5. InitializeConditions()      ← 保证 status 有 Ready 条件可写
 6. 组装组件链                  ← YAML 写了 Transformer 才加 Transformer
 7. 组件逐个 Reconcile          ← 生成 Deployment / Knative Service / HPA
 8. Ingress + modelConfig       ← 流量入口 + 多模型 ConfigMap
 9. updateStatus()              ← 子资源状态翻译成 Conditions 写回

第 4 步,删除路径全靠它(controller.go:177-213 精简):

go 复制代码
if isvc.DeletionTimestamp.IsZero() {   // 不是删除请求
	if !controllerutil.ContainsFinalizer(isvc, finalizerName) {
		controllerutil.AddFinalizer(isvc, finalizerName) // MergePatch 只改 finalizers
	}
} else {
	if err := r.deleteExternalResources(ctx, isvc); err != nil {
		return ctrl.Result{}, err // ← 删不干净就报错重试,对象"卡"在删除中
	}
	controllerutil.RemoveFinalizer(isvc, finalizerName) // ← 摘掉后放行删除
	return ctrl.Result{}, nil
}

为什么手动清理?deleteExternalResources 按标签 ParentInferenceServiceLabel 删 TrainedModel------不归 isvc 的 ownerRef 管,级联回收够不着。

第 7 步的失败处理模式(controller.go:291-304):

go 复制代码
result, err := reconciler.Reconcile(ctx, isvc)
if err != nil {
	r.Recorder.Eventf(isvc, corev1.EventTypeWarning, "InternalError", err.Error())
	if err := r.updateStatus(ctx, isvc, deploymentMode); err != nil { ... }
	return reconcile.Result{}, errors.Wrapf(err, "fails to reconcile component")
}
// result.RequeueAfter > 0 时直接透传------组件说"稍后再来",主循环照办

这个模式贯穿整个文件:先落盘状态,再返回错误 ------失败原因进 conditions,kubectl get isvc 看得到;重试交给 workqueue。


双模式:一个 annotation 决定两条路

deploymentMode 不是 spec 字段,是 annotation------serving.kserve.io/deploymentMode(constants.go:115)。解析器 GetDeploymentMode(utils/utils.go:224-248)优先级三级:status 记录 > annotation > ConfigMap 默认值;旧写法 RawDeployment/Serverless 归一成 Standard/Knative

status 优先意味着模式一旦生效就锁定 ------updateStatus 会把生效模式写进 status.DeploymentMode(controller.go:423-424),改 annotation 也不会切模式;Deployment 已建好,中途改模式会让资源变孤儿。

两条路的分岔在 predictor.go:224:

go 复制代码
if p.deploymentMode == constants.Standard {
	rawDeployment = true
	if err := p.reconcileRawDeployment(...); err != nil { ... }   // ← Deployment+Svc+HPA
} else {
	if kstatus, err = p.reconcileKnativeDeployment(...); err != nil { ... } // ← Ksvc
}
复制代码
Standard(Raw):   Predictor ─▶ Deployment + Service + HPA  ← 更新、扩缩容自己管
Knative(Serverless):Predictor ─▶ Knative Service         ← revision/KPA/流量全托管

模式还在控制器层设卡:解析出 Knative 但集群没装 Knative,发 Warning 事件 ServerlessModeRejected 并返回 reconcile.TerminalError(controller.go:249-254)------"重试没用,等新事件"。ModelMesh 且没 transformer 的 isvc 跳过整个调和(controller.go:158-174):predictor 归 ModelMesh 控制器管。


Reconciler 链:factory 如何把小组件装起来

KServe 不是一个大函数把 Deployment 拼出来,而是一棵小 reconciler 组成的树。"积木规格"在 reconcilers/interfaces.go:WorkloadReconcilerServiceReconcilerIngressReconciler 三个接口,每个都有 ReconcileSetControllerReferences(盖 ownerRef)。

按模式选积木的是 ReconcilerFactory(reconcilers/factory.go),注释里的 Phase 1: Only supports Deployment 说明重构进行中:工厂目前只在 ingress(controller.go:360 起)和 Raw 路径内被调用。树的全貌:

scss 复制代码
InferenceServiceReconciler                    controller.go
├── CaBundleConfigMapReconciler
├── Predictor.Component ──┬─(Standard)▶ RawKubeReconciler ─┬▶ Deployment
│                         │                                ├▶ Service
│                         │                                └▶ HPA(autoscaler)
│                         └─(Knative)─▶ KsvcReconciler ─────▶ Knative Service
├── Transformer / Explainer(结构同上,YAML 写了才有)
├── IngressReconciler(factory 按模式选:Ingress / HTTPRoute / Istio VS)
└── ModelConfigReconciler

这是组合 ,不是责任链:每层只干自己的活,错误逐级 wrap 上抛。中间层 RawKubeReconciler 最典型(reconcilers/raw/raw_kube_reconciler.go:48-56):

go 复制代码
type RawKubeReconciler struct {
	Workload  reconcilers.WorkloadReconciler    // ← Deployment
	Service   reconcilers.ServiceReconciler     // ← k8s Service
	Scaler    *autoscaler.AutoscalerReconciler  // ← HPA/KEDA
	URL       *knapis.URL
}

它的 Reconcile 有两个讲究。一是先统一盖章 :所有子资源先 SetControllerReferences(owner) 再 apply------"组件永远不可能创建出孤儿资源"。二是顺序:Service 先于 Deployment------serving-cert 等注解会触发 secret 创建,Pod 要挂载这些 secret,Service 先建避免启动延迟。


失败重试与状态机:错误去哪了

三种"再来一次":

  1. 返回 error:workqueue 指数退避重试(默认 5ms 起步、封顶 1000 秒),任何相关资源变化也会重新触发。
  2. RequeueAfter :定时再入队。predictor.go:285-289 在模型加载期间返回 ctrl.Result{RequeueAfter: time.Second}------每秒回看模型状态。
  3. TerminalError:上节 Knative 缺失的场景,直接不重试。

状态机的心跳在 pkg/apis/serving/v1beta1/inference_service_status.go。总 Ready 只由两个条件决定:conditionSet = apis.NewLivingConditionSet(PredictorReady, IngressReady)

子资源状态怎么翻译成这些条件?PropagateRawStatus(同文件 379-448 行)读 Deployment 的条件:Available=True 即 True;Progressing=False 带消息即 False(常见 ProgressDeadlineExceeded);ReplicaFailure=True 即 False;Progressing=True 但 Available 未确认即 Unknown。

模型加载走另一条线 PropagateModelStatus(751-781 行):看 storage-initializer 这个 init 容器------Running 即 Loading,Terminated(Error)/CrashLoopBackOff 即 FailedToLoad,exitCode 和报错进 modelStatus.lastFailureInfo。第 1 节 Pod watch 的落点就在这。

最后一道保险在 updateStatus(controller.go:432-436):写之前先 DeepEqual 比较------"informer 缓存可能过期,别拿旧状态覆盖新状态";写成功后再比对 Ready 翻转------变 NotReady 发 InferenceServiceNotReady 事件(附全部 False 条件),反之发 InferenceServiceReadykubectl describe 的 Events 就是这么来的。


一句话总结

kubectl apply 之后:informer 把事件塞进限速队列,Reconcile() 用名字把对象捞出来------补 finalizer、按 serving.kserve.org/deploymentMode 选 Raw/Knative 路径、把组件装配成 reconciler 树逐层生成资源;失败先写回状态再报错,交给退避重试;最后把子资源的 Conditions 翻译成 InferenceService 的 Ready。

下一篇预告:第 4 篇 从 Spec 到资源。Predictor/Transformer/Explainer 的 reconciler 内部------PodSpec 怎么拼、storage-initializer 和 agent 容器怎么注入、ServingRuntime 配置怎么合并进你的 YAML。

相关推荐
KAIWEILIUCC1 小时前
Ray:高性能、易扩展的Python分布式计算框架
分布式·llm
众人皆醒我独醉1 小时前
从 Spec 到资源:Predictor/Transformer/Explainer 如何变成 Deployment
面试·llm·gpu
面试鸭1 小时前
Codex 跑进法务部,周活居然暴涨了 108 倍???
后端·面试·求职
tachibana22 小时前
RAGAS 指标解读
数据库·人工智能·算法·机器学习·架构·大模型·llm
a187927218312 小时前
从一条直线到大模型输出一个token(五):Transformer Block 全景与层归一化
ai·大模型·llm·transformer·layernorm·deepseek·层归一化
谢白羽2 小时前
vLLM-Omni 部署 IndexTTS 2.5
llm·agent·tts·vllm·大模型部署
Komorebi_99993 小时前
RAG23-RAG28
llm·rag
武子康3 小时前
单卡 A6000 跑 27B FP8:我把“能跑”拆成了五组证据
人工智能·llm·agent
网络工程小王3 小时前
【LLM开发实验】 QLoRA实现原理及实战
人工智能·深度学习·机器学习·llm·qlora