从零到一:在Kubernetes 1.25集群中深度集成NVIDIA GPU实战指南

最近在帮一个做AI推理服务的团队搭建生产环境,他们最头疼的就是如何让Kubernetes集群稳定、高效地调用GPU资源。Kubernetes 1.25版本带来了一些底层运行时接口的变化,这让原本就有些复杂的GPU支持配置,又多了几个需要特别注意的“坑”。如果你也正在为如何在K8s里用好GPU而折腾,特别是面对较新的1.25版本,这篇文章或许能帮你省下不少排查时间。我会从一个实际部署者的视角,带你走一遍从驱动检查、容器运行时配置,到设备插件部署和问题诊断的完整流程,重点分享那些官方文档里不会写的细节和“踩坑”经验。

1. 环境准备与前置条件深度核查

在开始动手配置之前,花点时间把基础环境摸清楚,能避免后续80%的莫名其妙的问题。很多人一上来就照着教程敲命令,结果卡在某个环节,回头发现是驱动版本不匹配或者容器运行时没配好。

1.1 节点层面的硬核检查

首先,你需要确保你的GPU节点(无论是物理机还是云上的GPU实例)已经具备了最基础的运行条件。这不仅仅是安装一个驱动那么简单。

1. 验证NVIDIA驱动安装与版本 通过SSH登录到你的节点,运行最基础的检查命令:

nvidia-smi

这个命令的输出信息量很大,你需要重点关注几个地方:

  • Driver Version:驱动版本。Kubernetes的NVIDIA设备插件对驱动有最低要求,通常需要>=384.81。但为了稳定性和兼容更新的GPU架构,我强烈建议使用450.x或更高版本。
  • CUDA Version:这里显示的是驱动内建的CUDA兼容性版本,并非系统安装的CUDA Toolkit版本。对于K8s设备插件来说,这个信息更多是参考。
  • GPU列表与状态:确认所有预期的GPU都被正确识别,并且状态是OK

如果nvidia-smi命令报错或找不到,那第一步就是安装官方驱动。不同Linux发行版的安装方式差异很大。以Ubuntu 22.04为例,你可以通过apt安装:

# 添加官方GPU驱动仓库
sudo add-apt-repository ppa:graphics-drivers/ppa -y
sudo apt update
# 安装推荐版本的驱动(通常会是最新的稳定版)
sudo apt install nvidia-driver-535 -y

安装完成后,务必重启节点

2. 安装与验证NVIDIA Container Toolkit 这是连接Docker/Containerd和GPU驱动的桥梁,至关重要。它的旧称是nvidia-docker2。安装步骤同样因发行版而异。

# 以Ubuntu/Debian为例,配置仓库并安装
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/libnvidia-container/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | sudo tee /etc/apt/sources.list.d/libnvidia-container.list

sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit

安装完成后,关键一步是将其配置为你的容器运行时的默认底层运行时(low-level runtime)。很多人后面发现GPU在容器中不可用,问题就出在这一步没做对。

1.2 容器运行时配置:Containerd与Docker的差异

Kubernetes 1.25版本,社区对容器运行时的支持重心进一步向Containerd倾斜。你需要根据集群实际使用的运行时进行配置。

对于使用Containerd的集群(当前主流推荐): Containerd的配置位于/etc/containerd/config.toml。你需要编辑此文件,在[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]部分添加nvidia运行时。

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
  ...
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia]
  privileged_without_host_devices = false
  runtime_engine = ""
  runtime_root = ""
  runtime_type = "io.containerd.runc.v2"
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options]
    BinaryName = "/usr/bin/nvidia-container-runtime"

然后,你还需要在[plugins."io.containerd.grpc.v1.cri"]部分,将default_runtime_name设置为"runc"(这是默认值),而对于需要GPU的Pod,K8s设备插件会通过注解等方式告知kubelet使用nvidia运行时。更常见的做法是,不设置默认运行时,而是让设备插件来管理。修改配置后,重启containerd服务:

sudo systemctl restart containerd

对于仍使用Docker作为运行时的集群: 配置Docker的daemon.json文件(通常位于/etc/docker/daemon.json):

