从“上线就崩”到稳如老狗:我的机器学习部署优化实战手记
上周五晚上十一点半,我正瘫在工位上啃着冷掉的黄焖鸡,突然收到一条钉钉报警:“模型服务 P99 延迟飙到 2.3 秒”。当时心里一咯噔——这可是我们新上的推荐算法服务,明天就是运营大促,要是挂了,产品经理怕不是要提着保温杯来我工位泡枸杞。
我是阿哲,在这家公司干了三年多,去年刚升技术组长。说实话,带团队之后才发现,“能跑就行”的代码和“能扛住流量”的服务完全是两码事。以前写个 Flask 接口丢给运维就完事,现在得考虑 GPU 利用率、批处理吞吐、冷启动延迟……甚至还得跟运营同学解释为什么他们的 A/B 测试数据波动是因为特征 pipeline 没对齐。
最近半年,团队接了个活:把原来离线跑的推荐模型改成实时在线服务。目标很朴素:响应时间 < 200ms,99 分位 < 500ms,支持每秒 1000+ QPS。听起来不难?但等你真上手,就会发现 ML 部署是个深坑,比相亲对象的前任还多。
起因:别再拿 Jupyter Notebook 当生产环境了!
事情起源于去年双11。运营团队搞了个“猜你喜欢”活动,算法同事用 PyTorch 训了个双塔模型,本地测试 AUC 0.89,效果炸裂。结果一上线,直接 OOM,连带着整个 Kubernetes Pod 被 kill。运维老哥一脸懵:“你们这模型加载就要 4GB 内存?我们线上容器才配了 2G!”
更离谱的是,他们居然直接把 .ipynb 文件里的 model.predict() 封装成 REST API。没有 batch inference,没有模型缓存,每次请求都重新 load 模型……我当时看到代码真的想砸键盘。
后来复盘会上,CTO 直接点名:“ML 不是玩具,部署必须走工程化流程。”于是这个“脏活”就落到了我头上。毕竟我平时爱扒开源项目源码(比如 Triton Inference Server 和 BentoML),领导觉得我“看起来比较靠谱”。
踩坑实录:从模型格式到服务框架
第一坑:模型格式选型
一开始我们直接用 PyTorch 的 .pt 文件部署。结果发现,Python GIL 限制下,多线程根本跑不满 CPU。换成 TorchScript 吧,序列化倒是快了,但某些自定义算子不支持,报错:
RuntimeError: Exporting the operator xxx to ONNX is not supported.
最后咬牙转成 ONNX。虽然转换过程像在玩拼图(特别是带 Attention 的结构),但好处是推理引擎选择多了:TensorRT、ONNX Runtime、甚至能塞进 WebAssembly(虽然没用上)。
小贴士:如果你用 HuggingFace Transformers,记得加
torchscript=True导出,不然动态 shape 会炸。
第二坑:服务框架乱斗
我们试过三种方案:
| 方案 | QPS (P99<500ms) | 内存占用 | 开发成本 | 备注 |
|---|---|---|---|---|
| Flask + Gunicorn | ~120 | 3.2 GB | 低 | Python GIL 瓶颈明显 |
| FastAPI + Uvicorn | ~280 | 2.8 GB | 中 | 异步好但模型加载仍是阻塞 |
| Triton Inference Server | ~1100 | 1.9 GB | 高 | 需要 Docker + GPU 驱动 |
最终选了 Triton。虽然前期折腾 CUDA 版本和驱动兼容性差点让我秃头,但它的动态批处理(dynamic batching)和模型并发(model concurrency)功能简直是性能救星。配置一段 config.pbtxt 就能自动合并请求:
dynamic_batching {
max_queue_delay_microseconds: 1000
preferred_batch_size: [4, 8, 16]
}
意思是:最多等 1ms,如果队列里有 4/8/16 个请求就立刻打包推理。实测 QPS 提升 3 倍,P99 从 1.8s 降到 320ms。
区块链?别笑,它真能帮上忙
说到这儿你可能疑惑:标题里的“区块链”是凑数的?还真不是。
我们有个场景:运营团队需要审计模型决策过程。比如用户 A 为什么被推荐商品 X?传统做法是 log 特征 + 模型版本,但日志可能被篡改或丢失。
于是我们搞了个骚操作:把每次推理的关键信息(用户 ID、特征哈希、模型版本、输出概率)写入私有区块链(Hyperledger Fabric)。虽然性能有损耗(每笔交易多 15ms),但换来的是不可篡改的审计追踪。
# 伪代码:推理后上链
def predict_and_log(user_id, features):
output = model.infer(features)
tx = {
"user": user_id,
"feature_hash": hashlib.sha256(features).hexdigest(),
"model_version": "v2.3",
"score": output[0]
}
blockchain_client.submit(tx) # 异步提交,不影响主路径
return output
运营同学现在拿着链上数据做合规报告,老板看了直呼“高大上”。虽然我知道这玩意儿本质是分布式数据库,但谁让“区块链”三个字能打动投资人呢?(手动狗头)
性能优化三板斧
1. 特征预计算 + 缓存
模型推理快了,但特征拉取成了新瓶颈。用户特征来自 5 个微服务,串行调用要 300ms。我们做了两件事:
- 离线预计算:夜间 ETL 把用户画像打成宽表,存 Redis。
- 在线兜底:若缓存 miss,走异步补全,返回默认策略。
结果:特征获取 P99 从 310ms → 28ms。
2. 模型瘦身
原模型 128MB,加载慢。我们用了知识蒸馏(Knowledge Distillation),用大模型教小模型:
# 教师模型(复杂)
teacher = BERTLarge()
# 学生模型(轻量)
student = TinyBERT()
# 蒸馏损失 = 交叉熵 + KL散度(学生logits, 教师logits/温度)
loss = ce_loss + alpha * kl_div(student_logits / T, teacher_logits / T)
最终模型体积压到 23MB,CPU 推理速度提升 4 倍,AUC 只降了 0.015——运营完全无感。
3. 监控闭环
部署不是终点。我们接入了 Prometheus + Grafana,监控:
- 模型输入分布偏移(用 KS 检验)
- 推理延迟分位数
- 错误率 & fallback 触发次数
一旦特征分布突变(比如某天突然全是老年用户),自动告警。上周就靠这个抓到一个数据管道 bug:运营同学误删了年轻用户标签。
心得:ML 工程师 ≠ 算法工程师
干了这摊子事后,我彻底明白:部署才是算法落地的最后一公里。再牛的 AUC,上线崩了等于零。
现在招人面试,我必问:“你上次部署模型时,怎么测 P99 延迟的?” 如果回答“本地跑了几百次取平均”,基本就 pass 了。
顺便说一句,我最近在看外面机会。倒不是对公司不满(团队氛围其实挺好,除了产品经理总在周五提需求),主要是想接触更大规模的 ML Infra。毕竟,谁不想亲手调优一个亿级用户的推荐系统呢?
后记:文章写完,运维老哥发来消息:“你那 Triton 服务昨晚扛住了 1500 QPS,P99 412ms。” 我回了个 😎,然后默默续了一杯咖啡——明天还得改 feature store 的 schema,运营说要加“用户最近浏览区块链新闻”的特征……
(全文完,共 2278 字。代码和配置均来自真实项目脱敏,如有雷同,纯属你也被坑过。)

评论 0