GPU-on-K8s-RKE2落地方案-POD算力多网卡

GPU-on-K8s-RKE2落地方案-POD算力多网卡

Deng YongJie's blog 2 2026-02-28

title: “GPU on Kubernetes 落地:RKE2、NVIDIA Runtime、Multus 与可登录训练 Pod”
tags:

  • Kubernetes
  • RKE2
  • GPU
  • NVIDIA
  • Multus
  • Whereabouts
  • MLOps

GPU on Kubernetes 落地:RKE2、NVIDIA Runtime、Multus 与可登录训练 Pod

这篇文章解决一个很常见、也很容易被低估的问题:

机器上 nvidia-smi 明明能看到 GPU,为什么 Kubernetes 还是调度不了 GPU Pod?

答案通常不在“显卡有没有插好”这一层,而在这条链路里:

Host NVIDIA Driver
  -> NVIDIA Container Toolkit
  -> containerd 的 nvidia runtime handler
  -> Kubernetes RuntimeClass
  -> NVIDIA device plugin 初始化 NVML
  -> kubelet 上报 nvidia.com/gpu
  -> 业务 Pod request/limit nvidia.com/gpu

只要其中一环断开,Kubernetes 调度器就可能“看不见” GPU。

本文基于 RKE2 + containerd + NVIDIA Container Toolkit + NVIDIA device plugin + Multus + Whereabouts 的组合,整理一套可复制的 GPU on K8s 落地方案。

gpu-on-k8s-architecture

1. 方案目标

本次目标不是单纯跑通一个 nvidia-smi 测试 Pod,而是要交付一个可用于训练、调试和后续扩展的基础形态:

  • GPU 节点加入 RKE2 集群。
  • Kubernetes 能正确识别并调度 nvidia.com/gpu
  • GPU Pod 显式使用 NVIDIA RuntimeClass。
  • 提供一个可 SSH 登录的 GPU 训练 Pod,便于交互式调试。
  • 保留默认 Pod 网络,同时通过 Multus 添加二层网卡。
  • 支持固定 IP 或地址池自动分配。
  • 支持后续扩展为管理网、算力网多网卡模型。
  • 明确容器模型下“系统盘”和“数据盘”的边界。

公开博客里的示例采用这些占位符:

项目 示例
GPU 节点 <gpu-worker-01>
GPU 节点内网 IP <node-ip>
GPU 节点主机二层网卡 <master-nic>,例如 enp5s0bond0
管理二层网段 10.20.30.0/24
Pod 固定二层 IP 10.20.30.240/24
Whereabouts 地址池 10.20.30.241-10.20.30.250/24
SSH NodePort 30022,仅作示例
镜像仓库 按你的环境替换为公网仓库或内网镜像仓库

2. 为什么“节点有 GPU”不等于“K8s 能调度 GPU”

很多 GPU 接入问题都卡在一个认知误区:

nvidia-smi 在宿主机能跑通,Kubernetes 就应该自动知道这台机器有 GPU。

实际上不是。

Kubernetes 不会天然扫描宿主机上的所有 GPU 并把它们变成可调度资源。它依赖 device plugin 框架把厂商设备注册给 kubelet。

NVIDIA GPU 的常见资源名是:

nvidia.com/gpu

只有当 NVIDIA device plugin 在节点上成功启动、成功初始化 NVML、成功向 kubelet 注册后,节点状态里才会出现类似:

status:
  capacity:
    nvidia.com/gpu: "1"
  allocatable:
    nvidia.com/gpu: "1"

如果这里没有 nvidia.com/gpu,调度器就不会把这个节点当作 GPU 节点。

gpu-resource-registration-chain

3. 本次问题的典型现象

落地过程中看到的典型现象是:

  • GPU 节点已经打了标签,例如 nvidia.com/gpu.present=true
  • 宿主机执行 nvidia-smi 正常。
  • 业务 Pod 申请了 nvidia.com/gpu: 1 后一直 Pending。
  • kubectl describe node 看不到 nvidia.com/gpu capacity。
  • NVIDIA device plugin 日志出现类似错误:
Failed to initialize NVML: could not load NVML library.

这类错误的关键不是“device plugin 本身坏了”,而是它的容器没有通过 NVIDIA runtime 启动。

device plugin 容器如果走默认 runc,容器内可能拿不到 NVIDIA 驱动库和 NVML 能力。它就无法发现 GPU,也无法向 kubelet 注册 nvidia.com/gpu

