机器学习部署最佳实践:一个后端搬砖人的血泪总结
大家好,我是字节基础架构组的后端开发,入职快五年了。平时主要在搞高并发、低延迟的基础服务,最近被“优化”压力逼着研究怎么把算法模型高效地塞进我们的 Springboot 服务里——别问,问就是领导说:“你不是喜欢看源码吗?那顺便看看怎么把推荐模型上线吧。”
其实我本来是想跳槽前刷 LeetCode 的,结果上周五晚上 9 点半,产品突然拉群说:“双11大促前必须上线新推荐算法,你们后端配合一下。” 我心里一万只羊驼奔腾而过,但嘴上只能回个“OK”。于是,我被迫从“纯后端”转型成“伪 MLOps 工程师”,踩了一堆坑,也总结了一些性能导向的机器学习部署最佳实践。
今天就来聊聊,一个只会写 CRUD 的后端,在 Springboot 里塞进算法模型时,是怎么活下来的。
背景:为什么不能直接扔个 .pkl 文件到生产?
事情起源于我们团队要给内容推荐系统加个“兴趣衰减因子”模型。算法同事训练完扔给我一个 model.pkl,说:“你加载一下就行,很简单。”
我心想:“不就是反序列化+预测嘛,能有多难?” 结果本地跑得飞快,一上测试环境,QPS 直接从 5000 掉到 200,CPU 爆满,监控告警响得像过年放鞭炮。
查了下日志,发现每次请求都要重新 load 模型(因为怕内存泄漏,我写了个“优雅”的 try-with-resources),而且 sklearn 的 predict 方法在高并发下根本扛不住——Python GIL 锁 + 单线程执行 = 高并发杀手。
更惨的是,运维兄弟吐槽:“你这服务内存波动太大,K8s 以为你 OOM 了,天天自动重启。” 那一刻,我真的想砸键盘。
第一步:模型格式选型 —— 别再用 pickle 了!
pickle 虽然方便,但它是 Python 特有的序列化格式,无法跨语言、无法版本管理、还容易反序列化漏洞。我们后端是 Java/Springboot,总不能为了一个模型引入 Jython 吧?
最后我们统一迁移到 ONNX(Open Neural Network Exchange)。ONNX 是微软和 Facebook 主导的开放格式,支持 PyTorch、TensorFlow、sklearn 等主流框架导出,还能被 Java 通过 ONNX Runtime 高效调用。
# 算法侧:导出为 ONNX
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
initial_type = [('float_input', FloatTensorType([None, feature_dim]))]
onnx_model = convert_sklearn(sklearn_model, initial_types=initial_type)
with open("interest_decay.onnx", "wb") as f:
f.write(onnx_model.SerializeToString())
ONNX Runtime 在 Java 里启动一次后,可以复用推理会话,避免重复加载,性能提升至少 10 倍。
第二步:Springboot 集成 ONNX Runtime —— 注意生命周期!
很多人直接在 Controller 里 new 一个 OrtSession,这是大忌!OrtSession 初始化很重(要解析模型、分配 GPU/CPU 内存),必须作为单例 Bean 管理。
我在 @Configuration 里这么干:
@Configuration
public class ModelConfig {
@Bean(destroyMethod = "close")
public OrtSession interestDecayModel() throws Exception {
OrtEnvironment env = OrtEnvironment.getEnvironment();
OrtSession.SessionOptions opts = new OrtSession.SessionOptions();
opts.setOptimizationLevel(OptLevel.BASIC_OPT); // 启用基本优化
return env.createSession("classpath:model/interest_decay.onnx", opts);
}
}
然后在 Service 里注入:
@Service
public class RecommendService {
private final OrtSession model;
public RecommendService(OrtSession interestDecayModel) {
this.model = interestDecayModel;
}
public float predict(float[] features) {
try (var input = OrtUtil.createTensor(features)) {
var result = model.run(Map.of("float_input", input));
return ((float[]) result.get(0).getValue())[0];
} catch (OrtException e) {
log.error("Model inference failed", e);
throw new RuntimeException("Inference error");
}
}
}
💡 关键点:OrtSession 是线程安全的!官方文档明确说了,一个 session 可以被多个线程并发调用。所以不用搞什么连接池,直接单例就行。
第三步:性能优化 —— 别让模型拖垮你的 P99
即使用了 ONNX,如果每次请求都做完整推理,P99 延迟还是会炸。我们做了三件事:
1. 特征预计算 + 缓存
很多特征(比如用户历史点击率、设备类型)是静态或低频更新的。我们在 Flink 里提前算好,写入 Redis,线上服务直接读缓存,减少实时特征拼接开销。
2. 批量推理(Batch Inference)
虽然推荐是实时请求,但我们可以用 异步攒批:用 Disruptor 或者自定义 RingBuffer,把多个请求攒成 batch,一次性喂给模型。
// 简化版攒批逻辑
public CompletableFuture<Float> asyncPredict(float[] features) {
BatchCollector.submit(features);
return future; // 稍后由 batch 线程完成并 complete
}
实测:batch_size=32 时,吞吐提升 4 倍,P99 从 80ms 降到 25ms。
3. 模型量化(Quantization)
算法同事配合把 FP32 模型转成 INT8,体积缩小 75%,推理速度提升 2 倍,精度损失 < 0.5% —— 产品经理看了都说“够用”。
| 模型格式 | 大小 | QPS(单核) | P99 延迟 |
|---|---|---|---|
| Pickle (sklearn) | 120MB | 180 | 120ms |
| ONNX (FP32) | 110MB | 1800 | 40ms |
| ONNX (INT8) | 30MB | 3600 | 22ms |
面试题预警:面试官最爱问的几个坑
最近边准备跳槽边复盘,发现这些部署细节成了高频面试题:
Q:如何保证模型服务的高可用?
- A:模型版本灰度 + 熔断降级。我们用 Nacos 管理模型版本,失败时 fallback 到规则引擎。
Q:Java 调用 Python 模型有哪些方案?各有什么问题?
- A:别用 JNI(维护地狱),别用 HTTP 调 Python 服务(网络开销大)。优先 ONNX,次选 Triton Inference Server + gRPC。
Q:如何监控模型效果漂移?
- A:线上打标 + 离线比对。我们每天抽样 1% 请求,把输入输出存到 Hive,用 Airflow 跑 A/B test 报告。
有一次面试官直接问我:“如果模型加载占了 2GB 内存,但你们容器只给 4GB,怎么办?”
我脱口而出:“那就别用 sklearn,改 LightGBM + ONNX,或者上模型蒸馏。” 面试官笑了,说:“看来真干过活。”
血泪教训:别信“简单集成”四个字
最开始我以为“调个 predict 就完事了”,结果光是 特征对齐 就搞了三天——算法用 pandas 处理特征,我们线上用 Flink,两者 NaN、Inf、类型转换逻辑不一致,导致线上预测值全是 0。
后来我们强制要求:
- 特征 pipeline 必须用 共享的 Protobuf Schema
- 训练和推理走同一套 特征工程代码(用 PySpark 写,两边都能跑)
另外,别在高峰期上线模型!去年双11前夜,我们灰度 5% 流量,结果模型有个边界 case 导致 JVM Full GC,差点背锅到明年。现在规定:所有模型变更必须经过 影子流量验证(Shadow Traffic),即真实请求同时走新旧模型,对比输出差异。
总结:后端眼中的“好模型”
作为一个只想写 clean code 的后端,我对算法同事的唯一请求是:
请把模型当成 API 来交付:有版本、有文档、有性能指标、有回滚方案。
现在的部署流程已经标准化:
- 算法训练 → 导出 ONNX + 特征 Schema
- CI 自动校验模型大小、精度、输入输出
- Helm Chart 打包模型文件,随 Springboot 一起发布
- 上线后自动采集 latency / error rate / prediction distribution
终于,我不用再半夜被 PagerDuty 叫醒修模型 bug 了。上周五,产品又来提需求,我说:“行,但模型得走 ONNX + 影子流量。” 他居然点头了!那一刻,我觉得自己像个真正的工程师了。
如果你也在被“快速上线算法”折磨,希望这篇血泪史能帮你少熬两个通宵。毕竟,我们后端的 KPI 不是准确率,是 P99 和 MTTR。
(P.S. 跳槽简历上我已经加上“主导 ML 模型高并发部署方案”,希望能骗到一个不让我写算法的 offer。)

评论 0