1. 项目概述:这不是一次“部署”,而是一场从实验室到产线的系统性迁移

“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题里藏着太多被轻描淡写却重若千钧的词。“Notebook”不是指纸质本子,而是Jupyter里那个写着 model.fit() plt.show() 、一切看起来都闪闪发光的交互式沙盒;“Production”也不是简单地把模型跑起来,而是它得在凌晨三点的订单洪峰里不掉链子,在客户上传模糊图片时给出稳定置信度,在数据库字段悄悄变更后仍能正确解析输入,在运维同事重启服务器后自动恢复服务,甚至在某天你休假时,它还在 quietly 处理着上万条实时风控请求。我做过27个从0到1落地的ML项目,其中19个卡在Part 2(模型训练完成)和Part 3(API封装)之间,真正走到Part 4并稳定运行超6个月的,只有8个。而这第4部分,恰恰是区分“AI玩具”和“AI资产”的分水岭。它不讲AUC有多高,只关心P99延迟是否压在120ms以内;不炫耀F1-score,只盯着日志里每小时出现几次 KeyError: 'user_profile' ;不谈Transformer结构多优雅,只问模型镜像体积能不能从1.8GB压到420MB以适配边缘网关。这篇内容面向的不是刚学完scikit-learn的新人,而是已经把模型调到满意、正对着Dockerfile发呆、被SRE同事微信轰炸“接口又503了”的实战者。它解决的核心问题很朴素: 当你的模型不再只服务于你自己,而要成为业务流水线中一个可信赖、可监控、可回滚、可计费的环节时,你该亲手拧紧哪几颗螺丝? 后面所有内容,都基于我在电商推荐、金融反欺诈、工业设备预测性维护三个垂直场景中踩过的坑、写的脚本、改过的K8s YAML、以及凌晨两点和值班工程师一起盯屏排查OOM的实录。

2. 整体设计思路:为什么必须放弃“一键部署”幻觉,转向分层治理架构

2.1 拒绝“Notebook即服务”的诱惑:从单点可靠到系统可靠

很多团队的第一反应是:把 .ipynb 文件用 nbconvert 转成Python脚本,再用Flask包一层,扔进Docker, docker run -p 5000:5000 ——完事。我试过,也上线过。结果呢?第一个月,模型API平均响应时间从180ms跳到420ms;第二周,因依赖库版本冲突导致特征工程模块静默失败,线上推荐列表变成随机播放;第三天,用户上传一张12MB的扫描件PDF,Flask直接OOM崩溃,整个服务不可用。问题出在哪?根本不在模型本身,而在于这种“单体式封装”把四个完全异构的系统强行焊死在一个进程里: 数据加载层(I/O密集)、特征计算层(CPU密集)、模型推理层(GPU/CPU混合)、服务编排层(网络/并发) 。它们对资源的需求、故障模式、扩缩容节奏、监控粒度全都不一样。就像把锅炉房、配电室、控制台和客服中心全塞进同一间玻璃房——温度一高,锅炉报警,配电跳闸,控制台黑屏,客服电话全占线。真正的生产就绪(Production-Ready),第一步就是解耦。我们最终采用的四层分离架构是:

  • 接入层(Ingress Layer) :Nginx + Lua脚本做请求预检(大小限制、格式校验、基础鉴权),拒绝非法流量于门外,避免脏数据一路穿透到模型层;
  • 服务层(Serving Layer) :使用Triton Inference Server(NVIDIA)或KServe(原KFServing)管理模型生命周期,支持同模型多版本灰度、GPU显存隔离、动态批处理(Dynamic Batching);
  • 计算层(Compute Layer) :将特征工程逻辑彻底剥离,用独立的Feature Store服务(如Feast或自建Redis+Presto集群)提供低延迟特征查询,模型服务只负责纯推理;
  • 可观测层(Observability Layer) :Prometheus采集指标(QPS、P99延迟、GPU利用率、内存RSS)、Loki收集结构化日志(含trace_id)、Jaeger追踪跨服务调用链。

这个架构不是为了炫技,而是每一层都对应一个明确的SLO(Service Level Objective)。比如接入层SLO是“99.9%请求在50ms内完成预检”,服务层SLO是“99.5%推理请求在150ms内返回”,计算层SLO是“99.99%特征查询在20ms内完成”。当某个SLO告警,你能精准定位到是哪一层出了问题,而不是在几百行日志里大海捞针。

2.2 模型交付物标准化:为什么 .pkl 文件永远不该出现在生产镜像里

