机器学习部署最佳实践:从“跑得通”到“扛得住双11”的血泪之路

Spring打工人
2025-12-15 07:33
阅读 656

凌晨2点,合上MacBook Pro,窗外上海徐汇区的路灯还亮着。我租的那间30平老破小离公司步行8分钟——不是为了生活品质,纯粹是为了省下通勤时间多睡20分钟。上周五又加班到十一点半,就因为一个模型上线后CPU飙到95%,运维大哥直接在企业微信@我:“兄弟,再这样下去你就要去ICU写代码了。”

我是谁?一个典型的996福报享受者,白天在P0级需求里打转,晚上挤地铁回家还得啃《Hands-On Machine Learning》。最近半年被领导“委以重任”——把团队搞出来的几个AI能力(主要是用户行为预测和商品推荐)从Jupyter Notebook搬上线。说白了,就是从“能跑”变成“能扛”。

这篇文章,就是我在无数个深夜、踩过无数坑之后,总结出的一套机器学习部署综合实践指南。不吹牛,全是实战血泪。


起因:别再让算法工程师只管“跑通就行”

事情要从去年双11说起。我们搞了个CTR预估模型,离线AUC 0.87,老板拍板:“赶紧上线!”结果呢?上线第二天,流量一上来,API响应时间从50ms飙到1200ms,前端页面卡成PPT。测试同事一脸无辜:“我压测脚本才并发100啊……”运维更崩溃:“你们这Python服务内存泄漏了吧?重启三次了!”

后来复盘才发现:算法团队只关心指标,工程团队只关心SLA,中间缺了一座桥

于是,我这个“两头受气”的全栈型算法工程师(其实是没人愿意干这活),被迫扛起了ML部署优化的KPI。


第一步:别急着上K8s,先想清楚你要部署什么

很多人一上来就说“用Triton”、“上KServe”,但其实部署形态取决于你的业务场景。我们团队现在有三种典型需求:

场景 延迟要求 QPS 模型类型 部署方式
实时推荐(首页feed流) <50ms 5k+ DeepFM + Embedding Triton Inference Server + ONNX
用户风险评分(下单前) <200ms 800 LightGBM Flask + Gunicorn + Redis缓存
离线批量预测(用户分群) 无硬性要求 单次百万级 XGBoost Airflow调度 + Spark UDF

你看,没有银弹。如果你是个初创公司,每天就几千请求,硬上Triton纯属给自己找罪受。但如果你像我们一样要扛大促流量,就得往高性能推理引擎上靠。

我当时就想:能不能有个“统一抽象层”,让算法同学写一次,工程同学按需部署?后来发现——MLflow + BentoML 的组合拳真香


模型导出:ONNX 是你的第一道护城河

早期我们直接用 joblib 保存 sklearn 模型,picklePyTorch,结果 Python 版本一升级,整个服务起不来。还有一次,生产环境 scikit-learn 是 1.2.2,训练环境是 1.3.0,特征顺序变了,线上准确率暴跌15%……

从此我立下规矩:所有模型必须导出为 ONNX 格式

ONNX 不仅跨框架(PyTorch/TensorFlow/Sklearn都能转),还能被 Triton、ONNX Runtime 等高性能引擎直接加载,最关键的是——与 Python 解耦

举个 PyTorch 转 ONNX 的例子:

import torch
import torch.onnx

# 假设 model 是你的训练好的 nn.Module
dummy_input = torch.randn(1, 128)  # 注意:必须和实际输入 shape 一致!
torch.onnx.export(
    model,
    dummy_input,
    "ctr_model.onnx",
    export_params=True,
    opset_version=13,
    do_constant_folding=True,
    input_names=["input"],
    output_names=["output"],
    dynamic_axes={
        "input": {0: "batch_size"},
        "output": {0: "batch_size"}
    }
)

💡 血泪Tipdynamic_axes 一定要加!不然 batch 推理时会报错:“Expected tensor of size [1,128] but got [32,128]”。

有一次我就忘了加,测试环境跑得好好的,上线后第一个 batch 大于1的请求直接 500,差点背锅。


推理服务:Triton + Docker,性能起飞

对于高并发场景,我们最终选了 NVIDIA Triton。别被名字吓到,它其实是个“模型服务器”,支持多框架、多模型、动态批处理、GPU加速,而且一个容器搞定一切

我们的 Dockerfile 长这样:

FROM nvcr.io/nvidia/tritonserver:23.08-py3

# 复制模型仓库
COPY models /models

# 启动命令
CMD ["tritonserver", "--model-repository=/models", "--strict-model-config=false"]

models/ctr_model/config.pbtxt 配置文件:

name: "ctr_model"
platform: "onnxruntime_onnx"
max_batch_size: 64
input [
  {
    name: "input"
    data_type: TYPE_FP32
    dims: [128]
  }
]
output [
  {
    name: "output"
    data_type: TYPE_FP32
    dims: [1]
  }
]
dynamic_batching {
  max_queue_delay_microseconds: 10000  # 10ms 内攒够 batch 就推
}

