容器化部署
这一课你将能做到
- 写出生产级 Dockerfile:层缓存友好的依赖安装、非 root 用户、健康检查、小体积
- 用 docker compose 一键拉起本地 ML 栈(推理服务 + MLflow),服务按别名动态加载模型
- 编写 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"
讲清楚要点
- 层缓存顺序是构建速度的命门:先 COPY requirements 再 pip install,最后 COPY 代码。顺序反了,改一行代码就要重装全部依赖。
- 镜像 tag 即版本:CI 用
sha-xxxx标签,Deployment 引用具体 tag。latest让「现在是哪个版本」「怎么回滚」变成悬案。 - requests/limits 不是装饰:requests 决定调度到多强的节点,limits 决定 OOM 边界;HPA 按 CPU 利用率(相对 requests)伸缩。模型加载吃内存的服务要实测峰值再定额度。
- readiness 与 liveness 职责不同:readiness 未就绪只是摘流量(适合模型加载慢的场景),liveness 失败会重启容器(配置错了会陷入重启循环)。推理服务两者都要配。
- 模型文件两种交付方式:打进镜像(简单、不可变,但镜像大、换模型必发版)与运行时从注册中心拉(服务常驻、模型随 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% 余量定额度 |
卡点急救
- build 报找不到 requirements.txt:.dockerignore 误伤了文件或 COPY 相对路径不对;确认构建上下文是项目根目录。
- 容器内连不上 MLflow:localhost 在容器里指容器自身;compose 用服务名,K8s 用 Service DNS,本机 Docker 用 host.docker.internal。
- 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)未安装。
自测三问
- Dockerfile 层缓存的排序原则是什么?为什么?
- readiness 和 liveness 探针的职责区别?
- 模型文件打进镜像与运行时拉取,各自适合什么场景?
小结一句话
容器把「代码+依赖+系统库」固化成不可变镜像,K8s 用 Deployment/探针/HPA 把镜像变成可自愈、可弹性、可平滑发布的线上服务。
延伸
- Docker 官方文档:最佳实践——层缓存、多阶段构建、镜像瘦身。
- Kubernetes 官方文档:工作负载与伸缩——Deployment、探针、HPA 的权威定义。
- 下一课:云原生 ML——托管训练与 Endpoint,批处理与在线推理的形态选择。