AI部署最佳实践:这次怎么落地的

回想这几年在 AI 项目上的折腾,真正让我在夜深人静时抱着电脑叹气的,从来不是模型推理本身——那通常是技术方案中最确定的一环——而是那些为了把一个本地能跑的东西变成稳定服务的琐碎:资源隔离、版本管理、灰度发布、监控告警。

哪怕起初只是用 Docker Compose 起两个服务,这也是在为后续迁到 K8s 打基础。

先把容器和编排说清楚

我现在的做法基本围绕 Docker + Kubernetes 展开。哪怕起初只是用 Docker Compose 起两个服务,这也是在为后续迁到 K8s 打基础。原因很简单:环境隔离和统一配置能省掉 80% 的“换台机器就跑不通”的麻烦。

Dockerfile 要做到两点:尽量减少层数,用好构建缓存。下面这个模板用过几次,拿来做推理服务还行:

FROM python:3.10-slim as builder

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    gcc \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.10-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    libgomp1 \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
COPY . .

EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

注意那几行 apt-get 清理和 --no-cache-dir。我吃过镜像太大导致拉取超时的亏,也试过给别人一个几乎没人用的镜像,后来补上了这几行,体积从 1.2GB 缩到 400MB 左右,不少 CI 跑得也快了。

K8s 的 Deployment 和 Service 模板照着官方例子改就行,关键在资源限制。GPU 情况下我一般这样写:

resources:
  requests:
    memory: "2Gi"
    cpu: "1000m"
    nvidia.com/gpu: 1
  limits:
    memory: "8Gi"
    cpu: "4000m"
    nvidia.com/gpu: 1

别忘记给内存和 CPU 留点余量。我曾经在一次突发的流量高峰里看到服务 OOM,才发现 limits.memory 写得太紧,模型在处理长序列时耗得比预期多。后来稍微上调了一些,再看监控曲线,总算没那么尖了。

监控和告警比我想象的重要得多

之前总想“先把功能上线再说”,结果常常是被一个本来早该发现的 bug 从被窝里叫起来。现在,我基本在部署阶段就顺手把监控加进去。

常用的工具是 Prometheus + Grafana。下面是一个比较基础的 Prometheus 抓取配置:

scrape_configs:
  - job_name: 'ai-inference'
    scrape_interval: 30s
    static_configs:
      - targets: ['ai-inference:8000']

Grafana 里的看板,我起码会关注三条曲线:请求 QPS、推理延迟、GPU 利用率。前两个看流量和性能,第三个看资源有没有撑起来。记得有一次 QPS 上来了,但 GPU 利用率始终在 20% 左右徘徊,一看代码,才发现模型加载时没有设 torch.device("cuda"),被默默地放在了 CPU 上,后来修正后 GPU 利用率直接到了 80%,响应时间也降下来了。

告警的话,我习惯用 Alertmanager,下面这个规则很简单但实用:

groups:
  - name: ai-inference-alerts
    rules:
      - alert: HighLatency
        expr: histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, instance)) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "推理延迟偏高: {{ $labels.instance }}"
      - alert: ServiceDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "服务宕机: {{ $labels.instance }}"

这几条规则把常见的几种紧急情况都挡了前面。别偷懒不配告警,半夜三更收到第一封邮件时你会感谢早上的自己。

灰度发布不是奢侈,是保命

把一个刚修好的模型直接扔给全部用户,我干过一次,结果是一次线上事故——新版本有概率在特定输入上崩溃,而偏偏测试集没覆盖到。后来我给自己定了一条规矩:宁可麻烦,也要灰度。

最简单的做法是 Traffic Splitting,把 10% 的流量导到新版本,观察一段时间没问题再慢慢放开。下面用 Istio 的 VirtualService 做个示例:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ai-inference-vs
spec:
  http:
  - match:
    - uri:
        prefix: /predict
    route:
    - destination:
        host: ai-inference
        subset: v1
      weight: 90
    - destination:
        host: ai-inference
        subset: v2
      weight: 10

subset 定义在 DestinationRule 里:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: ai-inference-dr
spec:
  host: ai-inference
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2

你可以手动调整 weight,也可以写一个脚本根据监控指标自动调整权重。我试过后者,有时候你未必看得到细节指标的变化,但让工具帮你按延迟和成功率来自动切流,总体上比盯着屏幕几个小时靠谱。

再给一个小建议:从灰度到全量之间,最好留一个“回滚按钮”。那次线上事故最后还是靠回滚到上一个版本止血的,这让我明白,回滚路径必须和发布路径一样清晰。

配置管理与环境隔离

别把配置硬编码到镜像里,这条已经被无数的部署事故验证过了。我现在的做法是用 ConfigMap 挂载配置文件,用 Secret 放敏感信息。