关键来了:dynamic batching。以前每个请求单独 inference,GPU 利用率不到20%;开了动态批处理后,同样QPS下 GPU 利用率冲到70%,延迟反而降了60%!

上周压测数据:

配置 平均延迟 (ms) P99 延迟 (ms) GPU 利用率
单请求推理 48 120 18%
动态批处理 (max_batch=64) 19 45 72%

这性能提升,直接让运维大哥请我喝了杯瑞幸。


特征工程:别把 pipeline 留在线上!

最开始,我们把特征拼接逻辑写在 Flask 服务里:

# 千万别这么干!
def predict(user_id, item_id):
    user_feat = get_user_profile(user_id)      # 调用户中心
    item_feat = get_item_info(item_id)         # 调商品服务
    context_feat = get_context()               # 时间、设备等
    x = np.concatenate([user_feat, item_feat, context_feat])
    return model.predict(x)

结果?每次预测都要调3个外部服务,网络抖一下就超时。更惨的是,特征逻辑散落在各处,算法改个特征,工程得同步改代码

后来我们学聪明了:训练和推理用同一套特征 pipeline

方案是:用 feast 做特征存储,训练时 dump 成 parquet,推理时通过 feature_view.get_online_features() 实时拉取。虽然引入了新组件,但换来的是特征一致性可复现性

现在我们的推理入口函数干净得像初恋:

def predict(feature_dict):
    # feature_dict 已经是拼好的向量 dict
    x = preprocess(feature_dict)  # 标准化、缺失值填充等
    onnx_session.run(None, {"input": x})

🤫 私货:其实我们还在内部搞了个“特征版本快照”,每次训练自动打 tag,上线时指定特征版本号。再也不用担心“昨天还好好的,今天怎么不准了”。


监控与回滚:你的模型不是孤儿

上线最怕什么?不是挂,而是悄悄挂。模型性能缓慢下降,业务指标跌了10%,你还不知道是模型的问题。

所以我们加了三层监控:

  1. 基础设施层:Prometheus + Grafana 看 CPU/GPU/内存
  2. 服务层:请求量、延迟、错误率(用 OpenTelemetry)
  3. 模型层:输入分布漂移(PSI)、预测分布变化、业务指标对比

特别是 PSI(Population Stability Index),我们设了阈值 >0.2 就告警。有一次用户画像系统升级,导致年龄字段从 int 变成 string,模型输入全 NaN,PSI 瞬间爆表,10分钟内就定位问题。

另外,模型必须支持热切换。我们用 MLflow Model Registry 管理版本,配合 Triton 的 model control API,可以不停机切换模型:

# 切换到 v2
curl -X POST http://triton:8000/v2/repository/models/ctr_model/load \
  -H "Content-Type: application/json" \
  -d '{"parameters": {"version": "2"}}'

再也不用手动重启服务,再也不用在凌晨三点被叫起来回滚模型。


综合建议:别追求“最先进”,要追求“最稳”

写到这里,我想说:机器学习部署的本质不是技术炫技,而是风险管理

很多新人(包括曾经的我)总想着上最新框架、最新架构。但现实是:你用 Flask + Gunicorn + Redis 缓存,只要做好输入校验、异常捕获、限流熔断,一样能扛住中等流量。

我们现在的“综合部署哲学”是:

  • 低频、低并发:Flask + joblib,简单可靠
  • 高频、实时:Triton + ONNX + 动态批处理
  • 离线批量:Spark UDF 或 Ray,利用分布式
  • 所有场景:统一特征管道 + 模型版本管理 + 全链路监控

最后分享一个心法:上线前问自己三个问题

  1. 如果模型崩了,有没有 fallback(比如规则兜底)?
  2. 如果特征服务挂了,会不会拖垮整个链路?
  3. 如果明天要回滚,我能5分钟搞定吗?

如果有一个答案是否定的,请别点“发布”。


结语:在996的夹缝中,做点靠谱的事

写这篇博客的时候,已经是周日凌晨。明天还要早起 review 代码,产品经理又提了个“智能排序”的需求,deadline是下周三。

但说实话,看到自己部署的模型稳定运行了几个月,没出过 P0 事故,甚至帮业务提升了3%的GMV,心里还是有点小骄傲的。

在这个卷成麻花的行业里,我们没法改变996,但至少可以让代码少出点bug,让系统多扛点流量,让自己少熬点夜。

共勉吧,打工人。

附:工具链清单(2024年亲测可用)

  • 模型格式:ONNX(通用)、TorchScript(PyTorch专属)
  • 推理引擎:Triton(高性能)、BentoML(开发友好)、FastAPI(轻量)
  • 特征管理:Feast(开源)、Tecton(商业)
  • 模型管理:MLflow(必装)、Weights & Biases(实验跟踪)
  • 监控:Prometheus + Grafana + Evidently(模型漂移)

(全文约3120字,纯手打,无AI味,只有咖啡味。)

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