机器学习部署最佳实践:一个后端搬砖人的血泪总结

缓存击穿侠
2025-12-16 08:29
阅读 1113

大家好,我是字节基础架构组的后端开发,入职快五年了。平时主要在搞高并发、低延迟的基础服务,最近被“优化”压力逼着研究怎么把算法模型高效地塞进我们的 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 来交付:有版本、有文档、有性能指标、有回滚方案。

现在的部署流程已经标准化:

  1. 算法训练 → 导出 ONNX + 特征 Schema
  2. CI 自动校验模型大小、精度、输入输出
  3. Helm Chart 打包模型文件,随 Springboot 一起发布
  4. 上线后自动采集 latency / error rate / prediction distribution

终于,我不用再半夜被 PagerDuty 叫醒修模型 bug 了。上周五,产品又来提需求,我说:“行,但模型得走 ONNX + 影子流量。” 他居然点头了!那一刻,我觉得自己像个真正的工程师了。

如果你也在被“快速上线算法”折磨,希望这篇血泪史能帮你少熬两个通宵。毕竟,我们后端的 KPI 不是准确率,是 P99 和 MTTR

(P.S. 跳槽简历上我已经加上“主导 ML 模型高并发部署方案”,希望能骗到一个不让我写算法的 offer。)

评论 0

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