从线上炸锅到稳如老狗:我在字节搞机器学习部署的血泪史

Linux夜行者
2026-04-05 06:36
阅读 2883

上周五晚上十一点半,我正坐在工位上盯着屏幕,咖啡已经凉透,脑子里还在跑着明天双周会要汇报的指标。突然钉钉“叮”一声,运维小哥发来一条消息:“推荐模型服务 P99 延迟飙到 800ms 了,用户刷不出内容,老板在群里@你了。”

我当时真的想砸键盘——这模型明明本地压测时只有 50ms,怎么一上线就原地爆炸?更离谱的是,这还是我亲手写的代码。

我是字节跳动基础架构组的一名后端开发,五年经验,坐标北京望京,每天通勤一小时雷打不动。平时喜欢深夜写代码,因为没人打扰,效率贼高。最近半年被“赶鸭子上架”,负责把算法同学训好的模型打包、上线、扛住流量。说白了,就是那个夹在算法和运维中间两头挨骂的“部署工程师”。

今天这篇不是什么高大上的理论科普,而是实打实的开发心得,记录了我们团队从去年到现在踩过的坑、熬过的夜,以及最终如何让模型服务从“线上炸锅”变成“稳如老狗”的全过程。如果你也在做 ML 模型部署,或者正在准备面试(后面我会埋几个高频面试题),建议收藏。


别再只关心 AUC 了,你的模型能扛住 QPS 吗?

很多算法同学以为,只要模型准确率高、AUC 漂亮,就万事大吉。但现实是:线上系统不看 AUC,只看延迟和错误率

去年双11前,我们接了一个新需求:给电商推荐场景加一个实时点击率预估模型。算法同学用 DeepFM 训得飞起,离线指标吊打 baseline。结果一上线,直接把网关打崩——因为模型输入特征拼接太重,单次推理耗时 600ms+,而上游要求 P99 < 100ms。

问题出在哪?

  • 模型本身没问题
  • 特征工程没做服务端优化
  • 特征拼接逻辑全在 Python 里,没预计算
  • 模型服务用的是 Flask + Gunicorn,默认同步阻塞

那一刻我才意识到:机器学习部署的本质,是性能工程


我们是怎么一步步优化的?

第一步:别用 Flask 裸跑模型!

Flask 开发快,但性能差。我们一开始图省事,直接 model.predict() 丢进去,结果并发一高就线程阻塞。后来换了 TorchServe / Triton Inference Server,效果立竿见影。

以 Triton 为例,它支持:

  • 多模型并发加载
  • 动态批处理(Dynamic Batching)
  • GPU 显存复用
  • 内置 metrics 监控
# 启动 Triton 示例
tritonserver --model-repository=/models --http-port=8000 --metrics-port=8002

配合 Prometheus + Grafana,P99 延迟直接从 800ms 降到 120ms。

第二步:特征必须“预热”

线上请求来了再拼特征?No!我们把高频用户的行为序列提前写入 Redis,模型服务启动时预加载特征模板。推理时只需查一次缓存,拼成固定维度向量。

# 伪代码:特征预计算 + 缓存
def get_user_features(user_id):
    cache_key = f"feat:{user_id}"
    if cached := redis.get(cache_key):
        return pickle.loads(cached)
    # fallback to real-time calc (slow path)
    feats = build_features_from_hive(user_id)
    redis.setex(cache_key, 300, pickle.dumps(feats))
    return feats

💡 开发心得:慢路径要有,但不能让用户走。99% 的请求应该命中缓存。

第三步:模型瘦身,量化上阵

原始模型 200MB,加载慢、显存占得多。我们做了两件事:

  1. ONNX 转换:统一框架,避免 PyTorch/TensorFlow 环境冲突
  2. INT8 量化:精度损失 <0.5%,体积缩小 75%,推理速度提升 2x
# 使用 onnxruntime + quantization
from onnxruntime.quantization import quantize_dynamic, QuantType

quantize_dynamic("model.onnx", "model_quant.onnx", weight_type=QuantType.QUInt8)

实测结果如下:

指标 原始模型 INT8 量化后
模型大小 200 MB 48 MB
GPU 显存占用 1.2 GB 300 MB
单次推理延迟 (V100) 65 ms 28 ms
准确率下降 - 0.32%

肉眼几乎看不出差异,但资源成本直接砍半。


面试题高频考点:你怎么保证模型服务 SLA?

这道题我在字节内部晋升答辩时被问过,也在面试候选人时经常抛出去。标准答案不是“我用了 Kubernetes”,而是“我有一套可观测 + 快速回滚机制”

我们的做法:

  • 健康检查/health 接口不仅检查进程存活,还要跑一次 dummy inference
  • 蓝绿发布:新模型先切 1% 流量,监控错误率 & 延迟,5 分钟无异常再全量
  • 自动熔断:如果错误率 > 1%,自动切回旧版本,并告警到值班群
  • 日志结构化:每条请求记录 model_version, input_hash, latency, output_score

有一次上线,新模型在某些长尾用户上输出 NaN,导致下游排序崩溃。因为我们有 input_hash,10 分钟内就定位到问题样本,紧急回滚。否则按传统日志 grep,怕是要熬到天亮。


代码人生:从“调参侠”到“系统匠人”

刚毕业那会儿,我觉得写算法才是高端活,部署就是搬砖。直到自己被线上事故教育了几次,才明白:再牛的模型,跑不起来等于零

现在我带新人,第一课不是教他怎么调 learning rate,而是让他写一个能扛住 1000 QPS 的 inference service。因为真实世界的 AI,不在 Jupyter Notebook 里,而在千万用户的手机屏幕上。

有时候深夜改完最后一行代码,看着 Grafana 上平稳的曲线,心里莫名踏实。这种感觉,比发一篇顶会论文还爽——毕竟,代码人生的意义,是让系统活着,而不是让 loss 下降


给正在踩坑的同学几点建议

  1. 别迷信“端到端”:特征工程、模型训练、服务部署必须解耦,否则迭代成本爆炸
  2. 压测要真实:用生产流量回放(traffic replay)测试,别只用 synthetic data
  3. 预留逃生通道:模型服务一定要支持 runtime 切换版本,别等挂了才手忙脚乱
  4. 和算法同学坐一起:他们懂业务,你懂系统,互相理解才能少掉头发

最后吐槽一句:产品经理总说“这个模型很简单,就加个字段”,但你知道光是为了兼容这个字段,我们要改特征 pipeline、重训模型、灰度验证……算了,不说了,我去改代码了。

毕竟,凌晨一点的北京,还有我的服务在跑。

评论 0

最热最新
暂无评论
Linux夜行者Lv.1
0
影响力
0
文章
0
粉丝