1. 项目概述:这不是一次模型训练,而是一场工程交付

“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题里藏着一个被太多人轻描淡写、却让无数团队在临门一脚时彻底卡死的真相: Notebook 是思考的草稿纸,Production 是交付的合同书 。它不讲怎么调参、不教怎么画 loss 曲线,它直指那个没人愿意多说但每天都在吞噬工程师时间的核心问题:当你在 Jupyter 里跑通了 accuracy 92.3% 的模型,下一步该把这串代码交给谁?用什么方式交?交过去之后,它会不会在凌晨三点因为一条脏数据崩掉,而你手机没响、告警没触发、业务方已经打电话来问“为什么推荐页全黑了”?

我做过 7 个从零到上线的机器学习服务,其中 4 个在模型准确率达标后,花了比训练周期长 2.3 倍的时间才真正稳定跑进生产环境。Part 4 这个编号很关键——它不是入门篇,不是原理篇,而是压轴的“交付实战篇”。它默认你已掌握模型开发(Part 1)、特征工程落地(Part 2)、模型监控基线(Part 3),现在要解决的是: 如何让一个“能跑”的模型,变成一个“敢签 SLA”的服务

核心关键词“Notebook to Production”背后,实际覆盖三个不可妥协的硬性要求: 可复现性(Reproducibility) ——今天在你本地跑的结果,和三个月后运维同事在 k8s 集群里拉起的镜像结果必须完全一致; 可观测性(Observability) ——不是只看 CPU 和内存,而是要实时知道模型输入分布是否漂移、预测置信度是否集体下滑、某类样本的延迟是否异常升高; 可演进性(Maintainability) ——当业务方下周突然要求增加“用户地域加权”逻辑,你能不能在不重启服务、不中断流量的前提下完成热更新?

这篇文章适合三类人:一是刚把模型跑通、正对着部署文档发愁的算法工程师;二是天天被算法同学喊“帮忙搭个 API”的后端工程师,想搞懂模型服务到底特殊在哪;三是技术负责人,需要判断团队当前的 MLOps 能力卡点究竟在 pipeline 编排、模型注册还是在线推理引擎选型。它不提供“一键部署脚本”,但会告诉你每一行部署命令背后的真实代价——比如为什么我们坚持用 Triton 而不是直接 Flask 封装,为什么模型版本号必须和 Git commit hash 绑定,以及,为什么你写的那行 model.predict() 在生产环境里可能正在悄悄吃掉 40% 的 P99 延迟。


2. 内容整体设计与思路拆解:放弃“最小可行”,拥抱“最小可靠”

2.1 为什么不能直接用 Flask + pickle 模型文件上线?

这是最常被问的问题,也是踩坑最深的起点。很多团队的第一版生产服务就是: flask run 启一个 API, joblib.load('model.pkl') 加载模型, return model.predict(data) 返回结果。短期看,它快、简单、5 分钟上线。但真实世界会立刻打脸:

  • 内存泄漏黑洞 :Pickle 加载的模型对象(尤其是 sklearn pipeline 或 PyTorch state_dict)在 Python 多进程模式下极易产生引用计数混乱。我们曾遇到一个文本分类服务,在持续请求 36 小时后,单个 worker 进程内存从 300MB 涨到 2.1GB,最终 OOM 被 k8s 杀掉——而日志里没有任何报错,只有 Killed process 这行内核日志。
  • 冷启动延迟灾难 :每次新 pod 启动,都要重新加载 GB 级模型文件。在 k8s 水平扩缩容时,新实例可能需要 47 秒才能响应第一个请求(实测 ResNet50 + ONNX Runtime)。这意味着流量洪峰到来时,大量请求会直接超时。
  • 无统一推理协议 :Flask 接口是自由格式的 JSON,前端传 {"text": "hello"} ,后端解析成 request.json['text'] 。但当你要接入 A/B 测试平台、灰度发布系统或统一网关时,它们需要的是标准化的 gRPC 接口定义( .proto 文件)和结构化 metadata(如 model_version, input_schema),而不是靠文档约定。

