MoreRSS

site iconLixueduan | 李学端修改

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

Inoreader Feedly Follow Feedbin Local Reader

Lixueduan | 李学端的 RSS 预览

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 三种资源。

推荐阅读:

Volcano 三大核心对象解析:Queue、PodGroup、VolcanoJob

2025-07-30 06:00:00

volcano-core-crd.png

上一篇简单介绍了 Volcano 及其使用场景,然后通过 helm 部署并跑通一个最简单的 Demo。

本文主要分析 Volcano 定义的几个主要资源对象的作用,包括 Queue、PodGroup 以及 VolcanoJob。

1. Queue、PodGroup、VolcanoJob 之间的关系

分析前,简单描述下每个对象的作用与定位:

  • Queue用于支持多租户场景下的资源分配与任务调度。通过队列,用户可以实现多租资源分配、任务优先级控制、资源抢占与回收等功能,显著提升集群的资源利用率和任务调度效率。

  • PodGroup一组强关联的 Pod 集合,这主要解决了 Kubernetes 原生调度器中单个 Pod 调度的限制。通过将相关的 Pod 组织成 PodGroup,Volcano 能够更有效地处理那些需要多个 Pod 协同工作的批处理工作负载任务。

    • 类似于 K8s 中的 Pod,对 kube-scheduler 来说 Pod 是最小调度单元,对 Volcano 来说 PodGroup 是最小调度单元,Volcano 的调度器会以 PodGroup 为单位进行调度,但最终创建的仍是单个 Pod。
  • VolcanoJobVolcano 自定义的 Job 资源类型,它扩展了 Kubernetes 的 Job 资源。VolcanoJob 不仅包括了 Kubernetes Job 的所有特性,还加入了对批处理作业的额外支持,使得 Volcano 能够更好地适应高性能和大规模计算任务的需求,更加适用于机器学习、大数据、科学计算等高性能计算场景。

    • 和 K8s 中原生的 Workload(deploy、sts、job 等) 作用一样

三者关系如下图所示:

volcano-core-crd.png

Queue 里面有多个 PodGroup,PodGroup 包含多个 Pod,其中的 Pod 由 VolcanoJob 生成。

一个 VolcanoJob 通常关联一个 PodGroup(自动或手动创建),该 Job 创建的 Pod 也会属于该 PodGroup,Volcano 以 PodGroup 为单位进行统一调度,并从关联的 Queue 中分配资源。

2. Queue

Queue 是Volcano的核心概念之一,用于支持多租户场景下的资源分配与任务调度。通过队列,用户可以实现多租资源分配、任务优先级控制、资源抢占与回收等功能,显著提升集群的资源利用率和任务调度效率。

即:Queue 是容纳一组 PodGroup 的队列,也是该组 PodGroup 获取集群资源的划分依据。

volcano 启动后,会默认创建名为 default 的 queue,weight 为 1。后续下发的 job,若未指定 queue,默认属于default queue。

  • 对于非关键或临时性任务,可以放心使用 default queue;

  • 对于重要业务,务必创建专属Queue并设置合适的 weightpriorityreclaimable等配置。

一个简单的 Queue 的 完整 yaml 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
 name: test
spec:
 capability:
 cpu: "8"
 memory: 16Gi
 # deserved字段仅用于capacity插件
 deserved:
 cpu: "4"
 memory: 8Gi
 guarantee:
 resource:
 cpu: "2"
 memory: 4Gi
 priority: 100
 reclaimable: true
 # weight字段仅用于proportion插件
 weight: 1

关键字段

  • guarantee,可选:表示该 queue 为自己队列中的所有 PodGroup 预留的资源,其他队列无法使用该部分资源。

    • 若需配置 guarantee 值,则需要小于等于 deserved 值的配置
  • deserved,可选:用于指定该 queue 的应得资源总量

    • 对于 capacity 插件可以直接配置 deserved 直观指定

    • 对于 proportion 插件,会根据 weight 自动计算 deserved

  • weight,可选:表示该 queue 在集群资源划分中所占的相对比重,该 queue 应得资源总量为 (weight/total_weight) * total_resource。

    • 该字段只有在 proportion 插件开启时可按需配置,若不设置 weight,则默认设置为1

    • 其中, total_weight 表示所有的 queue 的 weight 总和,total_resource 表示集群的资源总量。weight 是一个软约束,取值范围为[1, 2^31-1]

    • 假设有两个 queue,weight 分别为 2 和 1,那么各自可以使用集群 2/3 和 1/3 的资源。

  • capability,可选:表示该 queue 内所有 podgroup 使用资源量之和的上限,它是一个硬约束。

    • 若不设置该字段,则队列的 capability 会设置为 realCapability(集群的资源总量减去其他队列的总 guarantee值)

    • 一般建议设置 capability 来指定资源上限,以保证集群稳定。

  • reclaimable,可选:表示该 queue 在资源使用量超过该 queue 所应得的资源份额时,是否允许其他 queue 回收该 queue 使用超额的资源,默认值为true。

    • 对于使用 weight 设置资源的 queue 来说,随着 queue 越来越多,total_weight 也越大,当前 queue 分到的资源就会越少,该字段可以控制已经占用的资源是否可以被回收。
  • priority,可选:表示该 queue 的优先级,在资源分配和资源抢占/回收时,更高优先级的队列将会优先分配/抢占/回收资源

  • parent,可选:用来指定 queue 的父队列,若未指定 parent,则默认会作为 root queue 的子队列

    • Volcano 新增的层级队列功能。

Queue 的字段主要是用于资源限制,包括预留资源量应得资源量上限资源量以及是否允许资源回收等核心配置:

  • guarantee硬下限,是绝对约束,Queue 为自己队列中所有 PodGroup 预留的资源,其他 Queue 无法占用这部分资源。

  • deserved/weight:软性限制,根据开启不同插件,通过 deserved(弹性基准线) 或 weight(动态比例) 实现,二者互斥(选其一);

  • capability:​ 硬上限,该 Queue 内所有 PodGroup 使用资源量之和的上限。

  • reclaimable是否允许回收超额资源(> deserved 部分)(默认 true,允许回收超额部分)。

资源状态

Queue 包括以下状态

  • Open:该 queue 当前处于可用状态,可接收新的 PodGroup

  • Closed:该 queue 当前处于不可用状态,不可接收新的 podgroup

  • Closing:该 queue 正在转化为不可用状态,不可接收新的 podgroup

  • Unknown:该 queue 当前处于不可知状态,可能是网络或其他原因导致 queue 的状态暂时无法感知

即:Queue 只有在 Open 状态时可以接收新的 podgroup。

Queue 资源管理

灵活的资源配置

  • 支持多维度资源配额控制(CPU、内存、GPU、NPU等)

  • 支持动态资源配额调整

  • 提供三级资源配置机制:

    • capability: 队列资源使用上限

    • deserverd: 资源应得量(在无其他队列提交作业时,该队列内作业所占资源量可超过 deserverd 值,当有多个队列提交作业且集群资源不够用时,超过 deserverd 值的资源量可以被其他队列回收)

    • guarantee: 资源预留量(预留资源只可被该队列所使用,其他队列无法使用)

建议及注意事项:

  1. 进行三级资源配置时,需遵循: guarantee <= deserverd <= capability;

  2. guarantee / capability 可按需配置

  3. deserverd 配置建议:在平级队列场景,所有队列的 deserverd 值总和等于集群资源总量;在层级队列场景,子队列的 deserverd 值总和等于父队列的 deserverd 值,但不能超过父队列的 deserverd 值。

    1. 在开启 capacity 插件时需要配置 deserverd 值

    2. 在开启 proportion 插件时,会根据 weight 自动计算 deserverd 值

  4. capability 配置注意事项:在层级队列场景,子队列的 capability 值不能超过父队列的 capability 值,若子队列的capability 未设置,则会继承父队列的 capability 值。

智能资源调度

  • 资源借用:允许队列使用其他队列的空闲资源

    • 可以临时占用 deserverd 到 capability 范围内的资源
  • 资源回收:当资源紧张时,优先回收超额使用的资源

    • 会回收超过 deserverd 的资源
  • 资源抢占:确保高优先级任务的资源需求

队列调度插件

Volcano 提供了两个核心的队列调度插件:

  • capability 插件支持通过显式配置 deserverd 值来设置队列资源应得量

  • proportion 插件过配置队列的 Weight 值来自动计算队列资源应得量,无需显式配置 deserverd 值

1
kubectl -n volcano-system get cm volcano-scheduler-configmap -oyaml

例如 当前启用的是 proportion 插件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
data:
 volcano-scheduler.conf: |
 actions: "enqueue, allocate, backfill"
 tiers:
 - plugins:
 - name: priority
 - name: gang
 enablePreemptable: false
 - name: conformance
 - plugins:
 - name: overcommit
 - name: drf
 enablePreemptable: false
 - name: predicates
 - name: proportion
 - name: nodeorder
 - name: binpack
kind: ConfigMap
metadata:
 name: volcano-scheduler-configmap
 namespace: volcano-system
capability

capacity 插件通过精确的资源配置来进行配额控制,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
 name: capacity-queue
spec:
 deserved:
 cpu: "10"
 memory: "20Gi"
 capability:
 cpu: "20"
 memory: "40Gi"
proportion

proportion 插件通过权重比例自动计算队列的 deserverd 值,当集群资源发生变化时,proportion 插件会自动根据权重比例重新计算各队列的 deserverd 值,无需人工干预。

1
2
3
4
5
6
7
8
9
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
 name: proportion-queue
spec:
 weight: 1
 capability:
 cpu: "20"
 memory: "40Gi"

当集群总资源为 total_resource 时,每个队列的 deserverd 值计算公式为:

1
queue_deserved = (queue_weight / total_weight) * total_resource

其中,queue_weight 表示当前队列的权重,total_weight 表示所有队列权重之和,total_resource 表示集群总资源量。

小结

capacity 插件和 proportion 插件必须二选一,不能同时使用,二者都有有各自的优缺点:

选择哪个插件主要取决于你是想直接设置资源量(capacity 插件)还是通过权重自动计算(proportion 插件):

  • 动态集群选 proportion​:若集群频繁扩缩容(如使用 Cluster Autoscaler),优先选 proportion 避免人工维护成本

  • 精细控制选 capacity:需独立管理 CPU/GPU/内存等资源时(如为 AI 队列单独分配 A100 GPU),选 capacity 实现维度级控制。

