CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
MLOps 入门/课时/容器化部署
RawBlame
课时目录12 节

MLOps 入门

  1. 01MLOps 是什么
  2. 02数据工程基础
  3. 03版本控制与 DVC
  4. 04训练与实验跟踪
  5. 05MLflow 与血缘
  6. 06编排与调度
  7. 07CI/CD for ML
  8. 08容器化部署
  9. 09云原生 ML
  10. 10监控与可观测
  11. 11可解释 AI
  12. 12综合练习

容器化部署

Docker/K8s 训练与推理。

30 分钟

容器化部署

这一课你将能做到

  1. 写出生产级 Dockerfile:层缓存友好的依赖安装、非 root 用户、健康检查、小体积
  2. 用 docker compose 一键拉起本地 ML 栈(推理服务 + MLflow),服务按别名动态加载模型
  3. 编写 K8s Deployment + Service + HPA + 探针清单,理解资源配额与弹性伸缩

为什么要学

「在我机器上能跑」是部署事故的头号台词。scikit-learn 版本差一个小版本、CUDA 驱动不匹配、系统缺一个 C 库,都可能让服务在别人的机器上起不来。容器把「代码 + 依赖 + 系统库」打包成不可变镜像,任何环境运行结果一致。

人话:模型文件只是「程序」,镜像才是「可以发货的整机」。CI/CD 发布的产物、K8s 调度的单位、回滚的粒度,全都是镜像。本课把第 5 课的超时预测服务装进集装箱。

先建立图像

Dockerfile(配方)──docker build──▶ 镜像(不可变)──docker run──▶ 容器(进程)

镜像分层(自下而上,越常变的越靠后):
┌──────────────────────────┐ ◀─ 变化最频繁:src/ 代码
│  COPY src/ .             │
├──────────────────────────┤ ◀─ 偶尔变:requirements.txt
│  RUN pip install -r ...  │
├──────────────────────────┤ ◀─ 几乎不变:系统基础
│  FROM python:3.11-slim   │
└──────────────────────────┘
层可复用缓存:基础层没变就不重装依赖,build 秒级完成

K8s 视角:Deployment(期望副本数)→ Pod(容器)→ Service(稳定入口)→ HPA(按负载扩缩)

跟着做

步骤 1:写生产级 Dockerfile

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

# 依赖层在前:代码改动不会触发重装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src/ src/

# 安全:不用 root 跑服务
RUN useradd -m appuser
USER appuser

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD python -c "import urllib.request as u; u.urlopen('http://127.0.0.1:8000/health')"

CMD ["uvicorn", "src.serve:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]

配套 .dockerignore,防止把垃圾打进构建上下文:

# .dockerignore
data/
models/
.venv/
.git/
mlruns/
mlflow.db
__pycache__/

构建并运行:

docker build -t timeout-api:v1 .
docker run -d --name timeout-api -p 8000:8000 \
  -e MLFLOW_TRACKING_URI=http://host.docker.internal:5000 \
  timeout-api:v1
curl http://127.0.0.1:8000/health
curl -X POST "http://127.0.0.1:8000/predict?code_lines=180&loop_depth=3&lang_java=1&problem_rank=4"

步骤 2:compose 拉起本地 ML 栈

# docker-compose.yml
services:
  mlflow:
    image: ghcr.io/mlflow/mlflow:v2.17.2
    command: >
      mlflow server --host 0.0.0.0 --port 5000
      --backend-store-uri sqlite:///mlflow.db
      --artifacts-destination /artifacts
    ports: ["5000:5000"]
    volumes: ["./mlartifacts:/artifacts"]

  api:
    build: .
    ports: ["8000:8000"]
    environment:
      MLFLOW_TRACKING_URI: http://mlflow:5000
    depends_on: [mlflow]
docker compose up -d --build
curl -X POST "http://127.0.0.1:8000/predict?code_lines=180&loop_depth=3&lang_java=1&problem_rank=4"
docker compose down

容器内访问 http://mlflow:5000——compose 自动建了同网络与服务名 DNS,这就是本地版的「服务发现」。

步骤 3:K8s 部署清单

# k8s/timeout-api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: timeout-api
spec:
  replicas: 2
  selector: { matchLabels: { app: timeout-api } }
  template:
    metadata:
      labels: { app: timeout-api }
    spec:
      containers:
        - name: api
          image: ghcr.io/your-org/timeout-api:sha-abc1234   # 禁用 latest
          ports: [{ containerPort: 8000 }]
          env:
            - name: MLFLOW_TRACKING_URI
              value: http://mlflow.mlflow.svc.cluster.local:5000
          resources:                     # 配额:HPA 与调度的依据
            requests: { cpu: "250m", memory: "256Mi" }
            limits:   { cpu: "1",    memory: "512Mi" }
          readinessProbe:                # 就绪:没 ready 不接流量
            httpGet: { path: /health, port: 8000 }
            initialDelaySeconds: 5
          livenessProbe:                 # 存活:挂了自动重启
            httpGet: { path: /health, port: 8000 }
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: timeout-api
spec:
  selector: { app: timeout-api }
  ports: [{ port: 80, targetPort: 8000 }]
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: timeout-api
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: timeout-api }
  minReplicas: 2
  maxReplicas: 8
  metrics:
    - type: Resource
      resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
kubectl apply -f k8s/timeout-api.yaml
kubectl get pods -l app=timeout-api -w      # 观察扩缩
kubectl port-forward svc/timeout-api 8000:80
curl -X POST "http://127.0.0.1:8000/predict?code_lines=180&loop_depth=3&lang_java=1&problem_rank=4"

