KServe 源码拆解系列 · 第 6 篇
第 5 篇结束时,模型已经躺进 Pod 的 /mnt/models:storage-initializer 下载完,推理容器就绪。但集群外面一条 curl,凭什么能精确命中这个 Pod?中间隔着一整套指路系统:网关、VirtualService、Knative Route。本篇拆这套系统,也把概述文一笔带过的 canaryTrafficPercent 拆到源码层:百分比如何变成路由权重。而流量怎么分,直接连着下一篇的主角------自动扩缩容。
两层路由:请求进集群后先找谁
Knative 模式下,KServe 的流量路由是两层。看全景:
lua
curl → Istio Ingress Gateway:监听 *.example.com,TLS 在这终止
│ Host: sklearn-iris.default.example.com
▼
KServe 的 VirtualService(第 1 层)
match Host/URI → route 到 knative-local-gateway,
并改写 Host 头为 {isvc}-predictor.{ns}.svc...
│
▼
Knative 路由层(第 2 层)
local gateway → Route → Revision,
流量权重切分(canary)在这生效
│
▼
predictor Pod(第 4、5 篇建的那个)
为什么两层?KServe 负责"到服务",Knative 负责"到版本":VS 只表达"流量交给 knative-local-gateway",哪个 Revision 接、canary 怎么切是 Knative Route 的活。
代码顺序也如此:controller.go 里组件调和在前、// Reconcile ingress 在最后------先有服务,再指路;指路实现由工厂按部署模式选:
go
// reconcilers/factory.go
switch deploymentMode {
case constants.Standard, constants.LegacyRawDeployment: // ← Kubernetes Ingress / HTTPRoute
case constants.Knative, constants.LegacyServerless, constants.ModelMeshDeployment:
return ingress.NewIngressReconciler(...) // ← Istio VirtualService
换模式就换一套资源。下面拆两条路。
Serverless 模式:KServe 为什么依赖 Knative
Knative 模式下,每个组件不是 Deployment,而是 Knative Service(ksvc_reconciler.go,第 4 篇)。它除了把 PodSpec 塞进 RevisionTemplate,最关键的产出是 RouteSpec.Traffic------Knative Route 的流量权重表:
go
if componentExtension.CanaryTrafficPercent != nil && lastRolledoutRevision != "" {
trafficTargets = append(trafficTargets, knservingv1.TrafficTarget{
LatestRevision: proto.Bool(true),
Percent: proto.Int64(*componentExtension.CanaryTrafficPercent), // ← 新 Revision 拿 canary 百分比
})
if *componentExtension.CanaryTrafficPercent < 100 {
trafficTargets = append(trafficTargets, knservingv1.TrafficTarget{
RevisionName: lastRolledoutRevision, // ← 旧 Revision 拿余量
Percent: proto.Int64(100 - *componentExtension.CanaryTrafficPercent),
Tag: "prev",
})
}
} else { // ← 没写 canary:LatestRevision 一条,100%
trafficTargets = append(trafficTargets, knservingv1.TrafficTarget{
LatestRevision: proto.Bool(true), Percent: proto.Int64(100),
})
}
KServe 依赖 Knative 的三件事都在这段代码里:Revision 不可变版本 (canary/回滚载体)、Route 层流量切分 (权重表)、scale-to-zero 与 KPA(第 7 篇主角)。
细节:KsvcReconciler.Reconcile 更新前先做 dry-run Update ,把 Knative webhook 注入的默认值吸收进本地再比对------否则 EnableServiceLinks 这类服务端默认值会让调和误判"有变化"。"改没改"只看 ConfigurationSpec + RouteSpec + labels。
Raw 模式:Ingress 直连组件 Service
不装 Knative 时(模式 Standard,旧名 RawDeployment),组件是普通 Deployment + Service,工厂改派 RawIngressReconciler(kube_ingress_reconciler.go),产出一个普通的 Kubernetes Ingress:
go
&netv1.Ingress{
Spec: netv1.IngressSpec{
IngressClassName: ingressConfig.IngressClassName, // ← 交给集群的 Ingress Controller
Rules: rules, // ← backend 直指组件 Service,无中转、无 Revision
},
}
两种模式的路由差异:
| Knative(Serverless) | Standard(Raw) | |
|---|---|---|
| 组件 | Knative Service | Deployment + Service |
| 路由资源 | Istio VirtualService | Kubernetes Ingress |
| 中间层 | local gateway + Knative Route | 无,直达组件 Service |
| canary | Route 流量切分(Revision 级) | spec.Canary 多部署(见下节) |
| scale-to-zero | 有 | 无 |
注意一个源码事实:现在的 raw 模式不再生成 Istio VirtualService (早期版本会)------VS 只在 Knative 模式下创建。想用 Gateway API?配置 enableGatewayApi: true,工厂改走 httproute_reconciler.go。不想让 KServe 管入口?disableIngressCreation: true,它只写 URL 进 status,Ingress 你自己建。
Canary 切分:一个百分比如何变成路由权重
概述文里那句 canaryTrafficPercent: 10,字段定义在 component.go 的 ComponentExtensionSpec:
go
// traffic split between the candidate revision and the last ready revision
CanaryTrafficPercent *int64 `json:"canaryTrafficPercent,omitempty"`
它最终落在哪?不是 KServe 的 VirtualService ------VS 的 destination 权重恒为 100(createHTTPRouteDestination 写死 Weight: 100)。真正的权重在 Knative Route 的 Traffic 数组里(上一节那段代码):新 Revision 拿 10%,旧 Revision 拿 90%。KServe 的 VS 只把流量整体交给 predictor ksvc 的 Host:
go
httpRoutes = append(httpRoutes, &istiov1beta1.HTTPRoute{
Route: []*istiov1beta1.HTTPRouteDestination{
createHTTPRouteDestination(config.KnativeLocalGatewayService),
},
Headers: ...Set: map[string]string{
"Host": network.GetServiceHostname(backend, isvc.Namespace), // ← 改写成 predictor ksvc 的 Host
},
})
那 90% 给谁?lastRolledoutRevision 来自组件 status 的 LatestRolledoutRevision------上次"100% 上线"的 Revision。跟踪逻辑在 PropagateStatus(inference_service_status.go):只有最新 Revision 拿满 100% 才更新它,旧值挪进 PreviousRolledoutRevision 用于回滚。
默认只按权重分流。想直连某个 Revision?打注解 serving.kserve.io/enable-tag-routing: "true",ksvc 给新 Revision 加 Tag: "latest",Knative 随之生成 latest-{isvc}-predictor... 直连主机名------调试金丝雀好用。
顺带一提:main 分支还有条新 canary 路径。ISVC spec 新增 Canary []CanarySpec(inference_service.go):每个 canary 是独立的 predictor Deployment (固定副本、不开扩缩容,见 components/predictor.go 的 reconcileCanaryDeployments),权重目前只接到 Gateway API 的 HTTPRoute(httproute_reconciler.go 的 applyCanaryWeights):
go
stableWeight := int32(100) - totalReadyCanaryPercent // ← stable 权重 = 100 − 就绪 canary 之和
for _, canary := range isvc.Spec.Canary {
if !readyMap[canary.Predictor.Name] { continue } // ← 未就绪的 canary 剔除,不接流量
...Weight: &cw, // ← 就绪 canary 拿自己的 trafficPercent
}
两条路并存:经典 canaryTrafficPercent 走 Knative Route,新 spec.Canary 走 Gateway API。
主机名与证书:域名从哪来,TLS 谁负责
域名不是写死的,是 Go template 渲染的(ingress/domain.go):
go
DefaultDomainTemplate = "{{ .Name }}-{{ .Namespace }}.{{ .IngressDomain }}"
// sklearn-iris + default + example.com → sklearn-iris-default.example.com
GenerateDomainName 渲染完还要过 validation.IsFullyQualifiedDomainName 校验。一个 ISVC 三种主机名:外部 {isvc}-{ns}.{ingressDomain}(VS Hosts,匹配 IngressGateway);内部 {isvc}.{ns}.svc.cluster.local(localGateway/mesh,集群内互调);组件 {isvc}-predictor / -transformer / -explainer(Host 头改写目标)。ISVC 级主机名是"剥"出来的:getServiceHost 把 sklearn-iris-predictor.default.example.com 里的 -predictor 抠掉。只在内网用?打 networking.knative.dev/visibility: cluster-local(raw 模式 networking.kserve.io/visibility),VS 只挂内部主机名。
TLS 呢?KServe 自己不发证书 。证书配在入口网关(Istio Gateway / OpenShift),KServe 只把 scheme 写进 status------ingress 配置的 urlScheme(默认 http),getHostBasedServiceUrl 用它拼 Status.URL。KServe 转达的 TLS 注解只有一个:serving.knative.openshift.io/enablePassthrough(OpenShift 的 passthrough 模式),在 managedKsvcAnnotations 白名单里随 ksvc 下发。
最后澄清一个容易找错的地方:pkg/tls 管的是控制面自己的 HTTPS (manager 的 webhook/metrics 端口)------TLS 最低版本与密码套件(tls_default.go 的 Resolve),跟推理流量的 TLS 是两条线。
一句话总结
流量路由分两层:KServe 生成 VirtualService/Ingress 把"ISVC 级主机名"映射到组件服务,Knative Route 再按权重切到具体 Revision。canaryTrafficPercent 不落在 KServe 的路由资源里,而是写进 ksvc 的 RouteSpec.Traffic------新 Revision 拿百分比、旧 Revision 拿余量。域名由 DomainTemplate 渲染,TLS 终止在网关。
下一篇预告:第 7 篇自动扩缩容。Knative 模式下的权重表喂给了 KPA------请求来了才拉起 Pod、归零就缩到 0;raw 模式下 HPA/KEDA 怎么接上?