把模型塞进外卖App要踩多少坑
我在美团外卖做了四年Java后端,主要跟订单链路和高并发打交道。这两年算法团队的深度学习模型一个接一个往线上推,后端跟着折腾了不少部署方案。最近准备跳槽,刷面经发现机器学习部署被问得越来越频繁,索性把踩过的坑整理出来。
从一次线上事故说起
去年夏天,算法组推了一个新的配送时长预估模型,双塔结构,线上QPS峰值约两万。他们用Flask包了个HTTP服务,单机压测还行,一上生产直接被打爆。日志里全是timeout,订单详情页配送时间显示不出来。
排查下来问题很典型:Flask默认同步阻塞,模型推理那几百毫秒把worker线程全占住,请求排队排到天荒地老。后来紧急加了gunicorn多worker配置,又上了模型结果缓存,才勉强扛住。
模型部署不是起个HTTP服务就完事,得考虑并发模型、超时控制、降级策略、版本管理一堆东西。
模型服务化的基本盘
别用Flask直接裸奔。我们统一迁移到了FastAPI加uvicorn的组合,异步支持好,性能强不少。几个关键点得抠死。
模型加载要放在进程启动时,别每次请求都加载。正确姿势是用startup事件:
from fastapi import FastAPI
import torch
app = FastAPI()
model = None
@app.on_event("startup")
async def load_model():
global model
model = torch.load("/data/models/eta_model_v3.2.pt")
model.eval()
推理必须上线程池隔离。PyTorch推理是CPU密集型的,在async函数里直接调用会阻塞整个事件循环。我们用asyncio.to_thread把推理扔到线程池,配合uvicorn多worker模式,单机QPS从几百提到两三千。
版本管理和灰度发布
模型迭代比后端代码快多了,算法组一周能出好几个实验版本。我们搭了个模型版本管理服务,模型文件统一放对象存储,线上通过配置中心指定加载版本。
灰度踩过一个坑:模型输出格式变了,新版本返回字典,老版本返回数组,灰度时下游解析直接炸。后来定了规矩:模型接口的输入输出schema必须向后兼容,不兼容的走新接口。灰度策略用用户ID哈希分流,出问题随时回滚,比按百分比随机靠谱。
模型压缩和推理优化
配送时长预估对时延极度敏感。原始模型几百兆,单次推理CPU上要四五百毫秒,肯定不行。我们试了几条路:
| 优化手段 | 推理耗时(CPU) | 模型体积 | 线上效果 |
|---|---|---|---|
| 原始PyTorch | 480ms | 620MB | 不可用 |
| ONNX Runtime | 210ms | 580MB | 时延仍偏高 |
| ONNX + INT8量化 | 95ms | 160MB | 精度损失0.3%,可接受 |
| TorchScript + 多线程 | 180ms | 600MB | 部署复杂,放弃 |
最后选了ONNX加INT8量化,推理耗时压到一百毫秒以内,精度掉得不多。量化不是万能的,带LayerNorm的Transformer量化完精度能掉两三个点,得先在小数据集上验证。
监控和降级
模型服务最怕挂了之后影响主链路。我们的做法:所有模型调用都包一层超时和熔断,超时时间设得比P99略高,配送时长预估设300毫秒,超时返回兜底值,比如历史均值。用户感知不明显,但系统不会雪崩。
监控维度除常规的QPS、错误率、时延,还要加模型特有指标:推理耗时分布、输入数据分布漂移、空值率。有一次某模型输入字段缺失率从1%涨到15%,一查是上游改了字段名,模型还在读老字段,等于拿空值推理,输出全偏了。没有输入监控根本发现不了。
最后说几句
机器学习部署说到底是个工程问题,不是算法问题。模型再牛,服务写得烂、监控不到位、降级没有,上线就是定时炸弹。后端同学别觉得这是算法组的事,真出问题第一个被叫起来的还是我们。

评论 0