MoreRSS

site iconLixueduan | 李学端修改

博客名:指月小筑。专注云原生,Go,坚持分享最佳实践、经验干货。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Lixueduan | 李学端的 RSS 预览

⚡ AI集群通信革命:GB200 MNNVL通过Kubernetes DRA实现跨节点800Gbps通信

2026-01-07 04:00:00

nvidia-dra-gpu.jpeg

NVIDIA GB200 NVL72 正在将 AI 基础设施推向新的极限,使大规模语言模型训练和低延迟推理工作负载成为可能。随着 Kubernetes 在部署和扩展这些工作负载中的核心作用日益增强,快速演进的 AI 工作负载、基础设施需求和新硬件架构为 Kubernetes 编排和资源管理带来了新的挑战。

在本文中,我们将深入探讨如何通过 Kubernetes DRA (Dynamic Resource Allocation) 和 NVIDIA DRA Driver 在 GB200 平台上启用 Multi-Node NVLink (MNNVL),实现跨节点的 GPU 到 GPU 高带宽通信。

核心概念

Kubernetes DRA (Dynamic Resource Allocation) 简介

DRA (Dynamic Resource Allocation,动态资源分配) 是 Kubernetes v1.30 引入的革命性功能,用于解决传统 Device Plugin 框架在处理复杂异构硬件时的局限性。

DRA 最初始的版本(KEP-3063),于 1.26 版本引入,因可用性问题,在 1.32 版本被撤回 当前的 DRA 是第二个版本(KEP-4381) 于 1.30 引入。

为什么需要 DRA?

传统 Device Plugin 将硬件资源抽象为简单整数计数器,无法表达:

  • 设备特定参数(如 GPU 显存、计算能力、拓扑连接)

    • 无论什么 GPU,都展示为 nvidia.com/gpu
  • 资源共享需求(设备无法在容器间共享)

  • 硬件拓扑关系(NVLink 连接、PCIe 亲和性)

DRA 核心设计

DRA 遵循 Kubernetes 声明式原则,将硬件资源特性作为一等公民纳入 API:

  • ResourceClass:定义硬件资源类型和特性(类似 StorageClass)
  • ResourceClaim:工作负载的声明式资源请求(类似 PVC)
  • ResourceClaimTemplate:创建多个相似 ResourceClaim 的模板
  • DRA Driver:由硬件厂商实现的资源分配逻辑

DRA 的优势

  • 声明式管理:通过标准 API 声明复杂资源需求
  • 参数化配置:支持设备特定参数和资源共享
  • 拓扑感知:原生支持硬件拓扑关系的调度决策
  • 跨节点分配:支持分布式资源分配场景

GB200 MNNVL (Multi-Node NVLink) 的价值

GB200 NVL72 通过引入 Multi-Node NVLink (MNNVL) 技术,将单机 GPU 性能限制扩展到整个机架层面,为分布式 AI 工作负载带来革命性改进。

传统的单节点 DGX 系统受限于单机物理限制,MNNVL 改变了这一局面:

  • 全 NVLink 带宽跨节点通信:通过 NVIDIA NVLink Switch 实现全 NVLink 带宽通信
  • 无缝扩展:将整个机架转换为统一的 GPU 架构
  • 性能倍增:实现超快分布式训练和推理

ComputeDomains:连接底层硬件与 Kubernetes

IMEX (Internode Memory Exchange)

NVIDIA Internode Memory Exchange Service (IMEX) 是 GPU 驱动层面的软件,允许 GPU 跨节点通信。IMEX 对每个单独的 GPU 内存导出/导入操作进行细粒度访问控制,并在称为 IMEX 域的节点组中运行。

ComputeDomains 核心概念

作为 NVIDIA GPUs 的 DRA 驱动程序的一部分提供的 ComputeDomains,将底层 GPU 构造(NVIDIA NVLink 和 NVIDIA IMEX)与现代 Kubernetes 原生调度概念(动态资源分配,简称 DRA)连接起来,为在现代 GPU 硬件上运行分布式多节点工作负载提供所需的基础支持。

如果没有 ComputeDomains,多节点 NVLink 设置将不得不手动定义并固定到位,这限制了 Kubernetes 旨在提供的灵活性,并以牺牲安全隔离、故障隔离和成本效率为代价。

ComputeDomains 通过以下方式工作:

  • 动态创建 IMEX 域:根据工作负载调度自动形成 IMEX 域
  • 安全隔离:每个工作负载获得专用的隔离通信环境
  • 自动清理:工作负载完成后自动释放资源
  • 拓扑感知:理解并优化 GPU 连接关系

通过 ComputeDomains,运行分布式训练或跨复杂 NVLink 连接 GPU 架构的推理变得像部署标准 Kubernetes 工作负载一样简单。

环境部署

版本要求

软件信息:

  • Kubernetes:1.32 及以上,推荐 1.34
  • Containerd:支持 DRA 的版本 1.7.x,推荐 1.7.29
  • GPU Operator:25.3.x 及以上,推荐 25.10.0
  • DRA Driver:推荐部署最新的 25.8.0
  • NVIDIA GPU Driver:需要 565 或更新版本
    • 如果使用 DRA Driver 25.8.0,则需要驱动版本 >= 570.158.1

硬件信息:

  • 系统:GB200 NVL72(一柜系统中的 2 节点子集)
  • 节点数量:2 个节点
  • 每个节点配置
    • GPU:4 个 GB200 GPU(本次测试共 8 个 GPU)
    • GPU 显存:每个 GPU 192GB(189471 MiB)
    • CPU:2 个 Grace CPU

GPU 基本信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
root@GB200-POD2-F06-Node05:~/lixd/nccl-demo# nvidia-smi
Wed Dec 10 10:24:44 2025
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 580.95.05 Driver Version: 580.95.05 CUDA Version: 13.0 |
+-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 NVIDIA GB200 On | 00000008:01:00.0 Off | 0 |
| N/A 37C P0 169W / 1200W | 0MiB / 189471MiB | 0% Default |
| | | Disabled |
+-----------------------------------------+------------------------+----------------------+
| 1 NVIDIA GB200 On | 00000009:01:00.0 Off | 0 |
| N/A 37C P0 157W / 1200W | 0MiB / 189471MiB | 0% Default |
| | | Disabled |
+-----------------------------------------+------------------------+----------------------+
| 2 NVIDIA GB200 On | 00000018:01:00.0 Off | 0 |
| N/A 38C P0 158W / 1200W | 0MiB / 189471MiB | 0% Default |
| | | Disabled |
+-----------------------------------------+------------------------+----------------------+
| 3 NVIDIA GB200 On | 00000019:01:00.0 Off | 0 |
| N/A 37C P0 166W / 1200W | 0MiB / 189471MiB | 0% Default |
| | | Disabled |
+-----------------------------------------+------------------------+----------------------+

+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| No running processes found |
+-----------------------------------------------------------------------------------------+

GPU 拓扑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
root@GB200-POD2-F06-Node05:~/lixd/nccl-demo# nvidia-smi topo -m
	GPU0	GPU1	GPU2	GPU3	NIC0	NIC1	NIC2	NIC3	NIC4	NIC5	NIC6	NIC7	CPU Affinity	NUMA Affinity	GPU NUMA ID
GPU0	 X 	NV18	NV18	NV18	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	0-71	0,2-17		N/A
GPU1	NV18	 X 	NV18	NV18	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	0-71	0,2-17		N/A
GPU2	NV18	NV18	 X 	NV18	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	72-143	1,18-33		N/A
GPU3	NV18	NV18	NV18	 X 	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	72-143	1,18-33		N/A
NIC0	SYS	SYS	SYS	SYS	 X 	SYS	SYS	SYS	SYS	SYS	SYS	SYS
NIC1	SYS	SYS	SYS	SYS	SYS	 X 	SYS	SYS	SYS	SYS	SYS	SYS
NIC2	SYS	SYS	SYS	SYS	SYS	SYS	 X 	PIX	SYS	SYS	SYS	SYS
NIC3	SYS	SYS	SYS	SYS	SYS	SYS	PIX	 X 	SYS	SYS	SYS	SYS
NIC4	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	 X 	SYS	SYS	SYS
NIC5	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	 X 	SYS	SYS
NIC6	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	 X 	PIX
NIC7	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	PIX	 X

Legend:

 X = Self
 SYS = Connection traversing PCIe as well as the SMP interconnect between NUMA nodes (e.g., QPI/UPI)
 NODE = Connection traversing PCIe as well as the interconnect between PCIe Host Bridges within a NUMA node
 PHB = Connection traversing PCIe as well as a PCIe Host Bridge (typically the CPU)
 PXB = Connection traversing multiple PCIe bridges (without traversing the PCIe Host Bridge)
 PIX = Connection traversing at most a single PCIe bridge
 NV# = Connection traversing a bonded set of # NVLinks

NIC Legend:

 NIC0: mlx5_0
 NIC1: mlx5_1
 NIC2: mlx5_2
 NIC3: mlx5_3
 NIC4: mlx5_4
 NIC5: mlx5_5
 NIC6: mlx5_6
 NIC7: mlx5_7

环境准备

GPU Operator

部署

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 添加 NVIDIA Helm 仓库
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia && helm repo update

# 部署 GPU Operator
helm install --wait gpu-operator \
 -n gpu-operator --create-namespace \
 nvidia/gpu-operator \
 --version=v25.10.0 \
 --set driver.enabled=true \
 --set dcgmExporter.serviceMonitor.enabled=true \
 --set dcgm.enabled=true

提示:部署过程可能需要 5-10 分钟,请耐心等待。可以通过 kubectl get pods -n gpu-operator 监控部署进度。

验证

如果部署成功,那么节点上可以看到 nvidia.com/gpu 资源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
root@GB200-POD2-F06-Node05:~/lixd# kubectl describe node gb200-pod2-f06-node05|grep Capacity -C 7 Addresses:
 InternalIP: 10.0.6.41
 Hostname: gb200-pod2-f06-node05
Capacity:
 cpu: 144
 ephemeral-storage: 1840577300Ki
 hugepages-16Gi: 0
 hugepages-2Mi: 0
 hugepages-512Mi: 0
 memory: 1002717120Ki
 nvidia.com/gpu: 4

NVIDIA DRA Driver

部署

1
2
3
4
5
6
7
# 部署 NVIDIA DRA Driver
helm upgrade --install nvidia-dra-driver-gpu nvidia/nvidia-dra-driver-gpu \
 --version="25.8.0" \
 --create-namespace \
 --namespace nvidia-dra-driver-gpu \
 --set resources.gpus.enabled=false \
 --set nvidiaDriverRoot=/run/nvidia/driver

重要参数说明

  • resources.gpus.enabled=false:禁用默认 GPU 资源管理,由 GPU Operator 处理
  • nvidiaDriverRoot=/run/nvidia/driver:指定 NVIDIA 驱动路径

验证

正常情况下,可以看到节点间的 nvidia.com/gpu.clique label。

1
2
3
4
5
6
root@GB200-POD2-F06-Node05:~/lixd# (echo -e "NODE\tLABEL\tCLIQUE"; kubectl get nodes -o json | \
 /usr/bin/jq -r '.items[] | [.metadata.name, "nvidia.com/gpu.clique", .metadata.labels["nvidia.com/gpu.clique"]] | @tsv') | \
 column -t
NODE LABEL CLIQUE
gb200-pod2-f06-node05 nvidia.com/gpu.clique 69a19a31-f41c-45a5-8245-579b6bce5bdd.32766
gb200-pod2-f06-node06 nvidia.com/gpu.clique 69a19a31-f41c-45a5-8245-579b6bce5bdd.32766

创建 IMEX 负载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
cat <<EOF > imex-channel-injection.yaml
---
apiVersion: resource.nvidia.com/v1beta1
kind: ComputeDomain
metadata:
 name: imex-channel-injection
spec:
 numNodes: 1
 channel:
 resourceClaimTemplate:
 name: imex-channel-0
---
apiVersion: v1
kind: Pod
metadata:
 name: imex-channel-injection
spec:
 affinity:
 nodeAffinity:
 requiredDuringSchedulingIgnoredDuringExecution:
 nodeSelectorTerms:
 - matchExpressions:
 - key: nvidia.com/gpu.clique
 operator: Exists
 containers:
 - name: ctr
 image: ubuntu:22.04
 command: ["bash", "-c"]
 args: ["ls -la /dev/nvidia-caps-imex-channels; trap 'exit 0' TERM; sleep 9999 & wait"]
 resources:
 claims:
 - name: imex-channel-0
 resourceClaims:
 - name: imex-channel-0
 resourceClaimTemplateName: imex-channel-0
EOF

查看日志,正常能看到注入的 imex channel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
root@GB200-POD2-F06-Node05:~/lixd/demo# kubectl apply -f imex-channel-injection.yaml
computedomain.resource.nvidia.com/imex-channel-injection created
pod/imex-channel-injection created
root@GB200-POD2-F06-Node05:~/lixd/demo# kubectl get pods
NAME READY STATUS RESTARTS AGE
imex-channel-injection 1/1 Running 0 5s
root@GB200-POD2-F06-Node05:~/lixd/demo# kubectl logs imex-channel-injection
total 0
drwxr-xr-x 2 root root 60 Jan 5 08:31 .
drwxr-xr-x 6 root root 380 Jan 5 08:31 ..
crw-rw-rw- 1 root root 501, 0 Jan 5 08:31 channel0

至此,说明 nvidia-dra-driver 部署成功。

验证测试

安装 MPI Operator

首先安装 MPI Operator,用于运行多节点 MPI 作业:

1
kubectl create -f https://github.com/kubeflow/mpi-operator/releases/download/v0.6.0/mpi-operator.yaml

nvbandwidth 测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
cat <<EOF > nvbandwidth-test-job.yaml
---
apiVersion: resource.nvidia.com/v1beta1
kind: ComputeDomain
metadata:
 name: nvbandwidth-test-compute-domain
spec:
 numNodes: 2
 channel:
 resourceClaimTemplate:
 name: nvbandwidth-test-compute-domain-channel

---
apiVersion: kubeflow.org/v2beta1
kind: MPIJob
metadata:
 name: nvbandwidth-test
spec:
 slotsPerWorker: 4
 launcherCreationPolicy: WaitForWorkersReady
 runPolicy:
 cleanPodPolicy: Running
 sshAuthMountPath: /home/mpiuser/.ssh
 mpiReplicaSpecs:
 Launcher:
 replicas: 1
 template:
 metadata:
 labels:
 nvbandwidth-test-replica: mpi-launcher
 spec:
 affinity:
 nodeAffinity:
 requiredDuringSchedulingIgnoredDuringExecution:
 nodeSelectorTerms:
 - matchExpressions:
 - key: node-role.kubernetes.io/control-plane
 operator: Exists
 containers:
 - image: ghcr.io/nvidia/k8s-samples:nvbandwidth-v0.7-8d103163
 name: mpi-launcher
 securityContext:
 runAsUser: 1000
 command:
 - mpirun
 args:
 - --bind-to
 - core
 - --map-by
 - ppr:4:node
 - -np
 - "8"
 - --report-bindings
 - -q
 - nvbandwidth
 - -t
 - multinode_device_to_device_memcpy_read_ce
 Worker:
 replicas: 2
 template:
 metadata:
 labels:
 nvbandwidth-test-replica: mpi-worker
 spec:
 affinity:
 podAffinity:
 requiredDuringSchedulingIgnoredDuringExecution:
 - labelSelector:
 matchExpressions:
 - key: nvbandwidth-test-replica
 operator: In
 values:
 - mpi-worker
 topologyKey: nvidia.com/gpu.clique
 containers:
 - image: ghcr.io/nvidia/k8s-samples:nvbandwidth-v0.7-8d103163
 name: mpi-worker
 securityContext:
 runAsUser: 1000
 env:
 command:
 - /usr/sbin/sshd
 args:
 - -De
 - -f
 - /home/mpiuser/.sshd_config
 resources:
 limits:
 nvidia.com/gpu: 4
 claims:
 - name: compute-domain-channel
 resourceClaims:
 - name: compute-domain-channel
 resourceClaimTemplateName: nvbandwidth-test-compute-domain-channel
EOF

Apply

1
2
3
root@GB200-POD2-F06-Node05:~/lixd/demo# kubectl apply -f nvbandwidth-test-job.yaml
computedomain.resource.nvidia.com/nvbandwidth-test-compute-domain created
mpijob.kubeflow.org/nvbandwidth-test created

会自动启动 Pod 进行测试

1
2
3
4
5
root@GB200-POD2-F06-Node05:~/lixd/demo# k get po
NAME READY STATUS RESTARTS AGE
nvbandwidth-test-launcher-xl87m 1/1 Running 0 26s
nvbandwidth-test-worker-0 1/1 Running 0 7m41s
nvbandwidth-test-worker-1 1/1 Running 0 7m41s

查看日志

1
 kubectl logs --tail=-1 -l job-name=nvbandwidth-test-launcher

