1. 为什么机器学习工程师现在离不开 Docker 和 Kubernetes?

我带过三届校招新人,也帮五家不同行业的公司做过 MLOps 架构升级。最常听到的一句话是:“模型在本地跑得好好的,一上测试环境就报错;测试环境没问题,部署到生产又出问题。”——不是代码写得差,而是环境不一致这个老问题,在机器学习场景里被放大了十倍。Python 版本、CUDA 驱动、PyTorch 编译版本、甚至 OpenBLAS 的线程数配置,任何一个微小差异都可能让训练精度掉 0.3%,或者推理延迟翻三倍。这时候,你不能靠“再重装一遍环境”来解决,而需要一套能 把整个计算栈封存、验证、运输、启动 的机制。这就是容器化的真实价值,不是锦上添花,而是生存刚需。

Docker 和 Kubernetes 不是两个并列工具,而是一对分工明确的搭档:Docker 负责“打包”,把你的 Jupyter Notebook、训练脚本、模型权重、CUDA 库、甚至 GPU 监控脚本,全部塞进一个可复现、可签名、可版本化的镜像里;Kubernetes 则负责“调度”,当你要同时跑 12 个超参实验、要给线上推理服务自动扩到 50 个实例、要在 A100 集群上优先调度大模型训练任务时,它就是那个不眠不休的指挥官。我见过太多团队卡在“模型能跑通”和“模型能交付”之间,中间缺的不是算法,而是一套可靠的交付基础设施。关键词不是“容器”,而是“确定性”——确定性地构建、确定性地运行、确定性地扩展。这正是 Docker + Kubernetes 组合在机器学习领域不可替代的核心原因。它解决的从来不是“能不能跑”的问题,而是“能不能放心交给别人跑、能不能在任何时间点重新跑出来、能不能扛住流量洪峰还不出错”的问题。如果你还在用 pip install -r requirements.txt 然后祈祷环境别崩,那这篇文章值得你从头读到尾。

2. 容器化不是魔法,是精确控制环境的工程实践

2.1 容器的本质:进程隔离 + 文件系统快照

很多人把容器想象成轻量级虚拟机,这是个危险的误解。VM 是模拟硬件,启动慢、资源开销大;容器则是操作系统内核层面的进程隔离技术。它的核心就两件事:一是用 Linux 的 cgroups (control groups)限制 CPU、内存、GPU 显存等资源的使用上限,比如强制一个训练任务最多只能用 4 核 CPU 和 16GB 内存,避免它吃光整台服务器;二是用 namespaces (命名空间)给每个容器造一个“假世界”——它看到的进程 ID、网络接口、文件系统路径,都是独立的。你在容器里 ps aux 看到的 PID 1 是你的 Python 进程,但宿主机上它可能是个 PID 12897 的普通进程。这种隔离不是靠虚拟化,而是靠内核的精细管控。

而“镜像”就是这个隔离世界的快照。它不是一个压缩包,而是一层层只读文件系统的叠加。比如你用 FROM nvidia/cuda:11.8.0-devel-ubuntu22.04 作为基础镜像,这一层包含了完整的 CUDA 工具链和 Ubuntu 22.04 的系统库;接着 RUN apt-get update && apt-get install -y libsm6 libxext6 这条指令会生成新的一层,只记录你安装的两个图形库;最后 COPY model.pth /app/ 又加一层,只存模型文件。Docker 构建时,会按顺序加载这些层,最上层是可写的,用来存放运行时产生的日志或临时文件。这种分层设计带来两个巨大好处:一是复用——全公司所有 PyTorch 项目都基于同一个 CUDA 基础镜像,拉取时只需下载一次;二是可追溯——每一层都有唯一的 SHA256 哈希值,你能精确知道这个镜像里到底有没有包含某个有安全漏洞的 OpenSSL 版本。

提示:不要用 docker commit 去保存运行中容器的状态。这会破坏镜像的可重现性,因为你无法知道容器里到底执行过哪些命令、改过哪些配置。一切必须通过 Dockerfile 显式声明。