先定义一个 ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: ai-inference-config
data:
  config.yaml: |
    model_name: "gpt-4"
    max_tokens: 2048
    temperature: 0.7

然后在 Deployment 里挂载:

spec:
  containers:
    - name: ai-inference
      image: my-registry/ai-inference:latest
      volumeMounts:
        - name: config
          mountPath: /app/config
  volumes:
    - name: config
      configMap:
        name: ai-inference-config

Secret 也是类似的:

apiVersion: v1
kind: Secret
metadata:
  name: ai-inference-secret
type: Opaque
data:
  api_key: <base64-encoded-key>

挂载时同样使用 volumeMounts。这样改配置再也不用重新打镜像,而且开发和生产环境可以用不同的 ConfigMap,尽量把环境之间的差异从代码里抽出来。

日志和追踪,别等出事再补

日志的锅我也背过。一开始所有东西都打印到 stdout,可等到线上出问题,真正关键的信息淹没在几千行无关日志里,那时你才会明白结构化日志的重要。

我给日志加了几个固定字段:时间戳、日志级别、请求 ID、耗时。例如:

{
  "timestamp": "2026-07-16T21:10:05Z",
  "level": "INFO",
  "request_id": "abc-123",
  "latency_ms": 245,
  "message": "inference completed"
}

顺便把日志打到统一的日志平台,比如 ELK 或者 Loki,不然你依然会像在黑夜里抓鬼一样排查问题。

追踪 Distributed Tracing 对于微服务尤其有用,不过即使只是一个独立服务,给每个请求挂一个 ID 也能减少你排查问题的困扰。我一般会在最开始的网关生成一个 request_id,然后在服务之间透传。当你发现一条异常请求一路传下去,追踪链路能帮你快速定位到是哪个环节出了问题。

常见坑点总结

有几条经验我反复验证过:

  • 资源限制写得太紧会让模型在高负载时直接 OOM 或者被内核杀掉。给 requests 和 limits 留出 20–30% 的缓冲区,除非你对模型的资源消耗有非常准确的测试数据。

  • 监控指标选错了会让你被误导。有时候高 CPU 不代表负载高,内存占用增加也可能只是缓存增长。建议在训练阶段就收集几组你真正关心的指标:推理延迟、吞吐量、GPU 利用率、显存占用。

  • 灰度发布不是真的“稳”。我在一次 5% 的灰度里照样被一个角落的 bug 打中,后来才明白,灰度的真正作用是让你有更多时间观察、捕获和回滚,而不是彻底避免问题。

  • 配置写死在代码里是最大的雷。哪怕一开始只有一两个参数,也尽量抽到配置文件里,不然后面每次改参数都要重新构建和部署。

  • 日志写得太少和太多同样害人。太少时关键信息没有记录,太多则淹没真正有用的内容。适度结构化,尽量让每一行日志都能回答一个“为什么会这样”的问题。

一些个人取舍

我一直认为,所谓的“最佳实践”都是别人踩过坑后的产物。它们有用,但不意味着照搬就能无忧无虑。

容器、编排、监控、灰度、配置管理、日志追踪,这些工具堆在一起确实有点重。如果你的项目一开始只有几个调用次数有限的接口,你也许可以用更简单的方案,比如 Docker Compose + Nginx 反向代理 + 手动切流量。但只要你准备在生产上真正把服务交给别人用,这些重活是迟早要干的。

我曾经为了“快速上线”绕过了监控和灰度,结果花在回滚和熬夜修复上的时间,远比一开始就做好这些要多。也许你没经历过那些被异常流量突然冲垮的晚上,那你可以当这篇是故事;如果你已经经历过,那我希望上面那些条目能帮你少经历几次。

技术的选择,从来不是照着清单画勾,而是根据现实场景权衡。AI 推理服务尤甚:模型本身会变,负载会变,用户的期望也会变。把部署方案当成活的系统,定期审视和调整,别让它变成一份落灰的文档。

结束语

从本地那行 python app.py 到生产里的一条稳定的 API,中间需要的不仅是技术,还有对自己和系统边界的清醒认识。

把这篇文章读完,你可以先回去检查一下你当前的部署方案:资源有没有预留,监控有没有告警,灰度有没有路径,配置有没有抽离,日志有没有结构化。每补上一项,下次出事故时你就能少一次看日志却找不到线索的绝望。

生产环境没有银弹。只有一次次修补、调整和谨慎,才会在问题真的出现时,让你不至于措手不及。

祝你的线上服务比我的更稳一些。

版权声明: 本文首发于 指尖魔法屋-AI部署最佳实践:这次怎么落地的https://blog.thinkmoon.cn/post/999-ai-deployment-best-practices-from-dev-to-prod/) 转载或引用必须申明原指尖魔法屋来源及源地址!