最终表现就是:节点上有卡,但 Kubernetes 看不到卡。

4. 节点侧基线:驱动、Toolkit、containerd

GPU 节点首先要满足宿主机层面的条件:

  1. NVIDIA 驱动安装完成。
  2. 宿主机 nvidia-smi 正常。
  3. NVIDIA Container Toolkit 安装完成。
  4. containerd 已配置 nvidia runtime handler。
  5. RKE2 agent 重启后加载 containerd 配置。

Ubuntu/Debian 系节点安装 Toolkit 的官方流程大致是:

sudo apt-get update
sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg2

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
  | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
  | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
  | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

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

然后配置 containerd:

sudo nvidia-ctk runtime configure --runtime=containerd

普通 containerd 环境通常会生成或修改:

/etc/containerd/conf.d/99-nvidia.toml
/etc/containerd/config.toml

但 RKE2 使用的是内置 containerd。实际配置路径通常在:

/var/lib/rancher/rke2/agent/etc/containerd/config.toml

所以在 RKE2 场景里要特别确认:RKE2 最终生成的 containerd 配置里确实包含 nvidia runtime handler,并且 handler 指向 nvidia-container-runtime

可在 GPU 节点上检查:

sudo grep -n "nvidia" /var/lib/rancher/rke2/agent/etc/containerd/config.toml || true
which nvidia-container-runtime
nvidia-smi

如果修改过 runtime 配置,重启 RKE2 agent:

sudo systemctl restart rke2-agent

5. RuntimeClass:把 nvidia runtime 暴露给 Pod

Kubernetes 的 RuntimeClass 用来让 Pod 选择不同的容器运行时配置。

如果你没有把 NVIDIA runtime 设置成节点默认 runtime,就应该显式创建一个 RuntimeClass/nvidia

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: nvidia
handler: nvidia

验证:

kubectl get runtimeclass
kubectl get runtimeclass nvidia -o yaml

业务 Pod 使用时写:

spec:
  runtimeClassName: nvidia

这一步非常关键。

仅仅申请 nvidia.com/gpu: 1,并不必然代表容器会用 NVIDIA runtime 启动。资源分配和运行时选择是两件事。

在本文方案里,我们让两类 Pod 都显式使用 runtimeClassName: nvidia

  • NVIDIA device plugin DaemonSet。
  • 实际使用 GPU 的业务 Pod。

6. 修复关键:device plugin 自己也要走 NVIDIA runtime

很多人只关注业务 Pod 的 runtimeClassName,但这次真正卡住的是 device plugin 自己。

device plugin 的职责是发现 GPU、监控 GPU 健康状态、向 kubelet 注册资源。

如果 device plugin 这个容器没有通过 NVIDIA runtime 启动,它可能连 NVML 都加载不到。于是 kubelet 就收不到 GPU 注册信息。

下面是一个脱敏后的 DaemonSet 示例。

生产环境建议通过 Helm 或 GitOps 固定版本。示例中的镜像请按你的环境替换为官方镜像、企业镜像仓库或可信镜像代理。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: nvidia-device-plugin-daemonset
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: nvidia-device-plugin-ds
  template:
    metadata:
      labels:
        app: nvidia-device-plugin-ds
    spec:
      runtimeClassName: nvidia
      priorityClassName: system-node-critical
      nodeSelector:
        nvidia.com/gpu.present: "true"
      tolerations:
        - key: CriticalAddonsOnly
          operator: Exists
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      containers:
        - name: nvidia-device-plugin-ctr
          image: nvcr.io/nvidia/k8s-device-plugin:<固定版本>
          imagePullPolicy: IfNotPresent
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: device-plugin
              mountPath: /var/lib/kubelet/device-plugins
      volumes:
        - name: device-plugin
          hostPath:
            path: /var/lib/kubelet/device-plugins

几个点要讲清楚:

  • runtimeClassName: nvidia:让 plugin 容器能访问 NVIDIA runtime 注入的能力。
  • nodeSelector:只跑在标记过的 GPU 节点上。
  • priorityClassName:把它作为关键节点组件处理。
  • /var/lib/kubelet/device-plugins:device plugin 和 kubelet 通过这个目录下的 socket 通信。

给 GPU 节点打标签:

kubectl label node <gpu-worker-01> nvidia.com/gpu.present=true