所以 Part 4 的第一条设计铁律是: 拒绝“能用就行”的临时方案,从第一天就按服务契约(Service Contract)来构建 。我们选择 NVIDIA Triton Inference Server 作为推理底座,不是因为它“高大上”,而是它天然解决了上述三个问题:

  • Triton 用 C++ 实现,模型加载走内存映射(mmap),避免 Python GC 干扰;
  • 支持模型预热(model warmup),可在 pod 启动阶段自动执行 dummy inference,确保首请求延迟 < 50ms;
  • 强制定义 config.pbtxt ,明确声明输入输出 tensor 名称、shape、dtype,自动生成 gRPC/HTTP 接口,所有客户端都通过标准 protocol buffer 通信。

提示:Triton 不是唯一解,Seldon Core、KServe(原 KFServing)也符合这一设计哲学。但 Triton 对 ONNX/TensorRT/PyTorch 模型的支持成熟度、社区文档的实操细节(比如如何配置 dynamic batch)、以及对 GPU 显存碎片的优化策略,让它在我们 2022–2024 年的 5 个项目中始终是首选。选择依据不是“支持多少框架”,而是“当你的模型出现 1% 的精度波动时,它的日志能否帮你 5 分钟定位到是输入预处理 bug 还是 TensorRT kernel 编译错误”。

2.2 为什么模型注册中心(Model Registry)必须独立于代码仓库?

另一个常见误区是把模型文件( .onnx , .pt )直接提交到 Git。理由很朴素:“版本控制嘛,和代码一起管”。但现实很快教会我们:Git 不是二进制文件的家。一个 1.2GB 的 ResNet50 ONNX 模型, git clone 会卡住 11 分钟,CI 流水线每次 checkout 要额外消耗 3.2GB 磁盘空间,更别说 git blame 查模型变更记录这种反人类操作。

Part 4 的第二条设计原则是: 模型是制品(Artifact),不是源码(Source Code) 。它必须拥有独立生命周期——训练完成即生成唯一 ID(如 model-20240521-1423-7f8a3c ),经过离线评估(AUC > 0.85)、AB 测试(CTR +2.1%)、人工审核(法务确认无 PII 数据泄露)后,才能进入 Staging 环境。这个过程不能依赖 git tag v1.2.3 ,而必须由专用模型注册中心驱动。

我们选用 MLflow Model Registry,原因很务实:

  • 它不强制你改训练代码(不像 SageMaker Pipelines 要重写 estimator);
  • mlflow.pyfunc.log_model() 可以无缝包装任意 Python 函数(包括用 subprocess 调用 R 脚本的遗产模型);
  • UI 界面清晰展示每个版本的 training run ID、source commit、metrics 表格、stage(None/Staging/Production)状态,运营同学不用敲命令就能完成版本晋升。

注意:MLflow Registry 的后端存储我们坚持用 PostgreSQL(而非默认的 file store),因为 file store 在并发写入时会出现 race condition——当两个实验同时完成并尝试 client.transition_model_version_stage() ,其中一个会静默失败。PostgreSQL 的行级锁能保证原子性。这个细节在官方文档里藏得很深,但我们在线上因此丢过一次关键模型版本,教训深刻。

2.3 为什么监控体系必须包含“数据—模型—业务”三层联动?

很多团队的监控止步于“GPU 利用率 > 80%”或“API 响应时间 P95 < 200ms”。这就像只盯着汽车仪表盘的转速表,却不管油箱是不是空的、轮胎有没有爆。Part 4 的第三条设计底线是: 监控必须穿透技术栈,形成数据质量 → 模型性能 → 业务指标的因果链