2.2 Dockerfile 编写:从“能跑”到“好维护”的关键跃迁

我看过上百份 MLOps 团队的 Dockerfile,最常见的问题是“能跑就行”,结果半年后没人敢动。一个合格的 ML 镜像 Dockerfile,必须回答三个问题: 依赖是否精确?构建是否高效?运行是否安全? 下面是一个工业级的 PyTorch 训练镜像模板,我们逐行拆解:

# 第1行:选择最小、最可控的基础镜像
FROM pytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtime

# 第2行:创建非 root 用户,避免容器内进程以最高权限运行
RUN useradd -m -u 1001 -g root appuser
USER appuser

# 第3行:设置工作目录,所有后续操作都在此路径下
WORKDIR /home/appuser/app

# 第4行:先复制 requirements.txt 单独安装依赖,利用 Docker 层缓存
# 如果 requirements.txt 没变,这一步直接用缓存,跳过 pip install
COPY --chown=appuser:root requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

# 第5行:复制源码,放在依赖安装之后,这样改代码不会触发重装依赖
COPY --chown=appuser:root . .

# 第6行:设置环境变量,显式声明 CUDA 可见设备,避免 PyTorch 自动占用所有 GPU
ENV CUDA_VISIBLE_DEVICES=0
ENV PYTHONUNBUFFERED=1

# 第7行:定义健康检查,Kubernetes 会定期调用,判断容器是否真在“干活”
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD python -c "import torch; print(torch.cuda.memory_allocated())" || exit 1

# 第8行:指定默认命令,这里是一个训练入口脚本,而非直接跑 python train.py
CMD ["./train.sh"]

关键细节解析:

  • 基础镜像选型 pytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtime nvidia/cuda:11.8.0-devel-ubuntu22.04 更优,因为它已经预编译好了 PyTorch,且是 runtime 版本(不含编译工具链),体积更小、攻击面更少。 devel 版本只应在构建镜像时用,运行时绝对不用。
  • 非 root 用户 :这是安全红线。ML 容器常需挂载宿主机数据目录,如果以 root 运行,容器内一个 rm -rf / 就可能删掉宿主机关键文件。 useradd 创建用户并 USER 切换,是必须步骤。
  • 分层缓存策略 COPY requirements.txt 放在 COPY . 之前,是 Dockerfile 黄金法则。因为 requirements.txt 变更频率远低于代码,这样绝大多数时候 pip install 步骤都能命中缓存,构建时间从 8 分钟降到 30 秒。
  • HEALTHCHECK :很多团队忽略这点。Kubernetes 默认只看容器进程是否存活,但一个训练进程可能卡死在数据加载阶段,CPU 占用为 0,却一直不退出。 HEALTHCHECK torch.cuda.memory_allocated() 检查 GPU 显存是否在动态变化,能真实反映训练是否在进行。

2.3 构建与推送:镜像生命周期管理的实操要点

构建命令 docker build -t my-ml-project:1.2.0 . 看似简单,但生产环境必须加上关键参数:

# 生产级构建命令(含多阶段构建和构建参数)
docker build \
  --build-arg BUILD_ENV=prod \
  --build-arg PYTORCH_VERSION=2.1.0 \
  --build-arg CUDA_VERSION=11.8 \
  --progress=plain \  # 输出详细日志,便于排查卡在哪一步
  --cache-from type=registry,ref=my-registry.com/ml-base:latest \  # 复用远程镜像缓存
  -t my-registry.com/ml-train:1.2.0 \
  -f Dockerfile.train \
  .
  • --build-arg :允许在构建时注入变量,比如不同环境用不同日志级别,或切换 PyTorch 版本做兼容性测试。这些变量在 Dockerfile 中用 ARG 声明, RUN 时用 $PYTORCH_VERSION 引用。
  • --cache-from :大型 ML 镜像构建动辄半小时,开启远程缓存能节省 70% 时间。你需要一个私有镜像仓库(如 Harbor 或 AWS ECR),并在 CI 流水线中配置缓存推送。
  • -f Dockerfile.train :一个项目通常有多个 Dockerfile,如 Dockerfile.train (含完整训练依赖)、 Dockerfile.serve (精简版,只含推理所需)、 Dockerfile.dev (带 Jupyter 和调试工具)。用 -f 明确指定,避免混淆。

