Notebook到生产环境的MLOps工程化落地实战

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}

这样做的硬性收益有三点:

  1. 故障隔离 :当特征生成逻辑出错(如某天埋点字段名变更),Feature Serving 可降级返回空特征,Model Serving 仍可用默认值兜底,避免整个推荐链路雪崩;
  2. 迭代解耦 :算法组更新特征工程(v4),只需部署 Feature Serving 新版本,Model Serving 完全不用重启;
  3. 缓存效率 :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")

我们的重构四步法:

  1. 提取核心函数 :将训练、预测、评估逻辑分别封装为 train_model() , predict_batch() , evaluate_model() ,输入输出严格定义(如 predict_batch 接收 pd.DataFrame ,返回 np.ndarray );
  2. 注入依赖 :所有外部依赖(路径、超参、模型路径)通过 config.yaml 或环境变量注入,禁止硬编码;
  3. 添加类型注解与文档 def predict_batch(features: pd.DataFrame, model_path: str) -> np.ndarray: ,配合 docstring 说明输入字段含义(如 "features must contain columns: ['user_age', 'item_price_log', 'session_length']" );
  4. 编写单元测试 :用 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 服务。我们上线前强制检查:

  1. GPU 节点标签 kubectl get nodes -l accelerator=nvidia.com/gpu 必须返回至少 1 个节点,且 nvidia-device-plugin-daemonset 正常运行;
  2. 存储类(StorageClass) :必须存在 gp2 (AWS)或 standard-rwo (GCP)等支持 ReadWriteOnce 的存储,用于挂载模型文件(TorchServe 需要读取 .pt );
  3. 网络策略(NetworkPolicy) :允许 model-serving 命名空间内的 Pod 访问 feature-serving redis 命名空间;
  4. 资源配额(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 ,而是:

  1. Nginx 层 curl -X POST http://nginx-admin/switch-model -d '{"model":"rec","version":"v2.3.1"}' (200ms 内生效);
  2. TorchServe 层 curl -X DELETE http://torchserve:8081/models/rec/v2.4.0 (卸载问题版本,释放显存);
  3. 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 条军规

  1. 军规一:永远在 CI 中运行 torch.jit.trace 验证
    不只是“能 trace”,而是用与线上完全一致的 example_input (shape、dtype、device)trace,并保存 .pt 文件。我们曾因 CI 用 cpu trace,而线上用 cuda 加载,导致 RuntimeError: Expected all tensors to be on the same device 。解决方案:CI 中 torch.jit.trace(model.cuda(), example_input.cuda())

  2. 军规二: 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'

  3. 军规三:K8s 的 livenessProbe readinessProbe 必须分离
    livenessProbe 检查 /healthz (含模型推理), readinessProbe 检查 /ping (仅进程存活)。若两者相同,模型推理慢会导致 Pod 被反复重启,形成恶性循环。

  4. 军规四:模型文件上传 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; }
    
  5. 军规五:所有日志必须带 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 中查全链路日志,无需猜时间窗口。

5.3 故障复盘实录:一次

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值