测试结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
nvbandwidth Version: v0.7
Built from Git version: v0.7

MPI version: Open MPI v4.1.4, package: Debian OpenMPI, ident: 4.1.4, repo rev: v4.1.4, May 26, 2022
CUDA Runtime Version: 13000
CUDA Driver Version: 13000
Driver Version: 580.95.05

Process 0 (nvbandwidth-test-worker-0): device 0: NVIDIA GB200 (00000008:01:00)
Process 1 (nvbandwidth-test-worker-0): device 1: NVIDIA GB200 (00000009:01:00)
Process 2 (nvbandwidth-test-worker-0): device 2: NVIDIA GB200 (00000018:01:00)
Process 3 (nvbandwidth-test-worker-0): device 3: NVIDIA GB200 (00000019:01:00)
Process 4 (nvbandwidth-test-worker-1): device 0: NVIDIA GB200 (00000008:01:00)
Process 5 (nvbandwidth-test-worker-1): device 1: NVIDIA GB200 (00000009:01:00)
Process 6 (nvbandwidth-test-worker-1): device 2: NVIDIA GB200 (00000018:01:00)
Process 7 (nvbandwidth-test-worker-1): device 3: NVIDIA GB200 (00000019:01:00)

Running multinode_device_to_device_memcpy_read_ce.
memcpy CE GPU(row) -> GPU(column) bandwidth (GB/s)
 0 1 2 3 4 5 6 7
 0 N/A 822.16 821.69 821.92 821.45 821.30 821.53 821.69
 1 820.90 N/A 821.92 821.61 821.30 821.06 822.00 821.69
 2 820.59 821.77 N/A 821.69 821.45 821.06 821.37 821.30
 3 820.51 821.77 821.61 N/A 821.37 821.22 821.30 821.92
 4 820.75 821.53 821.45 821.85 N/A 821.37 821.61 821.85
 5 820.51 821.53 821.22 821.69 821.69 N/A 821.61 821.77
 6 820.35 821.30 821.53 821.37 821.30 820.90 N/A 821.14
 7 820.59 821.69 820.98 821.37 821.37 821.14 821.30 N/A

SUM multinode_device_to_device_memcpy_read_ce 45997.93

NOTE: The reported results may not reflect the full capabilities of the platform.
Performance can vary with software drivers, hardware clocks, and system topology.

测试结果显示跨节点 GPU-to-GPU 通信带宽稳定在 820 GB/s 左右,远超传统 InfiniBand 等网络互联方案的性能,为大规模分布式 AI 训练提供了强大的通信基础。

总结

通过本文的实战指南,您已经学会了如何在 GB200 平台上部署和配置 NVIDIA DRA Driver,以启用 Multi-Node NVLink (MNNVL) 功能。主要成就包括:

  • 理解核心概念:掌握了 DRA、IMEX 和 ComputeDomains 的工作原理
  • 完成环境部署:成功部署 GPU Operator 和 DRA Driver
  • 验证功能:通过 nvbandwidth 测试确认跨节点 GPU 通信正常工作

ComputeDomains 技术将复杂的底层 GPU 硬件抽象为 Kubernetes 原生资源,使得多节点分布式 AI 工作负载的管理变得简单而高效。未来,随着更多 NVIDIA 架构的支持,这项技术将在 AI 基础设施领域发挥越来越重要的作用。

参考资料

🚀 当 InfiniBand 也不够快:GB200 MNNVL 实测带宽提升 10 倍

2025-12-17 04:00:00

gb200-nvl72-nccl-test.jpeg

在上一篇 告别 TCP/IP 延迟:Kubernetes 中的 RDMA 高性能网络实战 中,我们介绍了如何在 Kubernetes 中启用 RDMA(InfiniBand)能力,实现了相比 TCP/IP 延迟降低 20-40 倍、带宽提升 40 倍以上的效果。然而在超大规模 AI 训练场景下,即便是 InfiniBand 的带宽也可能成为瓶颈——当 GPU 间需要频繁同步梯度时,跨节点通信效率直接决定了整体训练吞吐。那么,有没有比 InfiniBand 更高效的多节点互联方案?答案是 MNNVL(Multi-Node NVLink)

GB200 NVL72 是 NVIDIA 推出的超级系统,专为大规模 AI 训练和推理设计。完整一柜 GB200 NVL72 系统包含 72 个 GB200 GPU + 36 个 Grace CPU,通过第五代 NVLink 高速互连。本文测试的是其中的 2 个节点子集(每节点 4 个 GB200 + 2 个 Grace,共 8 个 GPU),主要关注不同互联方案在多节点场景下的带宽差异。

MNNVL(Multi-Node NVLink) 是 GB200 NVL72 的核心创新特性,它通过 NVLink 实现跨节点 GPU 直连,让多节点 GPU 集群的性能接近单节点。与传统的 InfiniBand 网络相比,MNNVL 具有以下优势(支持 MNNVL 的 NCCL 版本通常会优先选择该通道,也可通过 NCCL_MNNVL_ENABLE=1 显式开启或确认使用):

  • 超低延迟:GPU 之间直接通信,绕过网络协议栈,延迟极低
  • 超高带宽:实测带宽可达 811GB/s,远超传统网络方案
  • 无缝集成:NCCL 自动识别并优先使用 MNNVL,无需额外配置

在实际生产环境中,我们需要了解不同网络方案的性能差异,以便在混合集群、容错场景或特定拓扑下做出最优选择。本文通过 2 节点 8 GPU 的 NCCL micro-benchmark 实测数据 对比 MNNVL、InfiniBand 和 TCP 以太网的性能表现,为 AI 训练集群的网络选型提供参考(并非完整业务训练 benchmark)。


核心亮点:2 节点 8 GPU 配置下,MNNVL 实测带宽 811GB/s,InfiniBand 为 85GB/s性能提升 9.5 倍!实测数据证明 MNNVL 是 GB200 NVL72 多节点通信的最佳选择。

测试结果概览

连接方式 带宽 性能倍数 说明
MNNVL 811GB/s 9.5x 跨节点 GPU 直连,性能最优 ✅
InfiniBand (400G) 85GB/s 1x 传统网络方案,性能中等 ⚠️
TCP 以太网 2GB/s 0.02x 普通网络,性能最低 ❌

说明:上表中的带宽数值均来自 nccl-tests 输出的 Bus bandwidth(busbw) 指标,是 NCCL 对整体通信效率的“等效总线带宽”度量,并不等同于单条物理链路的单向线速。

关键发现

  • 🚀 MNNVL 性能是 IB 的 9.5 倍(811GB/s ÷ 85GB/s ≈ 9.5)
  • 🚀 MNNVL 性能是 TCP 的 405 倍(811GB/s ÷ 2GB/s ≈ 405)
  • 🚀 通信时间减少 89%(从 IB 的 9.5 倍时间降到 1 倍时间)

1. 🖥️ 环境说明

1.1 硬件配置

  • 系统:GB200 NVL72(一柜系统中的 2 节点子集)
  • 节点数量:2 个节点
  • 每个节点配置
    • GPU:4 个 GB200 GPU(本次测试共 8 个 GPU)
    • GPU 显存:每个 GPU 192GB(189471 MiB)
    • CPU:2 个 Grace CPU
    • 网络:1 张 400G InfiniBand 网卡(用于对比测试)
  • 节点间连接
    • MNNVL:通过 Multi-Node NVLink 实现跨节点 GPU 直连
    • InfiniBand:单 400G 网卡作为对比测试

1.2 软件环境

  • 操作系统:Linux
  • CUDA 版本:13.0
  • 驱动版本:580.95.05
  • 测试工具:nccl-tests

1.3 GPU 信息

GPU 基本信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
root@GB200-POD2-F06-Node05:~/lixd/nccl-demo# nvidia-smi
Wed Dec 10 10:24:44 2025
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 580.95.05 Driver Version: 580.95.05 CUDA Version: 13.0 |
+-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 NVIDIA GB200 On | 00000008:01:00.0 Off | 0 |
| N/A 37C P0 169W / 1200W | 0MiB / 189471MiB | 0% Default |
| | | Disabled |
+-----------------------------------------+------------------------+----------------------+
| 1 NVIDIA GB200 On | 00000009:01:00.0 Off | 0 |
| N/A 37C P0 157W / 1200W | 0MiB / 189471MiB | 0% Default |
| | | Disabled |
+-----------------------------------------+------------------------+----------------------+
| 2 NVIDIA GB200 On | 00000018:01:00.0 Off | 0 |
| N/A 38C P0 158W / 1200W | 0MiB / 189471MiB | 0% Default |
| | | Disabled |
+-----------------------------------------+------------------------+----------------------+
| 3 NVIDIA GB200 On | 00000019:01:00.0 Off | 0 |
| N/A 37C P0 166W / 1200W | 0MiB / 189471MiB | 0% Default |
| | | Disabled |
+-----------------------------------------+------------------------+----------------------+

+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| No running processes found |
+-----------------------------------------------------------------------------------------+

GPU 拓扑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
root@GB200-POD2-F06-Node05:~/lixd/nccl-demo# nvidia-smi topo -m
	GPU0	GPU1	GPU2	GPU3	NIC0	NIC1	NIC2	NIC3	NIC4	NIC5	NIC6	NIC7	CPU Affinity	NUMA Affinity	GPU NUMA ID
GPU0	 X 	NV18	NV18	NV18	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	0-71	0,2-17		N/A
GPU1	NV18	 X 	NV18	NV18	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	0-71	0,2-17		N/A
GPU2	NV18	NV18	 X 	NV18	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	72-143	1,18-33		N/A
GPU3	NV18	NV18	NV18	 X 	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	72-143	1,18-33		N/A
NIC0	SYS	SYS	SYS	SYS	 X 	SYS	SYS	SYS	SYS	SYS	SYS	SYS
NIC1	SYS	SYS	SYS	SYS	SYS	 X 	SYS	SYS	SYS	SYS	SYS	SYS
NIC2	SYS	SYS	SYS	SYS	SYS	SYS	 X 	PIX	SYS	SYS	SYS	SYS
NIC3	SYS	SYS	SYS	SYS	SYS	SYS	PIX	 X 	SYS	SYS	SYS	SYS
NIC4	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	 X 	SYS	SYS	SYS
NIC5	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	 X 	SYS	SYS
NIC6	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	 X 	PIX
NIC7	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	SYS	PIX	 X

Legend:

 X = Self
 SYS = Connection traversing PCIe as well as the SMP interconnect between NUMA nodes (e.g., QPI/UPI)
 NODE = Connection traversing PCIe as well as the interconnect between PCIe Host Bridges within a NUMA node
 PHB = Connection traversing PCIe as well as a PCIe Host Bridge (typically the CPU)
 PXB = Connection traversing multiple PCIe bridges (without traversing the PCIe Host Bridge)
 PIX = Connection traversing at most a single PCIe bridge
 NV# = Connection traversing a bonded set of # NVLinks

NIC Legend:

 NIC0: mlx5_0
 NIC1: mlx5_1
 NIC2: mlx5_2
 NIC3: mlx5_3
 NIC4: mlx5_4
 NIC5: mlx5_5
 NIC6: mlx5_6
 NIC7: mlx5_7

2. 🔧 安装 nccl-tests 环境

⚠️ 重要提示:需要在所有待测试节点安装环境,同时安装目录要保持一致。

nccl-tests 包含一系列测试项目,主要用于测试和验证 NCCL 的性能以及操作正确性。

These tests check both the performance and the correctness of NCCL operations.

2.1 OpenMPI-5.0.9

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 下载解压
wget https://download.open-mpi.org/release/open-mpi/v5.0/openmpi-5.0.9.tar.gz
tar -zxvf openmpi-5.0.9.tar.gz

# 编译安装
pushd openmpi-5.0.9
./configure --prefix=/usr/local/openmpi
make -j$(nproc)
make install
popd

参数说明:

  • --prefix=/usr/local/openmpi:指定 MPI 安装位置为 /usr/local/openmpi

2.2 NCCL

1
2
3
4
5
6
# clone 源码
git clone https://github.com/NVIDIA/nccl.git
# 编译安装
pushd nccl
make -j$(nproc) src.build CUDA_HOME=/cm/shared/apps/cuda13.0/toolkit/13.0/
popd

参数说明:

  • CUDA_HOME:CUDA 安装位置

注意:需要记录下这里的 nccl 文件夹路径,make 之后会在 nccl 目录下生成 build 目录,后续编译 nccl-tests 的时候会用到该目录。

2.3 nccl-tests

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# clone 代码
git clone https://github.com/NVIDIA/nccl-tests.git

# 编译安装
pushd nccl-tests
make MPI=1 NAME_SUFFIX=_mpi \
 MPI_HOME=/usr/local/openmpi \
 CUDA_HOME=/cm/shared/apps/cuda13.0/toolkit/13.0 \
 NCCL_HOME=/root/lixd/inference/nccl-demo/nccl/build/
popd

参数说明:

  • MPI=1:开启 MPI

  • NAME_SUFFIX=_mpi:编译产物带上 _mpi 后缀,便于区分

  • CUDA_HOME:CUDA 安装位置

  • MPI_HOME:上一步编译 OpenMPI 的位置

  • NCCL_HOME:上一步 nccl build 的目录,就是 nccl 源码目录增加 /build

构建完成后,会在 build 目录下生成可执行文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
root@GB200-POD2-F06-Node05:~# ll -lhS /root/lixd/inference/nccl-demo/nccl-tests/build/
total 316M
-rwxr-xr-x 1 root root 35M Nov 19 07:32 all_reduce_perf_mpi*
-rwxr-xr-x 1 root root 35M Nov 19 07:33 alltoall_perf_mpi*
-rwxr-xr-x 1 root root 30M Nov 19 07:33 hypercube_perf_mpi*
-rwxr-xr-x 1 root root 30M Nov 19 07:33 sendrecv_perf_mpi*
-rwxr-xr-x 1 root root 30M Nov 19 07:33 gather_perf_mpi*
-rwxr-xr-x 1 root root 30M Nov 19 07:33 scatter_perf_mpi*
-rwxr-xr-x 1 root root 30M Nov 19 07:32 reduce_scatter_perf_mpi*
-rwxr-xr-x 1 root root 30M Nov 19 07:32 reduce_perf_mpi*
-rwxr-xr-x 1 root root 30M Nov 19 07:32 broadcast_perf_mpi*
-rwxr-xr-x 1 root root 30M Nov 19 07:32 all_gather_perf_mpi*
-rw-r--r-- 1 root root 6.1M Nov 19 07:32 all_reduce.o
-rw-r--r-- 1 root root 5.9M Nov 19 07:33 alltoall.o
-rw-r--r-- 1 root root 170K Nov 19 07:33 hypercube.o
-rw-r--r-- 1 root root 167K Nov 19 07:33 sendrecv.o
-rw-r--r-- 1 root root 161K Nov 19 07:33 gather.o
-rw-r--r-- 1 root root 158K Nov 19 07:33 scatter.o
-rw-r--r-- 1 root root 157K Nov 19 07:32 reduce_scatter.o
-rw-r--r-- 1 root root 157K Nov 19 07:32 reduce.o
-rw-r--r-- 1 root root 155K Nov 19 07:32 broadcast.o
-rw-r--r-- 1 root root 151K Nov 19 07:32 all_gather.o
-rw-r--r-- 1 root root 22K Nov 19 07:28 timer.o

3. 🚀 开始测试

3.1 配置环境变量

1
2
3
4
5
export CUDA_HOME=/cm/shared/apps/cuda13.0/toolkit/13.0
export MPI_HOME=/usr/local/openmpi
export NCCL_HOME=/root/lixd/inference/nccl-demo/nccl/build/
export PATH=$CUDA_HOME/bin:$MPI_HOME/bin:$PATH
export LD_LIBRARY_PATH=$MPI_HOME/lib:$NCCL_HOME/lib:$CUDA_HOME/lib64:$LD_LIBRARY_PATH

3.2 运行测试命令

注意:以下是一个示例命令,实际测试中根据场景调整参数(如 -g 参数)。

1
2
3
4
5
6
7
8
mpirun --allow-run-as-root \
 -x NCCL_DEBUG=INFO \
 -x PATH -x LD_LIBRARY_PATH \
 -x CUDA_HOME -x MPI_HOME -x NCCL_HOME \
 -np 2 \
 -H 10.0.6.41:1,10.0.6.42:1 \
 /root/lixd/inference/nccl-demo/nccl-tests/build/all_reduce_perf_mpi \
 -b 16G -e 16G -f 2 -g 4 -c 1 -i 100

ENV 相关参数

  • -x NCCL_DEBUG=INFO:把环境变量 NCCL_DEBUG=INFO 随进程一起下发,使 NCCL 打印初始化、网络选择、通道等调试信息。

  • -x PATH:将当前 shell 的 PATH 原样传递到远端进程,保证可执行文件能被找到。

  • -x LD_LIBRARY_PATH:同样传递动态库搜索路径,避免远端节点找不到 CUDA/OpenMPI/NCCL 的 .so 文件。

  • -x CUDA_HOME -x MPI_HOME -x NCCL_HOME:同上,传递 CUDA、MPI、NCCL home 目录

  • -np 2:进程数为 2

  • -H 10.0.6.41:1,10.0.6.42:1:指定节点列表,这里控制哪台机器跑几个进程。

