KServe 源码拆解系列 · 第 4 篇
第 3 篇走完了调和循环主流程:事件进来,控制器按 spec 组装 reconciler 链,逐个组件调和。但"调和一个组件"这一步里发生了什么?你的 YAML 只有几行 predictor:、transformer:,集群里却长出了 Deployment、Service、探针、资源配额。意图声明(Spec)和集群现实(资源)之间的这段距离,就是本篇要拆的:组件构建器。结尾留下线头------模型怎么进 Pod------交给第 5 篇。
翻译分两段:webhook 改 Spec,构建器改现实
Spec → Deployment 的翻译分两个阶段:admission webhook 阶段改 Spec (第 2 篇讲过),Reconcile 阶段建现实 (本篇主场)。阶段一的伏笔:Defaulter 在对象落库前把老式框架 predictor 改写成 Model + ServingRuntime (pkg/apis/serving/v1beta1/inference_service_defaults.go):
go
isvc.Spec.Predictor.Model = &ModelSpec{...}
isvc.Spec.Predictor.SKLearn = nil // ← 老字段被清掉
你写的 predictor.sklearn 进集群时已被换成 predictor.model(modelFormat=sklearn),所以构建器主线里 Model 路线是第一优先级(ServingRuntime 细节第 8 篇)。
阶段二的代码在 pkg/controller/v1beta1/inferenceservice/components/:predictor.go、transformer.go、explainer.go 三个构建器,实现同一接口 Component{ Reconcile(ctx, isvc) }(component.go),三者结构同构,读懂 predictor 就懂全部。
predictor.go:一个 PodSpec 怎么拼出来
Predictor.Reconcile 的第一步是 buildPredictorResources,核心是二选一(写了 storageUri 则只记注解 internal.serving.kserve.io/storage-initializer-sourceuri,不动 PodSpec):
go
if isvc.Spec.Predictor.Model != nil {
sRuntime, runtimeAnnotations, err = p.reconcileModel(ctx, isvc, multiNodeEnabled) // ← 路线一:选 runtime
podSpec, err = p.buildPodSpec(isvc, sRuntime, runtimeAnnotations) // ← 拼容器
} else { // 路线二:框架/custom 兜底,直接用你写的容器
podSpec = corev1.PodSpec(isvc.Spec.Predictor.PodSpec)
podSpec.Containers = []corev1.Container{*predictor.GetContainer(...)}
}
镜像从哪里来
reconcileModel 按名字找 ServingRuntime(GetServingRuntime),没写名字就按 modelFormat 自动匹配(GetSupportingRuntimes,按 priority 排序)。选中的 runtime 长这样(config/runtimes/kserve-sklearnserver.yaml 节选):
yaml
- name: kserve-container
image: kserve-sklearnserver:replace # ← 镜像在这(安装时 kustomize 替换 tag)
args: [--model_name={{.Name}}, --model_dir=/mnt/models, --http_port=8080]
所以答案:runtime 的容器是模板,你的 Spec 是补丁 。buildPodSpec 完成拼装:
go
_, predContainer, mergedPodSpec, err = isvcutils.MergeServingRuntimeAndInferenceServiceSpecs(
sRuntime.Containers, isvc.Spec.Predictor.Model.Container, isvc,
constants.InferenceServiceContainerName, // ← 目标容器固定叫 "kserve-container"
sRuntime.ServingRuntimePodSpec, isvc.Spec.Predictor.PodSpec)
if err = isvcutils.ReplacePlaceholders(predContainer, isvc.ObjectMeta); err != nil {...} // ← {{.Name}} → sklearn-iris
合并规则(utils/utils.go):runtime 容器打底,你 Spec 覆盖同名字段,Args 是拼接不是替换 。ReplacePlaceholders 用 text/template 把整个容器 JSON 渲染一遍。之后 UpdateImageTag:指定 runtimeVersion 就改 tag;开了 GPU 且没指定版本时,给部分框架镜像补 -gpu 后缀。
存储挂载:只写注解,挂载交给别人
挂载在 Reconcile 按 deploymentMode 分岔:Raw(Standard)模式下 raw.NewRawKubeReconciler 构建期调 pod.CommonStorageInitialization,直接把 storage-initializer init 容器塞进 PodSpec;Knative 模式只创建 knative Service(源码注释原话:"Knative 不支持 init 容器"),注解留给 Pod webhook 在 Deployment 生成时注入------第 5 篇的主线,先埋个钩子。
三个组件如何串起来:拓扑与命名规则
三个构建器产出的资源,靠命名规则 和启动参数 咬合。以 sklearn-iris 为例:
arduino
┌──────────▼──────────┐
│ Ingress 网关 │ VirtualService / HTTPRoute(第 6 篇)
└──────────┬──────────┘
┌──────────▼──────────┐
│ transformer(可选) │ sklearn-iris-transformer
└──────────┬──────────┘
│ --predictor_host sklearn-iris-predictor.default
┌──────────▼──────────┐
│ predictor │ sklearn-iris-predictor
└──────────────────────┘
▲
┌──────────┴──────────┐
│ explainer(可选) │ sklearn-iris-explainer(旁路,不挡主链路)
└──────────────────────┘
命名规则集中在 pkg/constants/constants.go:
go
func PredictorServiceName(name string, predictorName ...string) string {
if len(predictorName) > 0 && predictorName[0] != "" {
return name + "-" + predictorName[0] + "-" + string(Predictor) // isvc-名-predictor
}
return name + "-" + string(Predictor) // isvc-predictor
}
TransformerServiceName = isvc-transformer,ExplainerServiceName = isvc-explainer,Deployment 和 Service 同名 。Service 由 reconcilers/service/service_reconciler.go 的 createDefaultSvc 生成:端口 80 转发到容器第一个端口,selector 是 app: isvc.<组件名>,gRPC 端口还会自动打上 kubernetes.io/h2c AppProtocol。transformer 怎么找到 predictor?不靠服务发现,而是构建期把地址写进启动参数(pkg/apis/serving/v1beta1/transformer_custom.go):
go
argumentPredictorHost := fmt.Sprintf("%s.%s", predictorHost[0], metadata.Namespace)
container.Args = append(container.Args,
constants.ArgumentModelName, metadata.Name, // --model_name sklearn-iris
constants.ArgumentPredictorHost, argumentPredictorHost) // --predictor_host <svc>.<ns>
transformer.go 把 PredictorServiceName(...) 算出来传给 GetContainer,explainer.go 同理。三组件间是构建期写死的点对点调用。
一条捷径:collocation(同 Pod 部署) 。predictor 的 buildPodSpec 检查 ServingRuntime 和 Spec 里有没有 transformer-container,有就合并进同一个 PodSpec,省一次网络往返。
资源配置:GPU/内存从 Spec 到 Pod 的路径
你写在组件实现里的 resources:,路径很直接:defaulter 补默认 → MergeRuntimeContainers 用你的值覆盖 runtime → 原样进 Deployment.Spec.Template.Spec。单节点场景控制器不加工 GPU/内存,真正动资源的是三处:
1. defaulter 补默认值。 setResourceRequirementDefaults(inference_service_defaults.go)从 inferenceservice-config ConfigMap 读 cpuRequest/memoryRequest/cpuLimit/memoryLimit,只填你没写的键------用户显式写的永远优先。
2. 多节点重算 GPU。 写了 workerSpec 时,multiNodeProcess 按 PipelineParallelSize/TensorParallelSize 算出节点数并分配 GPU,deployment_reconciler.go 的 addGPUResourceToDeployment 再写回 limits+requests。默认 GPU 类型 nvidia.com/gpu,可用注解 serving.kserve.io/custom-gpu-resource-types 扩展。
3. 扩缩容字段分流。 minReplicas/maxReplicas 不直接进 Deployment------它们交给 autoscaler reconciler(第 7 篇)。只有 autoscaler class 为 none 时,minReplicas 才直接写成 deployment.Spec.Replicas。
探针与优雅停机:Deployment 的最后一道加工
PodSpec 拼好后,createRawDefaultDeployment(reconcilers/deployment/deployment_reconciler.go)调 setDefaultPodSpec 收尾,最关键是默认 readiness 探针------只为 kserve-container 和 transformer-container 生成,仅在没定义时补:
go
if container.ReadinessProbe == nil { // ← 你/runtime 定义了就不动
container.ReadinessProbe = &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
TCPSocket: &corev1.TCPSocketAction{Port: intstr.IntOrString{IntVal: 8080}},
},
TimeoutSeconds: 1, PeriodSeconds: 10, FailureThreshold: 3,
}
}
默认探针只是 TCP 连 8080,怎么知道模型加载完没?答案在数据面:kserve/python 的模型服务器先加载模型、后监听端口 ------TCP 能连上,间接等于模型就绪。大模型加载要几分钟,runtime 就自带更精确的探针,如 config/runtimes/kserve-vllmserver.yaml 里 httpGet /v1/models(注释原话:"only returns 200 once the model is registered")。collocation 下同 Pod 两个服务都有探针,sidecar 一律不管。
优雅停机也在同一个函数:TerminationGracePeriodSeconds 未设置时补上 corev1.DefaultTerminationGracePeriodSeconds(30 秒)------SIGTERM 后有半分钟跑完 in-flight 请求。同函数还补:镜像拉取策略 IfNotPresent、滚动更新 MaxUnavailable 25% / MaxSurge 25%。多节点 worker 则强制 0% / 100%------先起新 Pod、就绪再杀旧 Pod。
最后一问:PodSpec 变了 Deployment 怎么更新?checkDeploymentExist 先 dry-run update 补全服务端默认值,再 kmp.SafeDiff 比对,有差异走 strategic merge patch。细节:挂了 HPA 的 Deployment,比对和 patch 都剔除 Replicas 字段------这个经典打架场景,KServe 在构建器里就防住了。
一句话总结
Spec 到 Deployment 的翻译分两段:webhook Defaulter 先把老框架 predictor 改写成 Model + ServingRuntime;构建器再以 runtime 容器为模板、你的 Spec 为补丁,合并出 PodSpec,交给 deployment/service/raw 三个 reconciler 落成同名 Deployment + Service。命名规则和构建期写入的 --predictor_host 把三个组件串成链路;资源直通 Pod,探针只补默认不覆盖,HPA 与 Deployment 的 replicas 冲突在比对阶段被主动隔离。
下一篇预告:第 5 篇 模型加载:storage-initializer 与节点级缓存。本篇埋下的注解
internal.serving.kserve.io/storage-initializer-sourceuri终于派上用场------它如何变成 Pod 里的 init 容器?模型为什么每起一个 Pod 就要重新下载?节点级缓存怎么把第二次启动提速?