3、Docker与容器基础:镜像构建、容器运行、资源限制

大模型从训练到推理部署,本质上是一系列复杂软件环境(CUDA、cuDNN、Python 依赖、推理框架如 vLLM/TensorRT-LLM、模型权重文件)的打包与交付过程。Docker 容器技术通过将应用及其依赖封装为标准化、可移植的镜像,解决了“在我机器上能跑”的经典难题。在 Kubernetes 集群中,Pod 是最小调度单元,而 Pod 内的容器则直接由容器运行时(如 containerd)管理。因此,理解 Docker 的镜像构建、容器生命周期与资源隔离机制,是掌握 Kubernetes 节点资源调度、自动扩缩容和故障隔离的基石。本章将聚焦于这三个核心维度,为后续在集群中部署大模型推理服务奠定坚实的操作与原理基础。

Docker 镜像构建 → 容器运行 → 资源限制 核心链路 ① 镜像构建 (Build) • Dockerfile 定义层 • 基础镜像 (CUDA/cuDNN) • 依赖安装 (pip/conda) • 模型权重复制 • 多阶段构建优化 ② 容器运行 (Run) • 镜像实例化 (可写层) • 环境变量/入口命令 • 端口映射 (host↔container) • 数据卷挂载 (模型路径) • 日志与监控输出 ③ 资源限制 (Limit) • CPU 配额 (cgroup cpu) • 内存上限 (cgroup memory) • GPU 显存控制 (nvidia) • OOM 保护与隔离 • 磁盘 IO 限制 Kubernetes 集群中的容器管理 • Pod 是调度单元,内含 1~N 个容器(共享网络命名空间) • kubelet 通过 CRI (containerd) 拉取镜像并启动容器 • requests/limits 字段直接映射到容器 cgroup 配置 • 镜像仓库 (Harbor/ACR) 存储与分发大模型镜像 核心:镜像不可变、容器轻量隔离、资源配额保障多租户稳定性

上图展示了 Docker 三大核心环节与 Kubernetes 的衔接关系。镜像构建阶段产生不可变的交付物;容器运行阶段将镜像实例化为隔离的进程环境;资源限制阶段则通过 Linux 内核的 cgroup 机制为容器划定 CPU、内存、GPU 等硬性边界。在 Kubernetes 中,这三个环节被抽象为 Image、Container 和 Resources 配置,由 kubelet 统一协调。

3.1 镜像构建:从 Dockerfile 到可交付的推理环境

3.1.1 核心原理:分层文件系统与联合挂载

Docker 镜像由多个只读层(Layer)叠加而成,每一层对应 Dockerfile 中的一条指令。这种设计使得镜像可以复用公共层(如 CUDA 基础镜像),大幅节省存储空间和网络传输时间。当容器启动时,Docker 在镜像层之上添加一个可写层(Container Layer),所有运行时修改(如写入日志、生成临时文件)都发生在此层,容器删除后该层随之消失。

对于大模型镜像,层设计尤为关键。一个典型的推理镜像包含以下层次:

  • 基础层:nvidia/cuda:12.1.1-cudnn8-devel-ubuntu22.04(约 4.5GB)
  • 系统依赖层:gcc、python3.10、curl 等(约 300MB)
  • Python 包层:torch、transformers、vllm 等(约 2.5GB)
  • 应用代码层:推理服务脚本、配置文件(约 50MB)
  • 模型权重层:Llama-2-7B 量化模型(约 4GB,通常通过数据卷挂载而非打入镜像)
经验建议:模型权重文件体积大且更新频繁,强烈建议使用 Kubernetes 的 PersistentVolume(PV)或对象存储挂载,而非打入镜像。镜像应保持“代码+依赖”的轻量形态,权重与镜像解耦,便于版本更新和灰度发布。

3.1.2 操作流程:编写高效 Dockerfile 的五个关键步骤

  1. 选择合适的基础镜像:优先使用官方 nvidia/cuda 镜像,并明确 tag(如 12.1.1-cudnn8-devel-ubuntu22.04),避免使用 latest。
  2. 利用构建缓存:将变化频率低的指令(如 apt 安装、pip 安装)放在 Dockerfile 前部,变化频繁的(如代码 COPY)放在后部。
  3. 多阶段构建:在 builder 阶段编译依赖,在 runtime 阶段仅保留运行所需文件,显著减小镜像体积。
  4. 设置非 root 用户:创建专用用户运行推理服务,增强安全性。
  5. 配置健康检查:使用 HEALTHCHECK 指令定义容器存活探针,便于 K8s 感知服务状态。