应用并等待:

kubectl apply -f nvidia-plugin.yaml
kubectl -n kube-system rollout status ds/nvidia-device-plugin-daemonset --timeout=600s

7. GPU 注册验证

先看 device plugin Pod:

kubectl -n kube-system get pod -o wide | grep nvidia-device-plugin
kubectl -n kube-system logs -l app=nvidia-device-plugin-ds --tail=200

期望看到类似:

Loading NVML
Registered device plugin with Kubelet

再看节点资源:

kubectl get node <gpu-worker-01> \
  -o jsonpath='capacity={.status.capacity.nvidia\.com/gpu} allocatable={.status.allocatable.nvidia\.com/gpu}{"\n"}'

期望:

capacity=1 allocatable=1

如果这里仍为空,排查顺序建议是:

  1. 宿主机 nvidia-smi 是否正常。
  2. RKE2 containerd 配置里是否有 nvidia runtime。
  3. RuntimeClass/nvidia 是否存在,handler 是否为 nvidia
  4. device plugin Pod 是否真的使用了 runtimeClassName: nvidia
  5. device plugin 日志是否还有 NVML 错误。

8. 最小 GPU 测试 Pod

节点上报 GPU 后,用一个最小 Pod 验证调度链路:

apiVersion: v1
kind: Pod
metadata:
  name: gpu-smoke-test
spec:
  restartPolicy: Never
  runtimeClassName: nvidia
  nodeSelector:
    kubernetes.io/hostname: <gpu-worker-01>
  tolerations:
    - key: nvidia.com/gpu
      operator: Exists
      effect: NoSchedule
  containers:
    - name: cuda
      image: nvidia/cuda:12.4.1-base-ubuntu22.04
      command: ["bash", "-lc", "nvidia-smi && echo GPU_OK"]
      resources:
        limits:
          nvidia.com/gpu: 1

执行:

kubectl apply -f gpu-smoke-test.yaml
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/gpu-smoke-test --timeout=300s
kubectl logs gpu-smoke-test
kubectl delete pod gpu-smoke-test

日志里看到 nvidia-smi 输出和 GPU_OK,说明 GPU 从节点注册到业务容器访问这条链路已经跑通。

9. 可 SSH 登录的 GPU 训练 Pod

很多训练场景早期需要交互式调试。最直接的交付形态是一个 StatefulSet:

  • runtimeClassName: nvidia
  • 申请 nvidia.com/gpu: 1
  • 用 Headless Service 保留稳定 DNS。
  • 用 NodePort 或后续 LoadBalancer 暴露 SSH。
  • 用 Secret 注入 SSH 公钥或临时密码。
  • 通过 startupProbe 避免首次安装工具时被 kubelet 误杀。

正式生产不建议直接 root + 密码 + NodePort。下面示例使用 SSH key,并禁用密码登录。

先创建 SSH 公钥 Secret:

kubectl create namespace gpu-train

kubectl -n gpu-train create secret generic gpu-ssh-authorized-keys \
  --from-file=authorized_keys=./authorized_keys

StatefulSet 示例:

apiVersion: v1
kind: Service
metadata:
  name: gpu-train-headless
  namespace: gpu-train
spec:
  clusterIP: None
  selector:
    app: gpu-train
  ports:
    - name: ssh
      port: 22
      targetPort: 22
---
apiVersion: v1
kind: Service
metadata:
  name: gpu-train-ssh
  namespace: gpu-train
spec:
  type: NodePort
  selector:
    app: gpu-train
  ports:
    - name: ssh
      port: 22
      targetPort: 22
      nodePort: 30022
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: gpu-train
  namespace: gpu-train