推送前务必做两件事:

  1. 镜像扫描 :用 trivy image --severity CRITICAL my-registry.com/ml-train:1.2.0 扫描高危漏洞。我曾发现一个团队的镜像里 libjpeg 存在 CVE-2023-30395,可能导致远程代码执行,而这个库是 Pillow 依赖的,他们根本不知道。
  2. 大小瘦身 :用 dive my-registry.com/ml-train:1.2.0 查看每层镜像内容。常见冗余包括: apt-get clean 没执行(残留 200MB 包缓存)、 pip install 时没加 --no-cache-dir (pip 缓存占 500MB)、 conda 环境没清理(conda 清理命令比 pip 复杂得多)。一个训练镜像从 4.2GB 压到 1.8GB,拉取时间从 6 分钟降到 2 分钟,对 CI/CD 效率提升巨大。

3. Kubernetes 不是“高级 Docker”,而是 ML 工作负载的智能调度中枢

3.1 从单机 Docker 到集群 Kubernetes:思维模式的根本转变

很多工程师第一次接触 Kubernetes 时,会试图把它当成“分布式 Docker CLI”来用—— kubectl run 启一个 Pod, kubectl exec 进去调试, kubectl logs 看输出。这完全错了。Kubernetes 的核心哲学是 Declarative(声明式) :你告诉它“我要什么状态”,而不是“让我执行什么命令”。你写一个 YAML 文件,声明“我要 3 个副本的训练 Pod,每个要 2 核 CPU、16GB 内存、1 块 A10G 显卡,暴露 8080 端口”,Kubernetes 就会持续监控,如果某个 Pod 崩溃了,它自动拉起一个新的;如果节点宕机,它把 Pod 迁移到其他节点;如果 GPU 显存不足,它拒绝调度并告警。你不需要写脚本去轮询、去重启、去迁移,Kubernetes 的控制器(Controller)会 24 小时自动完成。

这种模式对机器学习有天然适配性:

  • 超参搜索(HPO) :你不再需要写 Bash 脚本循环提交 100 个 docker run ,而是定义一个 Job 对象,Kubernetes 会确保这 100 个任务全部完成,并记录每个任务的退出状态(成功/失败/OOM)。
  • A/B 测试 :线上推理服务要同时灰度两个模型版本,你可以定义两个 Deployment ,各带 5 个副本,再用 Service weight 字段分配 90%/10% 流量,Kubernetes 自动做负载均衡。
  • 弹性伸缩 :晚高峰时用户请求激增, HorizontalPodAutoscaler (HPA) 根据 CPU 使用率或自定义指标(如每秒请求数 QPS)自动把推理 Pod 从 5 个扩到 20 个;凌晨流量低谷,再缩回 5 个,省下 75% 的 GPU 成本。

注意:Kubernetes 不是万能胶。它不解决模型算法问题,也不替代数据治理。它的价值在于把“运维复杂度”封装起来,让你专注在“业务复杂度”上。如果你的模型连单机都跑不稳,先别急着上 K8s。

3.2 核心对象深度解析:Pod、Deployment、Service 在 ML 场景中的真实角色

3.2.1 Pod:ML 工作负载的原子执行单元

Pod 是 Kubernetes 中最小的可部署单元,但它绝不是“一个容器”。一个典型的 ML Pod 往往包含多个协同工作的容器:

apiVersion: v1
kind: Pod
metadata:
  name: ml-train-pod
