流量路由:Istio 与 Knative 的集成
英文标题:流量路由:Istio 与 Knative 的集成
KServe 在 Knative Serverless 模式下采用两层流量路由:Istio VirtualService 将外部请求导向 Knative local gateway,Knative Route 再将流量按权重分配给具体 Revision。`canaryTrafficPercent` 不会写入 KServe 的 VirtualService,而是转换为 Knative Service 的 `RouteSpec.Traffic`:新 Revision 获得指定比例,上一稳定 Revision 获得剩余流量。Standard(Raw)模式则使用 Kubernetes Ingress 直接连接组件 Service,不具备 Knative Revision 级别的流量切分与 scale-to-zero 能力。对于开发者而言,理解两条 canary 路径、域名模板和网关 TLS 终止位置,是正确配置发布、灰度和入口流量的关键。
11.4 KB
中文
KServe 源码拆解系列 · 第 6 篇
第 5 篇结束时,模型已经躺进 Pod 的 /mnt/models:storage-initializer 下载完,推理容器就绪。但集群外面一条 curl,凭什么能精确命中这个 Pod?中间隔着一整套指路系统:网关、VirtualService、Knative Route。本篇拆解这套系统,也把概述文一笔带过的 canaryTrafficPercent 拆到源码层:百分比如何变成路由权重。而流量怎么分,直接连着下一篇的主角——自动扩缩容。
两层路由:请求进集群后先找谁
Knative 模式下,KServe 的流量路由分为两层。看全景:
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 负责“到版本”:VirtualService 只表达“流量交给 knative-local-gateway”,由哪个 Revision 接收、canary 如何切分则是 Knative Route 的职责。
代码顺序也是如此:controller.go 中组件调和在前,// Reconcile ingress 在最后——先有服务,再指路;指路实现由工厂按部署模式选择:
switch deploymentMode {
case constants.Standard, constants.LegacyRawDeployment:
case constants.Knative, constants.LegacyServerless, constants.ModelMeshDeployment:
return ingress.NewIngressReconciler(...)
切换模式就会切换一套资源。下面拆解两条路径。
Serverless 模式:KServe 为什么依赖 Knative
Knative 模式下,每个组件不是 Deployment,而是 Knative Service(ksvc_reconciler.go,第 4 篇)。它除了将 PodSpec 放进 RevisionTemplate,最关键的产出是 RouteSpec.Traffic——Knative Route 的流量权重表:
if componentExtension.CanaryTrafficPercent != nil && lastRolledoutRevision != "" {
trafficTargets = append(trafficTargets, knservingv1.TrafficTarget{
LatestRevision: proto.Bool(true),
Percent: proto.Int64(*componentExtension.CanaryTrafficPercent),
})
if *componentExtension.CanaryTrafficPercent < 100 {
trafficTargets = append(trafficTargets, knservingv1.TrafficTarget{
RevisionName: lastRolledoutRevision,
Percent: proto.Int64(100 - *componentExtension.CanaryTrafficPercent),
Tag: "prev",
})
}
} else {
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:
&netv1.Ingress{
Spec: netv1.IngressSpec{
IngressClassName: ingressConfig.IngressClassName,
Rules: rules,
},
}
两种模式的路由差异:
| Knative(Serverless) | Standard(Raw) |
|---|---|
| 组件 | Knative Service |
| 路由资源 | Istio VirtualService |
| 中间层 | local gateway + Knative Route |
| canary | Route 流量切分(Revision 级) |
| scale-to-zero | 有 |
注意一个源码事实:现在的 Raw 模式不再生成 Istio VirtualService(早期版本会)——VirtualService 只会在 Knative 模式下创建。想使用 Gateway API?配置 enableGatewayApi: true,工厂会改走 httproute_reconciler.go。不想让 KServe 管理入口?设置 disableIngressCreation: true,它只会将 URL 写入 status,Ingress 由你自行创建。
Canary 切分:一个百分比如何变成路由权重
概述文中那句 canaryTrafficPercent: 10,字段定义在 component.go 的 ComponentExtensionSpec:
CanaryTrafficPercent *int64 `json:"canaryTrafficPercent,omitempty"`
它最终落在哪里?不是 KServe 的 VirtualService——VirtualService 的 destination 权重恒为 100(createHTTPRouteDestination 将 Weight: 100 写死)。真正的权重在 Knative Route 的 Traffic 数组中(上一节那段代码):新 Revision 获得 10%,旧 Revision 获得 90%。KServe 的 VirtualService 只将整体流量交给 predictor ksvc 的 Host:
httpRoutes = append(httpRoutes, &istiov1beta1.HTTPRoute{
Route: []*istiov1beta1.HTTPRouteDestination{
createHTTPRouteDestination(config.KnativeLocalGatewayService),
},
Headers: ...Set: map[string]string{
"Host": network.GetServiceHostname(backend, isvc.Namespace),
},
})
那 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):
stableWeight := int32(100) - totalReadyCanaryPercent
for _, canary := range isvc.Spec.Canary {
if !readyMap[canary.Predictor.Name] { continue }
...Weight: &cw,
}
两条路径并存:经典 canaryTrafficPercent 走 Knative Route,新 spec.Canary 走 Gateway API。
主机名与证书:域名从哪来,TLS 谁负责
域名不是写死的,而是通过 Go template 渲染(ingress/domain.go):
DefaultDomainTemplate = "{{ .Name }}-{{ .Namespace }}.{{ .IngressDomain }}"
GenerateDomainName 渲染完成后还要经过 validation.IsFullyQualifiedDomainName 校验。一个 ISVC 有三类主机名:外部 {isvc}-{ns}.{ingressDomain}(VirtualService 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),VirtualService 只会挂载内部主机名。
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 又该怎么接入?
English / 原文
KServe 源码拆解系列 · 第 6 篇
第 5 篇结束时,模型已经躺进 Pod 的 /mnt/models:storage-initializer 下载完,推理容器就绪。但集群外面一条 curl,凭什么能精确命中这个 Pod?中间隔着一整套指路系统:网关、VirtualService、Knative Route。本篇拆这套系统,也把概述文一笔带过的 canaryTrafficPercent 拆到源码层:百分比如何变成路由权重。而流量怎么分,直接连着下一篇的主角——自动扩缩容。
两层路由:请求进集群后先找谁
Knative 模式下,KServe 的流量路由是两层。看全景:
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 在最后——先有服务,再指路;指路实现由工厂按部署模式选:
switch deploymentMode {
case constants.Standard, constants.LegacyRawDeployment:
case constants.Knative, constants.LegacyServerless, constants.ModelMeshDeployment:
return ingress.NewIngressReconciler(...)
换模式就换一套资源。下面拆两条路。
Serverless 模式:KServe 为什么依赖 Knative
Knative 模式下,每个组件不是 Deployment,而是 Knative Service(ksvc_reconciler.go,第 4 篇)。它除了把 PodSpec 塞进 RevisionTemplate,最关键的产出是 RouteSpec.Traffic——Knative Route 的流量权重表:
if componentExtension.CanaryTrafficPercent != nil && lastRolledoutRevision != "" {
trafficTargets = append(trafficTargets, knservingv1.TrafficTarget{
LatestRevision: proto.Bool(true),
Percent: proto.Int64(*componentExtension.CanaryTrafficPercent),
})
if *componentExtension.CanaryTrafficPercent < 100 {
trafficTargets = append(trafficTargets, knservingv1.TrafficTarget{
RevisionName: lastRolledoutRevision,
Percent: proto.Int64(100 - *componentExtension.CanaryTrafficPercent),
Tag: "prev",
})
}
} else {
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:
&netv1.Ingress{
Spec: netv1.IngressSpec{
IngressClassName: ingressConfig.IngressClassName,
Rules: rules,
},
}
两种模式的路由差异:
| 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:
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:
httpRoutes = append(httpRoutes, &istiov1beta1.HTTPRoute{
Route: []*istiov1beta1.HTTPRouteDestination{
createHTTPRouteDestination(config.KnativeLocalGatewayService),
},
Headers: ...Set: map[string]string{
"Host": network.GetServiceHostname(backend, isvc.Namespace),
},
})
那 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):
stableWeight := int32(100) - totalReadyCanaryPercent
for _, canary := range isvc.Spec.Canary {
if !readyMap[canary.Predictor.Name] { continue }
...Weight: &cw,
}
两条路并存:经典 canaryTrafficPercent 走 Knative Route,新 spec.Canary 走 Gateway API。
主机名与证书:域名从哪来,TLS 谁负责
域名不是写死的,是 Go template 渲染的(ingress/domain.go):
DefaultDomainTemplate = "{{ .Name }}-{{ .Namespace }}.{{ .IngressDomain }}"
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 怎么接上?