测试相关参数

  • -b 16G:最小消息 16GB(示例命令中使用)

  • -e 16G:最大消息 16GB(示例命令中使用)

  • -f 2:每次测试数据量按照 2 倍递增

  • -g 4:每个 MPI 进程使用的 GPU 数量,当前是 4 GPU

    • 总 GPU 数 = 进程数 × 每个进程 GPU 数 = 2 × 4 = 8
  • -c 1:使用 1 个通信通道

  • -i 100:warm-up 后跑 100 次取平均

4. 📊 测试结果对比

4.1 MNNVL 测试结果

通过显式启用 NCCL_MNNVL_ENABLE=1NCCL_IMEX_ENABLE=1,指定 NCCL 使用 MNNVL 进行跨节点通信。

测试命令

1
2
3
4
5
6
7
mpirun --allow-run-as-root \
 -x PATH -x LD_LIBRARY_PATH \
 -x CUDA_HOME -x MPI_HOME -x NCCL_HOME \
 -x NCCL_MNNVL_ENABLE=1 -x NCCL_IMEX_ENABLE=1 \
 -H $node1:4,$node2:4 \
 /opt/nccl-tests/build/all_reduce_perf_mpi \
 -b 16G -e 16G -f 2 -g 1 -c 1 -i 100

测试结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
root@nccl-test-2nsk2:/workspace# mpirun --allow-run-as-root \
 -x PATH -x LD_LIBRARY_PATH \
 -x CUDA_HOME -x MPI_HOME -x NCCL_HOME \
 -x NCCL_MNNVL_ENABLE=1 -x NCCL_IMEX_ENABLE=1 \
 -H $node1:4,$node2:4 \
 /opt/nccl-tests/build/all_reduce_perf_mpi \
 -b 16G -e 16G -f 2 -g 1 -c 1 -i 100
Warning: Permanently added '172.25.114.45' (ED25519) to the list of known hosts.
# nccl-tests version 2.17.6 nccl-headers=22809 nccl-library=22809
# Collective test starting: all_reduce_perf_mpi
# nThread 1 nGpus 1 minBytes 17179869184 maxBytes 17179869184 step: 2(factor) warmup iters: 1 iters: 20 agg iters: 1 validation: 1 graph: 0
#
# Using devices
# Rank 0 Group 0 Pid 740 on nccl-test-2nsk2 device 0 [0008:01:00] NVIDIA GB200
# Rank 1 Group 0 Pid 741 on nccl-test-2nsk2 device 1 [0009:01:00] NVIDIA GB200
# Rank 2 Group 0 Pid 742 on nccl-test-2nsk2 device 2 [0018:01:00] NVIDIA GB200
# Rank 3 Group 0 Pid 743 on nccl-test-2nsk2 device 3 [0019:01:00] NVIDIA GB200
# Rank 4 Group 0 Pid 1167 on nccl-test-hw5qh device 0 [0008:01:00] NVIDIA GB200
# Rank 5 Group 0 Pid 1168 on nccl-test-hw5qh device 1 [0009:01:00] NVIDIA GB200
# Rank 6 Group 0 Pid 1170 on nccl-test-hw5qh device 2 [0018:01:00] NVIDIA GB200
# Rank 7 Group 0 Pid 1169 on nccl-test-hw5qh device 3 [0019:01:00] NVIDIA GB200
#
# out-of-place in-place
# size count type redop root time algbw busbw #wrong time algbw busbw #wrong
# (B) (elements) (us) (GB/s) (GB/s) (us) (GB/s) (GB/s)
 17179869184 4294967296 float sum -1 37038.7 463.84 811.71 0 37101.0 463.06 810.35 0
# Out of bounds values : 0 OK
# Avg bus bandwidth : 811.031
#
# Collective test concluded: all_reduce_perf_mpi

性能表现

  • 带宽:811GB/s
  • ✅ 跨节点 GPU 直连,性能最优
  • ✅ NCCL 会将两台机器的 4+4 GPU 放在同一跨节点 NVLink 通信域中,跨节点不走 IB/TCP

4.2 InfiniBand 测试结果

  • 通过指定 -x NCCL_MNNVL_ENABLE=0 以及 -x NCCL_IMEX_ENABLE=0 配置,关闭 MNNVL,强制走 IB。
  • -x NCCL_IB_HCA="mlx5_0" 指定 IB 卡

测试命令

1
2
3
4
5
6
7
8
root@nccl-sleep-2nsk2:/workspace# mpirun --allow-run-as-root \
 -x PATH -x LD_LIBRARY_PATH \
 -x CUDA_HOME -x MPI_HOME -x NCCL_HOME \
 -x NCCL_MNNVL_ENABLE=0 -x NCCL_IMEX_ENABLE=0 \
 -x NCCL_IB_HCA="mlx5_0" \
 -H $node1:4,$node2:4 \
 /opt/nccl-tests/build/all_reduce_perf_mpi \
 -b 16G -e 16G -f 2 -g 1 -c 1 -i 100

测试结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
root@nccl-test-2nsk2:/workspace# mpirun --allow-run-as-root \
 -x PATH -x LD_LIBRARY_PATH \
 -x CUDA_HOME -x MPI_HOME -x NCCL_HOME \
 -x NCCL_MNNVL_ENABLE=0 -x NCCL_IMEX_ENABLE=0 \
 -x NCCL_IB_HCA="mlx5_0" \
 -H $node1:4,$node2:4 \
 /opt/nccl-tests/build/all_reduce_perf_mpi \
 -b 16G -e 16G -f 2 -g 1 -c 1 -i 100
Warning: Permanently added '172.25.114.34' (ED25519) to the list of known hosts.
# nccl-tests version 2.17.6 nccl-headers=22809 nccl-library=22809
# Collective test starting: all_reduce_perf_mpi
# nThread 1 nGpus 1 minBytes 17179869184 maxBytes 17179869184 step: 2(factor) warmup iters: 1 iters: 20 agg iters: 1 validation: 1 graph: 0
#
# Using devices
# Rank 0 Group 0 Pid 375 on nccl-test-6k8rm device 0 [0008:01:00] NVIDIA GB200
# Rank 1 Group 0 Pid 376 on nccl-test-6k8rm device 1 [0009:01:00] NVIDIA GB200
# Rank 2 Group 0 Pid 377 on nccl-test-6k8rm device 2 [0018:01:00] NVIDIA GB200
# Rank 3 Group 0 Pid 378 on nccl-test-6k8rm device 3 [0019:01:00] NVIDIA GB200
# Rank 4 Group 0 Pid 459 on nccl-test-dbzk7 device 0 [0008:01:00] NVIDIA GB200
# Rank 5 Group 0 Pid 460 on nccl-test-dbzk7 device 1 [0009:01:00] NVIDIA GB200
# Rank 6 Group 0 Pid 461 on nccl-test-dbzk7 device 2 [0018:01:00] NVIDIA GB200
# Rank 7 Group 0 Pid 462 on nccl-test-dbzk7 device 3 [0019:01:00] NVIDIA GB200
#
# out-of-place in-place
# size count type redop root time algbw busbw #wrong time algbw busbw #wrong
# (B) (elements) (us) (GB/s) (GB/s) (us) (GB/s) (GB/s)
 17179869184 4294967296 float sum -1 355729 48.29 84.52 0 353612 48.58 85.02 0
# Out of bounds values : 0 OK
# Avg bus bandwidth : 84.7689
#
# Collective test concluded: all_reduce_perf_mpi

性能表现

  • ⚠️ 带宽:85GB/s(实测值 84.77GB/s,四舍五入)
  • ⚠️ 性能大幅降低,只有 MNNVL 的 1/9.5(811GB/s ÷ 85GB/s ≈ 9.5)

4.3 TCP 网络测试结果

增加以下参数:

  • -x NCCL_MNNVL_ENABLE=0 -x NCCL_IMEX_ENABLE=0:关闭 MNNVL

  • -x NCCL_IB_DISABLE=1:禁用 IB 网络

看看纯 TCP 网络下的性能。

1
2
3
4
5
6
7
8
mpirun --allow-run-as-root \
 -x PATH -x LD_LIBRARY_PATH \
 -x CUDA_HOME -x MPI_HOME -x NCCL_HOME \
 -x NCCL_MNNVL_ENABLE=0 -x NCCL_IMEX_ENABLE=0 \
 -x NCCL_IB_DISABLE=1 \
 -H $node1:4,$node2:4 \
 /opt/nccl-tests/build/all_reduce_perf_mpi \
 -b 1G -e 1G -f 2 -g 1 -c 1 -i 10

ps:因为纯 TCP 太慢,为了缩短测试时间,这里将消息大小从 16GB 降到 1GB,迭代次数从 100 次降到 10 次,因此只能粗略对比数量级差距,而不是与 MNNVL/IB 完全同条件对比。

测试结果

1
2
# 输出
# Avg bus bandwidth : 2.00 GB/s

性能表现

  • 带宽:2GB/s(Bus bandwidth,缩小消息大小后的结果)
  • ❌ 性能极低,只有 MNNVL 的 约 1/405(811GB/s ÷ 2GB/s ≈ 405),更适合作为“数量级感知”而非严谨对比

5. 📝 小结

通过本次 2 节点 8 GPU 的 nccl-tests,对 GB200 NVL72 系统中的不同互联方案有了更直观的量化认识:

  • MNNVL 性能是 InfiniBand 的约 9.5 倍:811GB/s vs 85GB/s(Bus bandwidth 指标),在相同 all-reduce 条件下差距非常明显
  • GB200 NVL72 完整系统包含 72 个 GB200 GPU + 36 个 Grace CPU:硬件规格被设计为大规模 AI 训练/推理的基础设施(本文只是其中 2 节点 8 GPU 的子规模测试)
  • 第五代 NVLink + MNNVL 技术:通过跨节点 GPU 直连,让多节点的通信性能更接近单节点 NVSwitch 域

但是,这么好的性能也是有代价的——GB200 NVL72 售价高达数百万美元 😱

看完测试数据,我只想说: 🤑 贫穷限制了我的想象力,原来"带宽不够"也能用钱解决。


6. 📚 参考资料


告别 TCP/IP 延迟:Kubernetes 中的 RDMA 高性能网络实战

2025-12-03 06:00:00

RDMA in K8s

GPU 算力拉满了,网络却成了瓶颈?在大模型训练和推理场景中,传统 TCP/IP 网络的延迟和 CPU 开销正在严重制约集群性能。RDMA 技术通过绕过内核直接访问内存,降低网络延迟。本文将手把手教你在 Kubernetes 中启用 RDMA 能力,从 Device Plugin 部署到性能验证,让你的 AI 集群真正发挥出硬件的全部潜力。

1. 基本概念

RDMA (Remote Direct Memory Access) 是一种高性能网络通信技术,允许网络中的计算机直接从另一台计算机的内存中读取或写入数据,而无需涉及两台计算机的操作系统内核或 CPU。

1.1 核心特性

  • Zero Copy (零拷贝):应用程序可以直接将数据传输到网络适配器,无需在内核空间和用户空间之间复制数据。
  • Kernel Bypass (内核旁路):应用程序可以直接向硬件发送命令,绕过操作系统内核,从而显著降低延迟。
  • CPU Offload (CPU 卸载):网络传输逻辑由网卡硬件处理,释放 CPU 资源用于计算任务。

1.2 网络协议

RDMA 支持多种网络传输协议,常见的包括:

  • InfiniBand (IB)
    • 专为高性能计算设计的网络标准。
    • 提供极高的吞吐量和极低的延迟。
    • 需要专用的 IB 交换机和网卡,成本较高,但性能最好。
    • 采用基于信用的流控机制,保证无损传输。
  • RoCE (RDMA over Converged Ethernet)
    • 允许在以太网上运行 RDMA 协议。
    • RoCE v1:基于以太网链路层协议,只能在同一个二层广播域内通信,不可路由。
    • RoCE v2:基于 UDP/IP 协议,可以跨三层网络路由,是目前数据中心的主流选择。需要交换机支持 PFC (Priority Flow Control) 和 ECN (Explicit Congestion Notification) 以保证无损传输。
  • iWARP (Internet Wide Area RDMA Protocol)
    • 基于 TCP/IP 协议栈实现 RDMA。
    • 利用 TCP 的可靠传输机制,对网络设备要求较低(普通以太网交换机即可)。
    • 由于 TCP 协议栈的复杂性,性能通常低于 IB 和 RoCE。

在 Kubernetes 环境中,我们通常关注如何将这些高性能网络能力暴露给 Pod 使用。

2. 背景与目标

随着大模型(LLM)训练和推理需求的爆发式增长,分布式计算集群对网络带宽和延迟提出了极高的要求。在传统的 TCP/IP 网络架构中,数据传输需要经过操作系统内核的多次上下文切换和内存拷贝,这在高带宽(如 100Gbps+)场景下会消耗大量的 CPU 资源,并引入不可忽视的延迟,成为制约 GPU 集群性能的瓶颈。

为了解决这一问题,RDMA (Remote Direct Memory Access) 技术被广泛应用。它允许应用程序直接访问远程节点的内存,绕过内核网络栈,从而实现高吞吐、低延迟和低 CPU 占用。

然而,Kubernetes 原生并不直接支持 RDMA 设备的管理和调度。为了在 Kubernetes 集群中充分利用 RDMA 硬件能力,我们需要解决以下关键问题:

  • 设备发现与管理:如何将物理节点的 RDMA 设备(如 IB 卡、RoCE 网卡)暴露给 Kubernetes 调度器。
  • Pod 配置:如何将 RDMA 设备挂载到 Pod 内部,使其能够使用 IB Verbs API 进行通信。
  • 网络方案选择:在复杂的网络环境中,是否需要引入 Multus CNI 等多网卡方案。
  • 性能验证:如何验证 RDMA 功能是否正常开启,并测试实际带宽是否达标。

本文将基于 Mellanox 提供的 k8s-rdma-shared-dev-plugin,详细介绍在 Kubernetes 中启用原生 RDMA 的完整流程与最佳实践。

3. 部署 device-plugin

推荐使用由 Mellanox 官方维护的 DevicePlugin:k8s-rdma-shared-device-plugin

3.1 驱动安装

确保节点已安装对应版本的 MOFED/OFED 驱动及用户态库(libibverbsrdmacm 等)。驱动安装方式本文不赘述。

3.2 获取部署模板

先 clone 项目:

1
git clone https://github.com/Mellanox/k8s-rdma-shared-dev-plugin.git

3.3 部署 Device Plugin

1
2
cd deployment/k8s/base
kubectl apply -k .

部署成功后 kube-system 中会出现 rdma-shared-dp-ds DaemonSet,它会在每个节点挂载 RDMA 设备并注册可调度资源。

1
2
3
root@GB200-POD2-F06-Node05:~# kubectl -n kube-system get po|grep rdma
rdma-shared-dp-ds-vrswv 1/1 Running 1 (27h ago) 31h
rdma-shared-dp-ds-zg242 1/1 Running 1 (27h ago) 31h

3.4 配置

Device Plugin 的核心是 ConfigMap。默认配置可参考:configmap.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: ConfigMap
metadata:
 name: rdma-devices
 namespace: kube-system
data:
 config.json: |
 {
 "periodicUpdateInterval": 300,
 "configList": [
 {
 "resourceName": "hca_shared_devices_a",
 "rdmaHcaMax": 1000,
 "selectors": {
 "vendors": ["15b3"],
 "deviceIDs": ["1021"],
 "ifNames": ["ibp3s0"]
 }
 }
 ]
 }

关键字段:

  • resourceName:Pod 申请资源写的名字,建议自定义如 rdma/ib
  • rdmaHcaMax:单卡允许共享的 Pod 数量,默认 1000
  • selectors:定义哪些物理网卡由 device plugin 接管,是配置的关键
    • vendors:设备厂商 ID,例如 Mellanox 为 15b3
    • deviceIDs:设备 ID,和 IB 卡有关,例如 ConnectX-7为 1021
    • ifNames:设备接口名称。

接下来介绍如何收集所需信息。

4. 采集 RDMA 设备信息

4.1 列出 IB 设备

1
2
ls /sys/class/infiniband
mlx5_0 mlx5_1 mlx5_2 mlx5_3 mlx5_4 mlx5_5 mlx5_6 mlx5_7

4.2 关联网络接口