举个真实案例:某推荐系统上线后,点击率(CTR)连续 3 天下降 1.8%。运维查 GPU 监控一切正常,算法查模型 AUC 也没变。最后发现是上游数据管道故障——用户行为日志中的 device_type 字段,因新 App 版本升级,从 "ios" 变成了 "iOS" (首字母大写)。模型训练时用的是小写,线上推理时遇到大写, OneHotEncoder 直接返回全零向量,导致整个用户画像 embedding 崩溃。但这个异常没有触发任何告警,因为:

  • 数据层: device_type 的值分布监控只看 cardinality(枚举值数量),没看 value frequency( iOS 出现频次突增 300%);
  • 模型层: input_distribution_drift 检测只对比了数值型特征(如 age , spend_last_7d ),忽略了类别型特征的字符串规范化问题;
  • 业务层:CTR 下降告警阈值设为 5%,1.8% 的波动被淹没在日常噪声里。

因此,Part 4 的监控架构强制分三层:

  • 数据层 :用 Great Expectations 检查 device_type 必须满足 values_in_set = ["android", "ios", "web"] ,且 value_frequency ios 占比应在 45%±3% 区间;
  • 模型层 :用 Evidently AI 计算 category_feature_drift ,当 device_type 的 JS 散度 > 0.15 时触发 MODEL_INPUT_SKEW 告警;
  • 业务层 :将 CTR 指标与 model_input_drift_alert_count 做相关性分析,当两者皮尔逊系数 > 0.7 且持续 2 小时,自动创建跨团队工单(Data Eng + ML Eng + Product)。

这个三层联动不是理论设计,而是我们用 17 个线上事故反推出来的最小必要集。少一层,就会多一次“凌晨三点全员会议却找不到根因”的窘迫。


3. 核心细节解析与实操要点:从配置文件到告警规则的硬核拆解

3.1 Triton 配置文件 config.pbtxt 的 5 个生死参数

Triton 的灵魂不在代码,而在 config.pbtxt 。一份配置错误的文件,会让模型永远处于 UNAVAILABLE 状态,而日志里只有一行 failed to load model 。以下是我们在 12 个模型部署中反复验证的 5 个关键参数及其取舍逻辑:

参数 示例值 为什么重要 实操陷阱
max_batch_size 32 控制 Triton 是否启用动态批处理(dynamic batching)。设为 0 则禁用,每个请求单独推理;设为 32 则最多等 10ms(见 priority 参数)攒够 32 个请求再一起送 GPU。这对吞吐量提升极大,但会增加 P99 延迟。我们所有实时推荐模型都设为 32 ,而风控模型(要求绝对低延迟)设为 0 切勿盲目设 max_batch_size=1024 。显存会瞬间占满,Triton 启动失败。正确做法:用 tritonserver --model-repository=/models --model-control-mode=explicit 启动后,用 perf_analyzer -m mymodel -b 16,32,64 测不同 batch size 下的吞吐(infer/sec)和延迟(ms),找拐点。我们发现 ResNet50 在 V100 上,batch=64 吞吐达峰值,但 batch=128 时延迟跳变,故取 64
instance_group [{kind: KIND_GPU, count: 2}] 指定模型实例数及硬件类型。 count: 2 表示启动 2 个独立进程,各自绑定一块 GPU(需 CUDA_VISIBLE_DEVICES=0,1 )。这比单进程多线程更稳定——一个实例崩溃不影响另一个。注意: KIND_CPU 仅用于调试,生产环境必须 KIND_GPU 常见错误: count: 2 但宿主机只有 1 块 GPU。Triton 会静默降级为 count: 1 ,但日志不报错。务必用 nvidia-smi 确认 GPU 数量,并在 k8s Deployment 中用 resources.limits.nvidia.com/gpu: 2 锁定资源。
dynamic_batching max_queue_delay_microseconds: 10000 配合 max_batch_size 使用。表示最多等待 10ms(10000 微秒)凑 batch。设太小(如 1000 )则 batch 经常凑不满,吞吐上不去;设太大(如 100000 )则 P99 延迟飙升。我们所有服务统一设 10000 ,经 AB 测试,相比 5000 吞吐提升 22%,P99 延迟仅增 3.2ms,在业务可接受范围内。 这个值必须和业务 SLA 对齐。例如支付风控要求 P99 < 50ms,则 max_queue_delay_microseconds 绝不能超过 50000 。我们曾因抄错配置,把风控模型设成 100000 ,导致高峰期 12% 请求超时,损失订单。
input / output name: "INPUT__0", data_type: TYPE_FP32, dims: [3, 224, 224] 必须与模型导出时的 tensor name 和 shape 严格一致。ONNX 模型用 onnx.shape_inference.infer_shapes_path() 检查;PyTorch 用 torch.jit.trace() 导出时, example_inputs 的 shape 就是 dims。名称不匹配会导致 INVALID_ARG 错误。 最易错的是 dims 维度顺序。PyTorch 默认 NCHW (batch, channel, height, width),TensorFlow 是 NHWC 。Triton 不自动转换!必须在预处理代码里显式 permute(0, 3, 1, 2) ,或在 config.pbtxt 中用 reshape 参数修正。我们吃过亏:一个 TensorFlow 模型导出 ONNX 时没指定 opset=13 reshape 节点被优化掉,导致 Triton 加载后输入维度错乱。
version_policy specific: { versions: [1] } 控制 Triton 加载哪些模型版本。 specific 表示只加载 version 1; latest: { num_versions: 1 } 表示加载最新版。生产环境必须用 specific ,杜绝“自动升级到新版导致线上故障”。版本号必须和 MLflow Registry 中的 version 严格对应。 陷阱: version_policy 设为 latest 时,Triton 会在每次模型 repository 变更时自动 reload。如果新版本有 bug,服务会瞬间不可用。我们线上所有服务都强制 specific ,并通过 CI 流水线在 MLflow 中 transition_stage("Production") 后,自动更新 config.pbtxt 并触发 k8s rolling update。

实操心得: config.pbtxt 不是写一次就完事的静态文件。我们把它纳入 Git 仓库,和模型代码同目录,但用 .gitattributes 设置 config.pbtxt diff=ast ,用自定义 diff 工具(基于 protobuf 解析)对比语义差异,而非行差异。这样 max_batch_size 32 改到 64 会被识别为“重大变更”,触发强制 code review。

3.2 MLflow Model Registry 的 Stage 管理与自动化晋升

MLflow 的 Staging / Production stage 不是标签,而是具有强约束的发布状态。Part 4 的核心实践是: Stage 变更必须由自动化流水线驱动,禁止手动操作 。原因很简单:手动 transition_model_version_stage() 无法留下审计线索,也无法关联到具体的测试报告。

我们的 CI/CD 流水线(基于 GitHub Actions)设计如下:

  1. 训练完成 mlflow.run() 结束后,自动调用 mlflow.register_model() 创建新版本,初始 stage 为 None
  2. 离线评估 :流水线启动一个专用 job,加载该版本模型,用 holdout test set 计算 auc , f1_score , inference_time_p95 ,结果写入 MLflow Run 的 metrics
  3. 准入检查 :脚本读取 metrics.auc > 0.85 and metrics.inference_time_p95 < 150 ,若通过,自动执行 client.transition_model_version_stage(name=model_name, version=new_version, stage="Staging")
  4. AB 测试 :Staging 环境流量 5%,持续 24 小时,Prometheus 抓取 model_staging_latency_p95 , model_staging_error_rate
  5. 生产晋升 :当 model_staging_error_rate < 0.001 model_staging_latency_p95 < 180 持续 2 小时,流水线自动 transition_stage("Production") ,并触发 k8s 部署。