spec:
  serviceName: gpu-train-headless
  replicas: 1
  selector:
    matchLabels:
      app: gpu-train
  template:
    metadata:
      labels:
        app: gpu-train
    spec:
      runtimeClassName: nvidia
      nodeSelector:
        kubernetes.io/hostname: <gpu-worker-01>
      terminationGracePeriodSeconds: 10
      containers:
        - name: main
          image: nvidia/cuda:12.4.1-base-ubuntu22.04
          imagePullPolicy: IfNotPresent
          command: ["bash", "-lc"]
          args:
            - |
              set -euo pipefail
              export DEBIAN_FRONTEND=noninteractive

              apt-get -o Acquire::ForceIPv4=true -o Acquire::Retries=5 update
              apt-get -o Acquire::ForceIPv4=true -o Acquire::Retries=5 install -y --no-install-recommends \
                ca-certificates \
                openssh-server \
                iproute2 \
                net-tools \
                iputils-ping \
                curl \
                wget \
                git \
                vim-tiny \
                less \
                procps

              mkdir -p /run/sshd /root/.ssh
              cp /ssh/authorized_keys /root/.ssh/authorized_keys
              chmod 700 /root/.ssh
              chmod 600 /root/.ssh/authorized_keys

              sed -ri 's/^#?PermitRootLogin[[:space:]]+.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
              sed -ri 's/^#?PasswordAuthentication[[:space:]]+.*/PasswordAuthentication no/' /etc/ssh/sshd_config
              sed -ri 's/^#?PubkeyAuthentication[[:space:]]+.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config

              exec /usr/sbin/sshd -D -e
          volumeMounts:
            - name: ssh-authorized-keys
              mountPath: /ssh
              readOnly: true
          ports:
            - name: ssh
              containerPort: 22
          startupProbe:
            tcpSocket:
              port: 22
            periodSeconds: 5
            failureThreshold: 360
          readinessProbe:
            tcpSocket:
              port: 22
            periodSeconds: 5
          livenessProbe:
            tcpSocket:
              port: 22
            periodSeconds: 10
          resources:
            limits:
              nvidia.com/gpu: 1
      volumes:
        - name: ssh-authorized-keys
          secret:
            secretName: gpu-ssh-authorized-keys

部署并验证:

kubectl apply -f gpu-ssh.yaml
kubectl -n gpu-train rollout status statefulset/gpu-train --timeout=1200s
kubectl -n gpu-train get pod -o wide
kubectl -n gpu-train logs -f gpu-train-0

登录:

ssh root@<node-ip> -p 30022

进入容器后验证:

nvidia-smi -L
ip -br a

这个形态适合早期调试和小规模训练环境。规模化训练平台建议进一步封装为镜像、模板、队列和租户权限模型。

10. 为什么需要 Multus:默认 Pod IP 不适合所有训练流量

Kubernetes 默认只给 Pod 创建一张网卡,通常叫 eth0

这张网卡由主 CNI 负责,例如 Cilium、Calico、Flannel 等。它适合服务发现、集群内通信和普通业务访问。

但 GPU 训练场景常常还有额外需求:

  • Pod 需要一个和物理网络同网段的固定 IP。
  • 运维人员希望直接 SSH 到训练环境。
  • 分布式训练希望流量走独立算力网卡。
  • RoCE、IB 等网络可能需要绕过默认 Overlay。
  • 多租户场景希望管理网、数据网、训练网分离。

这时可以用 Multus 给 Pod 追加第二张、第三张网卡。

multus-ipvlan-whereabouts-flow

本文的网络模型是:

eth0: 主 CNI 创建,保留 Kubernetes 默认 Pod 网络
net1: Multus 创建,ipvlan L2,接入管理二层网络
net2: 可选,Multus 创建,接入算力网络

11. RKE2 安装 Multus + Whereabouts

RKE2 官方支持通过内置 HelmChart 方式安装 Multus,并可启用 rke2-whereabouts

示例:

apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
  name: rke2-multus
  namespace: kube-system
spec:
  repo: https://rke2-charts.rancher.io
  chart: rke2-multus
  targetNamespace: kube-system
  valuesContent: |
    rke2-whereabouts:
      enabled: true

应用:

kubectl apply -f rke2-multus.yaml
kubectl -n kube-system rollout status ds/rke2-multus --timeout=600s
kubectl -n kube-system rollout status ds/rke2-multus-rke2-whereabouts --timeout=600s

验证 NAD CRD:

kubectl get crd network-attachment-definitions.k8s.cni.cncf.io

验证 CNI 二进制:

ls -l /opt/cni/bin | egrep 'multus|whereabouts|ipvlan|macvlan|static|host-local' || true

注意一个容易踩的点:

rke2-whereabouts DaemonSet 起来,不代表 Whereabouts 所需的所有 CRD 一定已存在。使用 ipam.type: whereabouts 前,应确认:

kubectl get crd | grep whereabouts.cni.cncf.io

至少应看到类似:

ippools.whereabouts.cni.cncf.io
overlappingrangeipreservations.whereabouts.cni.cncf.io