新手常犯的致命错误:把训练好的 model.pkl 直接COPY进Docker镜像。这看似简单,实则埋下三颗雷: 环境漂移(Environment Drift) 安全漏洞(Security Vulnerability) 回滚失效(Rollback Failure) 。我亲眼见过一个项目,因为训练环境用的是 scikit-learn==1.0.2 ,而生产镜像里 pip install -r requirements.txt 装的是 1.2.0 ,导致 RandomForestClassifier.predict_proba() 返回的数组维度错乱,线上转化率报表连续三天显示为负数。更糟的是, .pkl 是Python专有二进制格式,无法跨语言调用,也无法被模型监控平台(如Evidently)直接解析其内部结构。我们的解决方案是强制推行 模型序列化标准协议

  • ONNX(Open Neural Network Exchange) :作为中间表示(IR),覆盖95%的PyTorch/TensorFlow/Sklearn模型。它不绑定Python版本,可被C++、Java、Go直接加载,且支持静态图优化(如算子融合、常量折叠)。我们用 skl2onnx 转换Sklearn模型,用 torch.onnx.export() 导出PyTorch模型,所有ONNX文件必须通过 onnx.checker.check_model() 验证;
  • Triton Model Repository 结构 :每个模型目录严格遵循 models/{model_name}/{version}/ ,其中 config.pbtxt 明确定义输入输出张量名、数据类型、动态批处理策略。例如一个图像分类模型的config:
    name: "resnet50"
    platform: "onnxruntime_onnx"
    max_batch_size: 32
    input [
      {
        name: "input"
        data_type: TYPE_FP32
        dims: [ 3, 224, 224 ]
        reshape: { shape: [ 3, 224, 224 ] }
      }
    ]
    output [
      {
        name: "output"
        data_type: TYPE_FP32
        dims: [ 1000 ]
      }
    ]
    
    这份配置不是可选的,而是Triton加载模型的唯一依据,它让模型行为完全可声明、可版本化、可审计。

提示:ONNX转换不是无损的。我们发现 torch.nn.Dropout 在ONNX中会被优化掉(训练/推理模式差异),必须在导出前手动替换为 torch.nn.Identity() ;Sklearn的 OneHotEncoder 若含 handle_unknown='ignore' ,需先用 skl2onnx.convert_sklearn() options 参数显式启用支持,否则转换失败。这些细节,文档里不会写,但线上故障单里全是。

2.3 基础设施即代码(IaC):为什么K8s YAML不能手写,而要用Helm Chart + Kustomize

有人觉得:“K8s不就是写几个YAML文件吗?复制粘贴改改名字就行。” 我们曾用纯YAML管理12个模型服务,结果一次紧急回滚,因忘记修改 imagePullPolicy: Always IfNotPresent ,导致所有Pod拉取旧镜像失败,服务中断47分钟。纯YAML的问题在于: 零复用、难审计、易出错 。不同环境(dev/staging/prod)的资源配置(CPU limit、HPA阈值、健康检查路径)差异巨大,手写意味着12份几乎相同的文件,每次变更都要同步修改12处。我们的实践是三层抽象:

  • Helm Chart 作为模板引擎 :定义 values.yaml 中的可变参数(如 replicaCount , resources.limits.memory ), templates/ 目录下用Go template语法生成YAML。一个Chart可同时部署图像识别、NLP文本分类、时序预测三个不同模型,只需传入不同 values-prod.yaml
  • Kustomize 作为环境叠加器 :为dev/staging/prod创建独立的 kustomization.yaml ,通过 patchesStrategicMerge 精准覆盖特定字段。例如prod环境强制添加 podSecurityContext: {runAsNonRoot: true} ,而dev环境禁用;
  • GitOps 流水线驱动 :所有Chart和Kustomize配置存于Git仓库,Argo CD监听变更,自动同步到集群。任何一次 kubectl edit 都是违规操作,所有变更必须走PR流程,附带变更影响说明和回滚预案。

这套组合拳带来的直接收益是:新模型上线时间从平均3.2天压缩到47分钟;配置错误导致的事故归零;审计时能清晰追溯“谁在何时为何修改了哪个服务的内存限制”。

3. 核心细节与实操要点:从模型打包到服务上线的17个关键决策点

3.1 镜像构建:为什么Alpine Linux不是最优解,而Distroless才是生产首选