这个流程的关键细节在于:

  • Stage 变更必须带 reason :MLflow API 支持 archive_existing_versions=True comment="Promote after 24h AB test, error_rate=0.0008" ,所有操作留痕;
  • Production 版本必须锁定 :在 config.pbtxt 中, version_policy specific 值必须和 Production 版本号一致,CI 流水线在晋升后自动更新 config 并提交 PR;
  • 回滚机制 :当 Production 版本出现故障,运维可立即执行 transition_stage("Staging") ,Triton 会在 30 秒内 reload 旧版本——这比重建 k8s pod 快 10 倍。

注意:MLflow Registry 的 search_model_versions() API 默认只返回 100 条结果。如果你有 200 个模型版本, client.search_model_versions("name='recommender'") 会漏掉 100 个。必须加 max_results=500 参数。这个坑我们踩过,导致 AB 测试报告里找不到最新版本的 metrics。

3.3 Evidently AI 的数据漂移检测:从配置到告警的完整链路

Evidently 不是开箱即用的“检测工具”,而是需要深度定制的“诊断框架”。Part 4 的实践表明: 90% 的漂移告警无效,根源在于检测范围和阈值设置不合理

我们以用户画像模型为例,其输入包含 3 类特征:

  • 数值型: age , spend_last_30d , login_count_last_7d
  • 类别型: gender , city_tier , device_type
  • 文本型: user_interest_tags (逗号分隔的字符串,如 "sports,tech,travel" )。

Evidently 默认对所有特征做 drift 检测,但 user_interest_tags 的 JS 散度计算毫无意义——它本质是多标签集合,应该检测 tag_coverage (覆盖率)和 tag_cooccurrence (共现频率)。因此,我们的 evidently_config.yaml 关键配置如下:

data_drift:
  # 只检测关键数值特征,忽略 spend_last_30d(波动大,噪声多)
  numerical_features: ["age", "login_count_last_7d"]
  # 类别特征必须指定 allowed_values,否则新值出现即告警
  categorical_features:
    gender: {allowed_values: ["M", "F", "O"]}
    city_tier: {allowed_values: ["1", "2", "3"]}
    device_type: {allowed_values: ["android", "ios", "web"]}
  # 文本特征自定义检测器
  text_features:
    user_interest_tags:
      detector: "tag_coverage"
      threshold: 0.05  # 当覆盖率下降 >5%,触发告警

告警链路设计为三级:

  • Level 1(数据层) :Evidently 生成 data_drift_report.html ,每日凌晨 2 点运行,上传至 S3;
  • Level 2(模型层) :Python 脚本解析 report 中的 drift_detected 字段,若 True ,则提取 feature_name js_distance ,写入 Prometheus 的 evidently_drift_score{feature="age"} 指标;
  • Level 3(业务层) :Grafana 面板设置告警规则 evidently_drift_score{feature="age"} > 0.25 for 1h ,触发 Slack 通知,并自动创建 Jira ticket,assignee 为 Data Engineer。

实操心得:Evidently 的 js_distance 阈值不能拍脑袋定。我们用历史 30 天数据做 baseline,计算每个特征的 js_distance 的 P95 值,再乘以 1.5 作为阈值。例如 age 的 P95 是 0.12 ,则阈值设为 0.18 。这样既不过敏(每天 20 个告警),也不迟钝(漏掉真实漂移)。


4. 实操过程与核心环节实现:从本地验证到灰度发布的全流程手记

4.1 本地开发环境:用 Docker Compose 模拟生产全链路

在把代码推到 CI 之前,必须确保本地能 100% 复现生产行为。我们弃用了“本地跑 Flask + 远程连 Triton”的模式,改用 Docker Compose 一键拉起完整栈:

# docker-compose.yml
version: '3.8'
services:
  triton:
    image: nvcr.io/nvidia/tritonserver:24.04-py3
    ports: ["8000:8000", "8001:8001", "8002:8002"]
    volumes:
      - ./models:/models
      - ./config.pbtxt:/models/recommender/config.pbtxt
    command: tritonserver --model-repository=/models --strict-model-config=false

  mlflow:
    image: ghcr.io/mlflow/mlflow:2.12.2
    ports: ["5000:5000"]
    volumes:
      - ./mlruns:/mlruns

  prometheus:
    image: prom/prometheus:latest
    ports: ["9090:9090"]
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