spec:
  # 关键:为 GPU 训练指定资源请求和限制
  containers:
  - name: trainer
    image: my-registry.com/ml-train:1.2.0
    resources:
      limits:
        nvidia.com/gpu: 1  # 申请 1 块 GPU
        memory: "16Gi"
        cpu: "4"
      requests:
        nvidia.com/gpu: 1
        memory: "16Gi"
        cpu: "4"
    # 挂载数据卷,让容器能访问训练数据
    volumeMounts:
    - name: data-volume
      mountPath: /data
    - name: model-volume
      mountPath: /models
  # Sidecar 容器:负责日志收集和 GPU 监控
  - name: log-collector
    image: fluentd:latest
    volumeMounts:
    - name: data-volume
      mountPath: /data
  # Init 容器:在主容器启动前,预处理数据或下载大模型
  initContainers:
  - name: data-prep
    image: python:3.9-slim
    command: ['sh', '-c']
    args: ["python /prep.py --src gs://my-bucket/raw-data --dst /data"]
    volumeMounts:
    - name: data-volume
      mountPath: /data
  volumes:
  - name: data-volume
    persistentVolumeClaim:
      claimName: ml-data-pvc
  - name: model-volume
    persistentVolumeClaim:
      claimName: ml-models-pvc

这个 Pod 的设计体现了 ML 工作流的典型分层:

  • 主容器(trainer) :执行核心训练逻辑,严格限定 GPU 和内存,避免资源争抢。
  • Sidecar 容器(log-collector) :不干扰主流程,专职收集日志并推送到 ELK 或 Loki。Kubernetes 的 volumeMounts 共享机制,让两个容器能通过 /data 目录交换文件。
  • Init 容器(data-prep) :在主容器启动前运行一次,用于下载 TB 级数据集或预处理。它成功退出后,主容器才启动,确保数据就绪。
3.2.2 Deployment:保障 ML 服务稳定性的“永生契约”

Deployment Pod 的管理者,它确保任何时候都有指定数量的 Pod 副本在运行。对于 ML 推理服务,这是生命线:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ml-serve-deployment
spec:
  # 声明期望的副本数:10 个推理实例
  replicas: 10
  selector:
    matchLabels:
      app: ml-serve
  template:
    metadata:
      labels:
        app: ml-serve
    spec:
      # 关键:节点亲和性,确保只调度到有 GPU 的节点
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: hardware-type
                operator: In
                values: ["gpu-node"]
      # 容忍污点,允许调度到标记为“只跑 GPU 任务”的节点
      tolerations:
      - key: "nvidia.com/gpu"
        operator: "Exists"
        effect: "NoSchedule"
      containers:
      - name: predictor
        image: my-registry.com/ml-serve:1.2.0
        ports:
        - containerPort: 8080
        # 就绪探针:只有返回 200,Kubernetes 才把流量导入此 Pod
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        # 存活探针:连续 3 次失败,Kubernetes 会杀掉并重建 Pod
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 30

readinessProbe livenessProbe 是保障服务 SLA 的双保险:

  • readinessProbe 检查 /healthz 接口,这个接口应返回模型是否加载完成、GPU 是否就绪。如果返回 503,Kubernetes 会把这个 Pod 从 Service 的 Endpoint 列表中移除,新请求不会打过来,但已建立的连接仍保持(优雅下线)。
  • livenessProbe 是兜底机制。如果模型因 OOM 卡死, readinessProbe 可能一直返回 200(进程没死),但 livenessProbe 会检测到内存泄漏或响应超时,强制重启 Pod。
3.2.3 Service:ML 模型的稳定网络门面

Service 是 Kubernetes 的服务发现和负载均衡抽象。它给一组动态变化的 Pod 提供一个固定的 IP 和 DNS 名称。对于 ML,它解决了两个痛点:

  1. 内部服务调用 :你的特征工程服务(Feature Store)需要调用模型推理服务。如果直接用 Pod IP,Pod 重启后 IP 变了,调用就断了。而 Service 的 ClusterIP 是稳定的,DNS 名称如 ml-serve.default.svc.cluster.local 在整个集群内可解析。

  2. 外部流量接入 :线上用户通过 https://api.mycompany.com/predict 访问模型。你需要 Ingress 对象(Kubernetes 的七层路由)将域名映射到 Service ,再由 Service 负载均衡到后端的 10 个推理 Pod。

一个生产级的 ML Service YAML 示例:

apiVersion: v1
kind: Service
metadata:
  name: ml-serve-service