Volcano v1.9.0 版本后推荐使用 capacity 插件,因为它提供了更直观的资源配置方式。

用法 & 最佳实践

Weight 资源软约束

背景:

  • 集群 CPU 总量为 4C

  • 已默认创建名为 default 的 queue,weight 为 1

  • 集群中无任务运行

操作:

  1. 当前情况下,default queue 可是使用全部集群资源,即 4C。

  2. 再创建名为 test 的 queue,weight 为 3。此时,default weight:test weight = 1:3,即 default queue 可使用 1C,test queue 可使用 3C

    1. total weight 就是 1+3 = 4
  3. 创建名为 p1 和 p2 的 podgroup,分别属于 default queue 和 test queue

  4. 分别向 p1 和 p2 中投递 job1 和 job2,资源申请量分别为 1C 和 3C,两个 job 均能正常工作

Weight 驱逐资源

由于 Queue 能使用的资源时在 Queue 创建时通过 Weight 按比例划分的,后续有新 Queue 创建时,总 Weight 必然会增加,因此所有旧 Queue 能使用的资源比例肯定会减少。因此就会出现新 Queue 把旧 Queue 多使用的资源驱逐的情况。

背景:

  • 集群 CPU 总量为 4C

  • 已默认创建名为 default 的 queue,weight 为 1

  • 集群中无任务运行

操作:

  1. 当前情况下,default queue 可使用全部集群资源,即 4C

  2. 创建名为 p1 的 podgroup,属于 default queue。

  3. 分别创建名为 job1 和 job2 的 job,属于 p1,资源申请量分别为 1C 和 3C,job1 和 job2 均能正常工作

  4. 创建名为 test 的 queue,weight 为 3。此时,default weight:test weight = 1:3,即 default queue 可使用 1C,test queue 可使用 3C。但由于 test queue 内此时无任务,job1 和 job2 仍可正常工作,不会被驱逐。

  5. 创建名为 p2 的 podgroup,属于 test queue。

  6. 创建名为 job3 的 job,属于 p2,资源申请量为 3C。此时,job2 将被驱逐,将资源归还给 job3,即 default queue 将 3C 资源归还给 test queue。

capability 做硬限制

Weight 只能按比例划分资源,如果不希望 Queue 占用太多资源,那么可以使用 capability 进行硬限制,限制该 Queue 能使用的 CPU、Memory、GPU 等等资源的数量。

背景:

  • 集群 CPU 总量为 4C

  • 已默认创建名为 default 的 queue,weight 为 1

  • 集群中无任务运行

操作:

  1. 创建名为 test 的 queue,capability 设置 cpu 为 2C,即 test queue 使用资源上限为 2C

  2. 创建名为 p1 的 podgroup,属于 test queue

  3. 分别创建名为 job1 和 job2 的 job,属于 p1,资源申请量分别为 1C 和 3C,依次下发。由于 capability 的限制,job1 正常运行,job2 处于 pending 状态

    1. 虽然按照 Weight 划分该 Queue 能使用 100%资源,即 4C,但是 capability 硬限制只有 2C,job1 占用 1C 后,给 job2 的只有 1C 了。

使用 capability 可以严格限制 queue 中任务使用的资源,但是可能会造成集群资源的浪费,因此使用 weight 还是 capability 需要根据实际情况选择。

reclaimable 的使用

  • reclaimable: true 意味着该队列超量使用的资源(超过其按weight/deserved应得的份额)可以被回收以满足更高优先级队列的需求或本队列内更高优先级PodGroup的需求。

  • reclaimable: false 则表示即使超量使用,资源也不可被回收

如果我们的任务优先级比较高,或者不希望被打断,则可以设置 reclaimable: false,避免因为资源问题被回收。

背景:

  • 集群 CPU 总量 为4C

  • 已默认创建名为 default 的 queue,weight 为 1

  • 集群中无任务运行

操作:

  1. 创建名为 test 的 queue,reclaimable 设置为 false,weight 为 1。此时,default weight:test weight = 1:1,即default queue 和 test queue 均可使用 2C。

  2. 创建名为 p1、p2 的 podgroup,分别属于 test queue 和 default queue

  3. 创建名为 job1 的 job,属于 p1,资源申请量 3C,job1 可正常运行。此时,由于 default queue 中尚无任务,test queue 多占用 1C。

  4. 创建名为 job2 的 job,属于 p2,资源申请量 2C,任务下发后处于 pending 状态,因为 test queue 的 reclaimable 为 false 导致该 queue 不归还多占的资源,job 无法启动。

小结

  • Weight 为软约束,只是定义该 Queue 可以使用的资源份额, 是可以超过的(即:占用其他 Queue 未使用的部分资源),需要和 reclaimable 配合使用,当 Queue 使用资源超过 Weight 后,若 reclaimable 为 true 则其他 Queue 申请资源时,调度器会把当前 Queue 中低优先级 Pod 驱逐掉以释放资源。

  • Capability 为硬约束,不可以超过,资源不够时 Queue 下的 Pod 会直接 Pending

  • Guarantee 为保留资源,这部分资源不会被其他 queue 抢占。

  • Priority 优先级, 资源分配和资源抢占/回收生效。

  • 使用 weight (proportion插件) 或 deserved (capacity插件) 是为了实现资源共享和弹性,最大化利用集群资源,但可能导致重要任务资源被压缩。

  • 使用 capability 是为了严格限制一个队列的资源消耗上限(例如防止某个租户过度消耗),避免影响其他队列或系统组件,但可能导致集群整体利用率不高(该队列用不完,其他队列不能用)。

  • 使用 guarantee 是为了给关键队列提供资源保障,确保其最低资源需求总能满足。

实际生产环境往往是组合使用:为关键业务Queue设置 guarantee + capability + 高 priority + reclaimable: false;为普通业务Queue设置 weight/deserved + capability + reclaimable: true

3. PodGroup

PodGroup 是一组强关联的 Pod 集合,这主要解决了 Kubernetes 原生调度器中单个 Pod 调度的限制。通过将相关的 Pod 组织成 PodGroup,Volcano 能够更有效地处理那些需要多个 Pod 协同工作的批处理工作负载任务。

一个简单的 PodGroup 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
31
32
33
34
35
36
37
38
39
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
 creationTimestamp: "2020-08-11T12:28:55Z"
 generation: 5
 name: test
 namespace: default
 ownerReferences:
 - apiVersion: batch.volcano.sh/v1alpha1
 blockOwnerDeletion: true
 controller: true
 kind: Job
 name: test
 uid: 028ecfe8-0ff9-477d-836c-ac5676491a38
 resourceVersion: "109074"
 selfLink: /apis/scheduling.volcano.sh/v1beta1/namespaces/default/podgroups/job-1
 uid: eb2508f5-3349-439c-b94d-4ac23afd71ff
spec:
 minMember: 1
 minResources:
 cpu: "3"
 memory: "2048Mi"
 priorityClassName: high-prority
 queue: default
status:
 conditions:
 - lastTransitionTime: "2020-08-11T12:28:57Z"
 message: '1/0 tasks in gang unschedulable: pod group is not ready, 1 minAvailable.'
 reason: NotEnoughResources
 status: "True"
 transitionID: 77d5be3f-6169-4f86-8e65-0bdc621ce983
 type: Unschedulable
 - lastTransitionTime: "2020-08-11T12:29:02Z"
 reason: tasks in gang are ready to be scheduled
 status: "True"
 transitionID: 54514401-5c90-4b11-840d-90c1cda93096
 type: Scheduled
 phase: Running
 running: 1

关键字段

  • minMember:表示该 podgroup 下最少需要运行的 pod 或任务数量。如果集群资源不满足 miniMember 数量任务的运行需求,调度器将不会调度任何一个该 podgroup 内的任务。

    • 通过设置该字段可以避免无意义的 Pod 调度,只有当满足最小数量时才进行调度。
  • queue:表示该 podgroup 所属的 queue。

    • queue 必须提前已创建且状态为 open。
  • priorityClassName:表示该 podgroup 的优先级,用于调度器为该 queue 中所有 podgroup 进行调度时进行排序。

    • system-node-critical system-cluster-critical 是2个预留的值,表示最高优先级。

    • 不特别指定时,默认使用 default 优先级或 zero 优先级。

  • minResources:表示运行该 podgroup 所需要的最少资源。当集群可分配资源不满足 minResources 时,调度器将不会调度任何一个该 podgroup 内的任务。

以下为 status 部分

  • phase:表示该 podgroup 当前的状态。

  • conditions:表示该 podgroup 的具体状态日志,包含了 podgroup 生命周期中的关键事件。

  • running:表示该 podgroup 中当前处于 running 状态的 pod 或任务的数量。

  • succeed:表示该 podgroup 中当前处于 succeed 状态的 pod 或任务的数量。

  • failed:表示该 podgroup 中当前处于 failed 状态的 pod 或任务的数量。

资源状态

  • pending:表示该 podgroup 已经被 volcano 接纳,但是集群资源暂时不能满足它的需求。一旦资源满足,该 podgroup 将转变为 running 状态。

  • inqueue:表示该 podgroup 已经通过了调度器的校验并入队,即将为它分配资源。inqueue 是一种处于 pending和 running 之间的中间状态。

  • running:表示该 podgroup 至少有 minMember 个 pod 或任务处于 running 状态。

  • unknown:表示该 podgroup 中 minMember 数量的 pod 或任务分为 2 种状态,部分处于 running 状态,部分没有被调度。没有被调度的原因可能是资源不够等。调度器将等待 controller 重新拉起这些 pod 或任务。

状态流转如下图所示:

vcjob-status.png

自动创建 PodGroup

Volcano 会为每个 VolcanoJob 自动创建一个同名 PodGroup,若需自定义 PodGroup,可通过 spec.podGroup 字段指定。

自动创建的 PodGroup 取值如下:

  • minMember 默认为 vcjob 中所有 Task 的副本数总和,如果 vcjob 中指定了 minAvailable 则直接使用 minAvailable 填充

  • minResources 则是所有 Task 资源总和,当 minAvailable 存在时,minResources 按 前 minAvailable 个 Pod 的资源总和计算,且顺序由 Task 在 VolcanoJob 中的定义顺序决定

举个栗子🌰:

下面这个 vcjob 包含两个 task,nginx1 和 nginx2,副本数分别为 2 和 4,申请资源也不同。

 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
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
 name: nginx-job3