{
  "runtimes": {
    "nvidia": {
      "path": "/usr/bin/nvidia-container-runtime",
      "runtimeArgs": []
    }
  }
}

注意:早期教程可能会让你设置"default-runtime": "nvidia"在Kubernetes环境下,这通常是不必要且可能导致问题的。因为Kubernetes希望由设备插件来决定哪些Pod需要使用nvidia运行时,将所有容器都默认用nvidia运行时可能会引发兼容性问题。除非你确定节点上所有容器都需要GPU,否则不要设置默认运行时。

验证配置是否生效:

# 对于Docker
docker info | grep -i runtime
# 应该能看到 `Runtimes: nvidia runc`

# 对于Containerd,可以通过运行一个测试容器来验证
sudo ctr image pull docker.io/nvidia/cuda:12.5.0-base
sudo ctr run --rm --gpus 0 docker.io/nvidia/cuda:12.5.0-base test nvidia-smi

如果测试容器能成功输出nvidia-smi的信息,说明容器运行时这一层的配置基本正确。

2. 部署NVIDIA设备插件(k8s-device-plugin)

当节点层面的环境就绪后,下一步就是在Kubernetes集群中部署nvidia-device-plugin。它的核心作用是将节点的GPU资源“广告”给Kubernetes API Server,让调度器知道哪些节点有GPU、有多少,并负责在Pod创建时执行GPU设备的注入和清理工作。

2.1 理解Device Plugin的工作机制

在动手部署YAML之前,花两分钟理解它的工作原理,对排查问题有奇效。Device Plugin并不是一个魔法黑盒,它其实是一个遵循K8s Device Plugin接口的gRPC服务。它作为DaemonSet运行在每个GPU节点上,主要干三件事:

  1. 向kubelet注册自己:告诉kubelet:“嗨,我负责管理nvidia.com/gpu这种资源。”
  2. 上报设备列表与健康状态:持续监控本节点的GPU,将数量、型号(可选)和健康状态汇报给kubelet。
  3. 分配与清理设备:当kubelet要创建一个申请了GPU的Pod时,Device Plugin会收到“分配设备”的请求,它负责执行一些挂载设备、库文件等预处理操作;Pod删除时,则执行清理。

2.2 选择合适的部署清单

NVIDIA在GitHub上提供了官方的部署YAML。但直接使用kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.1/nvidia-device-plugin.yml可能会遇到镜像拉取问题(尤其是国内环境)。更稳妥的做法是,先查看当前可用的最新稳定版本,并考虑使用一些经过验证的配置。

下面是一个针对Kubernetes 1.25优化过的、更健壮的DaemonSet配置示例。我增加了一些在生产环境中很有用的参数:

# nvidia-device-plugin-daemonset-optimized.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: nvidia-device-plugin-daemonset
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: nvidia-device-plugin-ds
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
  template:
    metadata:
      labels:
        name: nvidia-device-plugin-ds
    spec:
      priorityClassName: "system-node-critical"
      tolerations:
      - key: nvidia.com/gpu
        operator: Exists
        effect: NoSchedule
      - key: "CriticalAddonsOnly"
        operator: "Exists"
      - operator: "Exists"
        effect: "NoExecute"
      - operator: "Exists"
        effect: "NoSchedule"
      nodeSelector:
        # 假设你给GPU节点打了这个标签
        hardware-type: nvidia-gpu
      containers:
      - image: nvcr.io/nvidia/k8s-device-plugin:v0.14.1
        # 建议使用确定的版本标签,而非latest
        name: nvidia-device-plugin-ctr
        args:
          - --fail-on-init-error=true
          - --mig-strategy=single
          # 启用兼容模式,对某些旧版本K8s或特定场景有帮助
          - --pass-device-specs=false
          - --device-list-strategy=envvar
        resources:
          requests:
            memory: "100Mi"
            cpu: "100m"
          limits:
            memory: "300Mi"
            cpu: "500m"
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop: ["ALL"]
        volumeMounts:
        - name: device-plugin
          mountPath: /var/lib/kubelet/device-plugins
        - name: nvidia-driver-root
          mountPath: /usr/local/nvidia
          readOnly: true
      volumes:
      - name: device-plugin
        hostPath:
          path: /var/lib/kubelet/device-plugins
          type: DirectoryOrCreate
      - name: nvidia-driver-root
        hostPath:
          path: /usr/local/nvidia
          type: Directory