# 多阶段构建示例:大模型推理镜像
# 阶段1:编译依赖
FROM nvidia/cuda:12.1.1-cudnn8-devel-ubuntu22.04 AS builder
RUN apt-get update && apt-get install -y python3.10 python3-pip git
RUN pip3 install torch==2.1.0 vllm==0.3.1 --index-url https://download.pytorch.org/whl/cu121

# 阶段2:运行时镜像
FROM nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04
COPY --from=builder /usr/local/lib/python3.10/dist-packages /usr/local/lib/python3.10/dist-packages
COPY --from=builder /usr/local/bin/vllm /usr/local/bin/vllm
RUN useradd -m -u 1000 inference && mkdir /models && chown inference:inference /models
USER inference
WORKDIR /app
COPY --chown=inference:inference ./server.py /app/server.py
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -f http://localhost:8000/health || exit 1
ENTRYPOINT ["python3", "/app/server.py"]

3.1.3 案例拆解:构建一个 7B 模型推理镜像的优化过程

背景与约束:某团队需要为 Llama-2-7B 模型构建推理镜像,目标部署到 4 节点 GPU 集群。初始镜像体积高达 18GB,导致每次发布需传输 5 分钟以上,且镜像仓库存储压力大。

分析判断:通过 docker history 分析,发现主要体积来自:① 基础镜像中的 devel 组件(编译器、头文件)占 3GB;② pip 安装时保留了缓存和 .pyc 文件;③ 模型权重被误打入镜像(8GB)。

优化操作:

  • 将基础镜像从 devel 切换为 runtime 版本,减少 2.8GB。
  • 在 pip 安装后执行 rm -rf ~/.cache/pip 清理缓存。
  • 将模型权重从镜像中移除,改用 NFS 挂载至 /models 目录。
  • 采用多阶段构建,仅复制必要的 Python 包。

结果对比:优化后镜像体积降至 6.2GB,传输时间缩短至 1 分钟以内,镜像构建时间从 15 分钟降至 6 分钟。同时,由于权重独立管理,模型更新无需重新构建镜像。

风险提示:切勿在生产环境中使用 docker build --no-cache 强制重建所有层,这会显著增加 CI 时间。应合理利用缓存,仅在依赖变更时失效相关层。

3.2 容器运行:从镜像到隔离的推理进程

3.2.1 核心原理:命名空间与进程视图隔离

容器本质上是宿主机上的普通进程,通过 Linux 命名空间(Namespace)实现隔离。Docker 默认启用以下命名空间:

命名空间隔离内容大模型场景意义
PID进程编号容器内 PID 1 为推理服务主进程,便于信号管理
Network网络栈(IP、端口)容器独立 IP,端口映射避免冲突
Mount文件系统挂载点模型权重卷挂载,不影响宿主机目录
UTS主机名容器独立 hostname,便于日志区分
IPC进程间通信防止跨容器共享内存干扰
User用户 ID容器内 root 映射为宿主机非特权用户

当执行 docker run 时,Docker 守护进程会调用 runc 创建容器,runc 负责配置命名空间和 cgroup,然后启动容器进程。在 Kubernetes 中,kubelet 通过 CRI 调用 containerd,containerd 再调用 runc,流程一致。

3.2.2 操作流程:启动大模型推理容器的关键参数

# 启动一个 vLLM 推理容器示例
docker run -d --name llama2-7b \
  --gpus all \
  -p 8000:8000 \
  -v /data/models:/models:ro \
  -e MODEL_NAME=/models/llama2-7b \
  -e MAX_LENGTH=4096 \
  --shm-size=8g \
  --ulimit memlock=-1 \
  --ulimit stack=67108864 \
  --restart=unless-stopped \
  registry.example.com/inference/llama2:1.2.0

参数说明:

  • --gpus all:暴露所有 GPU 给容器,需安装 NVIDIA Container Toolkit。
  • -v /data/models:/models:ro:只读挂载模型目录,防止容器内误修改。
  • --shm-size=8g:增大 /dev/shm 大小,避免 PyTorch DataLoader 多进程共享内存不足。
  • --ulimit memlock=-1:允许锁定内存,防止 GPU 显存交换到系统内存。
  • --restart=unless-stopped:容器异常退出时自动重启,但 K8s 中由 Pod 重启策略接管。

3.2.3 实战场景:容器启动失败的排障路径

初始现象:执行 docker run 后容器立即退出,docker logs 显示 CUDA error: out of memory。

分析过程:

  1. 检查 GPU 显存占用:nvidia-smi 发现已有其他容器占用 80% 显存。
  2. 确认容器请求的显存:vLLM 默认预分配全部可用显存,导致冲突。
  3. 解决方案:设置 --gpus '"device=0"' 指定 GPU,并配置 vLLM 的 --gpu-memory-utilization=0.6 限制显存使用率。