spec:
 minAvailable: 3
 queue: weight
 tasks:
 - replicas: 2
 name: nginx2
 template:
 metadata:
 labels:
 app: nginx
 spec:
 containers:
 - name: nginx-container
 image: nginx:latest
 ports:
 - containerPort: 80
 resources:
 requests:
 cpu: "200m"
 memory: "1000m"
 restartPolicy: OnFailure
 - replicas: 4
 name: nginx
 template:
 metadata:
 labels:
 app: nginx
 spec:
 containers:
 - name: nginx-container
 image: nginx:latest
 ports:
 - containerPort: 80
 resources:
 requests:
 cpu: "100m"
 memory: "500m"
 restartPolicy: OnFailure

当我们不指定 vcjob 中的minAvailable 参数时自动创建的 PodGroup 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 spec:
 minMember: 6
 minResources:
 count/pods: "6"
 cpu: 800m
 memory: "4"
 pods: "6"
 requests.cpu: 800m
 requests.memory: "4"
 minTaskMember:
 nginx: 4
 nginx2: 2
  • minMember 为 6,包括了 vcjob 中两个 task 的副本数之和

  • minResources 则是包括了两个 task 对应的 6 个 Pod 所需资源总和

当我们指定 vcjob 中的 minAvailable=3 时自动创建的 PodGroup 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 spec:
 minMember: 3
 minResources:
 count/pods: "3"
 cpu: 500m
 memory: 2500m
 pods: "3"
 requests.cpu: 500m
 requests.memory: 2500m
 minTaskMember:
 nginx: 4
 nginx2: 2
  • minMember 为 3,直接使用 minAvailable 填充

  • minResources 则是只计算了前 3 个 Pod 所需的资源

minResources 是按照 task 定义的先后顺序计算的,这 3 个 Pod 包括 nginx2 中的两个和 nginx1 中的 1 个,如果调整 task 的顺序,minResources 也会变化。

例如,将 task nginx1 放前面,生成的 PodGroup 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 spec:
 minMember: 3
 minResources:
 count/pods: "3"
 cpu: 300m
 memory: 1500m
 pods: "3"
 requests.cpu: 300m
 requests.memory: 1500m
 minTaskMember:
 nginx: 4
 nginx2: 2

这里计算的 3 个 Pod 都是 nginx1 中的 Pod。

用法 & 最佳实践

minMember 的使用

在某些场景下,可能需要至少启动 N 个 Pod 本次任务才会成功,如机器学习训练,这种情况下适合使用 minMember 字段。

例如:本次训练要启动 6 个 Pod,如果不能都启动那整体任务肯定会失败,这种情况直接把 minMember 设置为 6,这样就可以保证只有 6 个 Pod 都可以启动时才会真正调度。

priorityClassName 的使用

priorityClassName 用于 podgroup 的优先级排序,可用于任务抢占调度场景。它本身也是一种资源。

minResources 的使用

在某些场景下,任务的运行必须满足最小资源要求,不满足则不能运行该任务,如某些大数据分析场景。这种情况下适合使用 minResources 字段。

使用 vcjob

对于下面这个 vcjob,task 会启动 3 个 Pod,同时 minAvailable 也设置为 3。

 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
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
 name: nginx-job
spec:
 minAvailable: 3 # 必须所有副本同时就绪才调度(Gang-Scheduling)
 queue: weight  # 指定队列(需提前创建,若未创建请参考注释*)
 tasks:
 - replicas: 3
 name: nginx
 template:
 metadata:
 labels:
 app: nginx
 spec:
 containers:
 - name: nginx-container
 image: nginx:latest
 ports:
 - containerPort: 80
 resources:
 requests:
 cpu: "100m"
 memory: "500m"
 restartPolicy: OnFailure

该 vcjob 创建后自动生成的 PodGroup 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# kubectl get pg nginx-job2-04f9fb09-c3b4-40b4-a3fd-632a0439eb0c -oyaml
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
 name: nginx-job2-04f9fb09-c3b4-40b4-a3fd-632a0439eb0c
 namespace: default
 uid: 1340be0d-1be1-4399-9539-cf3c3828ce28
spec:
 minMember: 3
 minResources:
 count/pods: "3"
 cpu: 300m
 memory: 1500m
 pods: "3"
 requests.cpu: 300m
 requests.memory: 1500m
 minTaskMember:
 nginx: 3
 queue: weight

使用 k8s 原生工作负载

除了通过 vcjob 创建工作负载之外,也可以使用 k8s 原生工作负载。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
 name: test-resource-pod
 annotations:
 # 指定作业到weight队列
 scheduling.volcano.sh/queue-name: "weight"
spec:
 # 指定调度器为Volcano
 schedulerName: volcano
 containers:
 - name: nginx
 image: nginx:latest
 resources:
 requests:
 cpu: "100m"
 memory: "500"
 ports:
 - containerPort: 80
 restartPolicy: Never
  • 通过 Annoations scheduling.volcano.sh/queue-name指定 queue

  • 并通过 schedulerName 指定调度为 volcano

4. VolcanoJob

VolcanoJob 是 Volcano 自定义的 Job 资源类型,它扩展了 Kubernetes 的 Job 资源。VolcanoJob 不仅包括了 Kubernetes Job 的所有特性,还加入了对批处理作业的额外支持,使得 Volcano 能够更好地适应高性能和大规模计算任务的需求,更加适用于机器学习、大数据、科学计算等高性能计算场景。

一个简单的 VolcanoJob 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
31
32
33
34
35
36
37
38
39
40
41
42
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
 name: test-job
spec:
 minAvailable: 3
 schedulerName: volcano
 priorityClassName: high-priority
 policies:
 - event: PodEvicted
 action: RestartJob
 plugins:
 ssh: []
 env: []
 svc: []
 maxRetry: 5
 queue: default
 volumes:
 - mountPath: "/myinput"
 - mountPath: "/myoutput"
 volumeClaimName: "testvolumeclaimname"
 volumeClaim:
 accessModes: [ "ReadWriteOnce" ]
 storageClassName: "my-storage-class"
 resources:
 requests:
 storage: 1Gi
 tasks:
 - replicas: 6
 name: "default-nginx"
 template:
 metadata:
 name: web
 spec:
 containers:
 - image: nginx
 imagePullPolicy: IfNotPresent
 name: nginx
 resources:
 requests:
 cpu: "1"
 restartPolicy: OnFailure

关键字段

  • schedulerName:表示该 job 的 pod所 使用的调度器,默认值为 volcano,也可指定为 default-scheduler。它也是 tasks.template.spec.schedulerName 的默认值。

    • 如果指定为 default-scheduler,则意味着该Job的Pod将由Kubernetes默认调度器调度,将无法利用Volcano提供的高级调度策略(如Gang Scheduling, Fair-Share, Queue, Preemption等)

    • 强烈建议对于需要Volcano特性的Job保持 schedulerName: volcano

  • minAvailable:表示运行该 job 所要运行的最少 pod 数量。只有当 job 中处于 running 状态的 pod 数量不小于minAvailable 时,才认为该 job 运行正常

    • 用于控制什么时候才切换该任务到 running 状态。
  • volumes:表示该j ob 的挂卷配置。volumes 配置遵从 kubernetes volumes 配置要求。

  • tasks.replicas:表示某个 task pod 的副本数。

  • tasks.template:表示某个 task pod 的具体配置定义。

  • tasks.policies:表示某个 task 的生命周期策略。

  • policies:表示 job 中所有 task 的默认生命周期策略,在 tasks.policies 不配置时使用该策略。

  • plugins:表示该 job 在调度过程中使用的插件。

  • queue:表示该 job 所属的队列。

  • priorityClassName:表示该 job 优先级,在抢占调度和优先级排序中生效。

  • maxRetry:表示当该 job 可以进行的最大重启次数。

相比之下,VolcanoJob 的字段都比较容易理解,大分部含义和 K8s 原生对象中的字段含义一致。

资源状态

  • pending:表示 job 还在等待调度中,处于排队的状态。

  • aborting:表示 job 因为某种外界原因正处于中止状态,即将进入 aborted 状态。

  • aborted:表示 job 因为某种外界原因已处于中止状态。

  • running:表示 job 中至少有 minAvailable 个 pod 正在运行状态。

  • restarting:表示 job 正处于重启状态,正在中止当前的 job 实例并重新创建新的实例。

  • completing:表示 job 中至少有 minAvailable 个数的 task 已经完成,该 job 正在进行最后的清理工作。

  • completed:表示 job 中至少有 minAvailable 个数的 task 已经完成,该 job 已经完成了最后的清理工作。

  • terminating:表示 job 因为某种内部原因正处于终止状态,正在等到 pod 或 task 释放资源。

  • terminated:表示 job 因为某种内部原因已经处于终止状态,job 没有达到预期就结束了。

  • failed:表示 job 经过了 maxRetry 次重启,依然没有正常启动。

用法

Tensorflow

以 tensorflow 为例,创建一个具有 1 个 ps 和 2 个 worker 的工作负载。

 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
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
 name: tensorflow-dist-mnist