这个配置做了几处关键增强:

  • priorityClassName: system-node-critical:确保这个DaemonSet被当作关键插件,在资源紧张时优先被调度。
  • 更宽松的tolerations:除了GPU污点,还容忍了常见的节点污点,确保插件能在各种节点上运行。
  • nodeSelector:通过节点选择器,只将插件部署到打了hardware-type: nvidia-gpu标签的节点上,避免在没有GPU的节点上运行无用Pod。
  • 明确的启动参数:如--fail-on-init-error=true让初始化错误快速失败,便于发现问题。
  • 资源限制:为容器设置了合理的资源请求与限制,避免其占用过多节点资源。

使用以下命令部署:

kubectl apply -f nvidia-device-plugin-daemonset-optimized.yaml

然后检查部署状态:

kubectl get daemonset -n kube-system -l name=nvidia-device-plugin-ds
kubectl get pods -n kube-system -l name=nvidia-device-plugin-ds -o wide

查看Pod日志,确认没有报错:

kubectl logs -n kube-system <nvidia-device-plugin-pod-name>

2.3 验证集群GPU资源发现

部署成功后,最令人兴奋的一步就是验证Kubernetes是否真的“看见”了GPU。

  1. 检查节点资源容量与可分配量

    kubectl describe node <your-gpu-node-name>
    

    在输出的CapacityAllocatable部分,你应该能看到类似这样的条目:

    Capacity:
      cpu:                64
      memory:             251Gi
      nvidia.com/gpu:     2
      ...
    Allocatable:
      cpu:                63500m
      memory:             250Gi
      nvidia.com/gpu:     2
      ...
    

    这明确表示该节点有2块可分配的GPU。

  2. 检查节点标签: 设备插件通常会给节点打上标签,标明GPU厂商、数量等信息。

    kubectl get node <your-gpu-node-name> --show-labels | grep nvidia
    

    你可能会看到nvidia.com/gpu.count=2nvidia.com/gpu.product=Tesla-V100-SXM2-32GB等标签。这些标签可以被用来做更精细的Pod调度。

3. 运行GPU工作负载与实战测试

资源注册成功,接下来就是真刀真枪地跑一个GPU应用了。这里我们不止步于简单的测试,而是探讨几种不同场景的部署方式。

3.1 基础测试Pod:快速验证

这是一个最简化的Pod定义,用于快速验证GPU是否能在Pod内被访问和使用。

# gpu-test-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: cuda-vectoradd-test
spec:
  restartPolicy: OnFailure
  containers:
  - name: cuda-vectoradd
    image: nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0
    # 这是一个NVIDIA官方提供的极简CUDA示例,只做向量加法,运行完即退出
    resources:
      limits:
        nvidia.com/gpu: 1 # 申请1个GPU
  tolerations:
  - key: nvidia.com/gpu
    operator: Exists
    effect: NoSchedule

创建并查看日志:

kubectl apply -f gpu-test-pod.yaml
kubectl logs cuda-vectoradd-test

如果看到类似[Vector addition of 50000 elements]Test PASSED的输出,恭喜你,最基本的GPU调用链路已经打通。

3.2 部署真实的AI推理服务:以TensorFlow Serving为例

让我们看一个更贴近生产的例子:部署一个TensorFlow Serving服务,它加载一个预训练的模型,并提供gRPC/REST API进行推理。