讲清楚要点

  1. 层缓存顺序是构建速度的命门:先 COPY requirements 再 pip install,最后 COPY 代码。顺序反了,改一行代码就要重装全部依赖。
  2. 镜像 tag 即版本:CI 用 sha-xxxx 标签,Deployment 引用具体 tag。latest 让「现在是哪个版本」「怎么回滚」变成悬案。
  3. requests/limits 不是装饰:requests 决定调度到多强的节点,limits 决定 OOM 边界;HPA 按 CPU 利用率(相对 requests)伸缩。模型加载吃内存的服务要实测峰值再定额度。
  4. readiness 与 liveness 职责不同:readiness 未就绪只是摘流量(适合模型加载慢的场景),liveness 失败会重启容器(配置错了会陷入重启循环)。推理服务两者都要配。
  5. 模型文件两种交付方式:打进镜像(简单、不可变,但镜像大、换模型必发版)与运行时从注册中心拉(服务常驻、模型随 champion 指针切换)。本课程用后者,代价是启动依赖 MLflow 可用。

你可能会懵的地方

  • 容器里为什么还配 --workers? uvicorn 单进程单事件循环,CPU 型推理要多个 worker 才吃满多核。经验值 2×CPU 核数起步,配合压测调整;K8s 里每 Pod 也承担并发,两者叠加后总体压测定容量。
  • GPU 服务怎么调度? 节点打 GPU 标签,Pod 加 resources.limits: nvidia.com/gpu: 1 与 nodeSelector/taints,集群需装 NVIDIA device plugin。推理服务按「每 Pod 一张卡 + 副本数扩容」规划;小模型别抢 GPU,CPU 便宜得多。
  • 镜像太大怎么办? 四板斧:基础镜像换 slim/alpine;pip 加 --no-cache-dir;多阶段构建(构建阶段装编译依赖,运行阶段只拷产物);.dockerignore 排除数据与缓存。ML 镜像常见反例是把 torch CUDA 全量装进 CPU 服务。
  • 模型加载要 30 秒,探针怎么配? readiness 的 initialDelaySeconds 给足加载时间(或用 startupProbe 宽限启动),liveness 从容器启动后再计。更稳的做法是服务内部加载完成后才真正监听端口。
  • compose 和 K8s 的边界? compose 是单机开发/演示工具;K8s 是多机生产编排(自愈、滚动更新、HPA、服务发现)。本课程路径:compose 本地联调 → K8s 生产。中间规模也可用轻量 K8s(k3s/kind)过渡。

错法对照

错法问题正解
先 COPY . . 再 pip install改一行代码重装全部依赖requirements 层在前,代码层在后
Deployment 引用 latest 标签版本不可追溯、回滚失效引用 sha/语义化具体 tag
容器用 root 运行容器逃逸时直接拿到宿主机权限useradd + USER 降权
不配 readiness 就上流量模型未加载完就收到请求报错就绪探针 + 启动宽限
limits.memory 低于模型峰值运行数小时后被 OOMKill压测峰值 + 20% 余量定额度

卡点急救

  1. build 报找不到 requirements.txt:.dockerignore 误伤了文件或 COPY 相对路径不对;确认构建上下文是项目根目录。
  2. 容器内连不上 MLflow:localhost 在容器里指容器自身;compose 用服务名,K8s 用 Service DNS,本机 Docker 用 host.docker.internal。
  3. Pod 反复 CrashLoopBackOff:kubectl logs pod名 --previous 看上次崩溃日志,常见是环境变量缺失或模型 URI 不存在;同时检查 liveness 是否过早探活。

面试常问

  • 如何优化一个 8GB 的 ML 推理镜像? 按收益排序:换 slim 基础镜像、多阶段构建去掉编译工具链、--no-cache-dir 与清理 apt 缓存、依赖按需裁剪(CPU 服务剔除 CUDA 版 torch)、.dockerignore 排除数据/缓存,最后合并后量化模型文件体积。
  • K8s 上部署模型服务的要点? 镜像固定 tag;requests/limits 按压测配置;readiness/liveness 探针分开语义;HPA 结合 CPU 与业务指标(QPS/延迟);滚动更新配 maxSurge/maxUnavailable 保证平滑;配置走环境变量或 ConfigMap,模型走注册中心。
  • HPA 为什么有时不扩容? 常见原因:没设 resources.requests(CPU 利用率无分母)、指标低于阈值、扩缩冷却窗口(stabilization)未过、副本已达 maxReplicas、或指标服务(metrics-server)未安装。

自测三问

  1. Dockerfile 层缓存的排序原则是什么?为什么?
  2. readiness 和 liveness 探针的职责区别?
  3. 模型文件打进镜像与运行时拉取,各自适合什么场景?

小结一句话

容器把「代码+依赖+系统库」固化成不可变镜像,K8s 用 Deployment/探针/HPA 把镜像变成可自愈、可弹性、可平滑发布的线上服务。

延伸

  • Docker 官方文档:最佳实践——层缓存、多阶段构建、镜像瘦身。
  • Kubernetes 官方文档:工作负载与伸缩——Deployment、探针、HPA 的权威定义。
  • 下一课:云原生 ML——托管训练与 Endpoint,批处理与在线推理的形态选择。
加载本节练习…
← 上一课CI/CD for ML下一课 →云原生 ML