spec:
 minAvailable: 3 # 该job的3个pod必须都可用
 schedulerName: volcano  # 指定volcano为调度器
 plugins:
 env: []
 svc: []
 policies:
 - event: PodEvicted # 当pod被驱逐时,重启该job
 action: RestartJob
 tasks:
 - replicas: 1 # 指定1个ps pod
 name: ps
 template: # ps pod的具体定义
 spec:
 containers:
 - command:
 - sh
 - -c
 - |
 PS_HOST=`cat /etc/volcano/ps.host | sed 's/$/&:2222/g' | sed 's/^/"/;s/$/"/' | tr "\n" ","`;
 WORKER_HOST=`cat /etc/volcano/worker.host | sed 's/$/&:2222/g' | sed 's/^/"/;s/$/"/' | tr "\n" ","`;
 export TF_CONFIG={\"cluster\":{\"ps\":[${PS_HOST}],\"worker\":[${WORKER_HOST}]},\"task\":{\"type\":\"ps\",\"index\":${VK_TASK_INDEX}},\"environment\":\"cloud\"};
 python /var/tf_dist_mnist/dist_mnist.py
 image: volcanosh/dist-mnist-tf-example:0.0.1
 name: tensorflow
 ports:
 - containerPort: 2222
 name: tfjob-port
 resources: {}
 restartPolicy: Never
 - replicas: 2 # 指定2个worker pod
 name: worker
 policies:
 - event: TaskCompleted  # 2个worker完成任务时认为该job完成任务
 action: CompleteJob
 template: # worker pod的具体定义
 spec:
 containers:
 - command:
 - sh
 - -c
 - |
 PS_HOST=`cat /etc/volcano/ps.host | sed 's/$/&:2222/g' | sed 's/^/"/;s/$/"/' | tr "\n" ","`;
 WORKER_HOST=`cat /etc/volcano/worker.host | sed 's/$/&:2222/g' | sed 's/^/"/;s/$/"/' | tr "\n" ","`;
 export TF_CONFIG={\"cluster\":{\"ps\":[${PS_HOST}],\"worker\":[${WORKER_HOST}]},\"task\":{\"type\":\"worker\",\"index\":${VK_TASK_INDEX}},\"environment\":\"cloud\"};
 python /var/tf_dist_mnist/dist_mnist.py
 image: volcanosh/dist-mnist-tf-example:0.0.1
 name: tensorflow
 ports:
 - containerPort: 2222
 name: tfjob-port
 resources: {}
 restartPolicy: Never

MindSpore

以 MindSpore 为例,创建一个具有 8 个 pod 副本的工作负载,要求 1 个可用即可。

 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
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
 name: mindspore-cpu
spec:
 minAvailable: 1
 schedulerName: volcano
 policies:
 - event: PodEvicted
 action: RestartJob
 plugins:
 ssh: []
 env: []
 svc: []
 maxRetry: 5
 queue: default
 tasks:
 - replicas: 8
 name: "pod"
 template:
 spec:
 containers:
 - command: ["/bin/bash", "-c", "python /tmp/lenet.py"]
 image: lyd911/mindspore-cpu-example:0.2.0
 imagePullPolicy: IfNotPresent
 name: mindspore-cpu-job
 resources:
 limits:
 cpu: "1"
 requests:
 cpu: "1"
 restartPolicy: OnFailure

5. 小结

Queue 实现多租户资源隔离与弹性分配

通过权重(weight)或容量(deserved)策略动态划分集群资源,支持资源预留(guarantee)、硬上限(capability)和弹性复用(reclaimable)。例如:

  • 多租户隔离:为不同团队/项目分配独立队列,避免资源抢占

  • 弹性分配:空闲时超用资源,紧张时按比例回收,提升集群利用率

  • 层级扩展:支持父/子队列嵌套,实现部门级资源再分配

PodGroup 确保强关联 Pod 的原子调度(Gang Scheduling)

通过 minMemberminResources 实现 All-or-Nothing 调度,解决分布式任务(如AI训练)的协同启动问题。例如:

  • 原子性保障:若集群无法满足最小Pod数,则全部不调度,避免死锁

  • 优先级控制:通过 priorityClassName 实现任务抢占,保障高优先级作业

VolcanoJob 定义复杂作业结构与生命周期策略

  • 多任务模板:支持异构Pod角色(如TensorFlow的PS/Worker),通过 tasks 字段声明依赖关系

  • 增强生命周期:基于 policies 配置故障恢复(如Pod失败重启)、超时终止(如 PodPending: 10m 自动终止)

  • 插件扩展:集成 ssh/svc/env 等插件,提供跨Pod互信、服务发现等分布式能力

一句话描述:Queue 是资源池,实现多租户资源隔离与动态分配;PodGroup 是原子调度单元,确保强关联 Pod 的协同调度(All-or-Nothing);VolcanoJob 是批作业抽象,定义多角色任务拓扑与生命周期策略。

解锁 Kubernetes 批处理新范式:Volcano 调度引擎初体验

2025-07-23 06:00:00

volcano-demo.png

还在为 Kubernetes 大规模批处理任务调度烦恼?Volcano——CNCF 官方批处理调度引擎,提供 Gang Scheduling、队列优先级、异构设备支持等高级特性。本文以最小化实践带你完成 Volcano 安装到验证。

1.Volcano 简介

Volcano 是什么?

官方描述:Volcano is a batch system built on Kubernetes. 基于 Kubernetes 的高性能工作负载调度引擎。

Volcano 能做什么?

支持 Kubernetes 原生负载及主流计算框架 (如TensorFlow、Spark、PyTorch、Ray、Flink等) 的一体化作业调度。

  • 强大的批处理调度能力:通过 VcJob 完美支持 Ray、TensorFlow、PyTorch、MindSpore、Spark、Flink 等主流 AI 和大数据框架

  • 完整的 Kubernetes 工作负载支持:可直接调度 Deployment、StatefulSet、Job、DaemonSet 等原生工作负载

为什么需要 Volcano?

或者说 Volcano 之前有哪些痛点。

随着各种新兴高性能计算需求的持续增长,Job 的调度和管理能力变得必要和复杂。下面罗列了一些共性需求:

  • 调度算法的多样性

  • 调度性能的高效性

  • 无缝对接主流计算框架

  • 对异构设备的支持

Volcano 正是针对这些需求应运而生的。同时,Volcano 继承了 Kubernetes 接口的设计风格和核心概念。用户可以在充分享受 Volcano 的高效性和便利性的同时不用改变任何以前使用 Kubernetes 的习惯。

场景 原生 Kubernetes Volcano 优势
分布式任务调度 Pod 独立调度 Gang Scheduling (All-or-Nothing) 避免死锁,提高集群利用率
资源争用处理 简单 FIFO 优先级队列+公平共享 保障关键任务资源
GPU 调度 基础设备分配 拓扑感知+虚拟化 提升 GPU 利用率 30%+
批量作业 Job 资源限制弱 作业级资源配额 精准控制批量任务资源占用

现在以一个分布式训练场景为例,说明为什么需要 Volcano。

在 AI 中大规模任务比较常见,如果按照 k8s 默认的以 Pod 为单位进行调度,可能会出现资源浪费,甚至死锁的情况。

例如:启动一个 PyTorchJob 包括 1 Master 2 Worker,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
31
32
33
34
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
 name: pytorch-job-example
 namespace: default
spec:
 cleanPodPolicy: None
 pytorchReplicaSpecs:
 Master:
 replicas: 1 # 主节点数量
 restartPolicy: OnFailure
 template:
 spec:
 containers:
 - name: pytorch
 image: pytorch/pytorch:1.9.0-cuda11.1-cudnn8-runtime
 command: ["python", "/workspace/train.py"]
 args: ["--epochs", "10"]
 resources:
 limits:
 nvidia.com/gpu: 1 # 请求 GPU 资源
 Worker:
 replicas: 2 # Worker 节点数量
 restartPolicy: OnFailure
 template:
 spec:
 containers:
 - name: pytorch
 image: pytorch/pytorch:1.9.0-cuda11.1-cudnn8-runtime
 command: ["python", "/workspace/train.py"]
 args: ["--epochs", "10"]
 resources:
 limits:
 nvidia.com/gpu: 1 # 每个 Worker 使用 1 个 GPU

按照 K8s 默认的以 Pod 为单位进行调度,可能出现只有部分 Pod 能成功调度并启动的情况,这样的结果对该 Job 来说都是无意义的。

如果有多个这样的 Job 可能都只调度了一部分 Pod,最终集群资源耗尽,所有 Job 都在等待其他 Job 释放资源。

我们需要该 Job 在调度时满足满足 Gang 调度语义,任务的所有工作负载必须全部运行或全部不运行(All or nothing),从而避免 Pod 的任意调度导致集群资源的浪费。

2. Volcano 架构

volcano-arch.png

可以看到 Volcano 完全构建于 Kubernetes 之上,与 Kubernetes 天然兼容,并为高性能计算而生,遵循 Kubernetes的设计理念和风格。

volcano-arch2.png

Volcano 包括以下组件:

  • Volcano Scheduler:通过一系列的action和plugin调度Job,并为它找到一个最适合的节点。与Kubernetes default-scheduler相比,Volcano与众不同的 地方是它支持针对Job的多种调度算法。

  • Volcano ControllerManager: Volcano controllermanager管理CRD资源的生命周期。它主要由 Queue ControllerManager、 PodGroupControllerManager、 VCJob ControllerManager构成。

  • Volcano Admission: Volcano admission 负责对 CRD API资源进行校验。

  • CRD:Job、Queue、PodGroup 等自定义资源。

  • vcctl:Volcano vcctl是Volcano的命令行客户端工具。

Volcano 通过 CRD 扩展了 K8s 对象,然后自定义 Controller 和 Scheduler 进行管理以及调度。

3. Volcano 部署

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

这里我们部署 Volcano 的 1.12.0 版本。

helm 部署

官方提供了 chart,直接使用 helm 安装即可

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

验证

安装完成,验证 Volcano 组件的状态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[root@volcano ~]# kubectl get all -n volcano-system
NAME READY STATUS RESTARTS AGE
pod/volcano-admission-dcf9bb957-l294f 1/1 Running 0 85s
pod/volcano-controllers-bd48dd8f8-l5z9c 1/1 Running 0 85s
pod/volcano-scheduler-64f9655f97-vhk9w 1/1 Running 0 85s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/volcano-admission-service ClusterIP 10.99.238.1 <none> 443/TCP 86s
service/volcano-scheduler-service ClusterIP 10.104.54.47 <none> 8080/TCP 86s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/volcano-admission 1/1 1 1 86s
deployment.apps/volcano-controllers 1/1 1 1 86s
deployment.apps/volcano-scheduler 1/1 1 1 86s

NAME DESIRED CURRENT READY AGE
replicaset.apps/volcano-admission-dcf9bb957 1 1 1 86s
replicaset.apps/volcano-controllers-bd48dd8f8 1 1 1 86s
replicaset.apps/volcano-scheduler-64f9655f97 1 1 1 86s

查看相关 crd

1
2
3
4
5
6
7
8
[root@volcano ~]# kubectl get crd|grep volcano
commands.bus.volcano.sh 2025-03-14T08:05:09Z
jobflows.flow.volcano.sh 2025-03-14T08:05:09Z
jobs.batch.volcano.sh 2025-03-14T08:05:09Z
jobtemplates.flow.volcano.sh 2025-03-14T08:05:09Z
numatopologies.nodeinfo.volcano.sh 2025-03-14T08:05:09Z
podgroups.scheduling.volcano.sh 2025-03-14T08:05:09Z
queues.scheduling.volcano.sh 2025-03-14T08:05:09Z

4. Volcano 简单使用

接下来我们跑一个简单的 Demo 测试一下 Volcano 的能力,确认 Volcano 正确运行。

首先创建一个名为 test 的 Queue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
cat <<EOF | kubectl apply -f -
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
 name: test
spec:
 weight: 1
 reclaimable: false
 capability:
 cpu: 2
EOF

再创建一个名为job-1 的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
cat <<EOF | kubectl apply -f -
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
 name: job-1
spec:
 minAvailable: 1
 schedulerName: volcano
 queue: test
 policies:
 - event: PodEvicted
 action: RestartJob
 tasks:
 - replicas: 1
 name: nginx
 policies:
 - event: TaskCompleted
 action: CompleteJob
 template:
 spec:
 containers:
 - command:
 - sleep
 - 10m
 image: nginx:latest
 name: nginx
 resources:
 requests:
 cpu: 1
 limits:
 cpu: 1
 restartPolicy: Never
EOF

可以看到,这个 Volcano Job 和 K8s 原生 Job 类似,不过也多了几个字段:

1
2
3
4
5
6
7
spec:
 minAvailable: 1
 schedulerName: volcano
 queue: test
 policies:
 - event: PodEvicted
 action: RestartJob
  • minAvailable:需要满足最小数量后才进行调度

  • schedulerName:指定调度器

  • queue:指定 Job 所属的队列,就是前面我们创建的队列

  • policies:一些特殊策略

检查 Job 的状态

1
kubectl get vcjob job-1 -oyaml

检查 PodGroup 的状态

1
kubectl get podgroup -oyaml

检查 Queue 的状态

1
kubectl get queue test -oyaml

最后检查 Pod 状态

1
2
3
[root@volcano ~]# kubectl get po
NAME READY STATUS RESTARTS AGE
job-1-nginx-0 0/1 ContainerCreating 0 24s

能够正常启动,说明 Volcano 能够正常使用,具体各个 CRD 对象的作用,后面在详细分析。

5. 小结

Volcano 是 Kubernetes 生态中一个强大的批处理调度系统,通过高级调度算法、增强的 Job 管理能力、异构设备支持和多框架集成等等,解决了 Kubernetes 在处理高性能工作负载时的局限性。

本文主要是介绍了什么是 Volcano 以及使用场景,然后通过 helm 部署 Volcano 并运行了一个最简单的 Demo。

后续详细介绍 Volcano 的各个 CRD 的作用,调度策略,以及 HAMi vGPU 和 Volcano 如何搭配使用。

AI 赋能 K8s 运维:K8sGPT 让集群故障 “自动诊断 + 秒出方案”

2025-07-02 06:00:00

ai-analyzer-k8sgpt.png

还在为 Kubernetes 集群故障排查头疼?试试 K8sGPT—— 这款基于 AI 的智能诊断工具,能自动扫描集群异常,并通过 OpenAI、DeepSeek 等模型生成 step-by-step 解决方案。本文手把手教你用 CLI 或 Operator 模式部署,从安装到实战验证,让 K8s 运维效率飙升!

1. k8sgpt 是什么

K8sGPT 是一款基于 AI 的 Kubernetes 智能诊断工具,自动扫描集群异常并通过 AI 生成解决方案。它支持多类 AI 后端(OpenAI、本地模型等),提供两种核心使用模式:

  • Cli 方式安装:安装 cli 工具方式使用,通过 kubeconfig 连接集群,即时诊断问题

  • Operator 方式安装:通过在集群中安装 Operator 方式使用,这种方式非常适合持续监控集群,并且可以与 Prometheus 和 Alertmanager 等现有监控集成

在 k8s 环境部署,推荐使用 Operator 方式安装。

2.Cli 方式安装

2.1 安装

k8sgpt 的能力由 cli 工具提供,可以直接使用 brew 安装:

1
brew install k8sgpt

或者到 Releases 界面下载

1
2
3
4
5
6
7
8
9
version=v0.4.20

wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/${version}/k8sgpt_Linux_x86_64.tar.gz

tar -zxvf k8sgpt_Linux_x86_64.tar.gz
mv k8sgpt /usr/local/bin/

# 查看版本
k8sgpt version

2.2 配置 AI Provider

k8sgpt 支持多种 AI Provider,具体如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[root@kc-master ~]# k8sgpt auth list
Default:
> openai
Active:
Unused:
> localai
> openai
> ollama
> azureopenai
> cohere
> amazonbedrock
> amazonsagemaker
> google
> noopai
> huggingface
> googlevertexai
> oci
> customrest
> ibmwatsonxai

常见的 openai、ollama 等,也支持使用 localai 对接任意满足 OpenAI API 格式的外部模型。

这里我们使用 localai Provider 来对接 DeepSeek:

1
2
3
4
5
baseurl=https://api.deepseek.com/v1
model=deepseek-reasoner
key=sk-xxx

k8sgpt auth add -b localai -u $baseurl -m $model -p $key

并将其设置为 默认 Provider

1
2
$ k8sgpt auth default -p localai
Default provider set to localai

2.3 扫描集群

运行 k8sgpt analyze 扫描集群中的问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# k8sgpt analyze
AI Provider: AI not used; --explain not set

0: Deployment default/broken-image()
- Error: Deployment default/broken-image has 1 replicas but 0 are available with status running

1: Pod default/broken-image-8896f7cf4-vf47k(Deployment/broken-image)
- Error: Back-off pulling image "nginx:invalid-tag"

2: ConfigMap calico-apiserver/kube-root-ca.crt()
- Error: ConfigMap kube-root-ca.crt is not used by any pods in the namespace

...

也可以指定 kubeconfig 访问远程集群

1
k8sgpt analyze --kubeconfig mykubeconfig

默认情况下不会使用 AI,只是简单扫描集群中的问题,需要增加--explain flag 才会与 AI 交互给出对应解决方案:

1
2
# -b 指定使用上一步配置的 Provider
k8sgpt analyze --explain

效果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@kc-master ~]# k8sgpt analyze --explain
AI Provider: localai

0: Deployment default/broken-image()
- Error: Deployment default/broken-image has 1 replicas but 0 are available with status running
Error: The deployment "broken-image" has 1 pod defined, but 0 pods are running successfully.
Solution:
1. Check pod status: `kubectl get pods -n default`
2. View pod logs: `kubectl logs <pod-name> -n default`
3. Inspect errors: `kubectl describe pod <pod-name> -n default`
4. Fix image/configuration issue (e.g., correct image name in deployment YAML)
5. Apply changes: `kubectl apply -f deployment.yaml`

(275 characters)
1: Pod default/broken-image-8896f7cf4-vf47k(Deployment/broken-image)
- Error: Back-off pulling image "nginx:invalid-tag"
Error: Kubernetes cannot pull the specified container image because the tag "invalid-tag" doesn't exist in the Docker registry for "nginx".

Solution:
1. Verify valid nginx tags on Docker Hub or via `docker search nginx --limit 5`.
2. Edit your deployment: `kubectl edit deployment <deployment-name>`.
3. Replace `image: nginx:invalid-tag` with a valid tag (e.g., `image: nginx:latest` or `image: nginx:alpine`).
4. Save changes to restart pods automatically.
5. Confirm fix: `kubectl get pods` should show running status.

3. Operator 方式安装

3.1 部署 Operator

直接 helm 部署,命令如下:

1
2
3
4
helm repo add k8sgpt https://charts.k8sgpt.ai/
helm repo update

helm upgrade --install -n k8sgpt-operator-system k8sgpt k8sgpt/k8sgpt-operator --create-namespace

查看 Pod 运行情况

1
2
3
# kubectl -n k8sgpt-operator-system get po
NAME READY STATUS RESTARTS AGE
release-k8sgpt-operator-controller-manager-69b6fd9696-zc9t5 2/2 Running 0 33m

会启动一个 k8sgpt-operator-controller-manager Pod,这样就算部署好了

3.2 创建 K8sGPT 对象

首先创建一个 K8sGPT 对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
kubectl apply -f - << EOF
apiVersion: core.k8sgpt.ai/v1alpha1
kind: K8sGPT
metadata:
 name: k8sgpt-local-ai
 namespace: k8sgpt-operator-system
spec:
 ai:
 enabled: true
 model: deepseek-reasoner
 backend: localai
 baseUrl: https://api.deepseek.com/v1
 secret:
 name: k8sgpt-sample-secret
 key: openai-api-key
 noCache: false
 repository: ghcr.io/k8sgpt-ai/k8sgpt
 version: v0.4.1
EOF

大致包含两部分信息:

  • 1)K8sgpt 仓库地址以及版本,会使用该信息创建对应 Pod

    • repository:ghcr.io/k8sgpt-ai/k8sgpt

    • version:v0.4.1

  • 2)对接的 LLM 信息,会使用这部分信息与 LLM 交互,这里用的是 DeepSeek

    • backend:localai

    • baseUrl:https://api.deepseek.com/v1

    • model:deepseek-reasoner

    • secret:指定存放 API Key 的 secret,如果 API 没有设置 Auth 的话不指定 secret 部分