1
2
3
4
5
6
7
8
9
ibdev2netdev
mlx5_0 port 1 ==> ibp3s0 (Up)
mlx5_1 port 1 ==> ibP2p3s0 (Up)
mlx5_2 port 1 ==> enP6p3s0f0np0 (Up)
mlx5_3 port 1 ==> bond0 (Up)
mlx5_4 port 1 ==> ibP16p3s0 (Up)
mlx5_5 port 1 ==> ibP18p3s0 (Up)
mlx5_6 port 1 ==> enP22p3s0f0np0 (Up)
mlx5_7 port 1 ==> bond0 (Up)

这里可以看到 IB 卡对应的接口名称,例如 mlx5_0 的接口名称为 ibp3s0

这里可以获取到配置中需要的 ifNames 参数。

4.3 获取 PCI 信息

mst status -v 可以列出所有 UP 状态网卡及其 PCI ID:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
root@GB200-POD2-F06-Node05:~/lixd/rdma# mst status -v
MST modules:
------------
 MST PCI module loaded
 MST PCI configuration module loaded
PCI devices:
------------
DEVICE_TYPE MST PCI RDMA NET NUMA
ConnectX7(rev:0) /dev/mst/mt4129_pciconf0 0000:03:00.0 mlx5_0 net-ibp3s0 0

ConnectX7(rev:0) /dev/mst/mt4129_pciconf1 0002:03:00.0 mlx5_1 net-ibP2p3s0 0

ConnectX7(rev:0) /dev/mst/mt4129_pciconf2 0010:03:00.0 mlx5_4 net-ibP16p3s0 1

ConnectX7(rev:0) /dev/mst/mt4129_pciconf3 0012:03:00.0 mlx5_5 net-ibP18p3s0 1

BlueField3(rev:1) /dev/mst/mt41692_pciconf0 0006:03:00.0 mlx5_2 net-enP6p3s0f0np0 0

BlueField3(rev:1) /dev/mst/mt41692_pciconf0.1 0006:03:00.1 mlx5_3 net-bond0 0

BlueField3(rev:1) /dev/mst/mt41692_pciconf1 0016:03:00.0 mlx5_6 net-enP22p3s0f0np0 1

BlueField3(rev:1) /dev/mst/mt41692_pciconf1.1 0016:03:00.1 mlx5_7 net-bond0 1

GB100(rev:0) /dev/mst/mt10561_pciconf0 0008:01:00.0 0

GB100(rev:0) /dev/mst/mt10561_pci_cr0 0008:01:00.0 0

GB100(rev:0) /dev/mst/mt10561_pciconf1 0009:01:00.0 0

GB100(rev:0) /dev/mst/mt10561_pci_cr1 0009:01:00.0 0

GB100(rev:0) /dev/mst/mt10561_pciconf2 0018:01:00.0 1

GB100(rev:0) /dev/mst/mt10561_pci_cr2 0018:01:00.0 1

GB100(rev:0) /dev/mst/mt10561_pciconf3 0019:01:00.0 1

GB100(rev:0) /dev/mst/mt10561_pci_cr3 0019:01:00.0 1

示例中 mlx5_0 的 PCI ID 为 0000:03:00.0

4.4 查询厂商/设备 ID

根据上一步获取到的 PCI ID 查询:

1
2
lspci -n | grep 0000:03:00.0
0000:03:00.0 0207: 15b3:1021
  • 15b3:Mellanox/NVIDIA 厂商 ID
  • 1021:具体设备 ID

这里可以获取到配置中需要的 vendors、deviceIDs 参数。

4.5 生成 selectors

汇总以上信息即可写出 selectors,示例如下(可配置多块网卡):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: ConfigMap
metadata:
 name: rdma-devices
 namespace: kube-system
data:
 config.json: |
 {
 "periodicUpdateInterval": 300,
 "configList": [
 {
 "resourceName": "hca_shared_devices_a",
 "rdmaHcaMax": 1000,
 "selectors": {
 "vendors": ["15b3"],
 "deviceIDs": ["1021"],
 "ifNames": ["ibp3s0"]
 }
 }
 ]
 }

更新 ConfigMap 后重启 Device Plugin:

1
kubectl -n kube-system rollout restart ds rdma-shared-dp-ds

5. 验证

5.1 Node 资源验证

1
kk describe node gb200-pod2-f06-node05 | grep Capacity -A 9

已经能在 Node 上看到 rdma/hca_shared_devices_a 资源了。

5.2 构建测试镜像

基于 Ubuntu 安装 ibv_devicesibv_devinfoibstatibstatus 等工具构建一个用于测试的镜像。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
FROM ubuntu:22.04

