KServe 源码拆解系列 · 第 3 篇
第 2 篇里,predictor、transformer、explainer 安静地躺在一个 YAML 里。可你 kubectl apply 一按回车,这几百行配置就要在几秒内变成 Deployment、Service、VirtualService------谁在动手?顺序是什么?中途失败怎么办?本篇跟一次完整的调和循环:起点是 pkg/controller/v1beta1/inferenceservice/controller.go 的 Reconcile(),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:WorkloadReconciler、ServiceReconciler、IngressReconciler 三个接口,每个都有 Reconcile 加 SetControllerReferences(盖 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 先建避免启动延迟。
失败重试与状态机:错误去哪了
三种"再来一次":
- 返回 error:workqueue 指数退避重试(默认 5ms 起步、封顶 1000 秒),任何相关资源变化也会重新触发。
- RequeueAfter :定时再入队。predictor.go:285-289 在模型加载期间返回
ctrl.Result{RequeueAfter: time.Second}------每秒回看模型状态。 - 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 条件),反之发 InferenceServiceReady。kubectl 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。