需要提供一个 LLM 服务配置,不一定是 chatgpt,只要是兼容 OpenAI API 格式的服务都可以。

比如可以用 DeepSeek https://platform.deepseek.com/usage,也可以使用本地的服务

如果 LLM 服务需要认证,提前创建 secret 方式存储 API Key,secret 名称和 key 需与 K8sGPT 对象 spec 中的配置一致”,避免配置不匹配导致的异常。

1
2
OPENAI_TOKEN=sk-xxx
kubectl create secret generic k8sgpt-sample-secret --from-literal=openai-api-key=$OPENAI_TOKEN -n k8sgpt-operator-system

K8sGPT 对象创建之后,上一步部署的 Operator 会根据信息启动一个 Pod

1
2
3
(base) [root@label-studio-k8s ~]# k -n k8sgpt-operator-system get pod -w
NAME READY STATUS RESTARTS AGE
k8sgpt-local-ai-5ff75b9b8f-mfv8t 0/1 ContainerCreating 0 19s

这个 Pod 就是真正与 AI 交互的 Pod,该 Pod 会启动一个 http 服务提供给 Controller, Controller 扫描集群故障后,通过 http 请求该服务,该服务与 AI 交互得到解决方案后返回给 Controller,最终 Controller 将其存储到 result 对象中。