关键技巧:

  • Triton 启动加 --strict-model-config=false ,允许 config.pbtxt 中缺失非关键字段(如 dynamic_batching ),方便本地快速迭代;
  • volumes 映射 ./models 到容器内,修改本地 config 文件后,Triton 会自动 reload(需开启 --model-control-mode=none );
  • 所有服务共享一个 Docker network,Python client 可用 http://triton:8000/v2/health/ready 检查健康状态。

本地验证 checklist:

  1. curl http://localhost:8000/v2/health/ready 返回 {"ready": true}
  2. python client.py --model recommender --input '{"user_id": 123}' 返回有效预测;
  3. curl http://localhost:5000/api/2.0/mlflow/model-versions/search?filter="name='recommender'" 返回最新版本;
  4. curl http://localhost:9090/api/v1/query?query=evidently_drift_score 返回指标数据。

提示:Docker Compose 的 depends_on 不保证服务就绪。我们在 client.py 开头加了重试逻辑: while not is_triton_ready(): time.sleep(1) ,避免 CI 流水线因启动时序失败。

4.2 CI/CD 流水线:GitHub Actions 的 7 个关键 Job

我们的 GitHub Actions 流水线( .github/workflows/ml-deploy.yml )不是线性流程,而是网状依赖,确保每个环节失败都不影响其他环节:

name: ML Model Deploy
on:
  push:
    branches: [main]
    paths: ["models/recommender/**"]

jobs:
  # Job 1: 模型训练与注册
  train:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - name: Setup Python
        uses: actions/setup-python@v4
        with: {python-version: '3.10'}
      - name: Train & Register
        run: |
          python train.py --model-name recommender
          # 自动注册,version 由 MLflow 生成
        env:
          MLFLOW_TRACKING_URI: http://mlflow:5000

  # Job 2: 离线评估(依赖 train)
  evaluate:
    needs: train
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - name: Evaluate
        run: python evaluate.py --model-version ${{ needs.train.outputs.version }}
        env:
          MODEL_VERSION: ${{ needs.train.outputs.version }}

  # Job 3: Triton 配置验证(独立运行,不依赖 train)
  validate-config:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - name: Check config.pbtxt syntax
        run: |
          # 用 protoc 检查 pbtxt 是否合法
          docker run --rm -v $(pwd):/work -w /work alpine/protoc \
            protoc --decode_raw < models/recommender/config.pbtxt

  # Job 4: 模型打包(生成 ONNX,上传 S3)
  package:
    needs: [train, evaluate]
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - name: Export ONNX
        run: python export_onnx.py --model-version ${{ needs.train.outputs.version }}
      - name: Upload to S3
        uses: jakejarvis/s3-sync-action@v0.5.1
        with:
          args: --acl public-read
        env:
          AWS_S3_BUCKET: my-ml-artifacts
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

  # Job 5: Staging 部署(k8s apply)
  deploy-staging:
    needs: [package, validate-config]
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Staging
        run: kubectl apply -f k8s/staging/

  # Job 6: AB 测试监控(拉取 Prometheus 数据)
  ab-test:
    needs: deploy-staging
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - name: Check AB Metrics
        run: python check_ab.py --duration 24h

  # Job 7: Production 晋升(手动触发,需 approval)
  promote-prod:
    needs: ab-test
    if: github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - name: Promote to Production
        run: python promote.py --model-name recommender

这个设计的精妙之处在于:

  • validate-config 独立运行,即使训练失败,也能提前发现配置语法错误;
  • package 依赖 train evaluate ,确保只打包通过评估的模型;
  • promote-prod 是手动触发( workflow_dispatch ),且 require team lead approval,杜绝误操作;
  • 所有 Job 的输出(如 MODEL_VERSION )通过 outputs 传递,避免硬编码。

