机器学习模型生产化落地:从Notebook到K8s的系统性工程实践
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:
这份配置不是可选的,而是Triton加载模型的唯一依据,它让模型行为完全可声明、可版本化、可审计。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 ] } ]
提示: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的转换检查清单 :
-
删除所有
%matplotlib inline、!pip install、os.chdir()等魔法命令和副作用代码 ; -
将
df = pd.read_csv('data/train.csv')替换为df = DataLoader().load('train'),数据路径由环境变量DATA_ROOT注入 ; -
将
model = RandomForestClassifier().fit(X, y)拆为两步:model = RandomForestClassifier()(定义)和model.fit(X, y)(训练),训练逻辑移入train.py; -
所有硬编码路径(如
'/home/user/model.pkl')改为pathlib.Path(__file__).parent.parent / 'models' / 'resnet50.onnx'; -
为每个模块编写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测量)
-
输入输出shape匹配(
-
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:
-
动态批处理(Dynamic Batching)
:
dynamic_batching { max_queue_delay_microseconds: 100 },允许Triton将10ms窗口内的请求合并为一批,充分利用GPU并行能力; -
内存预分配(Pinned Memory)
:
optimization { execution_accelerators { gpu_execution_accelerator [ { name: "tensorrt" } ] } },启用TensorRT加速,自动进行算子融合和精度校准; -
实例组(Instance Group)
:
instance_group [ { count: 2, kind: KIND_GPU } ],为单个模型启动2个GPU实例,分担请求压力; -
显存限制(Memory Limit)
:
model_repository_path: "/models"+strict_model_config: false,避免Triton因配置不全拒绝加载; -
输入预处理卸载(Preprocessing Offload)
:将图像resize/crop等CPU密集操作,用Triton的
ensemble功能卸载到CPU实例,GPU只做纯推理; -
输出后处理(Postprocessing)
:用
custombackend编写Python后处理,将原始logits转为带label的JSON,避免客户端解析负担; -
健康检查路径
:
/v2/health/ready设为Liveness Probe,/v2/health/live设为Readiness Probe,确保K8s只将流量导向健康实例; -
日志级别
:
--log-verbose=1,开启详细日志,但用--log-file=/var/log/triton.log重定向,避免stdout污染K8s日志; -
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设备可见性; -
NUMA绑定
:
numactl --cpunodebind=0 --membind=0 tritonserver ...,将Triton进程绑定到CPU节点0和对应内存,减少跨NUMA访问延迟; -
模型实例命名
:
name: "resnet50_v2",明确版本号,便于灰度发布时路由; -
指标暴露
:
--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”的沟通灾难。
更多推荐


所有评论(0)