# tf-serving-gpu-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tensorflow-serving-resnet
spec:
  replicas: 1 # GPU资源宝贵,通常每个Pod独占GPU,副本数受限于GPU数量
  selector:
    matchLabels:
      app: tf-serving-gpu
  template:
    metadata:
      labels:
        app: tf-serving-gpu
    spec:
      nodeSelector:
        # 确保调度到有GPU的节点
        hardware-type: nvidia-gpu
      tolerations:
      - key: nvidia.com/gpu
        operator: Exists
        effect: NoSchedule
      containers:
      - name: serving
        image: tensorflow/serving:latest-gpu
        # 使用官方GPU镜像,已包含CUDA和cuDNN
        args:
          - "--model_name=resnet"
          - "--model_base_path=/models/resnet"
          - "--port=8500"
          - "--rest_api_port=8501"
        ports:
        - containerPort: 8500
          name: grpc
        - containerPort: 8501
          name: restapi
        resources:
          limits:
            nvidia.com/gpu: 1
            memory: "4Gi"
            cpu: "2"
          requests:
            memory: "2Gi"
            cpu: "1"
        volumeMounts:
        - name: model-storage
          mountPath: /models/resnet
          readOnly: true
        livenessProbe:
          httpGet:
            path: /v1/models/resnet
            port: restapi
          initialDelaySeconds: 60
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /v1/models/resnet
            port: restapi
          initialDelaySeconds: 30
          periodSeconds: 5
      volumes:
      - name: model-storage
        # 这里可以是PersistentVolumeClaim、HostPath或ConfigMap,根据实际情况配置
        hostPath:
          path: /data/models/resnet-v2
---
apiVersion: v1
kind: Service
metadata:
  name: tf-serving-service
spec:
  type: NodePort
  selector:
    app: tf-serving-gpu
  ports:
  - port: 8501
    targetPort: 8501
    name: rest
    nodePort: 30085
  - port: 8500
    targetPort: 8500
    name: grpc

这个部署清单体现了几个生产级考量:

  • 资源限制与请求:明确设置了GPU、CPU和内存的限制,这对于调度和稳定性至关重要。
  • 就绪和存活探针:确保服务真正准备好才接收流量,并在异常时重启。
  • 节点选择器:将Pod精准调度到GPU节点。
  • 服务暴露:通过Service对外提供访问入口。

3.3 多GPU与GPU共享策略

如果你的节点有多块GPU,或者想让一个GPU被多个低负载的Pod共享(通过时间片或MIG技术),配置会有所不同。

申请多个GPU: 在Pod的resources.limits中直接指定数量即可。

resources:
  limits:
    nvidia.com/gpu: 4

Kubernetes调度器会确保Pod被调度到至少有4块可用GPU的节点上。

使用NVIDIA MIG(Multi-Instance GPU): 对于A100、H100等支持MIG的GPU,可以将一块物理GPU划分为多个更小的、隔离的实例。设备插件通过--mig-strategy参数支持MIG。在Pod中申请资源时,资源名称会发生变化:

resources:
  limits:
    # 申请一个MIG实例,例如A100 40GB可以划分成7个5GB的实例
    nvidia.com/mig-1g.5gb: 1 # 具体资源名称取决于MIG配置

MIG的配置和管理是一个相对高级的话题,需要在节点上预先通过nvidia-smi工具配置好MIG实例。

4. 深度问题排查与性能调优指南

即使按照步骤一步步来,也难免会遇到问题。这一章我们集中火力,解决那些最常见的“坑”。

4.1 典型问题排查清单

当GPU Pod无法启动或无法访问GPU时,可以按照以下清单自上而下排查:

问题现象可能原因排查命令与步骤
Pod状态为Pending1. 节点没有GPU资源或资源不足。
2. Pod的tolerations不匹配节点的taints
3. nodeSelector不匹配。
1. kubectl describe pod <pod-name> 查看事件,通常是调度失败。
2. kubectl describe node <node-name> 检查节点资源、污点和标签。
Pod状态为CrashLoopBackOffError1. 容器内nvidia-smi命令失败。
2. CUDA库缺失或版本不匹配。
3. 镜像本身有问题。
1. kubectl logs <pod-name> 查看容器日志。
2. kubectl exec -it <pod-name> -- nvidia-smi 尝试在容器内执行命令。
3. 检查基础镜像是否包含正确的CUDA版本。
Pod运行但报告找不到GPU1. 容器运行时(Docker/Containerd)未正确配置nvidia运行时。
2. 设备插件DaemonSet Pod未在该节点运行或运行异常。
3. kubelet与设备插件通信失败。
1. 在节点上运行 docker info | grep -i runtime 或检查containerd配置。
2. kubectl get pods -n kube-system -o wide | grep device-plugin 确认Pod状态。
3. 查看设备插件Pod日志:kubectl logs -n kube-system <device-plugin-pod>
4. 检查节点/var/log/kubelet.log(或journalctl -u kubelet)有无相关错误。
nvidia.com/gpu未出现在节点资源中1. 设备插件未成功向kubelet注册。
2. 节点驱动未安装或nvidia-smi在节点上失败。
3. 设备插件启动参数有误。
1. kubectl describe node 确认无GPU资源。
2. 在节点上直接运行nvidia-smi,确认驱动正常。
3. 检查设备插件DaemonSet的YAML,特别是argsvolumeMounts路径是否正确。