RUN apt-get update && \
 apt-get install -y --no-install-recommends \
 perftest \
 ibverbs-utils \
 ibverbs-providers \
 libibumad3 \
 libibverbs1 \
 librdmacm1 \
 infiniband-diags \
 iproute2 && \
 rm -rf /var/lib/apt/lists/*

ENTRYPOINT ["sleep","infinity"]

构建镜像:

1
docker build -t rdma-test:v1 .

已经推送到 dockerhub:lixd96/rdma-test:latest

5.3 Pod 申请资源

启动一个 Pod 申请 RDMA 资源:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: Pod
metadata:
 name: mofed-test-pod
spec:
 restartPolicy: OnFailure
 containers:
 - image: rdma-test:v1
 name: mofed-test-ctr
 securityContext:
 capabilities:
 add: [ "IPC_LOCK" ]
 resources:
 limits:
 rdma/hca_shared_devices_a: 1
 command:
 - sh
 - -c
 - |
 ls -l /dev/infiniband /sys/class/infiniband /sys/class/net
 sleep 1000000

进入 Pod,使用 ibv_devicesibv_devinfo 查看 IB 卡信息:

1
2
ibv_devices
ibv_devinfo

可以看到 Pod 内已经识别到 mlx5_0 等 RDMA 设备。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
root@mofed-test-pod:/# ibv_devices
 device 	 node GUID
 ------ 	----------------
 mlx5_0 	7c8c090300c3cfee
root@mofed-test-pod:/# ibv_devinfo
hca_id:	mlx5_0
	transport:			InfiniBand (0)
	fw_ver:				28.44.2524
	node_guid:			7c8c:0903:00c3:cfee
	sys_image_guid:			7c8c:0903:00c3:cfee
	vendor_id:			0x02c9
	vendor_part_id:			4129
	hw_ver:				0x0
	board_id:			NVD0000000054
	phys_port_cnt:			1
		port:	1
			state:			PORT_ACTIVE (4)
			max_mtu:		4096 (5)
			active_mtu:		4096 (5)
			sm_lid:			1
			port_lid:		1336
			port_lmc:		0x00
			link_layer:		InfiniBand

5.4 性能验证

为了全面验证 RDMA 的性能,我们需要启动两个 Pod 进行带宽和延迟测试。

Pod 1 (Server)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: Pod
metadata:
 name: rdma-server
spec:
 restartPolicy: OnFailure
 containers:
 - image: rdma-test:v1
 name: mofed-test-ctr
 securityContext:
 capabilities:
 add: [ "IPC_LOCK" ]
 resources:
 limits:
 rdma/hca_shared_devices_a: 1
 command:
 - sh
 - -c
 - |
 ls -l /dev/infiniband /sys/class/infiniband /sys/class/net
 sleep 1000000

Pod 2 (Client)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: Pod
metadata:
 name: rdma-client
spec:
 restartPolicy: OnFailure
 containers:
 - image: rdma-test:v1
 name: mofed-test-ctr
 securityContext:
 capabilities:
 add: [ "IPC_LOCK" ]
 resources:
 limits:
 rdma/hca_shared_devices_a: 1
 command:
 - sh
 - -c
 - |
 ls -l /dev/infiniband /sys/class/infiniband /sys/class/net
 sleep 1000000

部署后查看 Pod 状态:

1
2
rdma-server 1/1 Running 0 7m18s 172.25.114.16 gb200-pod2-f06-node05
rdma-client 1/1 Running 0 5m24s 172.25.114.38 gb200-pod2-f06-node05

进入 Pod,使用 ibv_devicesibv_devinfo 查看 IB 卡信息:

1
2
ibv_devices
ibv_devinfo

可以看到 Pod 内已经识别到 mlx5_0 等 RDMA 设备。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
root@mofed-test-pod:/# ibv_devices
 device 	 node GUID
 ------ 	----------------
 mlx5_0 	7c8c090300c3cfee
root@mofed-test-pod:/# ibv_devinfo
hca_id:	mlx5_0
	transport:			InfiniBand (0)
	fw_ver:				28.44.2524
	node_guid:			7c8c:0903:00c3:cfee
	sys_image_guid:			7c8c:0903:00c3:cfee
	vendor_id:			0x02c9
	vendor_part_id:			4129
	hw_ver:				0x0
	board_id:			NVD0000000054
	phys_port_cnt:			1
		port:	1
			state:			PORT_ACTIVE (4)
			max_mtu:		4096 (5)
			active_mtu:		4096 (5)
			sm_lid:			1
			port_lid:		1336
			port_lmc:		0x00
			link_layer:		InfiniBand

5.4.1 带宽测试

使用 ib_write_bw 工具测试 RDMA 带宽性能。

进入 rdma-server 启动 server:

1
ib_write_bw -d mlx5_0 -i 1

进入 rdma-client 启动 Client:

1
2
# 其中 172.25.114.16 为 rdma-server 的 IP
ib_write_bw -d mlx5_0 -i 1 172.25.114.16

结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
root@rdma-server:/# ib_write_bw -d mlx5_0 -i 1

************************************
* Waiting for client to connect... *
************************************
---------------------------------------------------------------------------------------
 RDMA_Write BW Test
 Dual-port : OFF		Device : mlx5_0
 Number of qps : 1		Transport type : IB
 Connection type : RC		Using SRQ : OFF
 PCIe relax order: ON
 ibv_wr* API : ON
 CQ Moderation : 1
 Mtu : 4096[B]
 Link type : IB
 Max inline data : 0[B]
 rdma_cm QPs	 : OFF
 Data ex. method : Ethernet
---------------------------------------------------------------------------------------
 local address: LID 0x538 QPN 0x0171 PSN 0x7ef7d8 RKey 0x1fff00 VAddr 0x00aaaaaab00000
 remote address: LID 0x451 QPN 0x0165 PSN 0xac1c4 RKey 0x1fff00 VAddr 0x00aaaaaab80000
---------------------------------------------------------------------------------------
 #bytes #iterations BW peak[MB/sec] BW average[MB/sec] MsgRate[Mpps]
 65536 5000 46711.55 45733.81		 0.731741
---------------------------------------------------------------------------------------

测试结果为 46 GB/s 带宽,IB 卡为 400Gb/s,理论带宽为 50GB/s,该结果已经接近 IB 带宽上限,说明 RDMA 已能正常使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
root@rdma-server:/# ibstat
CA 'mlx5_0'
	CA type: MT4129
	Number of ports: 1
	Firmware version: 28.44.2524
	Hardware version: 0
	Node GUID: 0x7c8c090300c3a7ae
	System image GUID: 0x7c8c090300c3a7ae
	Port 1:
		State: Active
		Physical state: LinkUp
		Rate: 400
		Base lid: 1105
		LMC: 0
		SM lid: 1
		Capability mask: 0xa751e848
		Port GUID: 0x7c8c090300c3a7ae
		Link layer: InfiniBand

5.4.2 延迟测试

除了带宽,延迟也是 RDMA 的重要性能指标。使用 ib_write_lat 测试延迟:

在 rdma-server 中启动:

1
ib_write_lat -d mlx5_0 -i 1

在 rdma-client 中连接:

1
ib_write_lat -d mlx5_0 -i 1 172.25.114.16

测试结果示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
---------------------------------------------------------------------------------------
 RDMA_Write Latency Test
 Dual-port : OFF		Device : mlx5_0
 Number of qps : 1		Transport type : IB
 Connection type : RC		Using SRQ : OFF
 PCIe relax order: OFF
 ibv_wr* API : ON
 TX depth : 1
 Mtu : 4096[B]
 Link type : IB
 Max inline data : 220[B]
 rdma_cm QPs	 : OFF
 Data ex. method : Ethernet
---------------------------------------------------------------------------------------
 local address: LID 0x451 QPN 0x0167 PSN 0xca7dd8 RKey 0x1ffbba VAddr 0x00aaaaaab00000
 remote address: LID 0x538 QPN 0x0173 PSN 0x66403f RKey 0x1fecab VAddr 0x00aaaaaab00000
---------------------------------------------------------------------------------------
 #bytes #iterations t_min[usec] t_max[usec] t_typical[usec] t_avg[usec] t_stdev[usec]
 2 100 2.62 3.84 2.74 2.73 0.04

测试结果显示,RDMA 的单向延迟约为 2.73 微秒,相比传统 TCP/IP 网络(通常 50-100 微秒),延迟降低了 20-40 倍。同时标准差仅为 0.04 微秒,说明延迟非常稳定,充分体现了 RDMA 的低延迟优势。

6. 是否需要 Multus CNI?

在 Kubernetes RDMA 方案中,是否引入 Multus CNI 取决于具体的应用场景和网络需求。

6.1 不需要 Multus CNI 的场景

如果你的需求满足以下条件,通常不需要 Multus CNI:

  1. 仅需要 IB Verbs API:应用(如 vLLM、NCCL)直接调用 RDMA 接口,不依赖 IP 协议进行通信(或者仅用于控制面)。
  2. 使用 Shared Device Plugin:本文介绍的 k8s-rdma-shared-dev-plugin 可以在不改变 Pod 网络命名空间结构的情况下,将 RDMA 设备文件(/dev/infiniband/*)暴露给 Pod。
  3. 扁平网络架构:RDMA 网卡和 Kubernetes 管理网卡处于同一网络平面,或者你能够接受 Pod 沿用 Host 网络的部分特性。

在这种模式下,Pod 依然使用默认 CNI(如 Calico、Flannel)分配的 IP 进行常规通信,同时拥有了访问 RDMA 硬件的能力。这是最简单、维护成本最低的方案。

6.2 需要 Multus CNI 的场景

如果存在以下需求,则建议引入 Multus CNI:

  1. 网络隔离:需要将 RDMA 流量与 Kubernetes 管理/业务流量完全物理隔离。
  2. 独立 IP 地址:需要为 RDMA 网卡分配独立的 IP 地址(例如使用 IPoIB 或 RoCEv2 路由模式),以便应用通过特定 IP 进行绑定。
  3. SR-IOV 场景:如果使用 sriov-cni 将物理网卡虚拟化为 VF 直接透传给 Pod,通常需要 Multus 来管理这个额外的网络接口。

6.3 结论

对于大多数基于 k8s-rdma-shared-dev-plugin 的分布式推理和训练任务,不需要 额外部署 Multus CNI。直接通过 Device Plugin 暴露设备,配合应用层的 RDMA 库即可实现高性能通信。引入 Multus 会显著增加网络配置的复杂度,应仅在确有必要(如强隔离、SR-IOV)时使用。

7. 小结

本文系统性地梳理了在 Kubernetes 集群中落地 RDMA 技术的完整路径。从基础概念的认知,到硬件环境的准备,再到 Device Plugin 的部署与配置,最后通过实际的性能压测验证了 RDMA 性能的优越性。

核心要点回顾:

  1. 技术价值:RDMA 通过零拷贝和内核旁路技术,解决了 TCP/IP 网络在高带宽场景下的 CPU 瓶颈和延迟问题,是释放 GPU 集群算力的关键基础设施。
  2. 部署策略:对于大多数 AI 推理和训练场景,使用 k8s-rdma-shared-dev-plugin 配合 Shared 模式是最轻量级的方案。它无需复杂的网络改造,即可让 Pod 获得原生 RDMA 能力。
  3. 架构决策:在引入 Multus CNI 之前应审慎评估。如果不需要物理隔离或独立 IP,直接复用 Host 网络栈(通过 Device Plugin 暴露设备)往往能带来更低的操作复杂度和更高的稳定性。

随着云原生 AI 的持续演进,底层网络设施的性能优化将变得愈发重要。掌握 RDMA 在 Kubernetes 中的配置与管理,将成为构建高性能 AI 平台的必备技能。未来,我们还可以进一步探索 GPUDirect RDMA、SR-IOV 等进阶技术,以应对更大规模、更低延迟的计算挑战。

深入 Longhorn 高可用:数据如何在节点故障时依然安全可靠

2025-11-05 06:00:00

Longhorn HA

在云原生时代,存储的高可用性是生产环境的生命线。一个设计良好的存储系统,不仅要能在节点故障时保证数据不丢失,还要做到业务无感知、自动恢复。

本文将深入剖析 Longhorn 的高可用机制:从两层架构设计到 iSCSI 协议的巧妙运用,从多副本写入到 Raft 共识算法,再到自动故障恢复流程。通过理论分析和实战演示,带你彻底理解 Longhorn 如何在分布式环境中实现数据的高可用性

📚 系列文章:本文是 Longhorn 系列的第二篇,重点剖析高可用原理。如果你还不了解 Longhorn 的基本概念和部署方法,建议先阅读上一篇:云原生分布式存储系统:Longhorn 初体验

1. 架构设计

要理解 Longhorn 的高可用机制,首先需要了解其架构设计。好的架构是高可用的基础。

1.1 两层架构

Longhorn 采用了清晰的分层架构设计:

Longhorn Arch

Longhorn 设计为两层结构:

数据平面(Data Plane)

  • Longhorn Engine:存储控制器,每个卷(Volume)一个独立的 Engine
  • Replica:数据副本,负责将数据持久化到物理磁盘

控制平面(Control Plane)

  • Longhorn Manager:核心控制器,负责调度、管理和监控

这种设计的优势在于:

  • 故障隔离:每个 Volume 独立的 Engine,一个卷的问题不会影响其他卷
  • 灵活调度:Manager 可以根据节点资源动态调度 Engine 和 Replica
  • 易于扩展:数据平面和控制平面解耦,便于独立扩展

1.2 核心组件

在 Kubernetes 中,Longhorn 的核心组件包括:

Longhorn Engine

  • 作为 Pod 运行在需要使用卷的节点上
  • 每个 Volume 对应一个独立的 Engine 实例
  • 负责处理所有 I/O 操作,协调多副本数据同步

Longhorn Replica

  • 作为独立进程运行,存储卷数据的完整副本
  • 默认创建 3 个副本,分布在不同节点上
  • 负责将数据持久化到节点的物理磁盘(默认路径 /var/lib/longhorn

Longhorn Manager

  • 以 DaemonSet 形式运行在每个节点上
  • 负责卷的创建、删除、调度和健康监控
  • 提供 API 供 CSI Driver 调用

2. 工作流程详解

了解了 Longhorn 的架构设计后,让我们通过一个完整的实例,从 PVC 创建到 Pod 使用,详细了解 Longhorn 的工作流程。

2.1 StorageClass

Longhorn 部署完成后,会自动创建两个 StorageClass:

1
kubectl get sc

输出示例:

1
2
3
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
longhorn (default) driver.longhorn.io Delete Immediate true 15h
longhorn-static driver.longhorn.io Delete Immediate true 15h

2.2 创建测试 Pod

让我们通过一个实际例子来演示 Longhorn 的完整工作流程。创建一个使用 Longhorn PVC 的 MariaDB Pod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
 name: mariadb-pvc
 namespace: default
spec:
 storageClassName: longhorn  # 使用 Longhorn StorageClass
 accessModes:
 - ReadWriteOnce
 resources:
 requests:
 storage: 5Gi
---
apiVersion: v1
kind: Pod
metadata:
 name: mariadb-pod
 namespace: default
spec:
 containers:
 - name: mariadb
 image: bitnami/mariadb:latest
 env:
 - name: MYSQL_ROOT_PASSWORD
 value: "your-password"
 ports:
 - containerPort: 3306
 volumeMounts:
 - name: mariadb-data
 mountPath: /bitnami/mariadb
 volumes:
 - name: mariadb-data
 persistentVolumeClaim:
 claimName: mariadb-pvc

2.3 卷创建流程(Provisioning)

当创建 PVC 时,会触发以下流程:

Step 1:PVC 创建

  • 用户创建 PVC,指定使用 longhorn StorageClass

Step 2:CSI Driver 响应

  • Longhorn CSI Driver 监听到 PVC 创建事件
  • 调用 Longhorn Manager 的 API,请求创建 Longhorn Volume

Step 3:Volume CRD 创建

  • Longhorn Manager 创建对应的 Volume CRD 对象
  • 持续监听 API Server 的响应
1
2
# 查看创建的 Volume CRD
kubectl -n longhorn-system get volumes

输出示例:

1
2
NAME DATA ENGINE STATE ROBUSTNESS SCHEDULED SIZE NODE AGE
pvc-7aa74a8d-fa4c-4668-b11c-fc8a827034db v1 attached healthy 5368709120 node-1 2m

Step 4:Engine 和 Replica 调度

  • Longhorn Manager 监听到 Volume CRD 创建
  • 在 Pod 所在节点创建 Longhorn Engine(专用存储控制器)
  • 在多个不同节点创建指定数量的 Replica(默认 3 个)

2.4 卷挂载流程(Attach & Mount)

块设备的挂载分为两个阶段:

阶段 1:Attach(附加到节点)

当 Pod 被调度到某个节点时:

  1. 该节点的 Kubelet 通过 Longhorn CSI Driver 调用 Longhorn Manager
  2. Manager 确保 Volume 已准备就绪
  3. CSI Driver 通过 iSCSI 协议在节点上模拟出一个块设备

查看模拟的块设备:

1
ll /dev/longhorn/

输出示例:

1
2
3
4
total 0
drwxr-xr-x 2 root root 60 Oct 15 10:35 ./
drwxr-xr-x 20 root root 4380 Oct 15 15:36 ../
brw-rw---- 1 root disk 8, 16 Oct 15 10:35 pvc-a63baecf-4a01-480e-8c3d-6dac2a496265

注意文件类型为 b(block),表示这是一个块设备

阶段 2:Mount(挂载到 Pod)

Kubelet 将该块设备格式化为文件系统(如 ext4、xfs),并挂载到 Pod 的指定路径。

小结: 通过 Attach 和 Mount 两个阶段,Longhorn 成功将卷挂载到 Pod 中。其中,iSCSI 协议模拟的块设备是实现高可用的关键,它使得 Longhorn 能够拦截所有 I/O 操作,从而实现多副本同步。接下来我们将深入剖析这个高可用机制。

3. 高可用实现原理

3.1 数据写入流程

这是 Longhorn 高可用的核心机制。当应用写入数据时,经历以下流程:

核心思想: 通过 iSCSI 协议模拟块设备,将所有 I/O 操作拦截并转发给 Engine,由 Engine 使用 Raft 算法同步到多个 Replica,实现数据高可用。

longhorn-iscsi-work-flow

详细步骤:

Step 1:应用写入

  • Pod 中的应用(如 MariaDB)向挂载的目录写入数据
  • 例如:写入 /bitnami/mariadb/data.db

Step 2:文件系统层

  • 数据经过文件系统(ext4/xfs)处理

Step 3:块设备层

  • 文件系统将数据写入块设备 /dev/longhorn/xxx

Step 4:iSCSI 协议转发

  • 因为这是一个模拟的块设备,写入请求不会直接到达磁盘
  • 而是通过 iSCSI 协议转发给 Longhorn Engine

Step 5:Engine 多副本复制

  • Engine 接收到写入请求
  • 使用 Raft 共识算法将数据同步复制到所有 Replica
  • 必须获得多数副本(3个中的2个)的确认后,才返回写入成功

Step 6:Replica 持久化

  • 每个 Replica 将数据持久化到其所在节点的物理磁盘
  • 默认路径:/var/lib/longhorn/replicas/<volume-name>-<id>/

关键要点:

  • 📍 iSCSI 拦截:通过 iSCSI 协议模拟块设备,实现数据拦截和转发
  • 🔄 Raft 共识:使用 Raft 算法确保多副本强一致性
  • 多数派确认:需要 2/3 副本确认才返回写入成功(3副本中至少2个)
  • 🛡️ 分布式部署:三副本分布在不同节点,防止单点故障

3.2 关键技术点

从上面的数据写入流程图可以看出,Longhorn 的高可用实现依赖以下关键技术:

1) iSCSI 协议的巧妙运用

与传统存储(如 Ceph)不同,Longhorn 通过 iSCSI 协议模拟了一个块设备,从而能够:

  • 拦截所有 I/O 操作:应用的写入不会直接到达磁盘
  • 统一数据流向:所有数据必须经过 Engine 处理
  • 实现多副本同步:Engine 可以控制数据的复制流程

2) Raft 共识算法

Longhorn 使用 Raft 算法确保数据一致性:

  • 强一致性:写入必须获得多数副本确认
  • 容错能力:N 个副本可以容忍 (N-1)/2 个副本故障
  • 自动选主:Engine 故障时可以自动选举新的 Leader

3) 数据副本分布

1
2
# 查看副本分布
kubectl -n longhorn-system get replicas

Longhorn Manager 会智能调度,确保:

  • 同一卷的副本分布在不同节点上
  • 避免单点故障风险
  • 考虑节点的资源使用情况

4. 故障恢复机制

通过多副本和 Raft 共识算法,Longhorn 实现了数据的高可用性。但真正考验存储系统可靠性的,是当节点故障时的自动恢复能力。本章将详细介绍 Longhorn 如何检测故障、降级运行,并自动重建副本。

4.1 故障检测

健康监控

  • Longhorn Manager 持续监控所有节点和组件的健康状态
  • Engine 与所有 Replica 保持心跳检测
  • 当节点宕机或网络分区时,快速检测到连接丢失

4.2 降级运行

系统降级

  • 发现副本故障后,卷状态变为 Degraded(降级)
  • 但卷仍然继续可用,业务不中断
  • 原因:Raft 算法允许多数副本(N/2 + 1)存活时继续工作

举例:3 副本的卷

  • 1 个副本故障:剩余 2 个副本形成多数派,继续服务
  • 2 个副本故障:只剩 1 个副本,无法形成多数派,只读模式

4.3 自动重建

重建流程

Step 1:检测故障

1
2
# 查看卷状态变为 Degraded
kubectl -n longhorn-system get volumes

Step 2:调度新副本

  • Longhorn Manager 在健康节点上调度新 Replica
  • 选择资源充足且负载较低的节点

Step 3:数据同步

  • 新 Replica 启动后,从健康副本同步数据
  • 增量同步:只同步差异数据,加快恢复速度
  • 同步过程中,卷保持可用状态

Step 4:恢复健康

  • 数据同步完成后,新副本加入 Raft 组
  • 副本数恢复到预设值(如 3 个)
  • 卷状态从 Degraded 恢复为 Healthy

查看恢复进度:

1
2
3
# 通过 Web UI 查看
# 或使用命令行
kubectl -n longhorn-system describe volume <volume-name>

4.4 故障恢复示例

模拟节点故障:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1. 查看副本分布
kubectl -n longhorn-system get replicas -o wide

# 2. 关闭一个节点
# (例如:node-2)

# 3. 观察卷状态变化
kubectl -n longhorn-system get volumes -w

# 4. 查看新副本创建
kubectl -n longhorn-system get replicas -w

恢复时间线:

  • 0s:节点故障
  • ~10s:Longhorn 检测到故障,卷状态变为 Degraded
  • ~30s:新副本开始在其他节点上创建
  • ~5min:数据同步完成(取决于数据量)
  • 完成:卷状态恢复为 Healthy

5. 高可用优势

通过前面的分析,我们深入了解了 Longhorn 的高可用实现机制。那么,与其他存储方案相比,Longhorn 的优势在哪里?它适合什么样的场景?本章将进行详细对比分析。

5.1 与其他方案对比

特性 NFS Ceph Longhorn
高可用性 ❌ 单点故障 ✅ 高可用 ✅ 高可用
自动故障恢复 ❌ 需手动处理 ✅ 自动恢复 ✅ 自动恢复
部署复杂度 ✅ 简单 ❌ 复杂 ✅ 简单
资源占用 ✅ 低 ❌ 高 ✅ 中等
数据一致性 ⚠️ 弱一致性 ✅ 强一致性 ✅ 强一致性(Raft)
故障容忍度 0 N/2 N/2

5.2 核心优势总结

1. 微服务架构

  • 每个 Volume 独立的 Engine,故障隔离
  • 便于扩展和维护

2. 强一致性保证

  • Raft 共识算法确保数据一致性
  • 多数派写入机制

3. 自动故障恢复

  • 无需人工干预
  • 业务无感知
  • 快速数据重建

4. 云原生设计

  • 深度集成 Kubernetes
  • 使用 CRD 和 Operator 模式
  • 声明式 API

6. 小结

本文从架构、原理到实战,全面剖析了 Longhorn 如何实现云原生存储的高可用性。通过数据写入流程图和故障恢复示例,我们深入理解了其背后的核心技术机制:

架构层面

  • 采用两层架构设计(数据平面 + 控制平面),职责清晰
  • 每个 Volume 独立的 Engine,实现故障隔离
  • 多副本设计,默认 3 副本分布在不同节点

技术实现

  • 巧妙运用 iSCSI 协议模拟块设备,拦截所有 I/O 操作
  • 使用 Raft 共识算法确保多副本数据强一致性
  • 写入需要多数副本确认(3个中的2个),平衡性能与可靠性

高可用保证

  • 数据多副本:默认 3 副本,可容忍 1 个节点故障
  • 自动故障检测:持续监控组件健康状态
  • 降级运行:副本故障时卷仍可用,业务不中断
  • 自动恢复:在健康节点自动重建副本,增量同步数据

适用场景

  • 中小规模生产环境:比 Ceph 轻量,比 NFS 可靠
  • 对数据可靠性要求高:强一致性保证
  • 希望运维简单:自动故障恢复,无需人工干预

总结

Longhorn 通过精心的架构设计和成熟的分布式算法,在简单性、可靠性和性能之间取得了完美平衡。它的高可用机制不仅理论上完善(Raft 共识、多副本复制),更在实践中证明了可靠性(自动故障恢复、降级运行)。

对于追求"简单可靠"的中小规模 Kubernetes 集群来说,Longhorn 无疑是一个"刚刚好"的云原生存储解决方案——既不会像 NFS 那样让你担心数据安全,也不会像 Ceph 那样让你头疼运维复杂度

7. 参考资料

云原生分布式存储系统:Longhorn 初体验

2025-10-15 06:00:00

Longhorn

本文将介绍云原生分布式存储系统 Longhorn,包括其核心概念、架构原理,以及如何在 Kubernetes 集群中部署和使用。

1. 概述

1.1 Longhorn 简介

Longhorn 是一个开源的、云原生的分布式块存储系统,专为 Kubernetes 环境设计。它最初由 Rancher Labs 开发,现已成为云原生计算基金会(CNCF)的孵化项目。

主要特点:

  • 轻量级部署,易于安装和使用
  • 分布式架构,支持数据高可用
  • 云原生设计,完美集成 Kubernetes
  • 支持快照、备份和恢复
  • 提供友好的 Web UI 管理界面

1.2 适用场景

Longhorn 适用于以下场景:

  • 轻量级生产环境:相比 Ceph 等重量级方案更易部署和维护
  • 高可用需求:相比单点 NFS 提供更好的可靠性
  • 中小规模集群:在存储性能和运维复杂度之间取得平衡

2. 核心架构

2.1 整体架构

Longhorn Arch

Longhorn 的核心设计理念是为每个 Volume 创建独立的 Engine 进行管理,Engine 负责将 Volume 复制到多个节点,以实现高可用存储。

为什么为每个 Volume 创建一个独立的 Engine?

这样可以保证当某个 Engine 出现问题时,其他 Volume 不受影响,实现了故障隔离。

2.2 核心组件

Longhorn 主要包含以下核心组件:

控制平面组件:

  • longhorn-manager:核心控制器(DaemonSet),运行在每个节点,负责卷的创建、删除、调度等管理工作
  • longhorn-ui:Web 管理界面,提供可视化的存储管理功能

数据平面组件:

  • instance-manager:每个节点运行一个,管理该节点上所有的 Engine 和 Replica 实例
  • engine-image:Longhorn 引擎的可执行镜像

CSI 组件(Kubernetes 官方):

  • csi-provisioner:负责动态创建 PV
  • csi-attacher:负责将卷附加到节点
  • csi-resizer:负责卷的扩容
  • csi-snapshotter:负责快照功能
  • csi-plugin:CSI 驱动插件(DaemonSet),每个节点运行一个

2.3 数据存储与高可用

数据存放位置:Longhorn 使用集群节点的本地磁盘(默认 /var/lib/longhorn)组成分布式存储池,数据副本分布在这些节点的磁盘上。

1
2
3
root@node1:/var/lib/longhorn/replicas# ls
pvc-19d50ca0-780d-4073-bb72-36a7ffe9e3e2-1084ec0e
pvc-4f0bf5ea-0dc8-49be-8a3f-559f95416092-4c99b141

高可用原理

  • 默认为每个卷创建 3 个副本(可配置)
  • Longhorn Manager 自动将副本调度到不同节点,防止单点故障
  • 当节点或磁盘故障导致副本失效时,自动在健康节点上重建副本

3. 环境准备

3.1 系统要求

安装 Longhorn 前需要确保环境满足以下要求:

完整要求参考:https://longhorn.io/docs/1.7.3/deploy/install/#installation-requirements

  • Kubernetes >= 1.21
  • 所有节点需要:
    • 安装并运行 open-iscsi
    • 安装 NFSv4 client
    • 主机文件系统使用 ext4 或 XFS
    • 安装 bashcurlfindmntgrepawkblkidlsblk 等命令
    • 开启 Mount propagation

3.2 依赖安装说明

Longhorn 需要以下依赖:

1) open-iscsi

  • 用途:提供 iSCSI 协议支持,Longhorn 通过 iSCSI 将卷附加到节点
  • 安装:apt-get install open-iscsi(Ubuntu/Debian)

2) NFS 客户端

  • 用途:Longhorn 的备份功能需要 NFSv4/v4.1/v4.2,RWX 卷需要 NFSv4.1
  • 安装:apt-get install nfs-common(Ubuntu/Debian)

3) Mount Propagation

  • 用途:允许容器挂载的卷共享给同一 Pod 或节点的其他容器
  • Kubernetes 默认已开启,无需配置

注意:第4章会介绍使用 DaemonSet 自动安装这些依赖的方式(推荐),这里仅作说明。如需手动安装,参考 官方文档

4. 部署安装

4.1 环境初始化

使用 DaemonSet 自动安装依赖(推荐)

官方提供了 DaemonSet 方式自动在所有节点安装依赖,无需手动登录每个节点操作:

1
2
3
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.9.1/deploy/prerequisite/longhorn-iscsi-installation.yaml

kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.9.1/deploy/prerequisite/longhorn-nfs-installation.yaml

检查安装状态

使用以下脚本检查初始化是否成功:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash
echo "正在检查 Longhorn 安装 Pod 的日志..."
echo "=================================="

# 检查 iscsi-installation
echo "检查 ISCSI 安装:"
pods=$(kubectl -n longhorn-system get pods -l app=longhorn-iscsi-installation -o name)
for pod in $pods; do
 if kubectl -n longhorn-system logs $pod -c iscsi-installation | grep -q "install successfully"; then
 echo "✅ $pod: ISCSI 初始化成功"
 else
 echo "❌ $pod: ISCSI 初始化未成功或日志未找到成功信息"
 fi
done

echo "----------------------------------"

# 检查 nfs-installation
echo "检查 NFS 安装:"
pods=$(kubectl -n longhorn-system get pods -l app=longhorn-nfs-installation -o name)
for pod in $pods; do
 if kubectl -n longhorn-system logs $pod -c nfs-installation | grep -q "install successfully"; then
 echo "✅ $pod: NFS 初始化成功"
 else
 echo "❌ $pod: NFS 初始化未成功或日志未找到成功信息"
 fi
done

echo "=================================="
echo "检查完成。"

日志中输出 iscsi install successfullynfs install successfully 表示安装成功。

4.2 使用 Helm 安装 Longhorn

推荐使用 Helm 进行安装:

1
2
3
4
5
6
7
8
9
# 添加 Helm 仓库
helm repo add longhorn https://charts.longhorn.io
helm repo update

# 安装 Longhorn
helm install longhorn longhorn/longhorn \
 --namespace longhorn-system \
 --create-namespace \
 --version 1.7.3

注意:1.8.2 和 1.9.1 版本需要 Kubernetes 1.25+,1.7.3 只需要 1.21+

4.3 验证安装

1) 检查 Pod 状态

1
kubectl -n longhorn-system get pod

正常情况下,所有 Pod 应处于 Running 状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
NAME READY STATUS RESTARTS AGE
csi-attacher-6bc8956d47-67fp6 1/1 Running 0 52m
csi-attacher-6bc8956d47-nb869 1/1 Running 0 52m
csi-attacher-6bc8956d47-tnchc 1/1 Running 0 52m
csi-provisioner-b65c98945-5knc2 1/1 Running 0 52m
csi-provisioner-b65c98945-897p7 1/1 Running 0 52m
csi-provisioner-b65c98945-q9pf5 1/1 Running 0 52m
csi-resizer-7cd74679f-7wn4v 1/1 Running 0 52m
csi-resizer-7cd74679f-p2m2h 1/1 Running 0 52m
csi-resizer-7cd74679f-qkdl6 1/1 Running 0 52m
csi-snapshotter-74db7b474-6n2tm 1/1 Running 0 52m
csi-snapshotter-74db7b474-cwbjd 1/1 Running 0 52m
csi-snapshotter-74db7b474-cx5fs 1/1 Running 0 52m
engine-image-ei-d91f5974-7xlhz 1/1 Running 0 53m
instance-manager-290ed8a6055485f167480d21dec82fb9 1/1 Running 0 52m
longhorn-csi-plugin-94xjf 3/3 Running 1 52m
longhorn-driver-deployer-6fc5d44f84-nmqdl 1/1 Running 0 53m
longhorn-manager-rpkbn 2/2 Running 0 53m
longhorn-ui-5b45479667-8dmb2 1/1 Running 0 53m

5. Demo

5.1 创建 PVC 并使用

部署完成后,默认会创建两个 StorageClass:

1
kubectl get sc

输出示例:

1
2
3
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
longhorn (default) driver.longhorn.io Delete Immediate true 53m
longhorn-static driver.longhorn.io Delete Immediate true 53m

1) 创建 PVC

使用 Longhorn 存储类创建 PVC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
 name: longhorn-pvc
spec:
 accessModes:
 - ReadWriteOnce
 storageClassName: longhorn
 resources:
 requests:
 storage: 5Gi

2) 创建 Pod 使用 PVC

创建一个 Pod 挂载使用该 PVC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
 name: test-longhorn
spec:
 containers:
 - name: nginx
 image: nginx:latest
 volumeMounts:
 - name: longhorn-vol
 mountPath: /data
 volumes:
 - name: longhorn-vol
 persistentVolumeClaim:
 claimName: longhorn-pvc

部署后可以进入 Pod 验证挂载:

1
2
3
4
5
6
7
8
9
# 进入 Pod
kubectl exec -it test-longhorn -- bash

# 查看挂载点
df -h | grep /data

# 写入测试数据
echo "Hello Longhorn" > /data/test.txt
cat /data/test.txt

5.2 访问 Web UI

Longhorn 提供了 Web UI 用于管理和监控。

修改 Service 为 NodePort 类型

默认情况下,longhorn-frontend Service 是 ClusterIP 类型,需要修改为 NodePort:

1
kubectl -n longhorn-system patch svc longhorn-frontend -p '{"spec":{"type":"NodePort"}}'

查看分配的端口:

1
kubectl -n longhorn-system get svc longhorn-frontend

输出示例:

1
2
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
longhorn-frontend NodePort 10.96.123.456 <none> 80:30123/TCP 1h

通过任意节点 IP + NodePort 访问 Web UI:http://<node-ip>:30123

Web UI 界面

Longhorn Web UI

在 Web UI 中可以:

  • 查看和管理卷(Volume)
  • 查看节点和磁盘状态
  • 创建快照和备份
  • 监控存储使用情况

6. 小结

本文全面介绍了 Longhorn 这款云原生分布式存储系统,从架构原理到实际部署使用,涵盖了以下内容:

  • 核心架构:采用微服务架构,每个卷独立运行 Engine 和 Replica,实现故障隔离;详细说明了各核心组件的作用
  • 环境准备:系统要求和依赖说明(open-iscsi、NFS 客户端等)
  • 部署安装:通过 DaemonSet 自动安装依赖,使用 Helm 快速部署 Longhorn
  • 使用配置:创建 PVC、配置副本数、PVC 扩容、访问 Web UI 等实际操作
  • 常见问题:Pod 启动失败、数据库初始化、节点故障恢复、卸载清理等常见问题的解决方案

存储方案选型建议

在 Kubernetes 环境中,不同场景适合不同的存储方案:

测试/开发环境 → NFS

  • ✅ 部署最简单,一台 NFS Server 即可
  • ✅ 配置方便,无需额外组件
  • ❌ 单点故障风险,可靠性较低
  • ❌ 性能一般,不适合生产环境

大型生产环境 → Ceph

  • ✅ 高性能、高可用、高扩展性
  • ✅ 功能强大,支持对象存储、块存储、文件存储
  • ❌ 架构复杂,运维成本高
  • ❌ 资源占用大,需要专业团队维护
  • ❌ 对于小规模集群过于"重量级"

中小规模生产环境 → Longhorn

  • ✅ 比 Ceph 轻量级,部署运维简单
  • ✅ 比 NFS 可靠,支持副本和高可用
  • ✅ 云原生设计,与 Kubernetes 深度集成
  • ✅ Web UI 友好,易于管理和监控
  • ⚠️ 性能不如 Ceph,但满足大多数场景需求

总结:如果你的生产环境规模不大(几十个节点以内),又希望在存储可靠性和运维复杂度之间取得平衡,Longhorn 是最佳选择。它既保证了生产级的可靠性,又避免了 Ceph 的复杂度,是"刚刚好"的分布式存储方案。

7. 参考资料

Volcano vGPU实战:无硬件依赖的Kubernetes GPU共享与隔离方案

2025-08-20 06:00:00

volcano-vgpu.png

在上一篇《Volcano初探:批处理调度引擎的云原生实践》中,我们通过Helm快速部署了Volcano集群,并成功运行了首个测试任务,验证了其基础调度能力。本文将进一步探索Volcano的GPU虚拟化功能,聚焦如何通过HAMi vGPU 技术实现GPU资源的细粒度共享与硬隔离。

批处理调度引擎 Volcano 支持 GPU 虚拟化功能,该功能主要由 HAMi 提供。

HAMi vGPU 提供的 GPU 虚拟化包括 HAMi-Core 和 Dynamic MIG 两种模式:

Mode Isolation MIG GPU Required Annotation Core/Memory Control Recommended For
HAMI-core Software (VCUDA) No No Yes General workloads
Dynamic MIG Hardware Yes Yes MIG-controlled Performance-sensitive jobs

如果硬件支持 MIG 同时运行的是性能敏感型任务,那么推荐使用 Dynamic MIG 模型,不支持 MIG 依旧可以使用更加通用,对硬件无要求的 HAMi-Core 模式。

本文主要以 HAMi-Core 进行演示,HAMi vGPU 如何集成到 Volcano。

使用流程:

  • 1)创建集群

  • 2)安装 GPU-Operator,但是不安装 DevicePlugin

  • 3)安装 Volcano,并配置开启 vGPU 插件

  • 4)安装 volcano-vgpu-device-plugin

  • 5)验证

1. 环境准备

1.1 创建集群

使用 KubeClipper 部署一个集群进行验证。

Kubernetes教程(十一)—使用 KubeClipper 通过一条命令快速创建 k8s 集群

1.2 GPU-Operator

参考之前的文章 GPU 环境搭建指南:使用 GPU Operator 加速 Kubernetes GPU 环境搭建,使用 GPU Operator 部署环境。

1.3 Volcano

部署 Volcano

安装 Volcano,部署时需要注意 volcano 和 k8s 的版本兼容性问题,参考官方 README:Kubernetes compatibility

这里部署的 v1.12.0 版本

1
2
3
4
5
6
7
# 添加仓库
helm repo add volcano-sh https://volcano-sh.github.io/helm-charts

helm repo update

# 部署
helm upgrade --install volcano volcano-sh/volcano --version 1.12.0 -n volcano-system --create-namespace

部署完成后 Pod 列表如下:

1
2
3
4
5
root@node5-3:~# kubectl -n volcano-system get po
NAME READY STATUS RESTARTS AGE
volcano-admission-6444dd4fb7-8s8d9 1/1 Running 0 3m
volcano-controllers-75d5b78c7-llcrz 1/1 Running 0 3m
volcano-scheduler-7d46c5b5db-t2k42 1/1 Running 0 3m

修改调度器配置:开启 deviceshare 插件

Volcano 部署完成之后,我们需要编辑调度器配置,开启 deviceshare 插件。

1
kubectl edit cm -n volcano-system volcano-scheduler-configmap

完整内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kind: ConfigMap
apiVersion: v1
metadata:
 name: volcano-scheduler-configmap
 namespace: volcano-system
data:
 volcano-scheduler.conf: |
 actions: "enqueue, allocate, backfill"
 tiers:
 - plugins:
 - name: priority
 - name: gang
 - name: conformance
 - plugins:
 - name: drf
 - name: deviceshare
 arguments:
 deviceshare.VGPUEnable: true # enable vgpu
 deviceshare.SchedulePolicy: binpack # scheduling policy. binpack / spread
 - name: predicates
 - name: proportion
 - name: nodeorder
 - name: binpack

核心如下:

1
2
3
4
 - name: deviceshare
 arguments:
 deviceshare.VGPUEnable: true # enable vgpu
 deviceshare.SchedulePolicy: binpack  # scheduling policy. binpack / spread

开启 vgpu 同时调度策略我们选择 binpack。

HAMi 调度策略可以阅读这篇文章:HAMi vGPU 原理分析 Part4:Spread&Binpack 高级调度策略实现

修改后,不需要重启,Volcano 会自动检测,当文件变化后自动 reload。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
if opt.SchedulerConf != "" {
 var err error
 path := filepath.Dir(opt.SchedulerConf)
 watcher, err = filewatcher.NewFileWatcher(path)
 if err != nil {
 return nil, fmt.Errorf("failed creating filewatcher for %s: %v", opt.SchedulerConf, err)
 }
}

func (pc *Scheduler) watchSchedulerConf(stopCh <-chan struct{}) {
 if pc.fileWatcher == nil {
 return
 }
 eventCh := pc.fileWatcher.Events()
 errCh := pc.fileWatcher.Errors()
 for {
 select {
 case event, ok := <-eventCh:
 if !ok {
 return
 }
 klog.V(4).Infof("watch %s event: %v", pc.schedulerConf, event)
 if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
 pc.loadSchedulerConf()
 pc.cache.SetMetricsConf(pc.metricsConf)
 }
 case err, ok := <-errCh:
 if !ok {
 return
 }
 klog.Infof("watch %s error: %v", pc.schedulerConf, err)
 case <-stopCh:
 return
 }
 }
}

不过 k8s 将 Configmap 同步到 Pod 中也是有延迟的,不想等的话也可以手动重启下。

1
kubectl -n volcano-system rollout restart deploy volcano-scheduler

1.4 volcano-vgpu-device-plugin

接下来我们部署和 Volcano 集成用到的 DevicePlugin:volcano-vgpu-device-plugin

DevicePlugin 原理可以阅读这篇文章:HAMi vGPU 原理分析 Part1:hami-device-plugin-nvidia 实现,大致逻辑都是一样的。

部署 DevicePlugin

从项目 volcano-vgpu-device-plugin 根目录获取文件: volcano-vgpu-device-plugin.yml

1
wget https://github.com/Project-HAMi/volcano-vgpu-device-plugin/blob/main/volcano-vgpu-device-plugin.yml

部署

1
kubectl apply -f volcano-vgpu-device-plugin.yml

查看 Pod 列表:

1
2
root@node5-3:~# kubectl -n kube-system get po
volcano-device-plugin-xkwzd 2/2 Running 0 10m

验证 Node 资源

查看 Node 上的 Resource 信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@node5-3:~/lixd# k describe node node5-3 |grep Cap -A 10
Capacity:
 cpu: 160
 ephemeral-storage: 3750157048Ki
 hugepages-1Gi: 0
 hugepages-2Mi: 0
 memory: 2113442544Ki
 nvidia.com/gpu: 0
 pods: 110
 volcano.sh/vgpu-cores: 800
 volcano.sh/vgpu-memory: 39312
 volcano.sh/vgpu-number: 80

Volcano 新增了下面三个:

1
2
3
 volcano.sh/vgpu-cores: 800
 volcano.sh/vgpu-memory: 39312
 volcano.sh/vgpu-number: 80
  • volcano.sh/vgpu-cores: 800:每张 GPU 100 core,8 卡正好 800 core。

  • volcano.sh/vgpu-memory: 39312 :由于设置了 factor=10,因此实际代表总显存 39312 * 10 = 393120 MB。

    • 当前环境是 L40S*8,单卡显存 49140,49140 * 8 = 393120,正好符合,说明一切正常。
  • volcano.sh/vgpu-number: 80:默认 --device-split-count=10,将 GPU 数量扩大了 10 倍。

说明插件部署成功。

2. 简单使用

启动 Pod

首先启动一个简单 Pod

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
 name: test1
spec:
 restartPolicy: OnFailure
 schedulerName: volcano
 containers:
 - image: ubuntu:24.04
 name: pod1-ctr
 command: ["sleep"]
 args: ["100000"]
 resources:
 limits:
 volcano.sh/vgpu-memory: 1024
 volcano.sh/vgpu-number: 1

查看效果,vgpu-memory 申请的 1024,如下:

但是因为 factor=10,所以实际是 10240 MB。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
root@node5-3:~/lixd# k exec -it test1 -- nvidia-smi
[HAMI-core Msg(16:140249737447232:libvgpu.c:838)]: Initializing.....
Tue Jul 22 13:52:58 2025
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.161.08 Driver Version: 535.161.08 CUDA Version: 12.2 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 NVIDIA L40S On | 00000000:91:00.0 Off | Off |
| N/A 28C P8 34W / 350W | 0MiB / 10240MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| No running processes found |
+---------------------------------------------------------------------------------------+
[HAMI-core Msg(16:140249737447232:multiprocess_memory_limit.c:499)]: Calling exit handler 16

启动 Volcano Job

启动一个简单的 Volcano Job 试试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
 name: simple-vgpu-training
spec:
 schedulerName: volcano
 minAvailable: 3 # Gang Scheduling: 确保3个Pod同时启动
 
 tasks:
 - name: worker
 replicas: 3 # 启动2个Worker
 template:
 spec:
 restartPolicy: OnFailure
 containers:
 - name: python-trainer
 image: python:3.9-slim
 command: ["python", "-c"]
 args:
 - |
 # 简化的训练代码
 import os
 import time
 
 worker_id = os.getenv("VC_TASK_INDEX", "0")
 print(f"Worker {worker_id} started with vGPU")
 
 # 模拟训练过程
 for epoch in range(1, 10):
 time.sleep(6)
 print(f"Worker {worker_id} completed epoch {epoch}")
 
 print(f"Worker {worker_id} finished training!")
 env:
 # 获取任务索引 (0,1,...)
 - name: VC_TASK_INDEX
 valueFrom:
 fieldRef:
 fieldPath: metadata.annotations['volcano.sh/task-index']
 resources:
 limits:
 volcano.sh/vgpu-memory: 1024 # 每个Worker分配1024MB显存
 volcano.sh/vgpu-number: 1 # 每个Worker1个vGPU
 cpu: "1"
 memory: "1Gi"

一切正常:

1
2
3
4
5
root@node5-3:~/lixd# k get po -w
NAME READY STATUS RESTARTS AGE
simple-vgpu-training-worker-0 1/1 Running 0 5s
simple-vgpu-training-worker-1 1/1 Running 0 5s
simple-vgpu-training-worker-2 1/1 Running 0 5s

查看 Pod 中的 GPU 信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
root@node5-3:~/lixd# k exec -it simple-vgpu-training-worker-0 -- nvidia-smi
[HAMI-core Msg(7:140498435086144:libvgpu.c:838)]: Initializing.....
Tue Jul 22 15:02:36 2025
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.161.08 Driver Version: 535.161.08 CUDA Version: 12.2 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 NVIDIA L40S On | 00000000:91:00.0 Off | Off |
| N/A 28C P8 34W / 350W | 0MiB / 10240MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| No running processes found |
+---------------------------------------------------------------------------------------+
[HAMI-core Msg(7:140498435086144:multiprocess_memory_limit.c:499)]: Calling exit handler 7

日志

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@node5-3:~/lixd# k logs -f simple-vgpu-training-worker-2
Worker 2 started with vGPU
Worker 2 completed epoch 1
Worker 2 completed epoch 2
Worker 2 completed epoch 3
Worker 2 completed epoch 4
Worker 2 completed epoch 5
Worker 2 completed epoch 6
Worker 2 completed epoch 7
Worker 2 completed epoch 8
Worker 2 completed epoch 9
Worker 2 finished training!

3. 监控

调度器监控

1
curl {volcano scheduler cluster ip}:8080/metrics

包括 GPU core & memory 的分配信息,以及对应 Pod 信息,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# HELP volcano_vgpu_device_allocated_cores The percentage of gpu compute cores allocated in this card
# TYPE volcano_vgpu_device_allocated_cores gauge
volcano_vgpu_device_allocated_cores{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad"} 50

# HELP volcano_vgpu_device_allocated_memory The number of vgpu memory allocated in this card
# TYPE volcano_vgpu_device_allocated_memory gauge
volcano_vgpu_device_allocated_memory{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad"} 2048

# HELP volcano_vgpu_device_core_allocation_for_a_certain_pod The vgpu device core allocated for a certain pod
# TYPE volcano_vgpu_device_core_allocation_for_a_certain_pod gauge
volcano_vgpu_device_core_allocation_for_a_certain_pod{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podName="simple-vgpu-training-worker-0"} 10
volcano_vgpu_device_core_allocation_for_a_certain_pod{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podName="simple-vgpu-training-worker-1"} 10
volcano_vgpu_device_core_allocation_for_a_certain_pod{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podName="simple-vgpu-training-worker-2"} 10
volcano_vgpu_device_core_allocation_for_a_certain_pod{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podName="simple-vgpu-training-worker-3"} 10
volcano_vgpu_device_core_allocation_for_a_certain_pod{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podName="simple-vgpu-training-worker-4"} 10

# HELP volcano_vgpu_device_memory_allocation_for_a_certain_pod The vgpu device memory allocated for a certain pod
# TYPE volcano_vgpu_device_memory_allocation_for_a_certain_pod gauge
volcano_vgpu_device_memory_allocation_for_a_certain_pod{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podName="simple-vgpu-training-worker-0"} 128
volcano_vgpu_device_memory_allocation_for_a_certain_pod{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podName="simple-vgpu-training-worker-1"} 128
volcano_vgpu_device_memory_allocation_for_a_certain_pod{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podName="simple-vgpu-training-worker-2"} 128
volcano_vgpu_device_memory_allocation_for_a_certain_pod{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podName="simple-vgpu-training-worker-3"} 128
volcano_vgpu_device_memory_allocation_for_a_certain_pod{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podName="simple-vgpu-training-worker-4"} 128

# HELP volcano_vgpu_device_memory_limit The number of total device memory in this card
# TYPE volcano_vgpu_device_memory_limit gauge
volcano_vgpu_device_memory_limit{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad"} 4914

# HELP volcano_vgpu_device_shared_number The number of vgpu tasks sharing this card
# TYPE volcano_vgpu_device_shared_number gauge
volcano_vgpu_device_shared_number{NodeName="node5-3",devID="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad"} 5

设备插件监控

直接访问 DevicePlugin Pod 的 9394 端口获取监控信息:

1
curl http://<plugin-pod-ip>:9394/metrics

可以查看到该节点上的 GPU 使用情况,metrics 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# HELP Device_last_kernel_of_container Container device last kernel description
# TYPE Device_last_kernel_of_container gauge
Device_last_kernel_of_container{ctrname="pod1-ctr",deviceuuid="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podname="test1",podnamespace="default",vdeviceid="0",zone="vGPU"} 257114
# HELP Device_memory_desc_of_container Container device meory description
# TYPE Device_memory_desc_of_container counter
Device_memory_desc_of_container{context="0",ctrname="pod1-ctr",data="0",deviceuuid="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",module="0",offset="0",podname="test1",podnamespace="default",vdeviceid="0",zone="vGPU"} 0
# HELP Device_utilization_desc_of_container Container device utilization description
# TYPE Device_utilization_desc_of_container gauge
Device_utilization_desc_of_container{ctrname="pod1-ctr",deviceuuid="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podname="test1",podnamespace="default",vdeviceid="0",zone="vGPU"} 0
# HELP HostCoreUtilization GPU core utilization
# TYPE HostCoreUtilization gauge
HostCoreUtilization{deviceidx="0",deviceuuid="GPU-a11fe6d9-3dbe-8a24-34e9-535b2629babd",zone="vGPU"} 0
HostCoreUtilization{deviceidx="1",deviceuuid="GPU-b82090de-5250-44e2-a5ed-b0efc5763f8f",zone="vGPU"} 0
HostCoreUtilization{deviceidx="2",deviceuuid="GPU-8f563a66-d507-583f-59f1-46c2e97a393c",zone="vGPU"} 0
HostCoreUtilization{deviceidx="3",deviceuuid="GPU-1e5a0632-4332-f4d0-adf2-80ebfed56684",zone="vGPU"} 0
HostCoreUtilization{deviceidx="4",deviceuuid="GPU-384027fd-54f2-638b-cdfe-0d5f3b6630f5",zone="vGPU"} 0
HostCoreUtilization{deviceidx="5",deviceuuid="GPU-dbb95093-0147-7b3a-f468-8a3575a8dd4e",zone="vGPU"} 0
HostCoreUtilization{deviceidx="6",deviceuuid="GPU-f3eb6e71-e90a-bfc9-de06-dff90c3093b9",zone="vGPU"} 0
HostCoreUtilization{deviceidx="7",deviceuuid="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",zone="vGPU"} 0
# HELP HostGPUMemoryUsage GPU device memory usage
# TYPE HostGPUMemoryUsage gauge
HostGPUMemoryUsage{deviceidx="0",deviceuuid="GPU-a11fe6d9-3dbe-8a24-34e9-535b2629babd",zone="vGPU"} 5.14326528e+08
HostGPUMemoryUsage{deviceidx="1",deviceuuid="GPU-b82090de-5250-44e2-a5ed-b0efc5763f8f",zone="vGPU"} 5.14326528e+08
HostGPUMemoryUsage{deviceidx="2",deviceuuid="GPU-8f563a66-d507-583f-59f1-46c2e97a393c",zone="vGPU"} 5.14326528e+08
HostGPUMemoryUsage{deviceidx="3",deviceuuid="GPU-1e5a0632-4332-f4d0-adf2-80ebfed56684",zone="vGPU"} 5.14326528e+08
HostGPUMemoryUsage{deviceidx="4",deviceuuid="GPU-384027fd-54f2-638b-cdfe-0d5f3b6630f5",zone="vGPU"} 5.14326528e+08
HostGPUMemoryUsage{deviceidx="5",deviceuuid="GPU-dbb95093-0147-7b3a-f468-8a3575a8dd4e",zone="vGPU"} 5.14326528e+08
HostGPUMemoryUsage{deviceidx="6",deviceuuid="GPU-f3eb6e71-e90a-bfc9-de06-dff90c3093b9",zone="vGPU"} 5.14326528e+08
HostGPUMemoryUsage{deviceidx="7",deviceuuid="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",zone="vGPU"} 5.14326528e+08
# HELP vGPU_device_memory_limit_in_bytes vGPU device limit
# TYPE vGPU_device_memory_limit_in_bytes gauge
vGPU_device_memory_limit_in_bytes{ctrname="pod1-ctr",deviceuuid="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podname="test1",podnamespace="default",vdeviceid="0",zone="vGPU"} 1.073741824e+10
# HELP vGPU_device_memory_usage_in_bytes vGPU device usage
# TYPE vGPU_device_memory_usage_in_bytes gauge
vGPU_device_memory_usage_in_bytes{ctrname="pod1-ctr",deviceuuid="GPU-542efc47-39a1-9669-3d17-3b7dec8251ad",podname="test1",podnamespace="default",vdeviceid="0",zone="vGPU"} 0

4. 源码分析

4.1 DevicePlugin

https://github.com/Project-HAMi/volcano-vgpu-device-plugin

资源注册

DevicePlugin 都只会注册一个资源,而 Volcano DevicePlugin 却注册了三个资源,如何做到的?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@node5-3:~/lixd# k describe node node5-3 |grep Cap -A 10
Capacity:
 cpu: 160
 ephemeral-storage: 3750157048Ki
 hugepages-1Gi: 0
 hugepages-2Mi: 0
 memory: 2113442544Ki
 nvidia.com/gpu: 0
 pods: 110
 volcano.sh/vgpu-cores: 800
 volcano.sh/vgpu-memory: 39312
 volcano.sh/vgpu-number: 80

实际上,Volcano DevicePlugin 内部启动了三个 DevicePlugin 分别使用了三个 ResourceName:

  • volcano.sh/vgpu-number

  • volcano.sh/vgpu-memory

  • volcano.sh/vgpu-cores

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (s *migStrategyNone) GetPlugins(cfg *config.NvidiaConfig, cache *DeviceCache) []*NvidiaDevicePlugin {
 return []*NvidiaDevicePlugin{
 NewNvidiaDevicePlugin(
 //"nvidia.com/gpu",
 util.ResourceName,
 cache,
 gpuallocator.NewBestEffortPolicy(),
 pluginapi.DevicePluginPath+"nvidia-gpu.sock",
 cfg),
 NewNvidiaDevicePlugin(
 util.ResourceMem,
 cache,
 gpuallocator.NewBestEffortPolicy(),
 pluginapi.DevicePluginPath+"nvidia-gpu-memory.sock",
 cfg),
 NewNvidiaDevicePlugin(
 util.ResourceCores,
 cache,
 gpuallocator.NewBestEffortPolicy(),
 pluginapi.DevicePluginPath+"nvidia-gpu-cores.sock",
 cfg),
 }
}

对应 sock 文件如下:

1
2
root@node5-3:/var/lib/kubelet/device-plugins# ls /var/lib/kubelet/device-plugins
DEPRECATION kubelet.sock kubelet_internal_checkpoint nvidia-gpu-cores.sock nvidia-gpu-memory.sock nvidia-gpu.sock

在获取 Device 时也根据不同的 ResourceName 做了不同实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
func (m *NvidiaDevicePlugin) apiDevices() []*pluginapi.Device {
 if strings.Compare(m.migStrategy, "mixed") == 0 {
 var pdevs []*pluginapi.Device
 for _, d := range m.cachedDevices {
 pdevs = append(pdevs, &d.Device)
 }
 return pdevs
 }
 devices := m.Devices()
 var res []*pluginapi.Device

 if strings.Compare(m.resourceName, util.ResourceMem) == 0 {
 for _, dev := range devices {
 i := 0
 klog.Infoln("memory=", dev.Memory, "id=", dev.ID)
 for i < int(32767) {
 res = append(res, &pluginapi.Device{
 ID: fmt.Sprintf("%v-memory-%v", dev.ID, i),
 Health: dev.Health,
 Topology: nil,
 })
 i++
 }
 }
 klog.Infoln("res length=", len(res))
 return res
 }
 if strings.Compare(m.resourceName, util.ResourceCores) == 0 {
 for _, dev := range devices {
 i := 0
 for i < 100 {
 res = append(res, &pluginapi.Device{
 ID: fmt.Sprintf("%v-core-%v", dev.ID, i),
 Health: dev.Health,
 Topology: nil,
 })
 i++
 }
 }
 return res
 }

 for _, dev := range devices {
 for i := uint(0); i < config.DeviceSplitCount; i++ {
 id := fmt.Sprintf("%v-%v", dev.ID, i)
 res = append(res, &pluginapi.Device{
 ID: id,
 Health: dev.Health,
 Topology: nil,
 })
 }
 }
 return res
}

Allocate

具体分配 Device 逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
// Allocate which return list of devices.
func (m *NvidiaDevicePlugin) Allocate(ctx context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
 if len(reqs.ContainerRequests) > 1 {
 return &pluginapi.AllocateResponse{}, errors.New("multiple Container Requests not supported")
 }
 if strings.Compare(m.migStrategy, "mixed") == 0 {
 return m.MIGAllocate(ctx, reqs)
 }
 responses := pluginapi.AllocateResponse{}

 if strings.Compare(m.resourceName, util.ResourceMem) == 0 || strings.Compare(m.resourceName, util.ResourceCores) == 0 {
 for range reqs.ContainerRequests {
 responses.ContainerResponses = append(responses.ContainerResponses, &pluginapi.ContainerAllocateResponse{})
 }
 return &responses, nil
 }
 nodename := os.Getenv("NODE_NAME")

 current, err := util.GetPendingPod(nodename)
 if err != nil {
 lock.ReleaseNodeLock(nodename, util.VGPUDeviceName)
 return &pluginapi.AllocateResponse{}, err
 }
 if current == nil {
 klog.Errorf("no pending pod found on node %s", nodename)
 lock.ReleaseNodeLock(nodename, util.VGPUDeviceName)
 return &pluginapi.AllocateResponse{}, errors.New("no pending pod found on node")
 }

 for idx := range reqs.ContainerRequests {
 currentCtr, devreq, err := util.GetNextDeviceRequest(util.NvidiaGPUDevice, *current)
 klog.Infoln("deviceAllocateFromAnnotation=", devreq)
 if err != nil {
 klog.Errorln("get device from annotation failed", err.Error())
 util.PodAllocationFailed(nodename, current)
 return &pluginapi.AllocateResponse{}, err
 }
 if len(devreq) != len(reqs.ContainerRequests[idx].DevicesIDs) {
 klog.Errorln("device number not matched", devreq, reqs.ContainerRequests[idx].DevicesIDs)
 util.PodAllocationFailed(nodename, current)
 return &pluginapi.AllocateResponse{}, errors.New("device number not matched")
 }

 response := pluginapi.ContainerAllocateResponse{}
 response.Envs = make(map[string]string)
 response.Envs["NVIDIA_VISIBLE_DEVICES"] = strings.Join(m.GetContainerDeviceStrArray(devreq), ",")

 err = util.EraseNextDeviceTypeFromAnnotation(util.NvidiaGPUDevice, *current)
 if err != nil {
 klog.Errorln("Erase annotation failed", err.Error())
 util.PodAllocationFailed(nodename, current)
 return &pluginapi.AllocateResponse{}, err
 }

 if m.operatingMode != "mig" {

 for i, dev := range devreq {
 limitKey := fmt.Sprintf("CUDA_DEVICE_MEMORY_LIMIT_%v", i)
 response.Envs[limitKey] = fmt.Sprintf("%vm", dev.Usedmem*int32(config.GPUMemoryFactor))
 }
 response.Envs["CUDA_DEVICE_SM_LIMIT"] = fmt.Sprint(devreq[0].Usedcores)
 response.Envs["CUDA_DEVICE_MEMORY_SHARED_CACHE"] = fmt.Sprintf("/tmp/vgpu/%v.cache", uuid.NewUUID())

 cacheFileHostDirectory := "/tmp/vgpu/containers/" + string(current.UID) + "_" + currentCtr.Name
 os.MkdirAll(cacheFileHostDirectory, 0777)
 os.Chmod(cacheFileHostDirectory, 0777)
 os.MkdirAll("/tmp/vgpulock", 0777)
 os.Chmod("/tmp/vgpulock", 0777)
 hostHookPath := os.Getenv("HOOK_PATH")

 response.Mounts = append(response.Mounts,
 &pluginapi.Mount{ContainerPath: "/usr/local/vgpu/libvgpu.so",
 HostPath: hostHookPath + "/libvgpu.so",
 ReadOnly: true},
 &pluginapi.Mount{ContainerPath: "/tmp/vgpu",
 HostPath: cacheFileHostDirectory,
 ReadOnly: false},
 &pluginapi.Mount{ContainerPath: "/tmp/vgpulock",
 HostPath: "/tmp/vgpulock",
 ReadOnly: false},
 )
 found := false
 for _, val := range currentCtr.Env {
 if strings.Compare(val.Name, "CUDA_DISABLE_CONTROL") == 0 {
 found = true
 break
 }
 }
 if !found {
 response.Mounts = append(response.Mounts, &pluginapi.Mount{ContainerPath: "/etc/ld.so.preload",
 HostPath: hostHookPath + "/ld.so.preload",
 ReadOnly: true},
 )
 }
 }
 responses.ContainerResponses = append(responses.ContainerResponses, &response)
 }
 klog.Infoln("Allocate Response", responses.ContainerResponses)
 util.PodAllocationTrySuccess(nodename, current)
 return &responses, nil
}

核心部分:

包括指定环境变量,以及挂载 libvgpu.so 等逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
for i, dev := range devreq {
 limitKey := fmt.Sprintf("CUDA_DEVICE_MEMORY_LIMIT_%v", i)
 response.Envs[limitKey] = fmt.Sprintf("%vm", dev.Usedmem*int32(config.GPUMemoryFactor))
}
response.Envs["CUDA_DEVICE_SM_LIMIT"] = fmt.Sprint(devreq[0].Usedcores)
response.Envs["CUDA_DEVICE_MEMORY_SHARED_CACHE"] = fmt.Sprintf("/tmp/vgpu/%v.cache", uuid.NewUUID())

response.Mounts = append(response.Mounts,
 &pluginapi.Mount{ContainerPath: "/usr/local/vgpu/libvgpu.so",
 HostPath: hostHookPath + "/libvgpu.so",
 ReadOnly: true},
 &pluginapi.Mount{ContainerPath: "/tmp/vgpu",
 HostPath: cacheFileHostDirectory,
 ReadOnly: false},
 &pluginapi.Mount{ContainerPath: "/tmp/vgpulock",
 HostPath: "/tmp/vgpulock",
 ReadOnly: false},
)

同时由于启动了三个 DevicePlugin,为了避免重复调用,Allocate 方法中根据 ResourceName 进行了判断,只有 volcano.sh/vgpu-number 时才真正执行分配逻辑。

1
2
3
4
5
6
if strings.Compare(m.resourceName, util.ResourceMem) == 0 || strings.Compare(m.resourceName, util.ResourceCores) == 0 {
 for range reqs.ContainerRequests {
 responses.ContainerResponses = append(responses.ContainerResponses, &pluginapi.ContainerAllocateResponse{})
 }
 return &responses, nil
}

4.2 deviceshare 插件分析

https://github.com/volcano-sh/volcano/blob/master/pkg/scheduler/plugins/deviceshare/deviceshare.go

简单分析一下 Volcano 中的 deviceshare 插件。

这块和 HAMi 实现基本一致,可以参考以下两篇文章:

每个插件都要实现 Volcano 定义的 Plugin 接口:

1
2
3
4
5
6
7
type Plugin interface {
 // The unique name of Plugin.
 Name() string

 OnSessionOpen(ssn *Session)
 OnSessionClose(ssn *Session)
}

核心代码在 OnSessionOpen 实现中,包含了调度的两个方法:

  • Predicate

  • NodeOrder

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
func (dp *deviceSharePlugin) OnSessionOpen(ssn *framework.Session) {
 // Register event handlers to update task info in PodLister & nodeMap
 ssn.AddPredicateFn(dp.Name(), func(task *api.TaskInfo, node *api.NodeInfo) error {
 predicateStatus := make([]*api.Status, 0)
 // Check PredicateWithCache
 for _, val := range api.RegisteredDevices {
 if dev, ok := node.Others[val].(api.Devices); ok {
 if reflect.ValueOf(dev).IsNil() {
 // TODO When a pod requests a device of the current type, but the current node does not have such a device, an error is thrown
 if dev == nil || dev.HasDeviceRequest(task.Pod) {
 predicateStatus = append(predicateStatus, &api.Status{
 Code: devices.Unschedulable,
 Reason: "node not initialized with device" + val,
 Plugin: PluginName,
 })
 return api.NewFitErrWithStatus(task, node, predicateStatus...)
 }
 klog.V(4).Infof("pod %s/%s did not request device %s on %s, skipping it", task.Pod.Namespace, task.Pod.Name, val, node.Name)
 continue
 }
 code, msg, err := dev.FilterNode(task.Pod, dp.schedulePolicy)
 if err != nil {
 predicateStatus = append(predicateStatus, createStatus(code, msg))
 return api.NewFitErrWithStatus(task, node, predicateStatus...)
 }
 filterNodeStatus := createStatus(code, msg)
 if filterNodeStatus.Code != api.Success {
 predicateStatus = append(predicateStatus, filterNodeStatus)
 return api.NewFitErrWithStatus(task, node, predicateStatus...)
 }
 } else {
 klog.Warningf("Devices %s assertion conversion failed, skip", val)
 }
 }

 klog.V(4).Infof("checkDevices predicates Task <%s/%s> on Node <%s>: fit ",
 task.Namespace, task.Name, node.Name)

 return nil
 })

 ssn.AddNodeOrderFn(dp.Name(), func(task *api.TaskInfo, node *api.NodeInfo) (float64, error) {
 // DeviceScore
 nodeScore := float64(0)
 if dp.scheduleWeight > 0 {
 score, status := getDeviceScore(context.TODO(), task.Pod, node, dp.schedulePolicy)
 if !status.IsSuccess() {
 klog.Warningf("Node: %s, Calculate Device Score Failed because of Error: %v", node.Name, status.AsError())
 return 0, status.AsError()
 }

 // TODO: we should use a seperate plugin for devices, and seperate them from predicates and nodeOrder plugin.
 nodeScore = float64(score) * float64(dp.scheduleWeight)
 klog.V(5).Infof("Node: %s, task<%s/%s> Device Score weight %d, score: %f", node.Name, task.Namespace, task.Name, dp.scheduleWeight, nodeScore)
 }
 return nodeScore, nil
 })
}

主要实现调度过程中的节点过滤以及打分两部分逻辑。

Predicate

过滤不满足设备需求的节点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
ssn.AddPredicateFn(dp.Name(), func(task *api.TaskInfo, node *api.NodeInfo) error {
 predicateStatus := make([]*api.Status, 0)
 // Check PredicateWithCache
 for _, val := range api.RegisteredDevices {
 if dev, ok := node.Others[val].(api.Devices); ok {
 if reflect.ValueOf(dev).IsNil() {
 // TODO When a pod requests a device of the current type, but the current node does not have such a device, an error is thrown
 if dev == nil || dev.HasDeviceRequest(task.Pod) {
 predicateStatus = append(predicateStatus, &api.Status{
 Code: devices.Unschedulable,
 Reason: "node not initialized with device" + val,
 Plugin: PluginName,
 })
 return api.NewFitErrWithStatus(task, node, predicateStatus...)
 }
 klog.V(4).Infof("pod %s/%s did not request device %s on %s, skipping it", task.Pod.Namespace, task.Pod.Name, val, node.Name)
 continue
 }
 code, msg, err := dev.FilterNode(task.Pod, dp.schedulePolicy)
 if err != nil {
 predicateStatus = append(predicateStatus, createStatus(code, msg))
 return api.NewFitErrWithStatus(task, node, predicateStatus...)
 }
 filterNodeStatus := createStatus(code, msg)
 if filterNodeStatus.Code != api.Success {
 predicateStatus = append(predicateStatus, filterNodeStatus)
 return api.NewFitErrWithStatus(task, node, predicateStatus...)
 }
 } else {
 klog.Warningf("Devices %s assertion conversion failed, skip", val)
 }
 }

 klog.V(4).Infof("checkDevices predicates Task <%s/%s> on Node <%s>: fit ",
 task.Namespace, task.Name, node.Name)

 return nil
})

核心逻辑在 FilterNode 方法中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
code, msg, err := dev.FilterNode(task.Pod, dp.schedulePolicy)
if err != nil {
 predicateStatus = append(predicateStatus, createStatus(code, msg))
 return api.NewFitErrWithStatus(task, node, predicateStatus...)
}
func (gs *GPUDevices) FilterNode(pod *v1.Pod, schedulePolicy string) (int, string, error) {
 if VGPUEnable {
 klog.V(4).Infoln("hami-vgpu DeviceSharing starts filtering pods", pod.Name)
 fit, _, score, err := checkNodeGPUSharingPredicateAndScore(pod, gs, true, schedulePolicy)
 if err != nil || !fit {
 klog.ErrorS(err, "Failed to fitler node to vgpu task", "pod", pod.Name)
 return devices.Unschedulable, "hami-vgpuDeviceSharing error", err
 }
 gs.Score = score
 klog.V(4).Infoln("hami-vgpu DeviceSharing successfully filters pods")
 }
 return devices.Success, "", nil
}

过滤不满足条件的节点,并为剩余节点打分。

节点过滤

从 core、memory 几方面判断 Node 是否有足够资源,不满足则过滤。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
ctrdevs := []ContainerDevices{}
for _, val := range ctrReq {
 devs := []ContainerDevice{}
 if int(val.Nums) > len(gs.Device) {
 return false, []ContainerDevices{}, 0, fmt.Errorf("no enough gpu cards on node %s", gs.Name)
 }
 klog.V(3).InfoS("Allocating device for container", "request", val)

 for i := len(gs.Device) - 1; i >= 0; i-- {
 klog.V(3).InfoS("Scoring pod request", "memReq", val.Memreq, "memPercentageReq", val.MemPercentagereq, "coresReq", val.Coresreq, "Nums", val.Nums, "Index", i, "ID", gs.Device[i].ID)
 klog.V(3).InfoS("Current Device", "Index", i, "TotalMemory", gs.Device[i].Memory, "UsedMemory", gs.Device[i].UsedMem, "UsedCores", gs.Device[i].UsedCore, "replicate", replicate)
 if gs.Device[i].Number <= uint(gs.Device[i].UsedNum) {
 continue
 }
 if val.MemPercentagereq != 101 && val.Memreq == 0 {
 val.Memreq = gs.Device[i].Memory * uint(val.MemPercentagereq/100)
 }
 if int(gs.Device[i].Memory)-int(gs.Device[i].UsedMem) < int(val.Memreq) {
 continue
 }
 if gs.Device[i].UsedCore+val.Coresreq > 100 {
 continue
 }
 // Coresreq=100 indicates it want this card exclusively
 if val.Coresreq == 100 && gs.Device[i].UsedNum > 0 {
 continue
 }
 // You can't allocate core=0 job to an already full GPU
 if gs.Device[i].UsedCore == 100 && val.Coresreq == 0 {
 continue
 }
 if !checkType(pod.Annotations, *gs.Device[i], val) {
 klog.Errorln("failed checktype", gs.Device[i].Type, val.Type)
 continue
 }
 fit, uuid := gs.Sharing.TryAddPod(gs.Device[i], uint(val.Memreq), uint(val.Coresreq))
 if !fit {
 klog.V(3).Info(gs.Device[i].ID, "not fit")
 continue
 }
 //total += gs.Devices[i].Count
 //free += node.Devices[i].Count - node.Devices[i].Used
 if val.Nums > 0 {
 val.Nums--
 klog.V(3).Info("fitted uuid: ", uuid)
 devs = append(devs, ContainerDevice{
 UUID: uuid,
 Type: val.Type,
 Usedmem: val.Memreq,
 Usedcores: val.Coresreq,
 })
 score += GPUScore(schedulePolicy, gs.Device[i])
 }
 if val.Nums == 0 {
 break
 }
 }
 if val.Nums > 0 {
 return false, []ContainerDevices{}, 0, fmt.Errorf("not enough gpu fitted on this node")
 }
 ctrdevs = append(ctrdevs, devs)
}
节点打分

根据配置的调度策略进行打分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const (
 binpackMultiplier = 100
 spreadMultiplier = 100
 )

func GPUScore(schedulePolicy string, device *GPUDevice) float64 {
 var score float64
 switch schedulePolicy {
 case binpackPolicy:
 score = binpackMultiplier * (float64(device.UsedMem) / float64(device.Memory))
 case spreadPolicy:
 if device.UsedNum == 1 {
 score = spreadMultiplier
 }
 default:
 score = float64(0)
 }
 return score
}

逻辑比较简单:

  • Binpack :device 内存使用率越高,得分越高

  • Spread: device 有被共享使用得 100 分,否则 0 分。

NodeOrder

上一步已经为节点打好分了,这里只需要根据得分排序即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 ssn.AddNodeOrderFn(dp.Name(), func(task *api.TaskInfo, node *api.NodeInfo) (float64, error) {
 // DeviceScore
 nodeScore := float64(0)
 if dp.scheduleWeight > 0 {
 score, status := getDeviceScore(context.TODO(), task.Pod, node, dp.schedulePolicy)
 if !status.IsSuccess() {
 klog.Warningf("Node: %s, Calculate Device Score Failed because of Error: %v", node.Name, status.AsError())
 return 0, status.AsError()
 }

 // TODO: we should use a seperate plugin for devices, and seperate them from predicates and nodeOrder plugin.
 nodeScore = float64(score) * float64(dp.scheduleWeight)
 klog.V(5).Infof("Node: %s, task<%s/%s> Device Score weight %d, score: %f", node.Name, task.Namespace, task.Name, dp.scheduleWeight, nodeScore)
 }
 return nodeScore, nil
 })
}

核心部分:

1
nodeScore = float64(score) * float64(dp.scheduleWeight)

节点得分 * 权重得到最终得分。

5. FAQ

gpu-memory 显示为 0

现象device-plugin 部署后 gpu-memory 显示为 0 就像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@node5-3:~/lixd# k describe node node5-3 |grep Cap -A 10
Capacity:
 cpu: 160
 ephemeral-storage: 3750157048Ki
 hugepages-1Gi: 0
 hugepages-2Mi: 0
 memory: 2113442544Ki
 nvidia.com/gpu: 0
 pods: 110
 volcano.sh/vgpu-cores: 800
 volcano.sh/vgpu-memory: 0
 volcano.sh/vgpu-number: 80

具体原因https://github.com/volcano-sh/devices/issues/19

相关描述:

the size of device list exceeds the bound, and ListAndWatch failed as a result。

简而言之就是超过阈值的显存就会报错,导致 DevicePlugin 无法正常上报,因此显示为 0。

解决方案:需要在启动时设置参数 --gpu-memory-factor=10,将最小的显存块从默认 1MB 改成 10MB,就像这样:

1
2
3
 containers:
 - image: docker.io/projecthami/volcano-vgpu-device-plugin:v1.10.0
 args: ["--device-split-count=10","--gpu-memory-factor=10"]

这样最大能显示的数值就扩大了 10 倍,就可以避免该问题。

效果如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@node5-3:~/lixd# k describe node node5-3 |grep Cap -A 10
Capacity:
 cpu: 160
 ephemeral-storage: 3750157048Ki
 hugepages-1Gi: 0
 hugepages-2Mi: 0
 memory: 2113442544Ki
 nvidia.com/gpu: 0
 pods: 110
 volcano.sh/vgpu-cores: 800
 volcano.sh/vgpu-memory: 39312
 volcano.sh/vgpu-number: 80

volcano.sh/vgpu-memory: 39312 :由于设置了 factor=10,因此实际代表总显存 39312 * 10 = 393120 MB。

当前环境是 L40S*8,单卡显存 49140,49140 * 8 = 393120,正好符合,说明一切正常。

6. 小结

本文主要验证了 Volcano 如何通过集成HAMi vGPU技术实现 Kubernetes 环境下的 GPU 虚拟化,重点验证了HAMi-Core 模式的完整工作流程。

解答前面的问题:Volcano DevicePlugin 如何实现同时注册三个资源的?

通过启动三个 DevicePlugin 以实现注册 volcano.sh/vgpu-numbervolcano.sh/vgpu-memoryvolcano.sh/vgpu-cores 三种资源。

推荐阅读: