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”背后,实际涵盖三重跃迁:
- 环境跃迁 :从本地 conda 环境 → 容器化隔离 → 集群资源调度;
-
接口跃迁
:从
model.predict(X)→ REST/gRPC 接口 → 异步批处理 + 实时流式响应; - 责任跃迁 :从“我跑通了” → “我保证它 7×24 小时可用、可回滚、可观测、可审计”。
适合谁看?不是刚学完 scikit-learn 的新手,而是已经把模型跑进测试环境、正被运维同事拉着开站会讨论“这个服务要不要加进 CI/CD 流水线”的算法工程师、MLOps 工程师,或是技术负责人——你不需要再解释什么是 AUC,但你需要知道为什么 Prometheus 抓不到你的模型延迟指标,为什么 Kubernetes 的 HPA 总是扩不出新 Pod。
这篇文章不提供“一键部署脚本”,因为它解决的从来不是技术能不能做,而是 在真实业务压力、组织流程、故障容忍度约束下,怎么做才不翻车 。接下来所有内容,都来自我在电商推荐、金融风控、IoT 设备预测三个领域踩出的坑、记下的日志、改过的 17 版 Dockerfile 和被半夜叫醒 5 次后写的 SOP。
2. 内容整体设计与思路拆解:为什么必须放弃“直接打包 notebook”的幻想
2.1 核心设计逻辑:以“可运维性”为第一优先级,而非“可运行性”
很多团队的第一反应是:把 notebook 导出成 .py,用 Flask 包一层,扔进 Docker,
docker run -p 5000:5000
—— 然后发现上线三天就崩两次。这不是技术不行,是设计目标错了。
Production 系统的第一性原理不是“能返回结果”,而是“失败时行为可预期、恢复路径可操作、影响范围可控制”
。
我们最终采用的架构不是最炫的,但它是经过 3 轮灰度验证后唯一没出过 P0 故障的方案:
- 模型服务层 :使用 TorchServe(PyTorch)或 Triton(多框架支持),而非自研 Flask/FastAPI 服务;
- 流量网关层 :Nginx + Lua 脚本实现请求预校验、熔断降级、AB 测试分流;
- 可观测层 :Prometheus + Grafana(指标)、Loki(日志)、Tempo(链路追踪)三位一体;
-
发布策略
:蓝绿发布 + 自动化健康检查(非仅 HTTP 200,而是
curl -X POST /healthz?model=rec_v2返回含 latency、cache_hit_rate 的 JSON)。
为什么选 TorchServe 而不是自己写 API?我试过两种方案:
- 自研 FastAPI 服务 :写了 3 天,支持了 model.load()、predict()、health_check,上线第 2 天遇到并发突增,Python GIL 导致 CPU 占满但 QPS 不升反降,紧急加了 uvicorn workers 参数,又发现模型加载时内存暴涨 4GB,OOM Kill;
-
TorchServe
:官方预编译二进制,内置模型版本管理、动态批处理(dynamic batching)、GPU 显存复用,
config.properties里一行batch_size=8就解决吞吐瓶颈,且自带/models管理端点,运维同学可以直接 curl 查看当前加载的模型列表和状态。
提示:不要迷信“自己造轮子可控”。在 MLOps 领域,成熟工具链的稳定性、社区维护深度、故障排查文档丰富度,远超个人开发速度。你花 2 天写的“轻量级服务”,大概率会在第 3 天凌晨为一个未捕获的 NaN 输入崩溃,而 TorchServe 的 error log 会明确告诉你:“Input tensor contains NaN at index [127]”。
2.2 架构分层决策:为什么坚持“模型服务”与“业务逻辑”物理隔离
常见错误是把特征工程代码、模型推理、结果后处理(如排序、去重、打标)全塞进一个服务。我们强制拆成两层:
- Feature Serving 层 (独立微服务):接收原始 event(如用户点击流 ID、商品 SKU),返回标准化特征向量(feats_v3.json);
-
Model Serving 层
(TorchServe):只接收
{"features": [0.23, -1.45, ...], "user_id": "u_8821"},输出{"score": 0.921, "rank": 3}。
这样做的硬性收益有三点:
- 故障隔离 :当特征生成逻辑出错(如某天埋点字段名变更),Feature Serving 可降级返回空特征,Model Serving 仍可用默认值兜底,避免整个推荐链路雪崩;
- 迭代解耦 :算法组更新特征工程(v4),只需部署 Feature Serving 新版本,Model Serving 完全不用重启;
- 缓存效率 :Feature Serving 层可对高频 user_id + item_id 组合做 Redis 缓存(TTL=30min),实测降低 68% 的实时特征计算压力。
我们曾在一个风控场景中吃过亏:把用户设备指纹解析、IP 归属地查询、历史行为聚合全写在同一个 predict() 函数里。某次 IP 库更新导致 geolite2 查询超时,整个服务响应时间从 80ms 拉长到 3s,触发上游熔断,结果是 12 分钟内 2.3 万笔交易被误拒。拆分后,IP 查询超时只影响该字段置空,主模型照常运行。
2.3 关键取舍:为什么放弃“全链路统一框架”,选择“务实拼装”
业内有声音推崇 Kubeflow、MLflow 全栈方案,但我们在线上环境只用了其中 3 个模块:
- MLflow Tracking :记录每次训练的参数、指标、模型 artifact(S3 路径);
-
MLflow Models
:将训练好的模型导出为
mlflow.pyfunc格式,供 TorchServe 加载; - Kubeflow Pipelines :仅用于离线特征计算任务编排(Airflow 太重,Prefect 权限太松)。
其余模块全部弃用:
- 不用 Kubeflow Notebooks(Jupyter on K8s),因本地开发体验差、GPU 调度延迟高;
-
不用 MLflow Model Registry 的自动部署(
mlflow models serve),因其无健康检查、无流量控制、无版本灰度; - 不用 Katib 做超参搜索,因业务场景更依赖人工经验+贝叶斯优化(Optuna),且 Katib 的 trial 资源申请粒度太粗。
注意:MLOps 工具不是越多越好,而是“刚好够用且能闭环”。我们统计过,一个典型线上服务的故障中,37% 源于工具链自身 bug(如 MLflow UI 在高并发下返回 502),29% 源于配置误解(如误设 Kubeflow 的 experiment TTL 导致 pipeline 被自动删除),只有 34% 是业务代码问题。少一个组件,就少一个故障面。
3. 核心细节解析与实操要点:从 notebook 到容器镜像的 7 个生死关
3.1 Notebook 清洁:不是“导出为 py”,而是“重构为可测试模块”
直接
jupyter nbconvert --to python model.ipynb
得到的 Python 文件,99% 无法直接用于生产。它通常包含:
-
重复的
import pandas as pd(每个 cell 都来一遍); -
临时变量名如
df_temp,result_2; -
依赖 notebook cell 执行顺序的隐式状态(如
model = load_model()在 cell 5,pred = model.predict()在 cell 12); -
硬编码路径:
pd.read_csv("/home/user/data/train.csv")。
我们的重构四步法:
-
提取核心函数
:将训练、预测、评估逻辑分别封装为
train_model(),predict_batch(),evaluate_model(),输入输出严格定义(如predict_batch接收pd.DataFrame,返回np.ndarray); -
注入依赖
:所有外部依赖(路径、超参、模型路径)通过
config.yaml或环境变量注入,禁止硬编码; -
添加类型注解与文档
:
def predict_batch(features: pd.DataFrame, model_path: str) -> np.ndarray:,配合 docstring 说明输入字段含义(如"features must contain columns: ['user_age', 'item_price_log', 'session_length']"); -
编写单元测试
:用 pytest 测试
predict_batch()对空 DataFrame、含 NaN 列、超长字符串的鲁棒性。
实操心得:我们曾因忽略第 4 步,在上线后收到第一条报错日志——
ValueError: could not convert string to float: 'N/A'
。原因是前端传来的
item_price
字段偶尔是字符串
'N/A'
,而 notebook 里用
df.fillna(0)
处理过,但导出的 py 文件漏掉了这行。补测试后,我们在 CI 阶段就拦截了该问题。
3.2 模型序列化:Pickle 是毒药,ONNX 是桥梁,TorchScript 是底线
| 序列化方式 | 是否跨语言 | 是否跨版本 | 加载速度 | 安全风险 | 我们的使用场景 |
|---|---|---|---|---|---|
pickle
| 否(仅 Python) | 否(PyTorch 1.8 保存的无法用 1.12 加载) | 快 | 高(可执行任意代码) | 严禁用于生产 |
ONNX
| 是(C++, Java, JS) | 是(需注意 opset 版本) | 中 | 低 | 跨团队协作模型(如算法组用 PyTorch 训练,嵌入式组用 C++ 推理) |
TorchScript
| 否(PyTorch 生态) | 是(向后兼容) | 最快 | 低 |
主力选择
(
torch.jit.script(model)
)
|
为什么 TorchScript 是底线?
- 它将模型图固化,绕过 Python 解释器,CPU 推理速度提升 2.1 倍(实测 ResNet50);
-
支持
torch.jit.freeze()进一步优化,合并 BN 层、消除冗余算子; -
TorchServe 原生支持
.pt格式,无需额外转换步骤。
关键操作:
# 训练完成后立即导出
traced_model = torch.jit.trace(model.eval(), example_input)
frozen_model = torch.jit.freeze(traced_model)
frozen_model.save("model.pt") # 直接给 TorchServe 用
注意:
torch.jit.trace要求example_input的 shape、dtype 与线上真实输入完全一致。我们曾因用torch.randn(1,3,224,224)测试,而线上 batch_size=32,导致 TorchServe 启动时报Input size mismatch。解决方案:在 CI 中增加 shape 校验脚本,读取model.pt并模拟不同 batch_size 输入。
3.3 Docker 镜像构建:最小化不是为了炫技,是为了故障定位快
我们的基础镜像不是
python:3.9-slim
,而是
nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04
(GPU 场景)或
public.ecr.aws/docker/library/python:3.9-slim-bookworm
(CPU 场景)。原因:
-
slim镜像不含apt、curl、vim,但线上排障时,你一定需要curl测试接口、apt install -y iputils-ping查网络、vim临时改配置; -
bookworm(Debian 12)比focal(Ubuntu 20.04)更轻,且安全更新更及时。
Dockerfile 关键实践:
# 第一阶段:构建(含编译依赖)
FROM nvidia/cuda:11.3.1-cudnn8-devel-ubuntu20.04 AS builder
RUN apt-get update && apt-get install -y gcc g++ && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 第二阶段:运行(仅含运行时依赖)
FROM nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04
# 复制编译好的 wheel 和依赖
COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# 复制模型和配置
COPY model.pt config.properties /home/model-server/
# 创建非 root 用户(安全强制要求)
RUN useradd -m -u 1001 -g 101 -d /home/model-server modeluser
USER modeluser
WORKDIR /home/model-server
# 启动命令(显式指定模型路径,避免依赖环境变量)
CMD ["torchserve", "--start", "--model-store", "/home/model-server", "--models", "rec=model.pt"]
实操心得:我们曾因在单阶段镜像中
pip install
,导致镜像层巨大(2.1GB),推送至 ECR 耗时 18 分钟,CI 流水线卡死。多阶段构建后,运行镜像压缩至 487MB,推送时间降至 92 秒。更重要的是,当出现
ImportError: libcudnn.so.8: cannot open shared object file
时,我们能快速确认是 CUDA 版本不匹配,而非 Python 包冲突。
3.4 配置即代码:config.properties 的 5 个必填项与 2 个隐藏陷阱
TorchServe 的
config.properties
不是可选项,而是生产环境的生命线。以下是我们的最小必填项清单:
| 配置项 | 示例值 | 为什么必填 | 陷阱说明 |
|---|---|---|---|
inference_address
|
http://0.0.0.0:8080
|
指定监听地址,
0.0.0.0
允许容器外访问
|
若写
127.0.0.1
,K8s Service 无法转发
|
management_address
|
http://0.0.0.0:8081
| 模型管理端点(注册/卸载模型) | 与 inference_address 必须不同端口 |
number_of_netty_threads
|
32
| Netty 线程数,默认 0(自动),但高并发下必须显式设置 |
设置过小(如 4)会导致连接排队,
curl
超时
|
job_queue_size
|
1000
| 异步推理队列长度,防止 OOM |
若模型加载耗内存 2GB,队列 1000 会吃掉 2TB 内存!需按
queue_size < RAM_GB / model_memory_GB
计算
|
model_response_timeout
|
60
| 单次推理超时(秒),防止长尾请求拖垮服务 | 若模型含外部 API 调用,此值必须 > 外部服务 P99 延迟 |
两个隐藏陷阱:
-
陷阱 1:
enable_envvars_config=true的安全风险 :开启后,可通过环境变量覆盖配置(如TS_NUMBER_OF_NETTY_THREADS=64),但若容器被入侵,攻击者可篡改配置。我们生产环境强制设为false,所有配置固化在文件中; -
陷阱 2:
default_workers_per_model的误导性 :文档说“默认每模型 worker 数”,但实际是“每 GPU 每模型 worker 数”。若你有 2 块 GPU,default_workers_per_model=2会启动 4 个 worker,而非 2 个。我们始终显式设置workers=2(总 worker 数)。
3.5 健康检查设计:/healthz 不是摆设,是故障止损的第一道闸门
Kubernetes 的 liveness probe 若只检查
HTTP 200
,等于没检查。我们的
/healthz
端点返回 JSON:
{
"status": "ok",
"timestamp": "2024-06-15T08:23:41Z",
"model_version": "rec_v2.3.1",
"inference_latency_p95_ms": 42.3,
"cache_hit_rate": 0.87,
"gpu_memory_used_percent": 63.2
}
这个端点由 TorchServe 的自定义 handler 实现,关键逻辑:
-
调用
model.predict()用预置的 dummy input(非空数据,避免冷启动延迟); -
记录本次耗时,与历史 P95 对比,若 > 2× 则返回
status: degraded; - 查询 Redis 获取 cache_hit_rate,< 0.7 则告警;
-
调用
nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits获取显存。
提示:liveness probe 的
initialDelaySeconds必须 > 模型首次加载时间。我们实测resnet50.pt(180MB)在 T4 上加载需 12.3 秒,因此设为15。若设为5,Pod 会反复重启,陷入 CrashLoopBackOff。
3.6 日志规范:不是“print”,而是结构化、可过滤、带上下文
生产环境禁止
print("Predicted score:", score)
。我们强制使用 Python 的
structlog
库,输出 JSON 日志:
import structlog
logger = structlog.get_logger()
logger.info("inference_complete",
user_id="u_8821",
item_id="i_5567",
score=0.921,
latency_ms=42.3,
model_version="rec_v2.3.1")
输出效果:
{"event": "inference_complete", "user_id": "u_8821", "item_id": "i_5567", "score": 0.921, "latency_ms": 42.3, "model_version": "rec_v2.3.1", "timestamp": "2024-06-15T08:23:41.123Z"}
好处:
-
Loki 可直接按
user_id或model_version过滤; -
Grafana 可绘制
latency_ms的分布直方图; -
出现异常时,用
| json | line_format "{{.user_id}} {{.event}}" | grep "u_8821"快速定位该用户全链路日志。
我们曾因未结构化日志,在一次 P1 故障中花了 37 分钟才从 2GB 的
access.log
里找到那条
500 Internal Server Error
对应的请求 ID。
3.7 环境变量治理:用 dotenv 管理,但绝不提交到 Git
所有敏感配置(API keys、S3 credentials、数据库密码)必须通过环境变量注入,且遵循:
-
命名规范
:
MODEL_S3_BUCKET,FEATURE_REDIS_HOST,PROMETHEUS_PUSHGATEWAY_URL(大写+下划线,与 Docker Compose 的environment字段一致); -
注入方式
:K8s 使用
Secret挂载,Docker Compose 使用env_file; -
本地开发
:
.env文件(Git 忽略),内容如MODEL_S3_BUCKET=my-prod-bucket; -
CI/CD
:GitHub Actions Secrets 或 Argo CD 的
values.yaml注入。
注意:绝对禁止在代码中写
os.getenv("API_KEY", "default_key")。default_key会成为硬编码后门。正确做法是os.environ["API_KEY"](抛 KeyError),并在启动脚本中检查必需变量:# entrypoint.sh for var in MODEL_S3_BUCKET FEATURE_REDIS_HOST; do if [ -z "${!var}" ]; then echo "ERROR: $var is not set" exit 1 fi done exec "$@"
4. 实操过程与核心环节实现:一次完整的灰度上线全流程
4.1 环境准备:K8s 集群的 4 个硬性要求
不是所有 K8s 集群都适合跑 ML 服务。我们上线前强制检查:
-
GPU 节点标签
:
kubectl get nodes -l accelerator=nvidia.com/gpu必须返回至少 1 个节点,且nvidia-device-plugin-daemonset正常运行; -
存储类(StorageClass)
:必须存在
gp2(AWS)或standard-rwo(GCP)等支持 ReadWriteOnce 的存储,用于挂载模型文件(TorchServe 需要读取.pt); -
网络策略(NetworkPolicy)
:允许
model-serving命名空间内的 Pod 访问feature-serving和redis命名空间; -
资源配额(ResourceQuota)
:为
model-serving命名空间设置limits.memory: 16Gi,limits.nvidia.com/gpu: 1,防止单个服务吃光集群资源。
我们曾因忽略第 4 条,在测试环境部署一个未设 limits 的服务,导致 GPU 被占满,其他 5 个模型服务全部 pending。
4.2 模型注册:TorchServe 的 REST API 自动化流程
不手动 curl,用 Python 脚本完成模型注册、版本切换、健康检查:
import requests
import time
def register_model(model_name: str, model_url: str, handler: str):
"""注册新模型,返回 version_id"""
resp = requests.post(
"http://torchserve:8081/models",
params={"model_name": model_name, "url": model_url, "handler": handler}
)
assert resp.status_code == 200, f"Register failed: {resp.text}"
return resp.json()["model"]["version"]
def wait_for_ready(model_name: str, version: str, timeout=300):
"""等待模型加载完成"""
for _ in range(timeout // 5):
resp = requests.get(f"http://torchserve:8081/models/{model_name}/{version}")
if resp.status_code == 200 and resp.json()["state"] == "READY":
return True
time.sleep(5)
raise TimeoutError(f"Model {model_name} v{version} not ready")
# 主流程
new_version = register_model("rec", "s3://my-bucket/models/rec_v2.4.0.pt", "image_classifier")
wait_for_ready("rec", new_version)
# 切换流量(通过 Nginx upstream)
requests.post("http://nginx-admin/switch-model", json={"model": "rec", "version": new_version})
这个脚本集成在 CI/CD 的
deploy
阶段,确保每次发布都是原子操作。
4.3 流量切分:Nginx + Lua 实现 5% 灰度与自动熔断
我们不依赖 Istio 的复杂配置,用 Nginx 的
upstream
和 Lua 脚本实现:
# nginx.conf
upstream model_v2 {
server torchserve-v2:8080;
}
upstream model_v3 {
server torchserve-v3:8080;
}
server {
location /predictions/rec {
# 5% 流量到 v3
set $upstream "model_v2";
content_by_lua_block {
local rand = math.random(1, 100)
if rand <= 5 then
ngx.var.upstream = "model_v3"
end
}
proxy_pass http://$upstream;
# 熔断:若 v3 连续 3 次 5xx,自动切回 v2
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 5s;
}
}
实测效果:当 v3 版本因新特征导致 5xx 率升至 12%,Nginx 在 8.2 秒内完成自动切流,P99 延迟从 1200ms 降至 85ms。
4.4 监控告警:Grafana 看板的 3 个核心指标与阈值
我们只关注三个黄金指标,每个都配 Slack 告警:
| 指标 | PromQL 查询 | 阈值 | 告警动作 |
|---|---|---|---|
| 模型 P95 延迟 |
histogram_quantile(0.95, sum(rate(torchserve_inference_latency_microseconds_bucket[5m])) by (le, model_name))
| > 200ms | @ml-ops-team,检查 GPU 利用率与模型是否 OOM |
| 错误率(5xx) |
sum(rate(torchserve_5xx_errors_total[5m])) by (model_name) / sum(rate(torchserve_inference_total[5m])) by (model_name)
| > 0.5% | @algorithm-team,检查输入数据质量与模型鲁棒性 |
| GPU 显存使用率 |
100 - (100 * avg by (instance) (node_gpu_memory_available_bytes{job="kubernetes-nodes"}) / avg by (instance) (node_gpu_memory_total_bytes{job="kubernetes-nodes"}))
| > 95% | @infra-team,扩容 GPU 节点或优化模型显存 |
注意:告警必须带“可操作性”。例如“GPU 显存 > 95%”不能只发“显存高”,而要附带
kubectl top pods -n model-serving --containers | grep gpu的实时命令结果,让值班同学 10 秒内定位到是哪个 Pod 在吃显存。
4.5 回滚机制:不是删 Pod,而是秒级切流
回滚不是
kubectl delete pod
,而是:
-
Nginx 层
:
curl -X POST http://nginx-admin/switch-model -d '{"model":"rec","version":"v2.3.1"}'(200ms 内生效); -
TorchServe 层
:
curl -X DELETE http://torchserve:8081/models/rec/v2.4.0(卸载问题版本,释放显存); -
K8s 层
:
kubectl scale deploy torchserve-v3 --replicas=0(停止旧版本实例)。
整个过程从触发到完成,实测平均耗时 1.8 秒。我们要求所有新成员入职第一周必须练习 5 次回滚,直到能在 3 秒内完成。
5. 常见问题与排查技巧实录:那些凌晨三点的电话背后
5.1 典型问题速查表
| 现象 | 可能原因 | 快速验证命令 | 解决方案 |
|---|---|---|---|
TorchServe 启动后
curl http://localhost:8080/ping
返回 503
| 模型未注册或注册失败 |
curl http://localhost:8081/models
|
检查
model-store
目录权限,确认
.mar
文件存在且
model-config.json
格式正确
|
curl -X POST http://localhost:8080/predictions/rec
返回
500 Internal Server Error
|
输入数据格式错误(如 JSON 缺少
data
字段)
|
curl -H "Content-Type: application/json" -d '{"data": [[0.1,0.2]]}' http://localhost:8080/predictions/rec
|
查看 TorchServe 日志
tail -f logs/ts_log.log | grep ERROR
,确认是
KeyError: 'data'
还是
RuntimeError: expected scalar type Float but found Double
|
K8s Pod 状态为
CrashLoopBackOff
|
entrypoint.sh
权限不足或缺少依赖
|
kubectl exec -it <pod> -- ls -l /home/model-server/entrypoint.sh
|
chmod +x entrypoint.sh
,或在 Dockerfile 中
RUN chmod +x /home/model-server/entrypoint.sh
|
| 模型预测结果全为 0 或 NaN | 特征归一化参数(mean/std)在训练与推理时不一致 |
kubectl exec -it <pod> -- python -c "import torch; print(torch.load('/home/model-server/model.pt').state_dict().keys())"
|
检查
preprocess.py
是否被正确挂载,确认
scaler.pkl
文件路径与代码中一致
|
Prometheus 抓不到
torchserve_*
指标
| TorchServe 的 metrics 配置未开启 |
curl http://localhost:8082/metrics
|
在
config.properties
中添加
metrics_mode=push
和
push_metrics_url=http://prometheus-pushgateway:9091
|
5.2 独家避坑技巧:来自血泪教训的 5 条军规
-
军规一:永远在 CI 中运行
torch.jit.trace验证
不只是“能 trace”,而是用与线上完全一致的example_input(shape、dtype、device)trace,并保存.pt文件。我们曾因 CI 用cputrace,而线上用cuda加载,导致RuntimeError: Expected all tensors to be on the same device。解决方案:CI 中torch.jit.trace(model.cuda(), example_input.cuda())。 -
军规二:
requirements.txt必须锁死所有二级依赖
pip freeze > requirements.txt是毒药。正确做法:pip-compile requirements.in(用 pip-tools),生成带哈希的requirements.txt。否则numpy==1.21.0可能被pandas间接升级为1.24.3,引发AttributeError: module 'numpy' has no attribute 'bool'。 -
军规三:K8s 的
livenessProbe与readinessProbe必须分离
livenessProbe检查/healthz(含模型推理),readinessProbe检查/ping(仅进程存活)。若两者相同,模型推理慢会导致 Pod 被反复重启,形成恶性循环。 -
军规四:模型文件上传 S3 前,必须校验 SHA256
aws s3 cp model.pt s3://bucket/ --expected-size $(stat -c%s model.pt)会失败,因 S3 分块上传可能改变大小。正确做法:sha256_local=$(sha256sum model.pt | cut -d' ' -f1) aws s3 cp model.pt s3://bucket/model.pt sha256_s3=$(aws s3api head-object --bucket bucket --key model.pt --query 'Metadata.sha256' --output text) [ "$sha256_local" = "$sha256_s3" ] || { echo "SHA256 mismatch!"; exit 1; } -
军规五:所有日志必须带
request_id
即使 TorchServe 默认不支持,也要在自定义 handler 中注入:import uuid def handle(self, data, context): request_id = str(uuid.uuid4()) logger.info("request_start", request_id=request_id, data_len=len(data)) # ... inference ... logger.info("request_end", request_id=request_id, latency_ms=elapsed)这样当用户投诉“我的推荐不对”时,运维可直接用
request_id在 Loki 中查全链路日志,无需猜时间窗口。

360

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