3.3 模拟故障

接下来就可以开始验证了。

k8sgpt 会自动收集集群信息,并生成诊断结果,可以通过以下命令查看:

1
2
3
4
5
6
(base) [root@label-studio-k8s ~]# k -n k8sgpt-operator-system get result
NAME KIND BACKEND AGE
defaultyoloservice Service localai 60s
kubesystemhamidcuvgpudevicepluginktlg7 Pod localai 60s
kubesystemhamidevicepluginmonitor Service localai 60s
kubesystemkccsicontroller785cc89b7bbmdkh Pod localai 60s

其中每个 result 对象对应一个问题,如果集群不存在任何问题是不会生成 result 对象的。

因为我们可以自己创造一些问题进行模拟,例如模拟服务镜像拉取失败的场景。

使用以下命令创建 deployment,由于 tag 不存在肯定会出现无法拉取镜像的问题。

1
kubectl create deployment broken-image --image=nginx:invalid-tag

和预期一样,Pod 会因为无法拉取镜像,启动失败

1
2
3
# kubectl get po
NAME READY STATUS RESTARTS AGE
broken-image-8896f7cf4-qst86 0/1 ImagePullBackOff 0 4m33s

看下 k8sgpt 能否检测到该问题:

1
2
3
# kubectl -n k8sgpt-operator-system get result
NAME KIND BACKEND AGE
defaultbrokenimage8896f7cf4qst86 Pod localai 12s

k8sgpt 检测到故障并生成 result 通常需要 30 秒到 2 分钟(取决于集群规模)。

已经有新的 result 了,看起来没什么问题,result 对象名称由 namespace 和对象名(这里就是 PodName)组成。

k8sgpt 会自动将 error 信息发送给 AI,并将结果写入 result 中的 details 字段,内容如下:

 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
# kubectl -n k8sgpt-operator-system get result defaultbrokenimage8896f7cf4vf47k -oyaml
apiVersion: core.k8sgpt.ai/v1alpha1
kind: Result
metadata:
 creationTimestamp: "2025-06-26T04:23:06Z"
 generation: 1
 labels:
 k8sgpts.k8sgpt.ai/backend: localai
 k8sgpts.k8sgpt.ai/name: k8sgpt-local-ai
 k8sgpts.k8sgpt.ai/namespace: k8sgpt-operator-system
 name: defaultbrokenimage8896f7cf4vf47k
 namespace: k8sgpt-operator-system
 resourceVersion: "90019"
 uid: 1ab8b2ad-7398-4d6d-bd8b-e2d5beba70ae
spec:
 backend: localai
 details: "Error: Kubernetes cannot pull the container image because the tag \"invalid-tag\"
 doesn't exist in the Docker registry for nginx. \nSolution: \n1. Verify valid
 nginx tags at [hub.docker.com/_/nginx](https://hub.docker.com/_/nginx) \n2. Edit
 your deployment: \n```bash \nkubectl edit deployment <deployment-name> \n```
 \ \n3. Replace `image: nginx:invalid-tag` with a valid tag (e.g., `nginx:latest`
 or `nginx:1.25`) \n4. Save and exit. Kubernetes will automatically retry pulling
 the new image. \n\n*(273 characters)*"
 error:
 - text: Back-off pulling image "nginx:invalid-tag"
 kind: Pod
 name: default/broken-image-8896f7cf4-vf47k
 parentObject: ""
status: {}

格式化之后内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Error: Kubernetes cannot pull the container image because the tag "invalid-tag" doesn't exist in the Docker registry for nginx. 

Solution: 
1. Verify valid nginx tags at [hub.docker.com/_/nginx](https://hub.docker.com/_/nginx) 
2. Edit your deployment: 
```bash 
kubectl edit deployment <deployment-name> 
``` 
3. Replace `image: nginx:invalid-tag` with a valid tag (e.g., `nginx:latest` or `nginx:1.25`) 
4. Save and exit. Kubernetes will automatically retry pulling the new image. 

details 部分包含了具体问题,以及解决方案。

这里 AI 让我们编辑 deployment,将镜像 tag 修改为有效值。

3.4 修复故障

按照 AI 给出的解决方案修复:

1
kubectl set image deployment/broken-image nginx=nginx:1.25

调整镜像后 Pod 正常启动

1
2
3
# kubectl get po
NAME READY STATUS RESTARTS AGE
broken-image-564ff59bd-b4fdm 1/1 Running 0 20s

修复后,故障(result 对象)消失

1
2
# kubectl -n k8sgpt-operator-system get result
No resources found in k8sgpt-operator-system namespace.

4. 工作流程

k8sgpt 是如何工作的呢?

4.1 完整流程

分为以下步骤:

  • 1)初始化 Analysis:配置 Kubernetes 客户端、AI 后端等等

  • 2)运行 Analysis

    • 运行自定义 Analysis(如果有配置)

    • 运行内置 Analysis

  • 3)请求 AI 生成处理方案(如果指定 explain)

  • 4)结构化返回诊断报告

完整代码如下:

analyze.go#L11-L67

 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
func (h *Handler) Analyze(ctx context.Context, i *schemav1.AnalyzeRequest) (
 *schemav1.AnalyzeResponse,
 error,
) {
 if i.Output == "" {
 i.Output = "json"
 }

 if int(i.MaxConcurrency) == 0 {
 i.MaxConcurrency = 10
 }
 
 // 初始化 Analysis
 config, err := analysis.NewAnalysis(
 i.Backend,
 i.Language,
 i.Filters,
 i.Namespace,
 i.LabelSelector,
 i.Nocache,
 i.Explain,
 int(i.MaxConcurrency),
 false, // Kubernetes Doc disabled in server mode
 false, // Interactive mode disabled in server mode
 []string{}, //TODO: add custom http headers in server mode
 false, // with stats disable
 )
 if err != nil {
 return &schemav1.AnalyzeResponse{}, err
 }
 config.Context = ctx // Replace context for correct timeouts.
 defer config.Close()

 // 运行自定义 Analysis
 if config.CustomAnalyzersAreAvailable() {
 config.RunCustomAnalysis()
 }
 // 运行内置 Analysis
 config.RunAnalysis()

 // 请求 AI 生成处理方案(如果指定 explain)
 if i.Explain {
 err := config.GetAIResults(i.Output, i.Anonymize)
 if err != nil {
 return &schemav1.AnalyzeResponse{}, err
 }
 }

 // 结构化返回诊断报告
 out, err := config.PrintOutput(i.Output)
 if err != nil {
 return &schemav1.AnalyzeResponse{}, err
 }
 var obj schemav1.AnalyzeResponse

 err = json.Unmarshal(out, &obj)
 if err != nil {
 return &schemav1.AnalyzeResponse{}, err
 }

 return &obj, nil
}

4.2 Analyzer 工作逻辑

k8sgpt 中包括多个内置 Analyzer

以 Pod Analyzer 为例,流程也比较简单:

  • 1)使用 k8s client 获取 Pod 列表

  • 2)然后遍历检查每个 Pod 的状态,获取错误信息

  • 3)最终将错误信息结构化为 Result 格式返回

完整代码如下:

 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
func (PodAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {

 kind := "Pod"
 
 AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
 "analyzer_name": kind,
 })

 // search all namespaces for pods that are not running
 list, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{
 LabelSelector: a.LabelSelector,
 })
 if err != nil {
 return nil, err
 }
 var preAnalysis = map[string]common.PreAnalysis{}

 for _, pod := range list.Items {
 var failures []common.Failure

 // Check for pending pods
 if pod.Status.Phase == "Pending" {
 // Check through container status to check for crashes
 for _, containerStatus := range pod.Status.Conditions {
 if containerStatus.Type == v1.PodScheduled && containerStatus.Reason == "Unschedulable" {
 if containerStatus.Message != "" {
 failures = append(failures, common.Failure{
 Text: containerStatus.Message,
 Sensitive: []common.Sensitive{},
 })
 }
 }
 }
 }

 // Check for errors in the init containers.
 failures = append(failures, analyzeContainerStatusFailures(a, pod.Status.InitContainerStatuses, pod.Name, pod.Namespace, string(pod.Status.Phase))...)

 // Check for errors in containers.
 failures = append(failures, analyzeContainerStatusFailures(a, pod.Status.ContainerStatuses, pod.Name, pod.Namespace, string(pod.Status.Phase))...)

 if len(failures) > 0 {
 preAnalysis[fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)] = common.PreAnalysis{
 Pod: pod,
 FailureDetails: failures,
 }
 AnalyzerErrorsMetric.WithLabelValues(kind, pod.Name, pod.Namespace).Set(float64(len(failures)))
 }
 }

 for key, value := range preAnalysis {
 var currentAnalysis = common.Result{
 Kind: kind,
 Name: key,
 Error: value.FailureDetails,
 }

 parent, found := util.GetParent(a.Client, value.Pod.ObjectMeta)
 if found {
 currentAnalysis.ParentObject = parent
 }
 a.Results = append(a.Results, currentAnalysis)
 }

 return a.Results, nil
}

4.3 Prompt 模板与 AI 交互

在上一步 k8sgpt 拿到了集群中的异常信息,例如:

1
Back-off pulling image "nginx:invalid-tag"

接下来就拿这个错误信息给 AI 生成解决方案,这里k8sgpt 用到的 prompt 模板如下:

prompts.go#L4-L16

以下是用于处理 Kubernetes 错误的 Prompt 模板:

1
2
3
4
5
default_prompt = `Simplify the following Kubernetes error message delimited by triple dashes written in --- %s --- language; --- %s ---.
Provide the most possible solution in a step by step style in no more than 280 characters. Write the output in the following format:
Error: {Explain error here}
Solution: {Step by step solution here}
`

这也是为什么我们看到的 Result 中的 detail 是这样的:

 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@kc-master ~]# kubectl -n k8sgpt-operator-system get result defaultbrokenimage8896f7cf4vf47k -oyaml
apiVersion: core.k8sgpt.ai/v1alpha1
kind: Result
metadata:
 creationTimestamp: "2025-06-26T04:23:06Z"
 generation: 1
 labels:
 k8sgpts.k8sgpt.ai/backend: localai
 k8sgpts.k8sgpt.ai/name: k8sgpt-local-ai
 k8sgpts.k8sgpt.ai/namespace: k8sgpt-operator-system
 name: defaultbrokenimage8896f7cf4vf47k
 namespace: k8sgpt-operator-system
 resourceVersion: "90019"
 uid: 1ab8b2ad-7398-4d6d-bd8b-e2d5beba70ae
