容器基础:Docker与Kubernetes基础、容器运行时、容器资源管理

在深入GPU资源分配与隔离之前,必须先厘清容器技术栈的分层结构。很多GPU相关的问题,根源并不在GPU驱动或CUDA版本,而在于对容器运行时和编排器的资源管理机制理解错位。本节将梳理Docker、Kubernetes与容器运行时(Container Runtime)之间的关系,并重点剖析容器资源管理的底层逻辑——这是后续理解GPU隔离(如NVIDIA Container Toolkit、Device Plugin)的基石。

3.1 核心分层:Docker、Kubernetes与容器运行时

许多初学者将Docker等同于容器,实际上Docker只是容器生态中的一环。现代容器体系可拆分为三个逻辑层:

  • 编排层(Orchestration):Kubernetes负责调度、服务发现、扩缩容,它并不直接启动容器,而是通过CRI(Container Runtime Interface)调用下层运行时。
  • 容器管理层(Container Management):Docker Engine(dockerd)承担镜像构建、存储、网络管理等开发者体验功能,并通过containerd与底层内核交互。
  • 运行时层(Runtime):真正与Linux内核交互、创建cgroups和namespace的组件。典型如runc(OCI标准实现)、containerd内置的runC,以及支持GPU的nvidia-container-runtime。
关键认知:Kubernetes从1.24版本起彻底移除对Docker的直接支持(Dockershim),转而通过containerd或CRI-O直接对接运行时。这意味着GPU环境下,你配置的“运行时”路径(如/usr/bin/nvidia-container-runtime)必须被containerd或CRI-O正确识别,而非仅配置Docker。
编排层 Kubernetes (kubelet) — 调度GPU节点,通过CRI下发Pod创建请求 CRI (gRPC) 容器管理层 Docker Engine (dockerd) — 镜像/网络/卷管理 运行时层 containerd / CRI-O → runc / nvidia-container-runtime gRPC OCI (runc) Linux内核资源隔离 cgroups (CPU/内存/设备/GPU) + namespace (PID/Net/Mount/UTS) 设备cgroup (device cgroup) 控制GPU访问权限;cgroups v2 统一管理

3.2 容器资源管理:cgroups与namespace

容器资源管理的底层机制是Linux内核的cgroups(控制组)与namespace(命名空间)。namespace实现“隔离”——让容器看到独立的进程、网络、文件系统视图;cgroups实现“限制”——约束容器能使用的CPU、内存、设备等资源量。

对于GPU资源,cgroups扮演双重角色:

  1. 设备白名单:通过devices控制器(cgroups v1)或device cgroup(cgroups v2),控制容器能否访问/dev/nvidia0、/dev/nvidiactl等设备节点。未加入白名单的GPU设备对容器不可见。
  2. 资源配额:虽然GPU显存和算力不由标准cgroups CPU/内存控制器直接管理,但NVIDIA Container Toolkit通过LD_PRELOAD注入libnvidia-container,在用户态拦截CUDA调用,实现显存与算力限制(如--gpus '"device=0,1"')。
常见误区:不要试图用--cpus或--memory限制GPU算力或显存。CPU cgroups与GPU资源完全无关。GPU隔离必须依赖NVIDIA提供的运行时钩子(hook)或Kubernetes Device Plugin机制。

3.3 方法流程:从Docker到Kubernetes的GPU资源传递

理解GPU资源如何在容器栈中传递,是配置和排障的关键。以下是一个标准流程:

层级 组件 GPU相关操作
用户请求 kubectl / Docker CLI 声明nvidia.com/gpu: 1(K8s)或--gpus all(Docker)
调度决策 Kubernetes Scheduler 根据节点上的Device Plugin上报的可用GPU数量进行调度
资源上报 NVIDIA Device Plugin (DaemonSet) 监听/dev/nvidia*,向kubelet注册GPU资源,并监控健康状态
运行时注入 containerd + nvidia-container-runtime 根据环境变量NVIDIA_VISIBLE_DEVICES注入设备节点和驱动库
内核隔离 cgroups + namespace 设备cgroup放行指定GPU设备,Mount namespace隔离驱动路径

在Docker环境中,GPU透传的典型命令为:

# 暴露所有GPU
docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi

# 暴露指定GPU(按UUID或索引)
docker run --rm --gpus '"device=0,2"' \
  -e NVIDIA_VISIBLE_DEVICES=0,2 \
  nvidia/cuda:12.0-base nvidia-smi

在Kubernetes中,GPU资源通过Device Plugin机制声明式管理。Pod的YAML示例如下:

apiVersion: v1
kind: Pod
metadata:
  name: gpu-pod
spec:
  containers:
  - name: cuda-container
    image: nvidia/cuda:12.0-base
    command: ["nvidia-smi"]
    resources:
      limits:
        nvidia.com/gpu: 1   # 请求1块GPU

3.4 案例:多容器共享GPU与显存隔离

假设一台节点拥有2块NVIDIA A100(各40GB显存)。你需要运行两个推理服务:

  • 服务A:需要16GB显存,延迟敏感
  • 服务B:需要8GB显存,可容忍抖动

若直接使用--gpus all启动两个容器,两者都会看到全部2块GPU,且显存分配由CUDA运行时动态竞争——可能导致服务A因显存不足而OOM。正确做法是显式指定GPU编号,并利用NVIDIA_MPS(Multi-Process Service)或CUDA的CUDA_VISIBLE_DEVICES实现物理隔离:

# 服务A:独占GPU0
docker run -d --name service-a \
  --gpus '"device=0"' \
  -e CUDA_VISIBLE_DEVICES=0 \
  -e NVIDIA_MPS=on \
  your-image:latest

# 服务B:独占GPU1
docker run -d --name service-b \
  --gpus '"device=1"' \
  -e CUDA_VISIBLE_DEVICES=1 \
  your-image:latest

若需在同一块GPU上隔离显存(如A和B共享GPU0),则需借助NVIDIA MPS或CUDA的显存池控制(如CUDA_MAX_CONNECTIONS),但标准Docker/K8s并不提供细粒度显存隔离——这正是GPU资源管理相比CPU更复杂的核心原因。

3.5 误区与复盘:GPU资源管理的常见陷阱

陷阱1:忽略cgroups v2对设备插拔的影响
在cgroups v2下,设备访问控制不再通过devices文件,而是通过eBPF程序实现。若使用旧版NVIDIA Container Toolkit(<1.13),可能导致GPU设备无法在容器内访问。升级toolkit并重启containerd即可解决。
陷阱2:在Kubernetes中同时配置Docker GPU和containerd GPU
若节点上同时存在Docker和containerd,且两者都配置了nvidia-container-runtime,但kubelet使用containerd,则Docker的GPU配置不会生效。必须确保/etc/containerd/config.toml中正确设置runtime为nvidia-container-runtime。
复盘建议:当GPU容器启动失败时,按以下顺序排查:
1. 宿主机执行nvidia-smi确认驱动正常;
2. 检查docker run --gpus all是否能成功;
3. 若Docker成功但K8s失败,检查Device Plugin日志与kubelet的--node-labels;
4. 最后查看容器内ls -l /dev/nvidia*确认设备节点是否注入。