如果缺少 CRD,Pod 可能卡在 ContainerCreating,事件里出现找不到 ippools.whereabouts.cni.cncf.io 的错误。

12. 创建 NetworkAttachmentDefinition

NAD 是 Multus 读取的二网卡配置对象。

下面提供两种模式:

  • ipvlan-static:固定 IP。
  • ipvlan-pool:Whereabouts 从地址池自动分配 IP。

<master-nic> 替换成 GPU 节点上真实存在、已经连入对应二层网络的网卡名。

apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: ipvlan-static
  namespace: gpu-train
spec:
  config: |
    {
      "cniVersion": "0.4.0",
      "name": "ipvlan-static",
      "type": "ipvlan",
      "master": "<master-nic>",
      "mode": "l2",
      "ipam": {
        "type": "static",
        "addresses": [
          { "address": "10.20.30.240/24" }
        ]
      }
    }
---
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: ipvlan-pool
  namespace: gpu-train
spec:
  config: |
    {
      "cniVersion": "0.4.0",
      "name": "ipvlan-pool",
      "type": "ipvlan",
      "master": "<master-nic>",
      "mode": "l2",
      "ipam": {
        "type": "whereabouts",
        "range": "10.20.30.241-10.20.30.250/24"
      }
    }

应用:

kubectl apply -f gpu-train-nads.yaml
kubectl -n gpu-train get net-attach-def

让 Pod 挂载固定 IP 二网卡,在 Pod 模板 annotation 里加入:

metadata:
  annotations:
    k8s.v1.cni.cncf.io/networks: |
      [
        { "name": "ipvlan-static", "namespace": "gpu-train", "interface": "net1" }
      ]

如果使用地址池:

metadata:
  annotations:
    k8s.v1.cni.cncf.io/networks: |
      [
        { "name": "ipvlan-pool", "namespace": "gpu-train", "interface": "net1" }
      ]

重建 Pod:

kubectl apply -f gpu-ssh.yaml
kubectl -n gpu-train delete pod gpu-train-0
kubectl -n gpu-train wait --for=condition=Ready pod/gpu-train-0 --timeout=1200s

验证事件:

kubectl -n gpu-train describe pod gpu-train-0 | grep -A3 -E 'AddedInterface|multus'

期望看到类似:

Add net1 [10.20.30.240/24] from gpu-train/ipvlan-static

进入容器:

ip -br a
ip route

同网段机器可以尝试:

ssh root@10.20.30.240

如果你已经通过 net1 固定 IP 登录,NodePort 可以逐步退场,改成更清晰的二层访问模型。

13. net1 state UNKNOWN 不是一定坏了

使用 ipvlan 时,Pod 内可能看到:

net1@if2  UNKNOWN  10.20.30.240/24

这里的 UNKNOWN 是 Linux 接口 operstate,不等价于“网络不可用”。

很多虚拟接口没有物理网卡那样明确的 carrier 状态,所以内核无法准确显示 UPDOWN,会显示 UNKNOWN

更重要的是看 flags:

ip -d link show net1

如果能看到 UPLOWER_UP,并且同网段访问正常,就不需要因为 UNKNOWN 误判故障。

ipvlan L2 模式下,Pod 的 net1 复用宿主机 master 网卡收发二层流量。外部机器访问 10.20.30.240 时,流量会经宿主机物理网卡进入,再转到 Pod 网络命名空间。

需要注意:这类二层旁路网卡不一定受主 CNI 的 NetworkPolicy 约束。安全策略应在节点防火墙、上游交换网络、ACL 或独立 VLAN 上补齐。

14. 扩展:管理网与算力网分离

更接近生产的 GPU 训练网络通常不是一张网卡打天下,而是至少分成:

  • 管理网:SSH、运维、控制面访问。
  • 存储网:数据集、模型权重、检查点读写。
  • 算力网:分布式训练通信,例如 NCCL/Gloo。

如果宿主机有第二张物理网卡或 bond,可继续创建一个算力网 NAD:

apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: ipvlan-compute-pool
  namespace: gpu-train
spec:
  config: |
    {
      "cniVersion": "0.4.0",
      "name": "ipvlan-compute-pool",
      "type": "ipvlan",
      "master": "<compute-nic>",
      "mode": "l2",
      "ipam": {
        "type": "whereabouts",
        "range": "192.168.100.10-192.168.100.250/24"
      }
    }