spec:
 backend: localai
 details: "Error: Kubernetes cannot pull the container image because the tag \"invalid-tag\"
 doesn't exist in the Docker registry for nginx. \nSolution: \n1. Verify valid
 nginx tags at [hub.docker.com/_/nginx](https://hub.docker.com/_/nginx) \n2. Edit
 your deployment: \n```bash \nkubectl edit deployment <deployment-name> \n```
 \ \n3. Replace `image: nginx:invalid-tag` with a valid tag (e.g., `nginx:latest`
 or `nginx:1.25`) \n4. Save and exit. Kubernetes will automatically retry pulling
 the new image. \n\n*(273 characters)*"
 error:
 - text: Back-off pulling image "nginx:invalid-tag"
 kind: Pod
 name: default/broken-image-8896f7cf4-vf47k
 parentObject: ""
status: {}

格式化之后

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Error: Kubernetes cannot pull the container image because the tag "invalid-tag" doesn't exist in the Docker registry for nginx. 

Solution: 
1. Verify valid nginx tags at [hub.docker.com/_/nginx](https://hub.docker.com/_/nginx) 
2. Edit your deployment: 
```bash 
kubectl edit deployment <deployment-name> 
``` 
3. Replace `image: nginx:invalid-tag` with a valid tag (e.g., `nginx:latest` or `nginx:1.25`) 
4. Save and exit. Kubernetes will automatically retry pulling the new image. 

5. 小结

K8sGPT 作为一款基于 AI 的 Kubernetes 智能诊断工具,核心价值在于自动化集群故障检测与 AI 驱动的解决方案生成,大幅降低了 K8s 故障排查的技术门槛。

  • 两种部署模式适配不同场景:CLI 模式适合临时诊断(如手动触发集群扫描),Operator 模式适合持续监控(结合 Prometheus 等工具实现实时告警与自动修复建议)。

  • 工作流程清晰高效:通过内置的 Analyzer 扫描集群资源(Pod、Deployment 等)的异常状态,结合 LLM 模型(如 DeepSeek、OpenAI)生成结构化的解决方案,输出格式统一(错误描述 + 步骤化修复建议),便于快速落地。

  • 灵活性强:支持多类 AI 后端(本地模型、云服务模型等)

注意:对于复杂故障(如网络策略冲突、持久化存储异常),AI 生成的解决方案可能需要结合人工验证,不可完全依赖自动化结果。

探索 Easy Dataset:高效构建大模型训练数据的全流程指南

2025-06-18 04:00:00

easy-dataset.png

在大模型时代,高质量训练数据的重要性不言而喻。本文全面介绍了 Easy Dataset 这一强大工具,它能帮助开发者和数据科学家从各种文档中快速构建结构化的问答数据集,大幅简化大模型训练数据的准备过程。


Easy Dataset 是一个强大的大模型数据集创建工具,有以下特点:

  • 智能文档处理:支持 PDF、Markdown、DOCX、TXT 等多种格式智能识别和处理

  • 智能文本分割:支持多种智能文本分割算法、支持自定义可视化分段

  • 智能问题生成:从每个文本片段中提取相关问题

  • 领域标签:为数据集智能构建全局领域标签,具备全局理解能力

  • 答案生成:使用 LLM API 为每个问题生成全面的答案、思维链(COT)

  • 灵活编辑:在流程的任何阶段编辑问题、答案和数据集

  • 多种导出格式:以各种格式(Alpaca、ShareGPT)和文件类型(JSON、JSONL)导出数据集

  • 广泛的模型支持:兼容所有遵循 OpenAI 格式的 LLM API

  • 用户友好界面:为技术和非技术用户设计的直观 UI

  • 自定义系统提示:添加自定义系统提示以引导模型响应

Github 地址:https://github.com/ConardLi/easy-dataset

1. Easy Dataset 部署

这里简单使用 Docker 方式启动。

拉取镜像

1
docker pull kejod15117/easy-dataset:v1.3.6

部署

1
2
YOUR_LOCAL_DB_PATH=$(pwd)
docker run -d -p 17171:1717 -v ${YOUR_LOCAL_DB_PATH}:/app/local-db --name easy-dataset kejod15117/easy-dataset:v1.3.6

访问 17171 端口即可进入 Easy Dataset 界面。

1
http://172.20.150.112:17171

2. 准备工作

在正式生成数据集之前还需要做一点准备工作。

创建项目

EasyDataset 中以项目为单位进行管理,因此进入界面后需要先创建一个项目

create-project.png

配置模型

项目创建后需要先配置一个模型才能正常使用,更多 -> 项目设置 -> 模型配置

Easy Dataset 支持 OpenAI 标准协议 的模型接入,兼容 Ollama,用户仅需配置 模型名称、API地址、密钥 即可完成适配,内置模型库预填主流厂商端点。

config-model.png

这里我们使用 DeepSeek,只需要填写 API Key 即可

config-model2.png

完成后验证一下模型是否可用

config-model3.png

配置好模型之后,就可以开始生成数据集了。

3. 生成数据集

从原始文件到最终的数据集大概分为以下几个步骤:

  • 1)文献处理:支持 Markdown、PDF、World、Excel 等格式文件,将上传的文档智能切分为小的文本块

  • 2)问题生成:利用大模型,从文本块中提取问题

  • 3)构造答案:利用大模型,根据问题以及对应文本块内容信息生成答案

  • 4)导出数据集:以各种格式(Alpaca、ShareGPT)和文件类型(JSON、JSONL)导出数据集

文献处理

Easy Dataset 支持多种类型(PDF、World、Excel)的文档,系统会自动解析并分割文档内容。

这里我们使用一个 PDF 进行测试:

upload-doc.png

注意:点击上传之前需要先选择模型,如界面右上角所示。

选择好之后点击上传并处理文件,系统就会开始解析并分割文档。

分割后如下:

docs-split.png

切分参数也可以在 项目设置->任务配置 进行配置

split-config.png

问题生成

接下来则是从分割好的文本块中提取问题,这块会用到配置的大模型来处理。

点击 自动提取问题,系统会后台异步自动查询待生成问题的文本块,并提取问题。

question-generate.png

点击后可在任务管理中心可以看到处理进度

question-generate-task.png

跳转到问题管理界面,即可查看本次生成的问题

question-list.png

问题生成参数也可以在 项目设置->任务配置 进行配置

question-config.png

答案构建

Easy DataSet 会根据问题 + 问题对应的文本块 + 领域标签来一起生成答案,来保障答案和文献本身的相关性。

同样的,点击 自动生成数据集,Easy Dataset 会自动待生成答案的问题,并为其生成答案。

answer-generate.png

查看任务进度

answer-generate-task.png

答案生成完成后,就可以在 数据集管理 界面查看生成的数据集了,例如:

dataset-list.png

对于不满意的数据集,可以手动修改或者为 AI 提供修改建议,让 AI 进行修改。

修改完成后点击 确认保留 即可将该数据集标记为 已确认

dataset-edit.png

确认数据集操作并不是必须的,仅通过是否含有已确认标签便于进行区分。

导出数据集

数据集处理完成后,即可导出。

dataset-export.png

  • 选择文件格式:支持 JSON、JSONL、Excel 三种格式

  • 选择数据集风格:固定风格支持 Alpaca、ShareGPT

导出后的数据集内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[
 {
 "instruction": "如何指定每张vDCU上划分的计算单元数量和显存容量?",
 "input": "",
 "output": "划分vDCU时,需通过`hy-smi virtual`命令行工具指定...",
 "system": ""
 },
 {
 "instruction": "Kubernetes集群内使用vDCU时,静态虚拟化方案有哪些条件要求?",
 "input": "",
 "output": "在Kubernetes集群内使用vDCU时,静态虚拟化方案...",
 "system": ""
 },
 ....
]

至此,我们就完成了数据集生成工作,可以将该数据集用于后续的模型微调了。

4. 小结

Easy Dataset 是一个强大的大模型数据集创建工具,有以下特点:

  • 智能文档处理:支持 PDF、Markdown、DOCX、TXT 等多种格式智能识别和处理

  • 灵活编辑:在流程的任何阶段编辑问题、答案和数据集

  • 多种导出格式:以各种格式(Alpaca、ShareGPT)和文件类型(JSON、JSONL)导出数据集

整个流程可以分为以下几个步骤:

  • 1)文献处理:将上传的文档智能切分为小的文本块

  • 2)问题生成:利用大模型,从文本块中提取问题

  • 3)构造答案:利用大模型,根据文本块内容,为上一步生成的问题生成答案

  • 4)导出数据集:以各种格式(Alpaca、ShareGPT)和文件类型(JSON、JSONL)导出数据集

通过 Easy Dataset 可以轻松的将 PDF、Markdown、DOCX、TXT 等文件构建为大模型数据集。

相较于直接将文档喂给 AI,然后通过 Prompt 引导其按照格式生成数据集,Easy Dataset 将整个过程拆分为多个步骤之后,效果会更好。

MCP 实战:从零开始实现自己的 MCP Server

2025-06-05 04:00:00

llm-app-mcp.png

上一篇中介绍了 Agent、Function Calling、MCP 等概念,这篇则分享如何实现自己的 MCP Server,并将其添加到 Cline 进行验证。

1. 什么是 MCP

mcp-clearly-explained.webp

图源:https://blog.dailydoseofds.com/p/visual-guide-to-model-context-protocol

详细概念参考上文: 一文搞懂 Agent&Function Calling&MCP&A2A 是什么

1.1 Model Context Protocol

MCP(Model Context Protocol) 是由 Anthropic 推出的一项开放标准协议,旨在解决不同大模型与不同外部工具集成的标准化问题。官方解释是“其目标是为大型语言模型提供一种开放、标准化的方式,以便与外部数据源、工具和服务进行连接。”

  • 它规定了上下文与请求的结构化传递方式,并要求通信格式符合 JSON-RPC 2.0 标准。

  • 简单理解,MCP 相当于大模型领域的“HTTP 协议”,其并不绑定任何大模型,这意味着用户可以在支持 MCP 的工具中,用任何大模型调用 MCP 服务。

英文版描述:

The Model Context Protocol (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you’re building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need.

一句话描述:MCP 是一种开放、标准化的协议,以便大型语言模型能够与外部数据源、工具和服务进行连接。

1.2 General Architecture

架构如下:

mcp-general-architecture.png

MCP 涉及到 5 个组件或角色:

  • MCP Hosts: 真正使用 MCP 的程序,例如 Claude Desktop, IDEs 或者其他 AI 工具

  • MCP Clients: 实现 MCP 协议的客户端,维持和 MCP Server 之间的 1:1 连接

  • MCP Servers: 实现 MCP 协议的服务端,通过 MCP 协议公开某些功能,MCP服务器可以提供以下类型的功能:

    1. Resources: 客户端可以读取的类似文件的数据(如API响应或文件内容)

    2. Tools: 工具:可以由LLM调用的函数(经用户批准)

    3. Prompts: 帮助用户完成特定任务的预先编写的模板

  • Local Data Sources: 本地数据源,MCP 服务可以安全访问的本地文件、数据库和其他服务

  • Remote Services: 远程服务,MCP 服务可以连接的外部系统

需要重点关注的就是 MCP Host、MCP Client 以及 MCP Server 三部分,Local Data Sources、Remote Services 可以看做是 MCP Server 自身实现功能的一部分。

  • MCP Host 就是直接跟用户交互的应用程序,例如 Claude,而 MCP Client 一般是直接集成在 MCP Host 里,这样应用程序既可以和用户交互,也可以调用 MCP Server。

  • MCP Server 就是提供 Resources、Tools、 Prompts 等等功能的服务端,可以简单理解为把 Function Calling 中的函数封装为了服务,满足 MCP 标准从而实现功能复用。

2. 实现自己的 MCP Server

作为一个 Gopher,这里选择的是 MCP 的 Go 语言实现:mcp-go 这个库。

2.1 安装

1
go get github.com/mark3labs/mcp-go

2.2 Stdio 类型 MCP Server

以下是一个简单的 Stdio 类型的 MCP Server,提供了一个名为 calculate 的 tool,接收 x、y 以及 operation 三个参数。

 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
package main

import (
 "context"
 "fmt"

 "github.com/mark3labs/mcp-go/mcp"
 "github.com/mark3labs/mcp-go/server"
)

func main() {
 // Create a new MCP server
 s := server.NewMCPServer(
 "Calculator Demo",
 "1.0.0",
 server.WithToolCapabilities(false),
 server.WithRecovery(),
 )

 // Add a calculator tool
 calculatorTool := mcp.NewTool("calculate",
 mcp.WithDescription("Perform basic arithmetic operations"),
 mcp.WithString("operation",
 mcp.Required(),
 mcp.Description("The operation to perform (add, subtract, multiply, divide)"),
 mcp.Enum("add", "subtract", "multiply", "divide"),
 ),
 mcp.WithNumber("x",
 mcp.Required(),
 mcp.Description("First number"),
 ),
 mcp.WithNumber("y",
 mcp.Required(),
 mcp.Description("Second number"),
 ),
 )

 // Add the calculator handler
 s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 // Using helper functions for type-safe argument access
 op, err := request.RequireString("operation")
 if err != nil {
 return mcp.NewToolResultError(err.Error()), nil
 }
 
 x, err := request.RequireFloat("x")
 if err != nil {
 return mcp.NewToolResultError(err.Error()), nil
 }
 
 y, err := request.RequireFloat("y")
 if err != nil {
 return mcp.NewToolResultError(err.Error()), nil
 }

 var result float64
 switch op {
 case "add":
 result = x + y
 case "subtract":
 result = x - y
 case "multiply":
 result = x * y
 case "divide":
 if y == 0 {
 return mcp.NewToolResultError("cannot divide by zero"), nil
 }
 result = x / y
 }

 return mcp.NewToolResultText(fmt.Sprintf("%.2f", result)), nil
 })

 // Start the server
 if err := server.ServeStdio(s); err != nil {
 fmt.Printf("Server error: %v\n", err)
 }
}

接下来就可以开始验证我们的 MCP Server 了。

2.3 SSE 类型 MCP Server

SSE 类型的 MCP Server 和 Stdio 主要区别在 MCP Hosts(MCP Client) 如何与 MCP Server 做交互。

实际上代码很简单,只需要改动一点点:

Stdio 是这样启动服务的

1
2
3
4
// Start the server
if err := server.ServeStdio(s); err != nil {
 fmt.Printf("Server error: %v\n", err)
}

SSE 则是这样:

1
2
3
4
5
6
7
8
// 创建基于 SSE 的服务器实例
sseServer := server.NewSSEServer(s)

// 启动服务器,监听指定端口(如 :8080)
err := sseServer.Start(":8080")
if err != nil {
 panic(err)
}

然后启动服务

1
go run cmd/main.go

3. MCP Hosts 配置 MCP Server

3.1 安装 Cline

在验证 MCP Server 之前我们先安装一个 MCP Hosts,这里选择的是 Cline,大家也可以选择其他的。

首先在 VS Code 中安装 Cline 插件。

cline-install-1.png

安装之后在侧边栏点击图标即可打开

cline-install-2.png

3.2 配置模型

在 Cline 最下方有一个配置模型的地方,点开填写相关信息即可。

如果没有可用模型,可以到 DeepSeek 官网 注册。

cline-config-model.png

3.3 配置 MCP Server

接下来就开始配置 MCP Server 了,将我们本地跑的 MCP Server 配置到 Cline 里。

在 Cline 中点击服务器图标即,再在弹出窗口中点击齿轮图标即可进入 MCP Server 配置界面

cline-config-mcp1.png

点击 Configure

cline-config-mcp2.png

之后就可以在 json 文件中配置了,我们这里启动的是 Stdio 类型的 MCP Server,也就是输入输出分别使用 Stdin 和 Stdout 的,只需要配置启动命令即可:

先 go build cmd/main.go 即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
 "mcpServers": {
 "mymcp": {
 "command": "/Users/lixueduan/17x/projects/i-mcp/cmd/main",
 "args": [
 ],
 "disabled": false,
 "autoApprove": []
 }
 }
}

对于 SSE 类型 MCP Server 则是

1
2
3
4
5
6
7
8
{
 "mcpServers": {
 "mymcp": {
 "url": "http://localhost:8080/sse",
 "disabled": false
 }
 }
}

如果出现小绿点则说明 MCP Server 可以正常使用了。

cline-config-mcp3.png

3.4 验证

至此,就可以开始验证 MCP Server 了。

问 Cline 一个问题

1
帮我计算 1+1

此时 MCP Hosts(Cline) 会自动携带问题和我们 MCP Server 的信息发送给 LLM,然后 LLM 根据思考会得出要调用前面我们配置的 mymcp 这个 MCP Server 中的 名为 calculate 的 tool,参数 x、y 以及 operation 分别是 1、1 和 add。

可以看到,最终的结果和预想的是一致的

cline-mcp-check1.png

我们点击 Approve 之后 Cline 就会去调用 MCP 了。

cline-mcp-check2.png

其中 MCP Server 返回的结果是 2.00,然后模型整合后返回最终的结果为:

1
The result of 1 + 1 is 2.00

至此,我们就完成了一个 Stdio 类型的 MCP Server 的开发和验证。

4. MCP 工作流程

整个工作流程如下图所示:

mcp-workflow.jpg

该过程涉及到 3 个角色:

  • 1)MCP Server:也就是我们实现的 MCP 服务

  • 2)LLM:大模型,DeepSeek、ChatGPT 或者自己部署的其他模型都可以

  • 3)Cline(MCP Hosts):在 MCP Server 和 LLM 之间交互,可以看做是一个 Agent

流程如下:

  • 1)配置 MCP Server 到 Cline 之后,Cline 会获取该 MCP Server 提供的能力

  • 2)用户给 Cline 发送任务

  • 3)Cline 会带着问题和上一步获取到的信息发送给 LLM

  • 4)LLM 根据问题和 MCP 信息决定该调用哪个 MCP Server 的哪个 tool,具体通过 <use_mcp_tool> </use_mcp_tool> 标记,例如:

    1
    2
    3
    4
    5
    6
    7
    
    <use_mcp_tool>
     <server_name>mymcp</server_name>
     <tool_name>calculate</tool_name>
     <arguments>
     { "operation": "add", "x": 1,"y": 1}
     </arguments>
    </use_mcp_tool>
  • 5)Cline 解析 XML 并调用对应 MCP Server 拿到结果

  • 6)Cline 将 MCP Server 结果返回给 LLM

  • 7)LLM 整合后返回最终结果给 Cline

  • 8)Cline 将最终结果展现给用户

MCP Hosts 会和大模型有两次交互,另外根据 MCP Hosts 实现不同,MCP 可以依赖或者不依赖 FunctionCalling。

主要就是要把本地支持的函数信息告诉 LLM,可以在system提示词里也可以使用functionCalling 放到 functiions 里面,最终效果都是一样的。

  • 支持functionCalling的模型

    • 输入:函数放到 functions 里

    • 输出:output.tool_use 里面就是要调用的工具

  • 对不支持functionCalling的模型

    • 输入:函数放到提示词里,一般在system部分

    • 输出:使用xml标签,比如<user_mcp_tool><use_mcp_tool/> 之类的格式指定要调用的函数的参数信息

可以对于不支持 functionCalling的 LLM 也可以达到同样的效果。

5. 小结

本文通过实战演示了 Anthropic 开源协议 MCP 的核心价值 —— 通过标准化接口实现大模型与外部工具的解耦集成。我们基于 Go 语言实现了两种典型部署形态的 MCP Server:

  • Stdio 类型:通过进程的标准输入输出完成交互,方便本地工具集成。

  • SSE 类型:采用 Server-Sent Events 技术实现长连接通信,支持远程接入。

从架构层面看,MCP 定义的三方角色(MCP Server 提供工具能力、LLM 负责逻辑决策、MCP Hosts 协调交互流程)形成了清晰的职责分层。这不仅让工具开发者可专注于功能实现(如本文的计算器工具),也让模型开发者能聚焦算法优化,最终通过标准化协议推动大模型生态从 “烟囱式开发” 向 “组件化协同” 演进。

随着更多工具接入 MCP 生态,这种标准化交互协议将成为连接大模型与垂直领域数据的 “数字桥梁”,为企业级 AI 应用开发提供更高效的基础设施。

最后再贴一下工作流程:

mcp-workflow.jpg