很多人追求镜像体积小,第一反应是 FROM python:3.9-alpine 。我实测过:一个PyTorch模型服务,Alpine镜像体积382MB,但启动后RSS内存占用比Ubuntu镜像高18%,且 glibc 兼容性问题频发(尤其涉及NumPy底层BLAS加速时)。Alpine用musl libc替代glibc,而绝大多数科学计算库(OpenBLAS、Intel MKL、CUDA驱动)默认链接glibc。我们曾遇到 numpy.linalg.svd() 在Alpine上返回NaN,切换到 python:3.9-slim (Debian slim)后立即修复。但slim版仍有Python解释器、包管理器等非必要组件,存在攻击面。最终方案是 Google Distroless

# 使用distroless作为基础镜像,仅含运行时必需
FROM gcr.io/distroless/python3-debian11

# 复制已预编译的依赖(wheel)
COPY --from=builder /app/requirements.txt /app/requirements.txt
COPY --from=builder /app/wheels /app/wheels
RUN pip install --no-cache-dir --find-links /app/wheels --no-index -r /app/requirements.txt

# 复制应用代码和ONNX模型
COPY --from=builder /app/src /app/src
COPY --from=builder /app/models /app/models

# 指定非root用户
USER nonroot:nonroot

# 入口点必须是绝对路径,distroless无shell
ENTRYPOINT ["/app/src/entrypoint.py"]

关键点在于: 所有依赖(包括 torch , onnxruntime , numpy )必须预先在builder阶段编译为wheel包 ,因为distroless镜像里没有 gcc make 等编译工具。我们用 pip wheel --no-deps --wheel-dir /wheels -r requirements.txt 生成wheel,再用 auditwheel repair 修复manylinux兼容性。最终镜像体积压至215MB,启动内存降低22%,且CVE漏洞数量减少83%(Clair扫描结果)。更重要的是,它强制你思考:“这个包真的需要吗?”——我们因此删掉了 jupyter , matplotlib 等开发期依赖,模型服务纯净度大幅提升。

3.2 特征服务化:为什么不能在模型服务里写 pd.read_csv() ,而必须用Feature Store

一个典型反模式:模型服务启动时, __init__.py 里执行 self.user_features = pd.read_csv('/data/user_features.csv') 。这带来三大硬伤: 冷启动慢 (大CSV加载耗时)、 内存爆炸 (重复加载多份副本)、 数据陈旧 (CSV每周更新,服务不重启就永远用旧数据)。我们曾有个用户画像服务,因加载3.2GB的 user_embedding.csv ,Pod启动时间长达6分12秒,K8s Liveness Probe连续失败,触发反复重启。解决方案是构建轻量级Feature Store:

  • 存储层 :Redis Cluster(热特征,<10ms P99)、PostgreSQL(冷特征,结构化查询);
  • SDK层 :自研Python SDK,提供统一 get_features(entity_id, feature_list) 接口;
  • 缓存策略 :SDK内置两级缓存——本地LRU(1000条,避免Redis穿透)、Redis分布式缓存(TTL=300s);
  • 血缘追踪 :每次 get_features 调用自动记录 feature_name , entity_id , timestamp , latency_ms 到ClickHouse,用于分析特征使用热度和延迟分布。

实操中,我们要求所有模型服务必须通过SDK访问特征,禁止直连数据库或读文件。SDK初始化时会连接Redis并预热常用特征(如 user_active_days_7d ),确保首请求延迟可控。上线后,模型服务冷启动时间从6分钟降至3.2秒,特征更新延迟从小时级降到秒级(Kafka实时同步),且通过ClickHouse分析发现,87%的特征请求集中在23个高频特征上,据此优化了Redis分片策略。

3.3 模型监控:为什么只看准确率是危险的,而必须建立多维监控矩阵

很多团队的监控告警只有一条:“模型API 5xx错误率 > 0.1%”。这就像汽车仪表盘只显示“发动机故障灯亮了”,却不告诉你油压、水温、转速。我们构建了四维监控矩阵,每维都有明确阈值和自动处置动作:

维度 监控指标 阈值 自动处置 数据来源
基础设施 GPU显存使用率、CPU Load1、内存RSS >90%持续5min 触发HPA扩容,发送Slack告警 Prometheus Node Exporter
服务性能 QPS、P99延迟、HTTP 5xx率 P99 > 200ms持续10min 降级至备用模型(fallback model),记录trace_id Triton Metrics + Nginx Access Log
数据质量 输入特征分布偏移(KS检验)、缺失率、数值范围越界 KS统计量 > 0.2 或 缺失率 > 5% 冻结该特征,触发数据管道重跑 Evidently + 自研DataDriftDetector
模型效果 在线AUC滑动窗口、预测置信度分布、类别不平衡度 AUC下降 > 0.03 或 置信度<0.3占比 > 30% 启动模型重训Pipeline,通知算法团队 自研OnlineEvaluator

