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) ——当业务方下周突然要求增加“用户最近 30 分钟行为加权”,你能不能在不重启服务、不影响线上流量的前提下完成热更新?这三个词,就是 Part 4 的全部分量。它适合两类人:一类是刚把模型跑通、正对着部署文档发愁的算法工程师;另一类是被算法同学反复喊“再给我两天就能上线”、但已经等了三周的后端或 SRE 同事。这篇文章,就是给你们共同写的交接清单。
2. 整体设计思路:为什么放弃“一键部署”,选择“分层解耦”
很多团队在 Part 4 阶段会本能地走向两个极端:要么用 MLflow 或 Kubeflow 搞一套“全自动流水线”,结果半年过去 pipeline 跑得比模型还复杂,出了问题连日志都找不到在哪;要么干脆手写 Flask API + Gunicorn,模型 load 一次、全局变量存着,美其名曰“轻量”,实则成了线上最脆弱的单点故障。这两种方案,本质上都错在试图用“一个工具”解决“三层矛盾”: 开发态与运行态的矛盾、模型逻辑与基础设施的矛盾、快速迭代与系统稳定的矛盾 。
我们最终采用的方案是“四层解耦架构”,它不是炫技,而是从血泪教训里长出来的:
-
第一层:Notebook → Script(可执行脚本化)
不是简单把 .ipynb 导出为 .py,而是重构整个代码结构:把数据加载、预处理、模型加载、推理封装成独立函数,每个函数有明确输入输出契约(例如def predict(user_id: str, item_ids: List[str]) -> Dict[str, float]),并强制添加类型注解和 docstring。我试过直接导出的脚本,里面混着plt.show()、df.head()、%timeit这类调试代码,上线前漏删一行,服务就卡死在 matplotlib 后端初始化上。这一层的目标只有一个:让模型代码脱离 Jupyter 环境后,仍能通过python model_inference.py --user_id=123 --item_ids=456,789这种命令行方式干净运行。 -
第二层:Script → Container(容器标准化)
用 Dockerfile 显式声明所有依赖:Python 版本、PyTorch 版本、CUDA 版本、甚至pip install的源地址(国内必须指定清华源,否则 CI/CD 流水线会因网络超时失败)。关键细节在于: 模型权重文件不打包进镜像,而是通过挂载 volume 或对象存储 URL 加载 。原因很现实——一个 BERT 微调模型权重动辄 1.2GB,每次模型微调都重打镜像,镜像仓库会迅速膨胀到 TB 级,且版本回滚成本极高。我们约定:镜像只含代码和轻量依赖,模型权重存 OSS,启动时由容器内脚本下载到/model/weights/目录。这样,同一镜像可服务多个模型版本,只需改一个环境变量MODEL_VERSION=v2.1.3。 -
第三层:Container → Service(服务化抽象)
不直接暴露容器端口,而是套一层轻量 API 网关(我们选的是 Envoy,而非 Nginx,因为 Envoy 原生支持 gRPC-JSON 转换、熔断、重试策略)。重点在于定义清晰的接口契约:HTTP POST/v1/predict接收 JSON,返回标准格式{ "status": "success", "data": { "scores": [0.92, 0.15, ...] }, "meta": { "latency_ms": 42, "model_version": "v2.1.3" } }。这里埋了一个关键经验: 所有响应必须包含meta字段,且meta中必须有model_version和latency_ms。前者用于灰度发布时精准定位问题版本,后者是后续做 P99 延迟监控的原始数据源。没有这个字段,你后期想加监控就得改所有客户端代码。 -
第四层:Service → Platform(平台级治理)
这才是 Part 4 的真正战场。我们用 Kubernetes 的 Custom Resource Definition(CRD)定义了MLModel这个资源类型,它的 spec 包含:image: registry.example.com/ml-recommender:v1.2,modelUrl: oss://models/recommender/v1.2/weights.pt,trafficSplit: { stable: 80, canary: 20 },autoscaling: { minReplicas: 2, maxReplicas: 10, targetCPUUtilization: 60 }。运维同学不再需要 SSH 登服务器改配置,只需kubectl apply -f recommender-canary.yaml,K8s Operator 就会自动拉起新 Pod、注入模型、切流、扩缩容。这个设计让算法同学获得了“自助发布权”,而平台团队守住了“稳定性底线”。
这个四层结构的价值,在于它把“谁该对什么负责”划得清清楚楚:算法工程师只管第一、二层(代码和 Dockerfile),SRE 团队专注第三、四层(网关和 CRD 运维)。当线上出问题时,我们能立刻判断:如果是
500 Internal Server Error
,查网关日志;如果是
422 Unprocessable Entity
,查模型输入校验逻辑;如果是
model_version
字段缺失,那就是第一层契约没写好。责任边界清晰,才是高效协作的前提。
3. 核心细节解析:那些文档里不会写的“魔鬼参数”
3.1 模型加载的冷启动陷阱:为什么你的服务总在第一次请求时卡顿 3 秒?
几乎所有初版 ML 服务都会遇到这个问题:服务刚启动,第一个请求耗时 3200ms,后续请求稳定在 45ms。新手常归咎于“模型太大”,但真实原因往往藏在 PyTorch 的
torch.jit.load()
或 TensorFlow 的
tf.keras.models.load_model()
调用里。以 PyTorch 为例,
torch.jit.load()
默认会在首次调用时执行图优化(graph optimization),这个过程是单线程、不可跳过的。解决方案不是“多等几秒”,而是
预热(warm-up)
。
我们在容器启动脚本
entrypoint.sh
里加了这段逻辑:
#!/bin/bash
# 预热:加载模型并执行一次 dummy inference
echo "Warming up model..."
python -c "
import torch
model = torch.jit.load('/model/weights.pt')
dummy_input = torch.randn(1, 128) # 匹配模型输入 shape
with torch.no_grad():
_ = model(dummy_input)
print('Warm-up completed.')
"
# 启动真正的 API 服务
exec "$@"
关键点在于:
dummy_input
的 shape 必须和线上真实请求完全一致(包括 batch size=1),否则 PyTorch 会为每个新 shape 重新编译图。我们曾因 dummy_input 用了
torch.randn(32, 128)
(batch=32),导致线上 batch=1 的请求仍触发二次编译。另外,
torch.no_grad()
必须显式加上,否则会意外开启梯度计算,占用额外显存。
提示:如果你用的是 ONNX Runtime,预热方式不同——需调用
session.run()传入 dummy input,并确保providers参数与生产环境一致(如['CUDAExecutionProvider']),否则预热在 CPU 上跑,线上切 GPU 时仍会卡顿。
3.2 特征服务的“最后一公里”:如何避免线上特征与离线训练不一致?
特征不一致(Training-Serving Skew)是线上效果衰减的头号杀手。我们曾发现一个点击率模型在离线 AUC 0.82,线上 PV 点击率却下降 15%。排查三天后发现:离线训练用的是 Hive 表里
user_age
字段(范围 0-120),而线上特征服务读取的是 MySQL 用户表的
age
字段(范围 1-100),且 MySQL 表里
age
为空时默认填
0
,而 Hive 表里空值被转为
NULL
并做了特殊编码。一个字段的空值处理差异,直接让模型对“年龄未知”用户做出了错误预测。
解决方案是建立 特征一致性检查(Feature Consistency Check) 流程:
-
在模型训练 pipeline 末尾,自动采样 1000 条训练数据,保存其原始特征向量(
feature_vector.pkl); - 在线上服务启动时,用相同 ID 的用户请求,获取线上服务返回的特征向量;
- 启动一个后台线程,每 5 分钟比对一次:计算两组向量的余弦相似度,若低于 0.999,则触发告警并记录差异字段;
-
差异字段自动高亮显示,例如:
{"user_age": {"train": "NULL", "serving": "0", "diff": "NULL vs 0"}}。
这个检查不追求 100% 一致(浮点数精度允许微小误差),但能瞬间定位到“空值填充策略”、“时间窗口偏移”、“字符串大小写处理”这类致命差异。我们把它做成一个独立的 Python 包
featcheck
,所有模型服务启动时
import featcheck; featcheck.start_monitor()
即可。
3.3 日志与监控的黄金三角:不要只看“成功率”,要看“成功的原因”
很多团队的日志只记录
INFO: request_id=abc123 status=200 latency=45ms
,这远远不够。Part 4 要求日志必须回答三个问题:
这次成功是因为模型强,还是因为数据简单?这次失败是因为模型弱,还是因为上游传错了?这次延迟高是因为模型慢,还是因为特征加载慢?
我们强制所有服务日志包含三个结构化字段:
-
log_type: 取值为"inference"(主推理流程)、"feature_load"(特征加载子流程)、"model_load"(模型加载子流程); -
confidence: 模型输出的预测置信度(如 softmax 最大值),仅inference类型日志有; -
feature_stats: JSON 字符串,记录本次请求中关键特征的统计信息,例如{"user_click_count_7d": {"mean": 12.3, "std": 8.1, "min": 0, "max": 97}}。
这样,当监控系统发现
log_type=inference AND confidence < 0.3
的请求比例突增,就知道模型可能遇到了分布外样本(OOD);当
log_type=feature_load AND latency_ms > 1000
的日志增多,就该去查特征数据库连接池是否耗尽;当
log_type=model_load
日志消失,说明模型加载失败,服务根本没起来。
注意:
feature_stats不是全量特征都统计,而是由算法同学在model_config.yaml里明确定义“关键特征列表”,避免日志爆炸。我们规定:每个模型最多定义 5 个关键特征,且必须是业务敏感度最高的(如电商场景的user_gmv_level、item_price_bucket)。
4. 实操全流程:从本地验证到灰度发布的 7 个必做动作
4.1 动作一:本地端到端验证(Local E2E Validation)
在提交代码前,必须在本地完成一次完整闭环验证,步骤如下:
-
准备最小化测试数据集:从线上抽 10 条真实请求的 raw payload(脱敏后),存为
test_payloads.jsonl,每行一个 JSON 对象; -
运行
python model_inference.py --payload_file test_payloads.jsonl --output_dir ./local_test,生成./local_test/predictions.jsonl; -
启动本地 Flask 服务:
FLASK_ENV=development python app.py,端口 5000; -
用
curl发送相同 payload:cat test_payloads.jsonl | xargs -I {} curl -X POST http://localhost:5000/v1/predict -H "Content-Type: application/json" -d '{}'; -
比对
predictions.jsonl与 HTTP 响应中的data.scores字段,要求完全一致(包括浮点数精度,用numpy.allclose(a, b, atol=1e-6)); -
检查日志:确认
log_type=inference日志中confidence字段存在且合理(如分类任务 confidence 应在 0.5-1.0 区间); -
强制触发一次异常:修改 payload 中一个必填字段为 null,验证服务返回
400 Bad Request且meta.error_code为INVALID_INPUT。
这一步看似繁琐,但它能提前拦截 80% 的低级错误:比如忘记在 Flask route 里加
@app.route('/v1/predict', methods=['POST'])
,或者
model_inference.py
里用了
pandas.read_csv()
而没处理
io.StringIO
输入。我见过最惨的案例:算法同学本地用
sklearn
训练,但 Dockerfile 里装的是
scikit-learn==1.3.0
,而本地是
1.2.2
,版本差异导致
OneHotEncoder
的
handle_unknown='ignore'
行为不一致,本地测试全过,上线后批量报错。所以第 5 步的“完全一致”必须包含环境版本校验——我们在
requirements.txt
顶部加了注释
# Verified with Python 3.9.16, torch 1.13.1+cu117
,CI 流水线会严格检查。
4.2 动作二:CI/CD 流水线的三道闸门
我们的 GitLab CI 流水线设了三道硬性闸门,任何一道失败,PR 不得合并:
-
闸门一:静态检查(Static Check)
运行pylint --disable=all --enable=missing-docstring,invalid-name,too-few-public-methods model_inference.py,强制要求所有函数有 docstring,所有变量名符合snake_case,类方法不超过 5 个。这不是为了代码美观,而是为了可维护性——当一个predict()函数没有 docstring,三个月后没人记得它接收的是user_id还是user_hash。 -
闸门二:单元测试覆盖率(Unit Test Coverage)
要求model_inference.py的单元测试覆盖率达到 90% 以上,且必须覆盖边界条件:空输入、超长输入、非法字符、负数特征值。测试用例写在test_model_inference.py,用pytest运行。特别注意: 所有测试必须 Mock 外部依赖 ,例如用unittest.mock.patch('requests.get')模拟特征服务调用,确保测试不依赖网络,10 秒内跑完。 -
闸门三:镜像构建与健康检查(Image Build & Health Check)
docker build -t ml-recommender:dev .成功后,立即运行docker run --rm ml-recommender:dev /bin/sh -c "curl -f http://localhost:8000/healthz || exit 1"。这个/healthz接口必须返回{"status": "ok", "model_loaded": true, "feature_service_healthy": true},且响应时间 < 200ms。它验证了容器内所有依赖(CUDA、OSS SDK、数据库驱动)都能正常初始化。
这三道闸门的设计哲学是:
把“人肉检查”变成“机器检查”,把“上线后才发现”变成“提交前就拦截”
。曾经有个 PR 因为
pylint
报
invalid-name
(变量名用了
userID
而非
user_id
)被拒,开发者抱怨“太严格”,结果上线后前端传
userID
字段,后端解析失败,导致整页推荐失效 2 小时。命名规范,就是最基础的契约。
4.3 动作三:金丝雀发布(Canary Release)的 5% 流量切分实操
我们不用“全量发布”,而是严格走金丝雀流程。具体操作在 Kubernetes 中通过 Istio VirtualService 实现:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: ml-recommender-vs
spec:
hosts:
- ml-recommender.example.com
http:
- route:
- destination:
host: ml-recommender-stable
subset: v1.2.1
weight: 95
- destination:
host: ml-recommender-canary
subset: v1.2.2
weight: 5
关键细节在于
subset
的定义——它不是一个标签,而是一个精确的版本标识:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: ml-recommender-dr
spec:
host: ml-recommender-stable
subsets:
- name: v1.2.1
labels:
version: v1.2.1
model_version: v1.2.1
这里
model_version: v1.2.1
是核心。当金丝雀流量出现异常时,我们不是看“哪个 Pod 崩了”,而是直接查
model_version=v1.2.2
的所有日志。更进一步,我们在
/healthz
接口里动态返回当前
model_version
,这样监控大盘可以按
model_version
维度拆分指标:P99 延迟、错误率、平均置信度。如果
v1.2.2
的错误率比
v1.2.1
高 0.5%,系统自动触发 rollback:
kubectl patch virtualservice ml-recommender-vs -p '{"spec":{"http":[{"route":[{"weight":100,"destination":{"host":"ml-recommender-stable","subset":"v1.2.1"}},{"weight":0,"destination":{"host":"ml-recommender-canary","subset":"v1.2.2"}}]}]}}'
。
实操心得:金丝雀的 5% 流量不能随机切,必须按业务维度切。例如电商推荐,我们按
user_region切——先放量给“华东区”用户,因为华东区用户行为最典型,问题暴露最快;而“港澳台”用户流量小、特征稀疏,留到最后验证。这样既能快速发现问题,又把影响面控制在最小。
4.4 动作四:线上效果回归(Online A/B Testing)的埋点设计
模型上线不是终点,而是 A/B 测试的起点。我们要求每个新模型版本必须配套一个 A/B 测试计划,核心是埋点设计:
-
在
/v1/predict响应中,强制添加ab_test_group字段,值为"control"或"treatment"; -
所有前端展示推荐结果的页面,必须上报
exposure事件:{ "event": "exposure", "ab_group": "treatment", "model_version": "v1.2.2", "item_ids": ["456", "789"] }; -
当用户点击推荐商品时,上报
click事件:{ "event": "click", "ab_group": "treatment", "item_id": "456", "position": 1 }。
关键点在于:
ab_group
必须由后端统一分配,前端不得自行决定
。否则会出现“同一个用户在 APP 端是 control,在 H5 端是 treatment”,数据无法归因。我们用 Redis 实现了一个简单的分流服务:用户 ID 的 hash % 100,< 5 为 canary,其余为 stable,结果存 Redis 1 小时,保证同用户在短时间内看到一致分组。
数据分析师用这些埋点,计算核心指标:
CTR = click_count / exposure_count
,
Avg. Position = sum(position) / click_count
。只有当
treatment
组的 CTR 显著高于
control
组(p-value < 0.05),且
Avg. Position
更靠前,才算效果达标。我们曾有一个模型 CTR 提升 0.2%,但
Avg. Position
从 2.1 降到 3.8,意味着推荐更“安全”但更“平庸”,最终被否决——
业务目标永远优先于技术指标
。
4.5 动作五:模型版本回滚(Rollback)的 3 分钟极限操作
无论测试多充分,线上总有意外。我们定义了“3 分钟回滚”标准:从发现严重问题,到流量切回旧版本,全程不超过 180 秒。实现路径如下:
-
问题发现
:监控大盘报警
model_version=v1.2.2 AND error_rate > 5%,值班同学收到企业微信消息; -
一键切流
:执行预设脚本
rollback_v1.2.1.sh,内容为kubectl patch virtualservice ml-recommender-vs -p '...'(如前文所示),耗时 < 10 秒; -
验证生效
:
curl -s "http://ml-recommender.example.com/v1/predict" -d '{"user_id":"test"}' | jq '.meta.model_version',确认返回"v1.2.1",耗时 < 5 秒; -
清理资源
:
kubectl delete deploy ml-recommender-canary-v1.2.2,释放资源,耗时 < 5 秒; -
日志归档
:自动将
v1.2.2的所有 Pod 日志打包上传至 OSS,路径oss://logs/ml-recommender/rollback-20231015-142200/,供事后复盘。
这个流程能成立,前提是所有前置动作都已完成:旧版本镜像必须保留在镜像仓库(我们保留最近 3 个版本),VirtualService 的
stable
subset 必须始终指向可用版本,且
rollback_v1.2.1.sh
脚本必须经过每周演练。我们每月组织一次“红蓝对抗”:蓝队(SRE)随机制造一个线上故障,红队(算法+后端)必须在 3 分钟内完成回滚并提交复盘报告。去年全年 12 次演练,平均耗时 112 秒,最差一次 178 秒——刚好卡在红线内。
4.6 动作六:模型生命周期管理(Model Lifecycle Management)
模型不是“一次上线,永久服役”。我们制定了严格的生命周期规则:
| 生命周期阶段 | 触发条件 | 责任人 | 自动化动作 |
|---|---|---|---|
| Active | 新版本上线且 A/B 测试达标 | 算法工程师 | 启动 30 天倒计时监控 |
| Deprecated | 新版本上线满 30 天,且旧版本流量 < 1% | SRE | 发送邮件通知,标记为 deprecated |
| Archived | Deprecated 状态满 7 天,无业务方提出异议 | SRE | 删除旧版本 Deployment,保留镜像 90 天 |
| Deleted | Archiving 满 90 天 | SRE | 彻底删除镜像、OSS 权重文件、CI 流水线历史 |
这个规则的关键在于“时间驱动”而非“事件驱动”。我们不用“等业务方说不用了再删”,而是用固定周期强制推进。所有动作都通过 CronJob 自动执行:例如
archiver-job.yaml
每天凌晨 2 点扫描
Deprecated
状态超过 7 天的模型,自动生成
kubectl delete
命令并发送审批邮件。审批通过后,Job 自动执行删除。
注意:
Archived状态的模型权重文件必须保留 90 天,这是为合规审计留的底。我们曾因金融客户要求提供“2022 年 Q3 所有推荐模型的原始权重”,而当时已删除,被迫从备份磁带里恢复,耗时 17 小时。现在,所有Archived模型的 OSS 路径都加了 lifecycle rule,90 天后自动转为 Glacier 存储,成本极低且可审计。
4.7 动作七:知识沉淀(Knowledge Transfer)的 Checklist
Part 4 的终点不是“服务上线”,而是“知识移交”。每次新模型上线,必须完成一份《ML Model Runbook》,它不是文档,而是一份可执行的 checklist,包含:
-
环境依赖
:
Python 3.9.16,CUDA 11.7,NVIDIA Driver >= 515.65.01(精确到小数点后两位,因为 driver 版本不匹配会导致 CUDA 初始化失败); -
关键配置项
:
MODEL_URL=oss://models/recommender/v1.2.2/weights.pt,FEATURE_SERVICE_TIMEOUT=2000(单位毫秒,必须明确); - 紧急联系人 :算法负责人(张三,手机号 138****1234)、SRE 负责人(李四,企业微信 @lisi);
-
已知限制
:
不支持 user_id 长度 > 64 字符,item_ids 列表长度上限为 100; -
回滚命令
:
kubectl patch virtualservice ml-recommender-vs -p '...'(完整命令,复制即用); -
验证命令
:
curl -s "http://ml-recommender.example.com/v1/predict" -d '{"user_id":"test"}' | jq '.meta.model_version'。
这份 Runbook 存在 Confluence,但更重要的是,它被集成进我们的内部运维平台。当值班同学在平台点击“查看 ml-recommender 服务详情”,页面右侧直接弹出 Runbook 的可折叠章节,点击“执行回滚”按钮,平台自动填充命令并执行。知识不是存在文档里,而是活在操作流里。
5. 常见问题与排查技巧实录:那些凌晨三点的真实战场
5.1 问题一:P99 延迟突增 300%,但 CPU 和内存一切正常
现象
:监控大盘显示
ml-recommender
服务 P99 延迟从 80ms 突增至 380ms,持续 15 分钟,但 K8s metrics 显示 CPU 使用率 < 30%,内存使用率 < 50%,网络带宽无异常。
排查路径 :
-
先查日志:
kubectl logs -l app=ml-recommender --since=15m | grep "latency_ms" | awk '{print $NF}' | sort -n | tail -10,发现最高延迟达 3720ms; -
定位到具体 Pod:
kubectl get pods -l app=ml-recommender -o wide,找到延迟最高的 Pod 名称; -
查该 Pod 的详细日志:
kubectl logs <pod-name> --since=10m | grep "log_type=feature_load",发现大量feature_load日志的latency_ms超过 3000ms; -
进入 Pod:
kubectl exec -it <pod-name> -- /bin/sh,手动执行特征查询命令(如curl "http://feature-service/user/123?fields=click_count_7d"),发现超时; -
最终定位:特征服务的 Redis 连接池耗尽,
max_connections=100被打满,新请求排队等待。
根因 :特征服务未做连接池限流,而推荐服务并发请求量突增(因上游活动推送),导致连接雪崩。
解决方案 :
-
短期:扩容特征服务 Redis 连接池至
max_connections=500; -
长期:在推荐服务中加入熔断器(用
tenacity库),当feature_load错误率 > 20% 时,自动降级为返回缓存特征或默认值。
实操心得:P99 延迟突增,80% 的概率是下游依赖问题,而非本服务代码问题。永远先查
log_type=feature_load和log_type=model_load的延迟,再查log_type=inference。本服务的inference延迟高,往往是下游拖累的“症状”,不是“病因”。
5.2 问题二:模型预测结果全为 0.0,但日志显示“success”
现象
:线上监控发现
confidence
字段大量为
0.0
,但 HTTP 状态码全是
200
,
error_code
为空。
排查路径 :
-
抽样一条
confidence=0.0的请求日志,提取request_id; -
在日志系统中搜索该
request_id的全链路日志,发现feature_stats中user_click_count_7d字段为{"mean": 0.0, "std": 0.0, "min": 0, "max": 0}; -
追查特征服务:发现特征服务的定时任务
update_user_features因权限问题失败,过去 24 小时未更新,所有用户特征值被重置为 0; -
验证:手动触发
update_user_features,10 分钟后confidence恢复正常。
根因 :特征服务的数据新鲜度(Freshness)保障机制失效,而模型对全零特征的输出恰好是 0.0(如 sigmoid 输出)。
解决方案 :
-
在特征服务中增加 Freshness 监控:每 30 分钟检查
user_features表的last_updated_at,若 > 2 小时,触发告警; -
在模型服务中增加特征质量校验:若
user_click_count_7d.max == 0且user_click_count_7d.count > 100,则拒绝请求,返回422并error_code="STALE_FEATURES"。
注意:不要假设“特征服务一定可用”。所有外部依赖,都必须有 fallback 机制。我们现在的标准是:特征服务不可用时,返回预设的默认特征向量(如
[0.5, 0.5, ..., 0.5]),并记录fallback_reason="feature_service_timeout",保证服务不挂,只是效果打折。
5.3 问题三:GPU 显存 OOM,但
nvidia-smi
显示显存使用率仅 60%
现象
:Pod 频繁 OOMKilled,
kubectl describe pod
显示
OOMKilled
,但
nvidia-smi
在 Pod 内执行显示显存使用率仅 60%。
根因
:PyTorch 的 CUDA 缓存机制。PyTorch 会缓存 GPU 显存块,即使 tensor 已被
del
,缓存也不释放,导致
nvidia-smi
看到的显存使用率虚高,而实际可用显存不足。
验证方法 :
import torch
print(torch.cuda.memory_summary()) # 查看详细显存分配
print(torch.cuda.memory_allocated()) # 当前已分配
print(torch.cuda.memory_reserved()) # 当前已预留(缓存)
解决方案 :
-
在模型推理函数末尾,强制清空缓存:
torch.cuda.empty_cache(); -
更优方案:用
torch.inference_mode()替代torch.no_grad(),它比no_grad更激进,会禁用更多中间缓存; -
长期:升级到 PyTorch 2.0+,启用
torch.compile(),它会自动优化显存使用。
实操心得:GPU OOM 问题,90% 的时间不是模型太大,而是缓存没清。
empty_cache()不是万能药,但它必须成为每个predict()函数的最后一步。我们把它写进了团队的 Python 模板,所有新模型服务自动生成时就包含。
5.4 问题四:灰度流量中,新模型在部分区域效果好,部分区域效果差
现象
:A/B 测试数据显示,
treatment
组在“华东区” CTR +2.1%,但在“西北区” CTR -1.8%,整体持平。
排查路径 : 1.
2809

被折叠的 条评论
为什么被折叠?