4.2 容器运行时配置错误详解

这是最常见的问题之一。症状是:节点nvidia-smi正常,设备插件Pod运行也正常,节点也显示了GPU资源,但用户Pod就是无法使用GPU,在容器内运行nvidia-smi会报错Failed to initialize NVML: Unknown Error

根本原因:Kubelet在创建容器时,没有使用nvidia-container-runtime。对于Docker,需要确保/etc/docker/daemon.json中正确配置了runtimes,并且Kubernetes知道如何使用它。对于Containerd,配置更为关键。

针对Containerd的深度检查

  1. 确认config.toml[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia]部分配置正确,且BinaryName路径/usr/bin/nvidia-container-runtime存在。
  2. 关键一步:检查kubelet的参数。kubelet需要知道使用哪个容器运行时。在1.24及以上版本,使用Containerd时,kubelet的--container-runtime-endpoint参数通常是unix:///run/containerd/containerd.sock。确保你的kubelet配置正确。
  3. 一个高级技巧是检查容器创建时的底层调用。你可以查看问题Pod所在节点上,kubelet的日志,搜索CreateContainer相关的错误。或者,手动使用crictl工具测试:
    # 在节点上,使用crictl(需安装)模拟创建一个使用nvidia运行时的容器
    # 首先创建一个sandbox
    SANDBOX_ID=$(sudo crictl runp sandbox.json)
    # 然后用一个包含GPU请求的容器配置创建容器
    CONTAINER_ID=$(sudo crictl create $SANDBOX_ID container-gpu.json pod-config.json)
    
    如果手动创建也失败,那问题肯定出在容器运行时配置或nvidia-container-toolkit安装上。

4.3 性能监控与优化建议

配置成功只是第一步,让GPU在K8s中发挥最大效能需要持续监控和调优。

1. 监控GPU利用率 光有Prometheus和Grafana还不够,你需要集成NVIDIA DCGM Exporter或利用Kubernetes Metrics Server结合节点nvidia-smi的定期采集。

  • 部署DCGM Exporter:NVIDIA提供了一个官方的DaemonSet,能暴露丰富的GPU指标(利用率、显存、温度、功耗等)。
    helm repo add gpu-helm-charts https://nvidia.github.io/dcgm-exporter/helm-charts
    helm install dcgm-exporter gpu-helm-charts/dcgm-exporter
    
  • 简易脚本监控:对于小规模集群,可以写一个简单的DaemonSet,定期在节点上执行nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu --format=csv -l 5,并将日志输出到集中日志系统。

2. 优化Pod调度

  • 使用节点亲和性/反亲和性:将需要GPU通信(NVLink)的Pod调度到同一个节点的不同GPU上,或者避免将多个高负载GPU Pod挤到同一节点。
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - high-load-ai
              topologyKey: kubernetes.io/hostname
    
  • 设置合适的资源请求:除了nvidia.com/gpu,务必为CPU和内存设置合理的requestslimits。GPU任务通常也需要可观的CPU进行数据预处理。

3. 镜像优化 尽量使用NVIDIA官方优化过的基础镜像(如nvcr.io/nvidia/tensorflow:xx.xx-py3),它们已经集成了匹配的CUDA、cuDNN和TensorFlow版本。避免在Dockerfile里从头编译CUDA库,这能极大缩短镜像构建时间和减少镜像层数。

最后,关于集群升级,尤其是Kubernetes版本升级时,需要特别注意:先滚动更新节点,确保每个节点上的nvidia-container-toolkit、驱动和设备插件版本都兼容新版本的kubelet和容器运行时接口(CRI),再进行控制平面升级。在测试环境中充分验证升级流程,是保证生产环境GPU工作负载平稳运行的不二法门。

更多推荐