关键创新点在于 效果监控的在线化 。传统做法是每天抽样1万条日志离线计算AUC,滞后24小时。我们改为:对每1000次请求,采样100条(带权重),用 sklearn.metrics.roc_auc_score 实时计算,并用Exponential Moving Average平滑噪声。这样AUC波动能在5分钟内被感知。去年双十一,该监控捕获到一个图像分类模型因上游摄像头白平衡参数调整,导致输入图像整体偏蓝,AUC在22分钟内从0.927跌至0.841,系统自动触发重训,避免了数百万订单的误判。

注意:KS检验对小样本敏感。我们设定最小采样量为5000条/小时,不足则不触发告警。这是从一次误报中吸取的教训——某天凌晨流量低谷,采样仅832条,KS值虚高,导致误触发数据管道重跑,浪费了32核·小时计算资源。

4. 实操过程详解:从本地Notebook到K8s集群的完整流水线

4.1 本地开发:如何让Notebook里的代码无缝迁移到生产服务

核心原则: Notebook只做探索,不写逻辑 。所有可复用的代码(数据加载、特征工程、模型定义)必须拆分为独立的 .py 模块,并通过 pip install -e . 安装到本地Python环境。我们的项目结构强制要求:

my_ml_project/
├── notebooks/           # 仅存.ipynb,用于EDA、可视化、快速验证
├── src/
│   ├── __init__.py
│   ├── data/            # 数据加载器,支持S3/Local/DB多种后端
│   │   ├── loader.py
│   │   └── utils.py
│   ├── features/        # 特征工程函数,纯函数式,无状态
│   │   ├── user.py
│   │   └── item.py
│   ├── models/          # 模型定义,与训练/推理解耦
│   │   ├── resnet50.py
│   │   └── __init__.py
│   └── serving/         # 服务入口,适配Triton/KServe
│       ├── triton/      # Triton专用backend
│       └── entrypoint.py # 主服务入口
├── models/              # 存放ONNX模型文件(git-lfs管理)
├── tests/               # 单元测试覆盖所有src模块
└── pyproject.toml     # 定义build-system和dependencies

关键步骤是 Notebook-to-Module的转换检查清单

  1. 删除所有 %matplotlib inline !pip install os.chdir() 等魔法命令和副作用代码
  2. df = pd.read_csv('data/train.csv') 替换为 df = DataLoader().load('train') ,数据路径由环境变量 DATA_ROOT 注入
  3. model = RandomForestClassifier().fit(X, y) 拆为两步: model = RandomForestClassifier() (定义)和 model.fit(X, y) (训练),训练逻辑移入 train.py
  4. 所有硬编码路径(如 '/home/user/model.pkl' )改为 pathlib.Path(__file__).parent.parent / 'models' / 'resnet50.onnx'
  5. 为每个模块编写doctest,确保 python -m doctest src/features/user.py 能通过

这样做的好处是:当你在Notebook里调试完一个新特征 user_click_rate_24h ,只需将其函数体复制到 src/features/user.py ,加一行doctest,然后在服务代码里 from src.features.user import user_click_rate_24h 即可调用,零额外成本。

4.2 CI/CD流水线:GitHub Actions如何实现“提交即部署”