复盘经验:在 GPU 共享场景下,必须显式指定 GPU 编号和显存上限,不能依赖默认行为。同时建议在容器启动脚本中增加显存预检逻辑。

3.3 资源限制:cgroup 与 GPU 隔离

3.3.1 核心原理:cgroup 控制组

Linux cgroup(Control Groups)是内核提供的资源隔离机制,Docker 和 Kubernetes 均依赖它来限制容器的 CPU、内存、磁盘 IO 等资源。cgroup v2 是当前主流版本,在 Kubernetes 1.25+ 中默认启用。

对于大模型推理,资源限制的核心目标:

  • CPU 限制:防止推理进程占满所有 CPU 核,影响其他 Pod。
  • 内存限制:防止内存泄漏导致宿主机 OOM,触发内核杀进程。
  • GPU 显存限制:通过 NVIDIA 容器运行时将显存隔离,但需注意显存是独占资源,无法超卖。

3.3.2 操作流程:在 Docker 和 K8s 中配置资源限制

Docker 命令行方式:

docker run -d --name inference \
  --cpus=4 \
  --memory=16g \
  --memory-swap=16g \
  --pids-limit=512 \
  --device=/dev/nvidia0 \
  --gpus '"device=0"' \
  inference-image:latest

Kubernetes Pod 声明方式:

apiVersion: v1
kind: Pod
metadata:
  name: llama2-inference
spec:
  containers:
  - name: inference
    image: registry.example.com/inference/llama2:1.2.0
    resources:
      requests:
        cpu: 4
        memory: 16Gi
        nvidia.com/gpu: 1
      limits:
        cpu: 8
        memory: 20Gi
        nvidia.com/gpu: 1

在 K8s 中,requests 用于调度决策(保证资源),limits 用于运行时限制(硬上限)。当容器内存使用超过 limits 时,内核会触发 OOM Kill;当 CPU 超过 limits 时,会被限流(throttled)。

3.3.3 案例拆解:OOM 引发的推理服务中断与修复

背景:某生产环境部署了 8 个推理 Pod,每个 Pod 设置 memory limit 为 16Gi。运行一周后,部分 Pod 频繁重启,日志显示 Killed 信号。

关键指标分析:

  • 通过 kubectl top pod 发现内存使用率持续在 15.5Gi~16.2Gi 波动。
  • 查看 cgroup 事件:cat /sys/fs/cgroup/kubepods/podxxx/memory.events 显示 oom_kill 计数增加。
  • 进一步分析:vLLM 的 KV Cache 动态增长,加上 Python 内存碎片化,导致实际内存超出预期。

解决步骤:

  1. 将 memory limit 提升至 24Gi,并同步调整节点内存预留。
  2. 在 vLLM 启动参数中设置 --max-num-seqs=32 限制并发序列数,控制 KV Cache 大小。
  3. 启用 Python 内存优化:设置 PYTHONMALLOC=malloc 减少内存碎片。

结果:调整后 Pod 稳定运行 30 天无 OOM,内存使用率稳定在 18Gi 左右。

总结复盘:资源限制不是简单的数值设定,需要结合应用的内存模型(如 KV Cache 动态分配)和实际监控数据动态调整。建议为推理服务预留 20%~30% 的内存余量,并设置合理的 requests 与 limits 比例(通常 1:1.25 或 1:1.5)。

3.4 常见误区与生产实践建议

误区后果正确做法
镜像中打入模型权重镜像体积巨大,发布慢,版本管理混乱权重挂载到 PV 或对象存储
容器内以 root 运行安全漏洞可导致宿主机被攻陷创建非 root 用户,设置 USER 指令
不设置内存 limit内存泄漏拖垮整个节点始终设置 limits,并监控实际使用
GPU 显存不限制多个容器争抢显存导致 OOM使用 NVIDIA 工具限制显存或分卡
忽略 /dev/shm 大小PyTorch DataLoader 报共享内存不足设置 --shm-size=8g 或更大

在生产环境中,建议将 Docker 镜像构建纳入 CI/CD 流水线,使用 Trivy 或 Clair 进行漏洞扫描;容器运行时启用 seccomp 和 AppArmor 安全配置;资源限制配置通过 K8s 的 LimitRange 和 ResourceQuota 进行全局管控,避免每个团队自行定义导致集群资源碎片化。

掌握 Docker 的镜像构建、容器运行和资源限制三大基础能力,是理解 Kubernetes 调度器如何为 Pod 分配节点资源、如何通过 HPA(Horizontal Pod Autoscaler)基于 CPU/内存指标自动扩缩容的前提。下一阶段,我们将把这些容器化应用编排到 Kubernetes 集群中,实现大模型服务的高可用与弹性伸缩。