实操心得:GitHub Actions 的 secrets 不能跨 workflow 共享。我们把 AWS credentials 存在 GitHub Environment Secrets 中,并为 staging production 环境分别设置,确保 staging 流水线拿不到 prod 的密钥。

4.3 灰度发布:用 Istio VirtualService 实现 5% 流量切分

k8s 的 Service 只能做 round-robin,无法按比例切分流量。我们用 Istio 的 VirtualService 实现精准灰度:

# istio-virtualservice.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: recommender-vs
spec:
  hosts:
  - recommender.prod.svc.cluster.local
  http:
  - name: "production"
    route:
    - destination:
        host: recommender-prod
        subset: v1
      weight: 95  # 95% 流量到 v1(旧版)
  - name: "staging"
    match:
    - headers:
        x-canary:
          exact: "true"
    route:
    - destination:
        host: recommender-staging
        subset: v2
      weight: 100
  - name: "canary"
    match:
    - headers:
        x-canary:
          absent: true
    route:
    - destination:
        host: recommender-prod
        subset: v1
      weight: 95
    - destination:
        host: recommender-staging
        subset: v2
      weight: 5  # 5% 流量到 v2(新版)

关键点:

  • subset 由 k8s Service 的 label 定义: recommender-prod 的 pod 有 version: v1 recommender-staging version: v2
  • x-canary: true header 用于内部测试,100% 流量到 v2;
  • 主流量走 absent: true 分支,95%/5% 切分。

灰度期间,我们监控三个黄金指标:

  • istio_requests_total{destination_service="recommender-staging", response_code=~"5.."} / istio_requests_total{destination_service="recommender-staging"} —— v2 的错误率;
  • histogram_quantile(0.95, sum(rate(istio_request_duration_seconds_bucket{destination_service="recommender-staging"}[5m])) by (le)) —— v2 的 P95 延迟;
  • model_prediction_confidence{model_version="v2"} > 0.8 的占比 —— v2 的预测置信度是否稳定。

当这三个指标连续 30 分钟达标,才触发 promote-prod Job。

注意:Istio 的 weight 是整数,不能设 4.5 。我们用 95/5 而非 99/1 ,因为 1% 流量在 QPS=1000 时只有 10 rps,统计噪声太大,无法判断真实效果。


5. 常见问题与排查技巧实录:那些凌晨三点教会我的事

5.1 Triton 启动失败: failed to load model 的 7 种根因与速查表

Triton 日志里最让人绝望的一行就是 failed to load model ,但它背后有至少 7 种完全不同的原因。我们整理成速查表,按出现频率排序:

现象 日志关键词 根因 排查命令 解决方案
1. 模型文件路径错误 unable to open model directory config.pbtxt platform 声明为 pytorch_libtorch ,但实际放的是 .onnx 文件 ls -l /models/recommender/1/ 检查 1/ 目录下是否有 model.onnx ,并在 config.pbtxt 中设 platform: "onnxruntime_onnx"
2. CUDA 版本不兼容 CUDA driver version is insufficient Triton 镜像 CUDA 版本(如 12.2)高于宿主机驱动(如 11.8) nvidia-smi docker run --rm nvcr.io/nvidia/tritonserver:24.04-py3 nvidia-smi 升级宿主机驱动,或换 Triton 镜像(如 23.12-py3 对应 CUDA 12.1)
3. 输入 shape 不匹配 unexpected number of dimensions config.pbtxt dims: [3,224,224] ,但 ONNX 模型实际是 [1,3,224,224] (含 batch 维度) onnx.shape_inference.infer_shapes_path("model.onnx") config.pbtxt 中加 reshape: [3,224,224] ,或导出 ONNX 时设 dynamic_axes={"input": {0: "batch"}}
4. 模型权重损坏 invalid onnx model 模型文件下载中断,MD5 校验失败 `md5sum /models/recomm
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