Pod 同时挂两张二层网卡:

metadata:
  annotations:
    k8s.v1.cni.cncf.io/networks: |
      [
        { "name": "ipvlan-static", "namespace": "gpu-train", "interface": "net1" },
        { "name": "ipvlan-compute-pool", "namespace": "gpu-train", "interface": "net2" }
      ]

训练框架里显式绑定算力网卡,比改默认路由更稳妥。

NCCL 示例:

export NCCL_SOCKET_IFNAME=net2

Gloo 示例:

export GLOO_SOCKET_IFNAME=net2

不要随意把 Pod 默认路由改到算力网,否则可能影响访问 Kubernetes Service、DNS、镜像仓库和控制面。

15. IP 冲突:二层网络里最难排的坑

Multus + ipvlan/macvlan 把 Pod 接到真实二层网络后,IP 冲突会比普通 Overlay 网络更麻烦。

如果使用 static IPAM,两个 Pod 写了同一个 IP,常见表现是:

  • SSH 一会能连,一会超时。
  • 同一个 IP 的 ARP 记录来回变化。
  • 分布式训练随机断开、卡住、吞吐抖动。
  • 故障看起来像训练框架问题,但根因在二层地址冲突。

排查:

ip neigh show | grep '<pod-l2-ip>' || true
arp -an | grep '<pod-l2-ip>' || true
kubectl -n gpu-train describe pod <pod-name> | grep -A5 -E 'multus|whereabouts|Failed'

建议:

  • 多副本优先使用 Whereabouts 地址池。
  • 必须固定 IP 时,用模板生成每个副本独立 NAD。
  • 地址池避开网关、交换机、宿主机、BMC、已有服务器地址。
  • 更严谨的生产环境应使用独立 VLAN 或独立物理网络承载训练网。

16. 持久化:容器里的“系统盘”和“数据盘”

有些用户会提出一个很像虚拟机的需求:

我希望训练 Pod 有系统盘和数据盘。重装系统时清空系统盘,但保留数据盘。

在容器模型里,要先把语义讲清楚。

容器的 rootfs 来自镜像,默认是临时写层。你在容器里 apt-get install/usr/bin 的内容,不会因为 StatefulSet 重建而长期稳定保留。

更推荐的做法是:

  • 把基础工具、CUDA、SSH、Python、conda 等做进自定义镜像。
  • 用 PVC 挂载 /data 保存数据集、输出、模型权重。
  • 可选再用一个 PVC 挂载 /workspace/system 保存代码、环境产物和用户态工具。

StatefulSet 增加两块盘:

spec:
  volumeClaimTemplates:
    - metadata:
        name: workspace
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 50Gi
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 500Gi
  template:
    spec:
      containers:
        - name: main
          volumeMounts:
            - name: workspace
              mountPath: /workspace
            - name: data
              mountPath: /data

StatefulSet 会自动创建类似:

workspace-gpu-train-0
data-gpu-train-0

“重装系统但保留数据盘”的容器语义可以这样做:

kubectl -n gpu-train delete pod gpu-train-0
kubectl -n gpu-train delete pvc workspace-gpu-train-0

# 不删除 data-gpu-train-0
kubectl -n gpu-train get pvc data-gpu-train-0

但如果你真正需要 VM 级别的系统盘、快照、回滚和重装语义,应考虑 KubeVirt、Harvester 或传统虚拟化,而不是把容器强行当 VM 使用。

17. 安全基线

GPU 训练环境常常被临时开放成“能登录就行”,但公开或半公开网络中这样风险很高。

建议至少做到:

  • 不把 SSH 密码写进 YAML 。
  • 优先使用 SSH key,禁用密码登录。
  • NodePort 只允许内网、堡垒机或 VPN 访问。
  • 二层 net1/net2 所在 VLAN 做 ACL。
  • 不把 GPU Pod 暴露到办公网或公网。
  • 镜像使用固定 tag 或 digest。
  • 用独立 namespace、ResourceQuota 和 LimitRange 控制租户资源。
  • 训练数据盘和模型盘配置权限、审计和备份。
  • 对可登录 Pod 做生命周期管理,避免长期无人维护。

如果只是临时调试,可以接受 NodePort。

如果是生产平台,应优先走:

用户 -> VPN/堡垒机 -> 平台审计 -> Kubernetes API/Job -> 受控训练环境