模型服务化实战:从Notebook到生产就绪的12个关键环节
1. 项目概述这不是“部署”是让模型在真实世界里活下来“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题本身就像一句行话暗号老手一眼就懂前面三篇已经蹚过了数据清洗、特征工程、模型训练和验证的泥潭现在终于到了最硬核、也最容易被低估的一关把那个在Jupyter里跑得飞起、AUC 0.92、交叉验证稳如老狗的模型真正塞进业务系统里让它每天扛住真实流量、处理脏数据、不崩、不飘、不偷偷变笨。这不是“部署”两个字能概括的事这是给模型办身份证、签劳动合同、配工位、上KPI、买保险还要定期做体检。我带过7个从0到1落地的ML项目其中4个卡死在Part 4不是模型不行是没人告诉他们生产环境不认accuracy只认latency、reliability、observability和business impact。这篇讲的就是怎么让模型从“能跑”变成“敢用”从“实验品”变成“基础设施”。它适合三类人刚跑通第一个模型、正对着Flask API发愁的算法工程师天天被业务方追问“模型啥时候上线”的数据平台负责人还有那些在运维侧看着GPU显存突然飙到98%、日志里满屏NaN而头皮发麻的SRE。核心关键词——模型服务化Model Serving、实时推理Real-time Inference、可观测性Observability、模型监控Model Monitoring、生产就绪Production-Ready——每一个词背后都是一整套工程实践而不是一个pip install就能解决的问题。2. 整体设计思路为什么不能直接把notebook里的model.predict()扔进API2.1 从“单次推理”到“持续服务”的范式跃迁很多人以为模型服务化就是写个Flask接口把model.predict()包进去再加个gunicorn启动。我试过上线第一天就翻车。原因很简单notebook是单次、离线、可控的沙盒生产API是持续、在线、不可控的战场。举个最典型的例子你训练时用的是pandas 1.3.5特征工程里用了df.fillna(methodffill)但线上服务用的pandas是1.5.0这个method参数在新版本里被标记为deprecated虽然没报错但填充逻辑悄悄变了——结果就是模型输入特征分布偏移预测结果集体漂移。这不是bug是隐式耦合。真正的设计起点必须是“隔离”代码隔离、依赖隔离、数据隔离、计算隔离。我们团队现在强制执行“三镜像原则”训练镜像含完整conda env 特征生成脚本、推理镜像极简只含model inference runtime 必要transformer、监控镜像独立进程只拉取指标不碰模型。这三者之间除了约定好的输入输出schema绝不共享任何一行代码或一个环境变量。这样做的代价是CI/CD流水线变长了15分钟但换来的是故障定位时间从小时级降到分钟级。因为一旦出问题你立刻知道是训练环节的数据污染还是推理镜像的版本错配还是监控探针本身挂了边界清晰责任明确。2.2 选型逻辑为什么放弃TensorFlow Serving最终选了Triton KServe组合市面上模型服务框架不少TF Serving、TorchServe、KServe原KFServing、Seldon Core、BentoML……我们花了6周做POC结论很反直觉没有“最好”只有“最不痛”。TF Serving对TensorFlow模型支持确实深但它要求你把整个模型图导出成SavedModel格式而我们有个关键模型是PyTorch写的中间还混了几个自定义CUDA算子——硬转SavedModel等于重写一半代码。TorchServe对PyTorch友好但它的动态批处理Dynamic Batching在高并发下有锁竞争我们压测发现QPS超过1200时P99延迟会跳变式上涨。最后选了NVIDIA Triton KServe不是因为它多先进而是它解决了我们三个具体痛点第一Triton原生支持多框架PyTorch/TensorFlow/ONNX/Python Backend我们的混合模型栈不用拆第二它的并发模型是“每个模型实例独占CPU/GPU线程”彻底规避了共享内存导致的竞态第三KServe提供了Kubernetes-native的CRDCustom Resource Definition我们运维团队不用学新YAML语法直接用kubectl apply -f model.yaml就能上线/回滚/扩缩容。这里有个关键细节Triton的配置文件config.pbtxt里max_batch_size设为0意味着禁用批处理设为128意味着最多攒128个请求一起推断。我们实测发现对我们的NLP模型输入长度波动大设为32比128更稳——因为长文本会吃光显存短文本又浪费算力。这个数字不是拍脑袋是用Triton的perf_analyzer工具在真实流量采样数据上跑出来的。选型没有银弹只有针对自己业务负载的反复验证。2.3 架构分层为什么坚持“模型即服务MaaS”而非“模型嵌入应用”早期我们尝试过把模型直接打包进业务微服务比如用户推荐服务的jar包里理由很朴素省事调用快少一层网络。结果呢一次紧急修复模型bias问题要全站重启所有Java服务影响时长47分钟。后来改成MaaS架构所有模型统一由KServe集群托管业务服务通过gRPC调用。看似多了一跳但收益巨大。第一发布解耦模型更新不影响业务代码业务升级也不影响模型双方可以按自己的节奏迭代第二资源弹性推荐模型流量波峰波谷明显KServe能自动根据CPU/GPU利用率扩缩Pod而嵌入式模型只能靠业务服务整体扩缩浪费资源第三灰度可控KServe支持基于Header的流量切分比如x-model-version: v2的请求走新模型其余走旧模型AB测试、金丝雀发布一气呵成。我们甚至用这个能力做过“影子模式”Shadow Mode新模型不参与决策只并行跑一遍把结果和旧模型对比连续7天差异率0.1%才切流。这种精细控制嵌入式架构根本做不到。所以“多一跳”的代价换来的是整个系统的可维护性和可演进性。技术决策的本质从来不是比谁更快而是比谁更扛得住变化。3. 核心细节解析从模型打包到服务上线的12个生死关3.1 模型序列化Pickle不是生产环境的朋友很多教程教你怎么用joblib.dump(model, model.pkl)然后在API里joblib.load(model.pkl)。千万别Pickle有三大原罪第一版本锁定——Python 3.8 pickle的模型在3.9里可能加载失败第二安全风险——恶意构造的pkl文件能执行任意代码第三跨语言无解——你的前端是Go写的它不认识pkl。我们强制要求PyTorch模型必须导出为TorchScripttorch.jit.script()或torch.jit.trace()TensorFlow用SavedModel通用模型用ONNX。TorchScript的好处是它把模型和推理逻辑编译成字节码脱离Python解释器运行性能提升20%-35%且PyTorch版本兼容性极好。但要注意一个坑如果你模型里用了torch.nn.functional.interpolate在trace模式下可能出错必须改用script模式并确保所有分支都被覆盖。我们有个图像分割模型就因为一个if-else里漏了某个尺寸的插值trace后推理结果全黑。解决方案写个mini test script用所有可能的输入shape跑一遍确保trace成功。序列化不是终点是生产化的起点。3.2 特征工程流水线为什么必须和模型一起部署模型不是孤立的。它依赖一套精确的特征生成逻辑时间窗口聚合、类别编码、数值归一化……这些逻辑如果只在训练时跑一遍线上用SQL或业务代码临时拼必然不一致。我们见过最惨的案例训练时用sklearn.preprocessing.StandardScaler对用户年龄做Z-score线上却用业务库里的平均值硬编码结果所有年轻用户预测分集体虚高。正确做法是把特征工程流水线Feature Pipeline和模型一起打包、一起版本化、一起部署。我们用scikit-learn的Pipeline对象配合skops库安全的sklearn模型序列化工具保存。关键点在于Pipeline必须是“纯函数式”的——不能依赖外部数据库连接、不能读取本地文件路径、不能调用随机数。所有依赖都必须注入为参数。比如时间窗口聚合需要当前时间戳我们不在Pipeline里写datetime.now()而是定义一个get_current_time()函数作为参数传入。这样线上服务调用时把真实的请求时间戳传进去保证特征计算完全复现训练逻辑。这个Pipeline和模型权重文件放在同一个KServe的InferenceService YAML里作为一个原子单元发布。3.3 输入输出Schema用OpenAPI和Protobuf双保险API文档不是给开发者看的是给机器看的契约。我们坚持两套schema对外HTTP/gRPC客户端用OpenAPI 3.0定义JSON结构对内模型runtime用Protobuf定义二进制协议。为什么OpenAPI给人看Protobuf给机器跑。比如一个用户画像模型的输入OpenAPI里定义components: schemas: UserRequest: type: object properties: user_id: type: string example: U123456 event_timestamp: type: string format: date-time example: 2023-10-05T14:30:00Z而Protobuf里定义message UserRequest { string user_id 1; int64 event_timestamp_ms 2; // 统一毫秒时间戳避免时区歧义 }这个转换由KServe的pre-processing容器完成。好处是前端用OpenAPI自动生成SDK后端用Protobuf零拷贝传输性能损失趋近于零。更重要的是schema变更必须走严格流程新增字段可选删除字段需标注deprecated修改类型必须大版本号升级。我们用Swagger Codegen和Protoc工具链确保每次schema变更自动同步生成客户端代码、服务端校验逻辑、文档和mock server。没有schema的API就像没有交通规则的高速公路早晚出事。3.4 资源申请与限制GPU显存不是越大越好新手常犯的错误给模型服务Pod申请4块V100觉得“反正有”。结果呢集群GPU碎片化严重其他服务抢不到卡而你的服务显存只用了30%。我们制定了一套“显存预算制”首先用NVIDIA DCGM工具采集模型在真实流量下的显存峰值不是理论值加上20%缓冲其次根据模型类型设定上限BERT类NLP模型≤16GBResNet类CV模型≤12GB轻量级CTR模型≤8GB最后强制设置limits和requests相等避免Kubernetes调度器误判。比如一个BERT-base模型实测峰值10.2GB我们就设nvidia.com/gpu: 1memory: 12Gi。还有一个隐藏技巧Triton支持dynamic_batching但它的batch size不是固定值而是根据GPU显存剩余空间动态调整。我们把preferred_batch_size设为[8,16,32]让Triton在显存允许范围内智能选择最优batch size。实测下来相比固定batch32QPS提升18%P99延迟降低22%。资源不是堆出来的是算出来的。3.5 健康检查与就绪探针别让K8s把你健康的Pod杀掉Kubernetes的liveness probe存活探针和readiness probe就绪探针是双刃剑。设得太松故障Pod长期挂着设得太紧模型加载中就被K8s重启。我们踩过的最大坑Triton启动时要加载模型到GPU显存这个过程可能长达90秒尤其大模型而默认的readiness probe超时是1秒结果Pod永远处于ContainerCreating状态。解决方案分阶段探针。第一阶段启动期用exec探针检查Triton进程是否存在超时设为120秒初始延迟30秒第二阶段服务期用HTTP探针访问/v2/health/ready超时2秒每5秒检查一次。同时在KServe的InferenceService里配置timeout为300秒确保模型加载完成前K8s不会干预。另一个关键是probe的响应必须轻量。我们曾经在readiness probe里加了DB连接检查结果DB抖动导致所有模型服务被驱逐。现在probe只做两件事检查Triton进程健康、检查模型是否READY调用/v2/models/{model_name}/versions/{version}/ready。简单、快速、可靠才是生产探针的哲学。3.6 日志规范让每一行log都能回答“谁、在哪儿、干了什么、结果如何”生产环境的日志不是debug用的是审计、排障、计费的依据。我们强制所有模型服务遵循“五元组日志”标准[timestamp] [service_name] [request_id] [user_id] [action] [status] [latency_ms] [input_summary] [output_summary]。比如2023-10-05T14:30:00.123Z user-scoring-svc REQ-789abc U123456 predict SUCCESS 42ms {age:28,city:shanghai} {score:0.87,risk_level:low}关键点request_id必须透传从API网关一路带到模型服务user_id必须脱敏如哈希input_summary和output_summary只记录关键字段不打全量JSON防日志爆炸。我们用Fluentd收集ES存储Grafana看板。最实用的功能是输入request_id一键串联所有相关日志网关、鉴权、特征服务、模型服务5分钟内定位问题。没有request_id的日志等于没有日志。3.7 错误处理返回400还是500不是技术问题是产品问题HTTP状态码是服务和调用方的契约。我们定死三条铁律第一客户端错误一律4xx用户ID格式错误400 Bad Request、缺少必要参数400、请求频率超限429 Too Many Requests第二服务端错误一律5xx模型加载失败503 Service Unavailable、GPU显存不足503、特征服务超时504 Gateway Timeout第三永远返回结构化错误体{ error_code: MODEL_NOT_READY, message: Model user-scoring-v2 is loading, please retry in 30s, request_id: REQ-789abc, timestamp: 2023-10-05T14:30:00.123Z }error_code是机器可解析的枚举值message是给人看的友好提示。我们甚至把所有error_code做成内部知识库条目附带排查步骤。有一次业务方反馈大量MODEL_NOT_READY我们查知识库立刻知道是KServe的模型加载超时去查Triton日志发现是某个新版本ONNX模型有兼容性问题10分钟内回滚。错误处理不是兜底是建立信任。3.8 安全加固模型服务不是裸奔的API模型服务暴露在公网想都别想。我们所有KServe服务都部署在私有K8s集群内网对外通过API网关Kong暴露网关层强制JWT鉴权验证scope: model:predict、IP白名单仅允许业务服务Pod CIDR、请求大小限制≤1MB、速率限制1000 req/min per client。更关键的是模型层防护Triton支持model_repository权限控制我们把不同业务线的模型放在不同目录用Linux ACL限制读取权限KServe的InferenceService CRD里spec.securityContext强制设置runAsNonRoot: true和readOnlyRootFilesystem: true。还有一个容易被忽视的点输入数据校验。我们在pre-processing容器里用Pydantic定义严格schema对每个字段做类型、范围、格式校验。比如event_timestamp_ms必须是13位正整数user_id必须匹配^[a-zA-Z0-9_-]{8,32}$正则。校验失败直接返回400不进模型。这招挡住了83%的恶意探测和脏数据攻击。安全不是加个WAF是层层设防。3.9 监控指标只看P99延迟你已经输了模型服务的监控必须覆盖“数据-模型-服务”三层。我们用PrometheusGrafana搭建四维监控看板维度关键指标告警阈值业务含义服务层http_request_duration_seconds{code~2.., handlerpredict}P99 200ms用户感知卡顿模型层triton_inference_request_success{modeluser-scoring}rate(5m) 99.5%模型内部异常数据层feature_drift_score{featureage} 0.3用户年龄分布突变资源层container_gpu_memory_used_ratio{containertriton} 90%GPU显存即将耗尽特别强调feature_drift_score我们用Evidently库每小时计算线上输入特征vs训练数据的PSIPopulation Stability Index超过阈值自动触发告警并生成漂移报告。上周就靠这个发现新版本APP埋点把用户城市字段从“shanghai”改成了“Shanghai”大小写不一致导致one-hot编码维度错乱模型预测全乱。监控不是看数字是看故事。3.10 配置管理环境变量不是万能的.env文件在生产环境是定时炸弹。我们所有配置模型路径、超参数、超时时间、降级开关都通过K8s ConfigMap KServe的spec.env注入且禁止在代码里读取环境变量。为什么因为ConfigMap可以版本化、审计、回滚而环境变量改了就没了。更关键的是我们实现了“配置热更新”KServe监听ConfigMap变更当model_timeout_ms从1000改成500时服务无需重启5秒内生效。实现原理是在模型服务里用watchdog库监听ConfigMap挂载的文件变化触发内部参数重载。这个能力让我们在大促前夜把所有模型的超时从2s降到800ms流量洪峰下P99稳定在150ms没动一行代码。配置即代码配置即服务。3.11 降级与熔断当模型挂了业务不能跟着死最狠的保障是让模型服务“可牺牲”。我们设计三级降级第一级模型内部Triton支持ensemble模型把主模型和一个轻量级规则模型如XGBoost编排在一起主模型超时或失败自动fallback到规则模型第二级服务层KServe的canary rollout支持配置trafficSplit当主模型错误率5%自动切50%流量到备用模型第三级业务层API网关配置fallback策略当所有模型服务503返回缓存结果如最近24小时平均分或默认值如“低风险”。我们有个风控模型去年双十一凌晨因GPU驱动bug全挂靠着三级降级业务无感只是部分用户看到的评分略保守。降级不是妥协是专业。3.12 CI/CD流水线从git push到服务上线12分钟全自动我们用Argo CD GitHub Actions构建GitOps流水线。流程如下开发者push代码到main分支 → 触发GitHub Action自动运行单元测试模型预测一致性 集成测试调用本地KServe mock 安全扫描Trivy测试通过 → 自动生成KServe InferenceService YAML替换镜像tag、配置参数→ 提交到infra仓库Argo CD监听infra仓库变更 → 自动同步到K8s集群 → KServe创建新Revision新Revision就绪 → 自动运行金丝雀测试用1%真实流量验证→ 通过则全量切流。整个过程12分37秒误差±20秒。关键创新点测试数据即生产数据。我们用Flink实时消费Kafka里的生产请求抽样1%写入MinIOCI流水线直接用这批数据做集成测试。这意味着每次上线模型都已在真实数据上跑过一遍。流水线不是自动化是可信自动化。4. 实操过程详解以用户信用评分模型为例从零到上线4.1 环境准备K8s集群与KServe安装实操记录我们用k3s轻量K8s搭建测试集群版本v1.27.6k3s1。安装KServe v1.13最新稳定版# 1. 安装cert-managerKServe依赖 kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.12.3/cert-manager.yaml # 2. 等待cert-manager就绪约2分钟 kubectl wait --forconditionready pod -l app.kubernetes.io/instancecert-manager -n cert-manager --timeout180s # 3. 安装KServe核心组件 kubectl apply -f https://github.com/kserve/kserve/releases/download/v1.13.0/kserve.yaml kubectl apply -f https://github.com/kserve/kserve/releases/download/v1.13.0/kserve-rbac.yaml # 4. 验证安装 kubectl get pods -n kubeflow # 应看到kfserving-controller-manager, kfserving-webhook等注意k3s默认不启用Metrics Server而KServe的HPA自动扩缩容需要它。必须手动安装kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.6.3/components.yaml # 等待metrics-server就绪后验证 kubectl top nodes # 应显示节点CPU/MEM使用率这是最容易卡住的一步。我们第一次安装时metrics-server的pod一直CrashLoopBackOff查日志发现是k3s的cgroup driver不匹配。解决方案编辑/var/lib/rancher/k3s/agent/etc/containerd/config.toml把SystemdCgroup false改为true然后sudo systemctl restart k3s。这个坑我们踩了3小时。4.2 模型准备PyTorch模型导出为TorchScript完整命令与验证我们的用户信用评分模型是PyTorch写的结构如下class CreditScorer(nn.Module): def __init__(self, input_dim, hidden_dim): super().__init__() self.encoder nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.2) ) self.head nn.Linear(hidden_dim, 1) def forward(self, x): return torch.sigmoid(self.head(self.encoder(x)))导出TorchScript的关键是提供典型输入示例。我们用训练集的均值向量生成dummy inputimport torch import numpy as np # 加载训练好的模型权重 model CreditScorer(input_dim128, hidden_dim64) model.load_state_dict(torch.load(model.pth)) model.eval() # 创建典型输入128维值域[0,1] dummy_input torch.randn(1, 128) # batch_size1 dummy_input torch.clamp(dummy_input, 0, 1) # 限制在[0,1] # 导出为TorchScript traced_model torch.jit.trace(model, dummy_input) traced_model.save(credit-scorer-traced.pt) # 验证导出正确性 original_out model(dummy_input).item() traced_out traced_model(dummy_input).item() print(fOriginal: {original_out:.4f}, Traced: {traced_out:.4f}, Diff: {abs(original_out-traced_out):.6f}) # 输出Original: 0.6231, Traced: 0.6231, Diff: 0.000001 → 合格注意torch.jit.trace要求所有执行路径在dummy input下都能走到。如果模型有if-else分支必须准备多个dummy input分别trace再用torch.jit.script合并。我们有个分支逻辑判断用户是否VIP所以额外准备了dummy_vip_input torch.cat([dummy_input, torch.ones(1,1)], dim1)来覆盖。4.3 Triton模型仓库构建目录结构与config.pbtxt详解Triton要求严格的目录结构。我们的model-repository如下model-repository/ ├── credit-scorer/ │ ├── 1/ │ │ └── model.pt # TorchScript模型文件 │ └── config.pbtxt └── feature-processor/ ├── 1/ │ └── processor.pkl # scikit-learn Pipeline └── config.pbtxtcredit-scorer/config.pbtxt内容name: credit-scorer platform: pytorch_libtorch max_batch_size: 32 input [ { name: INPUT__0 data_type: TYPE_FP32 dims: [128] } ] output [ { name: OUTPUT__0 data_type: TYPE_FP32 dims: [1] } ] instance_group [ { count: 2 kind: KIND_CPU }, { count: 1 kind: KIND_GPU } ] dynamic_batching { preferred_batch_size: [8,16,32] max_queue_delay_microseconds: 100000 }关键参数解读platform: pytorch_libtorch指定Triton用LibTorch后端加载TorchScriptmax_batch_size: 32Triton最多攒32个请求一起推断instance_group启动2个CPU实例处理小请求 1个GPU实例处理大请求资源利用最大化dynamic_batchingmax_queue_delay_microseconds: 100000100ms意味着即使没攒够32个请求等100ms也强制推断防延迟堆积。4.4 KServe InferenceService部署YAML编写与调试技巧inference-service.yaml是上线核心apiVersion: kserve.kserve.io/v1beta1 kind: InferenceService metadata: name: credit-scorer namespace: kubeflow spec: predictor: serviceAccountName: kserve-sa # 绑定RBAC权限 containers: - name: kserve-container image: nvcr.io/nvidia/tritonserver:23.09-py3 args: - --model-repository/mnt/models - --http-port8080 - --grpc-port8081 ports: - containerPort: 8080 name: http - containerPort: 8081 name: grpc volumeMounts: - mountPath: /mnt/models name: model-storage resources: limits: nvidia.com/gpu: 1 memory: 12Gi requests: nvidia.com/gpu: 1 memory: 12Gi volumes: - name: model-storage persistentVolumeClaim: claimName: triton-pvc # 指向预置的PVC挂载model-repository transformer: containers: - name: transformer image: registry.example.com/feature-processor:v1.2 ports: - containerPort: 8080 env: - name: MODEL_NAME value: credit-scorer resources: limits: cpu: 1 memory: 2Gi requests: cpu: 500m memory: 1Gi部署后调试技巧查看KServe事件kubectl describe inferenceservice credit-scorer -n kubeflow重点看Events里是否有FailedMount或ImagePullBackOff进入Triton Podkubectl exec -it triton-pod-name -n kubeflow -- bash检查/mnt/models/credit-scorer/1/model.pt是否存在手动调用Triton健康接口curl http://triton-service-ip:8080/v2/health/ready返回{ready: true}才算加载成功最后一步用KServe自带的kserve-test工具验证端到端kserve-test \ --host credit-scorer-predictor-default.kubeflow.example.com \ --input {instances: [[0.1,0.2,...,0.9]]} \ --protocol v2如果返回{predictions: [0.723]}恭喜你的模型已活在生产世界。4.5 监控与告警配置Prometheus Rule实战我们为信用评分模型定义了核心告警规则credit-scorer-alerts.yamlgroups: - name: credit-scorer-alerts rules: - alert: CreditScorerHighErrorRate expr: rate(triton_inference_request_failure{modelcredit-scorer}[5m]) 0.01 for: 10m labels: severity: critical service: credit-scorer annotations: summary: Credit scorer error rate 1% for 10 minutes description: Current error rate is {{ $value }}. Check Triton logs and model health. - alert: CreditScorerLatencyHigh expr: histogram_quantile(0.99, sum(rate(triton_inference_request_duration_seconds_bucket{modelcredit-scorer}[5m])) by (le)) 0.2 for: 5m labels: severity: warning service: credit-scorer annotations: summary: Credit scorer P99 latency 200ms description: Current P99 is {{ $value }}s. Check GPU utilization and feature processing time. - alert: FeatureDriftDetected expr: max(feature_drift_score{featureincome}) 0.3 for: 1h labels: severity: info service: credit-scorer annotations: summary: Income feature drift detected description: PSI score for income is {{ $value }}. Trigger retraining pipeline.部署后在Grafana里创建Dashboard关键面板包括实时流量图rate(http_request_total{handlerpredict, code~2..}[1m])延迟热力图用histogram_quantile展示P50/P90/P99随时间变化GPU显存水位container_gpu_memory_used_ratio{containertriton}特征漂移仪表盘用Evidently生成的HTML报告嵌入iframe。4.6 上线后验证金丝雀发布与A/B测试全流程KServe的canaryrollout是神器。我们先发布v1旧模型# credit-scorer-v1.yaml apiVersion: kserve.kserve.io/v1beta1 kind: InferenceService metadata: name: credit-scorer spec: predictor: # ... 同上但image指向v1 canaryTrafficPercent: 0 # 0%流量再发布v2新模型# credit-scorer-v2.yaml apiVersion: kserve.kserve.io/v1beta1 kind: InferenceService metadata: name: credit-scorer spec: predictor: # ... image指向v2 canaryTrafficPercent: 5 # 先切5%流量上线后我们用以下命令实时观察效果# 查看当前流量分配 kubectl get inferenceservice credit-scorer -n kubeflow -o jsonpath{.status.canaryStatus} # 抓取100个v2请求的预测结果和v1对比 kubectl port-forward svc/credit-scorer-predictor-default 8080:80 -n kubeflow curl -H Host: credit-scorer-predictor-default.kubeflow.example.com \ -H x-canary: v2 \ -d {instances: [[...]]} \