我们摒弃了Jenkins等重型CI工具,全部用GitHub Actions实现端到端自动化。流水线分为四个阶段,每个阶段失败即阻断:

  • Stage 1: Code Quality & Unit Test
    并行执行: black --check . (代码格式)、 flake8 . (PEP8)、 pytest tests/ --cov=src --cov-report=xml (覆盖率>85%才通过)。特别加入 pylint --disable=all --enable=duplicate-code 检测代码重复,避免特征工程逻辑在多个Notebook里散落。

  • Stage 2: Model Validation
    加载最新ONNX模型,用 onnxruntime.InferenceSession 执行100次推理,验证:

    • 输入输出shape匹配( session.get_inputs()[0].shape == [None, 3, 224, 224]
    • 输出概率和为1( np.allclose(np.sum(outputs, axis=1), 1.0)
    • 推理时间P95 < 50ms( timeit 测量)
  • Stage 3: Docker Build & Scan
    构建distroless镜像后,用 trivy image --severity CRITICAL,HIGH my-model-service:latest 扫描CVE,任一CRITICAL漏洞即失败。

  • Stage 4: K8s Deployment
    helm upgrade --install --atomic --wait --timeout 5m 部署到staging集群,部署后自动执行 curl -s http://staging-api/healthz | jq '.status' 验证服务健康,再运行 kubectl wait --for=condition=available --timeout=120s deploy/my-model-service 确认滚动更新完成。

整个流水线平均耗时8分23秒,从 git push 到staging环境可用,全程无人工干预。最关键的是 --atomic --wait 参数:一旦部署失败,Helm自动回滚到上一版本,保证staging环境永远可用。我们曾因 values-staging.yaml 里写错 replicaCount: 0 ,导致服务Pod数为0,Helm在12秒内检测到 AvailableReplicas=0 ,立即回滚,未造成任何影响。

4.3 生产环境部署:Triton Inference Server的12项关键配置调优

Triton不是开箱即用的,必须根据模型特性和硬件深度调优。我们在A100 80GB GPU上部署ResNet50时,通过以下12项配置将P99延迟从312ms压到89ms:

  1. 动态批处理(Dynamic Batching) dynamic_batching { max_queue_delay_microseconds: 100 } ,允许Triton将10ms窗口内的请求合并为一批,充分利用GPU并行能力;
  2. 内存预分配(Pinned Memory) optimization { execution_accelerators { gpu_execution_accelerator [ { name: "tensorrt" } ] } } ,启用TensorRT加速,自动进行算子融合和精度校准;
  3. 实例组(Instance Group) instance_group [ { count: 2, kind: KIND_GPU } ] ,为单个模型启动2个GPU实例,分担请求压力;
  4. 显存限制(Memory Limit) model_repository_path: "/models" + strict_model_config: false ,避免Triton因配置不全拒绝加载;
  5. 输入预处理卸载(Preprocessing Offload) :将图像resize/crop等CPU密集操作,用Triton的 ensemble 功能卸载到CPU实例,GPU只做纯推理;
  6. 输出后处理(Postprocessing) :用 custom backend编写Python后处理,将原始logits转为带label的JSON,避免客户端解析负担;
  7. 健康检查路径 /v2/health/ready 设为Liveness Probe, /v2/health/live 设为Readiness Probe,确保K8s只将流量导向健康实例;
  8. 日志级别 --log-verbose=1 ,开启详细日志,但用 --log-file=/var/log/triton.log 重定向,避免stdout污染K8s日志;
  9. CUDA可见性 nvidia-container-cli --load-kmods configure --ldcache /etc/ld.so.cache --device=all --utility --require=cuda>=11.4 brand=tesla,driver>=470,compute>=8.0 ,精确控制GPU设备可见性;
  10. NUMA绑定 numactl --cpunodebind=0 --membind=0 tritonserver ... ,将Triton进程绑定到CPU节点0和对应内存,减少跨NUMA访问延迟;
  11. 模型实例命名 name: "resnet50_v2" ,明确版本号,便于灰度发布时路由;
  12. 指标暴露 --metrics-interval-ms=2000 ,每2秒向Prometheus暴露指标,配合Grafana看板实时监控。

实操心得: max_queue_delay_microseconds 是调优关键。设太小(如10μs),批处理失效,延迟高;设太大(如10000μs),请求等待过久,用户体验差。我们用真实流量压测,以P99延迟最低为准则,最终选定100μs。这个值必须针对每个模型单独测试,没有通用解。

5. 常见问题与排查技巧实录:来自27个项目的故障模式库

5.1 典型故障速查表:从现象到根因的5分钟定位法

现象 可能根因 快速验证命令 解决方案
P99延迟突增300%,CPU使用率正常 GPU显存碎片化,Triton无法分配连续显存块 nvidia-smi --query-compute-apps=pid,used_memory --format=csv 重启Triton Pod,或启用 --cuda-memory-pool-byte-size=1073741824 预分配1GB显存池
服务启动后立即OOM Killed distroless镜像中 ulimit -v 未设置,Python进程虚拟内存无上限 kubectl exec -it <pod> -- sh -c 'ulimit -v' 在K8s Deployment中添加 securityContext: { resources: { limits: { memory: "4Gi" } } }
特征查询延迟从20ms升至2s Redis连接池耗尽,新请求排队等待 redis-cli --stat 查看 instantaneous_ops_per_sec connected_clients 将SDK中 redis.ConnectionPool(max_connections=100) 提升至500,增加连接复用
模型输出全为0或NaN ONNX模型输入张量dtype不匹配(如模型期望 float32 ,客户端传 float64 curl -X POST http://api/v2/models/resnet50/infer -d '{"inputs":[{"name":"input","shape":[1,3,224,224],"datatype":"FP32","data":[...]}]}' 在客户端强制 input_array = input_array.astype(np.float32) ,并在Triton config中显式声明 data_type: TYPE_FP32
K8s HPA不扩缩容 Prometheus指标抓取失败,或HPA配置中 metrics 指向错误指标名 kubectl get hpa 查看 TARGETS 列是否为 <unknown> 检查 prometheus-operator ServiceMonitor是否匹配Triton Service标签,确认指标名 nv_gpu_duty_cycle 存在

5.2 “幽灵故障”深度复盘:一次持续37小时的间歇性503

现象 :某金融风控模型服务,每天凌晨2:15-2:25之间,随机出现5-10次503错误,持续37小时,期间所有监控指标(CPU、内存、GPU、QPS、延迟)均显示正常。

排查路径

  • 第一步:检查Nginx access log,发现503集中于 upstream: "http://triton:8000" ,指向Triton服务;
  • 第二步: kubectl logs -f <triton-pod> ,无ERROR日志,只有INFO级推理记录;
  • 第三步: kubectl top pods ,Triton Pod内存RSS稳定在3.2Gi,但 kubectl describe pod 显示 Events 中有 Warning OOMKilling
  • 第四步:深入 /sys/fs/cgroup/memory/kubepods/burstable/pod<id>/memory.usage_in_bytes ,发现每小时峰值内存达4.1Gi,超过K8s limit 4Gi,触发OOMKilling,但Triton进程未退出,只是被内核杀死部分线程,导致HTTP server暂时不可用;
  • 根因 :Triton的 --model-control-mode=poll 模式下,每30秒轮询模型仓库,当模型文件较大(>500MB)时,轮询会触发大量内存分配,叠加GPU显存映射,造成瞬时内存尖峰。

终极解法

  • 将模型仓库挂载为 hostPath 而非 emptyDir ,避免K8s层面对大文件的拷贝开销;
  • 在Triton启动参数中添加 --model-control-mode=none ,改用 kubectl rollout restart 手动触发模型重载;
  • 为Triton Pod设置 resources.requests.memory: "3.5Gi" resources.limits.memory: "4.5Gi" ,留出缓冲空间。

这次故障教会我们: 最危险的故障,往往藏在“一切正常”的监控盲区里。必须把K8s Events、cgroup指标、内核日志纳入常规巡检清单。

5.3 回滚与降级:当新模型上线失败时,如何30秒内切回旧版

“回滚”不是 git revert ,而是服务层面的原子切换。我们的双模型并行架构如下:

  • 主模型(primary) resnet50_v3 ,接收100%流量;
  • 备用模型(fallback) resnet50_v2 ,始终加载在内存中,但不接收流量;
  • 降级开关(Fallback Switch) :一个Redis键 model:fallback:resnet50 ,值为 true/false

当监控系统检测到 resnet50_v3 的AUC在5分钟内下跌>0.05,自动执行:

redis-cli SET model:fallback:resnet50 true EX 300  # 设置5分钟有效期

Nginx配置中嵌入Lua脚本:

location /v2/models/resnet50/infer {
    set $fallback "false";
    access_by_lua_block {
        local redis = require "resty.redis"
        local red = redis:new()
        red:set_timeout(1000)
        local ok, err = red:connect("redis.default.svc.cluster.local", 6379)
        if ok then
            local res, err = red:get("model:fallback:resnet50")
            if res == "true" then
                ngx.var.fallback = "true"
            end
        end
    }
    proxy_pass http://$fallback$resnet50_v2_upstream;
}

整个过程从检测到生效,实测耗时2.3秒。我们定期演练:每月一次,人工触发降级,验证 resnet50_v2 能否在10秒内承接全部流量。去年双十二, v3 因上游数据源变更导致特征缺失,系统在27秒内完成降级,业务无感。

最后分享一个小技巧:所有模型服务的 /healthz 端点,必须返回当前加载的模型版本号(如 {"status": "ok", "model_version": "resnet50_v2"} )。这样在故障时, curl http://api/healthz 一眼就能确认当前生效的是哪个版本,避免“我以为切回了v2,其实还是v3”的沟通灾难。

更多推荐