spec:
  # ClusterIP 类型:仅集群内部可访问(推荐用于特征工程服务调用)
  type: ClusterIP
  selector:
    app: ml-serve
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080  # Pod 的容器端口
---
# Ingress:对外暴露的 HTTPS 入口
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ml-serve-ingress
  annotations:
    # 使用 cert-manager 自动申请 Let's Encrypt 证书
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
      - api.mycompany.com
    secretName: ml-serve-tls
  rules:
  - host: api.mycompany.com
    http:
      paths:
      - path: /predict
        pathType: Prefix
        backend:
          service:
            name: ml-serve-service
            port:
              number: 80

3.3 实战:用 Kubernetes 部署一个端到端 ML 推理服务

我们以一个图像分类模型(ResNet50)为例,走一遍从镜像构建到线上服务的全流程。这不是理论,而是我在某电商公司落地的真实步骤。

第一步:构建精简推理镜像

# Dockerfile.serve
FROM pytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtime

# 安装必要依赖,但去掉所有训练相关工具(如 tensorboard)
RUN apt-get update && apt-get install -y \
      libglib2.0-0 libsm6 libxext6 libxrender-dev libglib2.0-0 && \
    rm -rf /var/lib/apt/lists/*

# 复制模型权重和推理代码
COPY model.pth /app/model.pth
COPY serve.py /app/serve.py
COPY requirements.serve.txt /app/requirements.serve.txt

# 安装推理依赖(无 torch, torchvision,它们已在基础镜像中)
RUN pip install --no-cache-dir -r /app/requirements.serve.txt

WORKDIR /app
EXPOSE 8080

# 使用 Gunicorn 启动 Web 服务,支持多 worker 并发
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "4", "serve:app"]

requirements.serve.txt 只包含 fastapi , uvicorn , numpy , Pillow —— 比训练镜像小 60%,启动更快。

第二步:编写 Deployment 和 Service

# deploy-serve.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: resnet50-serve
spec:
  replicas: 5
  selector:
    matchLabels:
      app: resnet50-serve
  template:
    metadata:
      labels:
        app: resnet50-serve
    spec:
      # 关键:GPU 资源请求,但推理通常只需 1/4 块 A10G
      resources:
        limits:
          nvidia.com/gpu: 0.25
          memory: "4Gi"
        requests:
          nvidia.com/gpu: 0.25
          memory: "4Gi"
      containers:
      - name: predictor
        image: my-registry.com/resnet50-serve:1.0.0
        ports:
        - containerPort: 8080
        env:
        - name: MODEL_PATH
          value: "/app/model.pth"
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 20
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: resnet50-serve-service
spec:
  selector:
    app: resnet50-serve
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080

第三步:应用配置并验证

# 1. 构建并推送镜像
docker build -f Dockerfile.serve -t my-registry.com/resnet50-serve:1.0.0 .
docker push my-registry.com/resnet50-serve:1.0.0

# 2. 部署到 Kubernetes 集群
kubectl apply -f deploy-serve.yaml

# 3. 等待 Pod 就绪(观察 READY 列为 1/1)
kubectl get pods -l app=resnet50-serve

# 4. 本地端口转发,测试服务
kubectl port-forward service/resnet50-serve-service 8080:80 &
curl -X POST http://localhost:8080/predict -F "image=@test.jpg"

# 5. 查看日志,确认 GPU 正常工作
kubectl logs -l app=resnet50-serve | grep "GPU memory"

实测结果:5 个 Pod 在 1 台 A10G 服务器上稳定运行,每个 Pod 处理 20 QPS,平均延迟 85ms。当流量突增至 150 QPS 时, HorizontalPodAutoscaler 在 90 秒内将副本数从 5 扩到 12,延迟维持在 90ms 以内。这套架构支撑了该公司双十一大促期间的实时图像审核服务,零故障。

4. 踩过的坑与独家避坑指南:来自一线战场的血泪经验

4.1 Docker 构建与镜像管理的 5 个致命陷阱

陷阱 1:在 Dockerfile 中用 pip install 安装 torch ,导致 CUDA 版本错配
现象:本地 nvidia-smi 显示 CUDA 11.8,但容器内 torch.cuda.is_available() 返回 False
根因: pip install torch 默认安装 CPU 版本,或安装了与基础镜像 CUDA 版本不匹配的 PyTorch。
解决方案:永远用官方镜像(如 pytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtime ),或严格指定 pip install torch==2.1.0+cu118 -f https://download.pytorch.org/whl/torch_stable.html 。在 Dockerfile 开头加一行 RUN python -c "import torch; print(torch.__version__, torch.version.cuda)" 验证。

陷阱 2: COPY . . 放在 RUN pip install 之前,导致每次改代码都重装所有依赖
现象:修改一行 Python 代码, docker build 耗时 12 分钟。
根因:Docker 层缓存失效。 COPY . . 这一层变了,后面所有层(包括 pip install )都无法复用缓存。
解决方案:严格遵守“变更频率低的文件先 COPY”原则。 requirements.txt pip install COPY . . 。如果 requirements.txt 依赖 git 仓库,用 pip install git+https://...@v1.0.0 锁定 commit,避免 pip install -e git+... 导致缓存失效。

陷阱 3:镜像中保留 root 用户,且未设置 USER ,导致安全审计不通过
现象:公司安全扫描报告标红“High Severity: Container runs as root”。
根因:Dockerfile 未声明 USER ,容器默认以 root 运行。
解决方案:在 FROM 后立即创建非 root 用户,并用 USER 切换。注意 chown 权限: COPY --chown=appuser:root . . ,否则用户无权读取文件。

陷阱 4: docker build 时未指定 --no-cache ,导致构建过程随机失败
现象:CI 流水线偶尔失败,错误信息是 pip install 时网络超时或包校验失败。
根因:Docker 默认启用构建缓存,但 CI 环境网络不稳定,缓存层可能损坏。
解决方案:CI 流水线中强制禁用缓存: docker build --no-cache ... 。开发时可用缓存,CI 必须禁用。

陷阱 5:镜像标签用 latest ,导致生产环境无法回滚
现象:上线后发现 bug,想回滚到上一版,但 latest 标签已被覆盖,找不到旧镜像。
解决方案:永远用语义化版本( 1.2.0 )或 Git Commit ID( a1b2c3d )作为标签。 latest 只用于开发测试,生产环境禁止使用。

4.2 Kubernetes 部署与运维的 7 个高频故障排查

故障 1:Pod 一直处于 Pending 状态
排查步骤:

  1. kubectl describe pod <pod-name> 查看 Events:常见原因是 0/3 nodes are available: 3 Insufficient nvidia.com/gpu (GPU 不足)或 0/3 nodes are available: 3 node(s) didn't match pod affinity/anti-affinity (亲和性不满足)。
  2. kubectl get nodes -o wide 确认节点状态和 GPU 数量。
  3. kubectl get nodes -o yaml | grep nvidia.com/gpu 确认 GPU 设备插件是否注册成功。

故障 2:Pod 启动后立即 CrashLoopBackOff
排查步骤:

  1. kubectl logs <pod-name> --previous 查看上次崩溃日志( --previous 关键!)。
  2. 常见原因: OSError: [Errno 12] Cannot allocate memory (内存不足),或 ImportError: libcudnn.so.8: cannot open shared object file (CUDA 库路径错误)。
  3. 进入容器调试: kubectl exec -it <pod-name> -- sh ,手动运行 python -c "import torch" 验证。

故障 3:Service 无法访问, Connection refused
排查步骤:

  1. kubectl get endpoints <service-name> 确认 Endpoint 列表是否为空(为空说明 selector 不匹配或 Pod 未就绪)。
  2. kubectl get pods -l <label> 确认 Pod 的 label 是否与 Service 的 selector 一致。
  3. kubectl exec -it <debug-pod> -- curl http://<service-name>:<port>/healthz 从集群内访问,排除 DNS 问题。

故障 4:GPU 显存显示为 0, nvidia-smi 无输出
根因:NVIDIA Device Plugin 未正确安装,或容器未正确请求 GPU 资源。
解决方案:

  • 确认节点已安装 nvidia-device-plugin DaemonSet: kubectl get ds -n kube-system | grep nvidia
  • Pod 的 resources.limits 必须明确写 nvidia.com/gpu: 1 ,不能只写 limits.memory
  • 基础镜像必须包含 nvidia-container-toolkit ,或使用 nvidia/cuda 官方镜像。

故障 5:训练速度比单机慢 3 倍
根因:数据 I/O 瓶颈。容器内从 NFS 或对象存储读取数据,网络带宽不足。
解决方案:

  • hostPath Local PV 挂载 SSD 本地盘存数据。
  • 在 InitContainer 中预下载数据到 emptyDir ,主容器从本地读。
  • 启用 tf.data torch.utils.data.DataLoader prefetch persistent_workers

故障 6:Ingress 返回 502 Bad Gateway
排查步骤:

  1. kubectl get ingress 确认 ADDRESS 字段有值(Ingress Controller 已就绪)。
  2. kubectl get svc -n ingress-nginx 确认 Ingress Controller Service 类型为 LoadBalancer NodePort
  3. kubectl logs -n ingress-nginx deploy/nginx-ingress-controller 查看 Ingress 日志,常见错误是 upstream connect error or disconnect/reset before headers (后端 Service 不可达)。

故障 7:HPA 无法触发扩容, targetCPUUtilizationPercentage 一直为 <unknown>
根因:Metrics Server 未安装,或未配置 --kubelet-insecure-tls (在私有云环境)。
解决方案:

  • kubectl top nodes kubectl top pods 测试 Metrics Server 是否工作。
  • 如果返回 error: Metrics not available for pod ,重装 Metrics Server: kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.6.3/components.yaml ,并在 Deployment 的 args 中添加 --kubelet-insecure-tls

4.3 MLOps 流水线集成:让 Docker + Kubernetes 真正跑起来

一个完整的 MLOps 流水线,不是手动 docker build kubectl apply ,而是自动化闭环。以下是我在某金融公司落地的 GitOps 流水线:

  1. 代码提交 :工程师向 main 分支提交训练脚本和 Dockerfile.train
  2. CI 触发 :GitHub Actions 启动:
    • 运行单元测试和 lint。
    • docker build -t $REGISTRY/ml-train:$COMMIT_ID -f Dockerfile.train .
    • trivy image --severity CRITICAL $REGISTRY/ml-train:$COMMIT_ID 扫描漏洞。
    • docker push $REGISTRY/ml-train:$COMMIT_ID
  3. CD 触发 :FluxCD(GitOps 工具)监听镜像仓库,发现新镜像后:
    • 更新 k8s/deploy-train.yaml 中的 image: $REGISTRY/ml-train:$COMMIT_ID
    • git commit -m "update train image to $COMMIT_ID" 并 push。
  4. Kubernetes 同步 :FluxCD 检测到 Git 仓库变更,自动 kubectl apply 新的 Deployment。
  5. 训练完成回调 :训练 Job 的 postStart hook 调用 Webhook,触发下一个流水线:构建推理镜像、部署 ml-serve

这个流水线的关键设计:

  • 镜像不可变 $COMMIT_ID 作为镜像标签,确保每次部署的镜像是唯一且可追溯的。
  • Git 作为唯一真相源 :所有 Kubernetes 配置(YAML)都存 Git,Kubernetes 状态必须与 Git 一致,避免手动 kubectl edit
  • 失败自动回滚 :如果新镜像部署后 HPA 检测到错误率上升,Prometheus 告警触发 FluxCD 回滚到上一个 Git commit。

最后分享一个小技巧:在 Dockerfile 中加入构建时的时间戳,方便追踪镜像来源:

ARG BUILD_DATE
LABEL org.opencontainers.image.created=$BUILD_DATE
# 构建时传入:docker build --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') ...

这样 docker inspect 就能看到镜像构建时间,排查问题时一目了然。

我在实际使用中发现,最大的

更多推荐