【集群】云原生批调度实战:Volcano 测试流程拆解

【集群】云原生批调度实战:Volcano 测试流程拆解

Fre5h1nd Lv6

本系列《云原生批调度实战:Volcano 监控与性能测试》计划分为以下几篇,点击查看其它内容。

  1. 云原生批调度实战:调度器测试与监控工具 kube-scheduling-perf
  2. 云原生批调度实战:调度器测试与监控工具 kube-scheduling-perf 实操注意事项说明
  3. 云原生批调度实战:调度器测试监控结果
  4. 云原生批调度实战:本地环境测试结果与视频对比分析
  5. 监控与测试环境解析:测试流程拆解篇
  6. 监控与测试环境解析:指标采集与可视化篇
  7. 监控与测试环境解析:自定义镜像性能回归测试
  8. 监控与测试环境解析:数据收集方法深度解析与Prometheus Histogram误差问题
  9. 云原生批调度实战:Volcano调度器enqueue功能禁用与性能测试
  10. 云原生批调度实战:Volcano Pod创建数量不足问题排查与Webhook超时修复
  11. 云原生批调度实战:Volcano版本修改与性能测试优化
  12. 云原生批调度实战:Volcano Webhook禁用与性能瓶颈分析
  13. 云原生批调度实战:Volcano性能瓶颈猜想验证与实验总结

本文将以 Volcano 为代表,解析kube-scheduler-performance工具一次 调度器性能测试 从集群启动到结果归档的完整链路,搞清楚 每个步骤调用了哪些文件、各自作用是什么,为后续指标剖析与实验扩展打下基础。

阅读完本文,希望能够帮你快速实现:

  1. 复现最小规模的 Volcano 性能测试;
  2. 在源码中快速定位某一步骤的入口脚本 / YAML。

1️⃣ 执行链路总览

执行一次最小测试的命令非常简单:

1
2
make prepare-volcano start-volcano end-volcano \
NODES_SIZE=1 JOBS_SIZE_PER_QUEUE=1 PODS_SIZE_PER_JOB=1

这背后却触发了 ≥ 10 个 Makefile 目标与 30+ 个 YAML / Shell / Go 文件。整体时序如图所示(流程简化,仅保留关键节点):

graph TD;
    A[prepare-volcano] --> B[up-volcano];
    B --> C[kind 创建测试集群];
    C --> D[wait-volcano];
    D --> E[test-init-volcano];
    A -.->|完成后调用| F[start-volcano];
    F --> G[reset-auditlog-volcano];
    G --> H[test-batch-job-volcano];
    F -.-> |完成后调用| I[end-volcano];
    I --> J[down-volcano];

Tips: prepare-volcano → start-volcano → end-volcano 由根 Makefile 的 define test-scheduler 宏在编译期自动展开。

宏展开示例:Volcano

define test-scheduler 是一个带占位符 $(1) 的宏,最后通过 $(foreach sched,$(SCHEDULERS),$(eval $(call test-scheduler,$(sched))))kueue / volcano / yunikorn 进行循环替换。下面以 Volcano 为例简要对比“模板”与“实例”——

模板片段(截自 Makefile:110:140)

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
define test-scheduler

.PHONY: prepare-$(1)
prepare-$(1):
make up-$(1)
make wait-$(1)
make test-init-$(1)

.PHONY: start-$(1)
start-$(1):
make reset-auditlog-$(1)
make test-batch-job-$(1)

.PHONY: end-$(1)
end-$(1):
make down-$(1)

.PHONY: up-$(1)
up-$(1):
make -C ./clusters/$(1) up

.PHONY: down-$(1)
down-$(1):
-make -C ./clusters/$(1) down

.PHONY: wait-$(1)
wait-$(1):
make -C ./clusters/$(1) wait

bin/test-$(1): $(shell find ./test/utils ./test/$(1) -type f)
$(GO_IN_DOCKER) go test -c -o ./bin/test-$(1) ./test/$(1)

展开后(自动生成)的部分规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.PHONY: prepare-volcano
prepare-volcano:
make up-volcano
make wait-volcano
make test-init-volcano

.PHONY: start-volcano
start-volcano:
make reset-auditlog-volcano
make test-batch-job-volcano

.PHONY: end-volcano
end-volcano:
make down-volcano

...

借助这种“写一次、生成三份”的做法,大幅减少了针对不同调度器写重复 Make 目标的工作量。


2️⃣ 关键 Makefile 目标拆解

目标所在文件主要命令职责说明
prepare-volcanoMakefilemake up-volcano make wait-volcano make test-init-volcano集群启动 + 基础就绪检查 + 预热测试二进制
up-volcanoclusters/volcano/Makefilekind create cluster & 部署 Volcano创建 Kind 集群并应用 Volcano 相关 Kustomize 资源
wait-volcano同上kubectl wait --for=condition=Ready等待所有 Pod Ready,含 controller / scheduler
test-init-volcanoMakefile运行 bin/test-volcano -run ^TestInit生成初始队列、扩容节点
start-volcanoMakefilemake reset-auditlog-volcano make test-batch-job-volcano清空上一轮 audit 日志 & 正式发压
reset-auditlog-volcanoclusters/volcano/Makefilekubectl delete audit-log ConfigMap置空历史日志,保证数据窗口准确
test-batch-job-volcanoMakefilebin/test-volcano -run ^TestBatchJob按参数批量提交 Job / Pod 并记录时间线
end-volcanoMakefilemake down-volcano销毁 Kind 集群,释放资源

3️⃣ clusters/volcano 目录速览

  • kind.yaml:集群版本、节点数量、containerd 本地镜像仓库挂载;
  • deployment.yaml:Volcano Controller 与 Scheduler 部署模板;
  • kustomization.yaml:声明所有资源并支持 image 覆盖;
  • service.yaml:暴露 Volcano webhook / metrics(如需)。

这些文件通过 kustomize build 管道被 up-volcano 目标应用到 Kind 集群中。


4️⃣ test/volcano 测试代码剖析

核心 Go 测试位于 test/volcano/ 目录,包含两个主要测试函数和一套完整的 Provider 实现。整体思路:通过 Go 语言编写测试用例,调用 Kubernetes API 在真实集群中创建资源,模拟大规模批量调度场景。

TestInit 和 TestBatchJob 流程

test/volcano/batch_job_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func TestInit(t *testing.T) {
err := provider.AddNodes(t.Context())
if err != nil {
t.Fatal(err)
}

err = provider.InitCase(t.Context())
if err != nil {
t.Fatal(err)
}
}

func TestBatchJob(t *testing.T) {
err := provider.AddJobs(t.Context())
if err != nil {
t.Fatal(err)
}

err = utils.WaitDeployment(t.Context(), utils.Resources)
if err != nil {
t.Fatal(err)
}
}

核心逻辑

  • TestInit集群预热阶段,创建假节点(通过 KWOK)和配置 Volcano 队列层级
  • TestBatchJob正式压测阶段,批量提交 Job 并等待调度完成

两个测试函数通过 **全局变量 provider**(VolcanoProvider 实例)共享状态,测试参数从环境变量或 Makefile 传入。

参数传递机制解析

测试参数的传递遵循 Makefile → 环境变量 → Go Flag → Provider 实例 的四级链路:

1. Makefile 参数定义

Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
CPU_PER_NODE ?= 128
MEMORY_PER_NODE ?= 1024Gi
NODES_SIZE ?= 1

QUEUES_SIZE ?= 1
JOBS_SIZE_PER_QUEUE ?= 1
PODS_SIZE_PER_JOB ?= 1

CPU_REQUEST_PER_POD ?= 1
MEMORY_REQUEST_PER_POD ?= 1Gi

GANG ?= false
PREEMPTION ?= false

2. 环境变量注入

当执行 make test-batch-job-volcano 时,Makefile 会将上述变量作为环境变量传递给测试进程:

Makefile
1
2
3
4
5
6
7
TEST_ENVS = \
NODES_SIZE=$(NODES_SIZE) \
CPU_PER_NODE=$(CPU_PER_NODE) \
MEMORY_PER_NODE=$(MEMORY_PER_NODE) \
QUEUES_SIZE=$(QUEUES_SIZE) \
JOBS_SIZE_PER_QUEUE=$(JOBS_SIZE_PER_QUEUE) \
...

3. Go Flag 解析

test/volcano/main_test.go 中,provider.AddFlags() 调用 test/utils/option.go 的标准 flag 库:

test/utils/option.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (o *Options) AddFlags() {
flag.StringVar(&o.CpuPerNode, "cpu-per-node", getEnv("CPU_PER_NODE", "32"), "CPU resources per node")
flag.StringVar(&o.MemoryPerNode, "memory-per-node", getEnv("MEMORY_PER_NODE", "256Gi"), "Memory resources per node")
flag.IntVar(&o.NodeSize, "nodes-size", getEnvInt("NODES_SIZE", 1), "Number of nodes to create")

flag.IntVar(&o.QueueSize, "queues-size", getEnvInt("QUEUES_SIZE", 1), "Number of queues to create")
flag.IntVar(&o.JobsSizePerQueue, "jobs-size-per-queue", getEnvInt("JOBS_SIZE_PER_QUEUE", 1), "Number of jobs per queue")
flag.IntVar(&o.PodsSizePerJob, "pods-size-per-job", getEnvInt("PODS_SIZE_PER_JOB", 1), "Number of pods per job")

flag.BoolVar(&o.Gang, "gang", getEnvBool("GANG", false), "Enable gang scheduling")
flag.BoolVar(&o.Preemption, "preemption", getEnvBool("PREEMPTION", false), "Enable preemption")
}

func getEnv(key, fallback string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return fallback
}

4. 优先级覆盖机制

参数读取有明确的优先级:命令行参数 > 环境变量 > 默认值

例如:

  • make test-batch-job-volcano NODES_SIZE=100:通过 Makefile 变量传递
  • NODES_SIZE=200 ./bin/test-volcano:直接设置环境变量
  • ./bin/test-volcano -nodes-size=300:命令行参数(最高优先级)

因此,当我们在前面代码中看到 p.CpuPerNodep.NodeSize 等字段时,它们的值最终来源于这套参数传递链路,确保了测试规模可以通过 Makefile 灵活调控。

VolcanoProvider 核心实现

1. AddNodes:批量创建假节点

test/volcano/provider_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (p *VolcanoProvider) AddNodes(ctx context.Context) error {
builder := utils.NewNodeBuilder().
WithFastReady().
WithCPU(p.CpuPerNode).
WithMemory(p.MemoryPerNode)
for i := range p.NodeSize {
err := utils.Resources.Create(ctx,
builder.
WithName(fmt.Sprintf("node-%d", i)).
Build(),
)
if err != nil {
return err
}
}
return nil
}

解析:通过 utils.NewNodeBuilder() 构造器模式批量生成 Node 对象。关键点:

  • WithFastReady():设置节点状态为 Ready,跳过真实硬件检测
  • WithCPU(p.CpuPerNode):每个节点的 CPU 容量(如 “128”)
  • WithMemory(p.MemoryPerNode):每个节点的内存容量(如 “1024Gi”)

2. InitCase:配置 Volcano 调度器

关键代码片段

test/volcano/provider_test.go
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
func (p *VolcanoProvider) InitCase(ctx context.Context) error {
// 1. 解析资源配额
cpuPerQueue, err := resource.ParseQuantity(p.CpuPerQueue)
memoryPerQueue, err := resource.ParseQuantity(p.MemoryPerQueue)

// 2. 计算层级资源限制
if p.CpuLendingLimit != "" {
cpuLendingLimit, err := resource.ParseQuantity(p.CpuLendingLimit)
hierarchy = true
cpuCapabilityTotal = utils.TimesQuantity(cpuPerQueue, p.QueueSize+p.ImpactingQueuesSize+p.CriticalQueuesSize).String()
cpuCapability = cpuPerQueue.String()
cpuPerQueue.Sub(cpuLendingLimit) // deserved = capability - lending
cpuDeserved = cpuPerQueue.String()
}

// 3. 更新 root Queue 配置
obj := &unstructured.Unstructured{}
obj.SetName("root")
obj.SetAPIVersion("scheduling.volcano.sh/v1beta1")
obj.SetKind("Queue")
err = utils.Resources.Patch(ctx, obj, k8s.Patch{
PatchType: types.MergePatchType,
Data: []byte(fmt.Sprintf(`{"spec":{"capability":{"cpu": %q, "memory": %q}}}`, cpuCapabilityTotal, memoryCapabilityTotal)),
})

// 4. 重启 Volcano Scheduler 使配置生效
err = utils.RestartDeployment(ctx, utils.Resources, "volcano-scheduler", "volcano-system")
}

核心逻辑解析

  1. 资源计算:根据队列数量计算总容量(cpuCapabilityTotal)和单队列配额(cpuCapability
  2. 层级调度:如果设置了 CpuLendingLimit,启用 hierarchy=true,计算 deserved(保证资源)和 capability(借用上限)
  3. 动态配置:通过 unstructured.Unstructured 直接操作 CRD,避免导入 Volcano 依赖
  4. 热重启:更新 ConfigMap 后重启 Scheduler Pod,确保新配置生效

3. AddJobs:分批提交作业

test/volcano/provider_test.go
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
func (p *VolcanoProvider) AddJobs(ctx context.Context) error {
steps := []struct {
queueSize int
jobsPerQueue int
podsPerJob int
priority string
duration string
delay time.Duration
}{
{p.QueueSize, p.JobsSizePerQueue, p.PodsSizePerJob, "long-term-research", p.PodDuration, 0},
{p.ImpactingQueuesSize, p.ImpactingJobsSizePerQueue, p.ImpactingPodsSizePerJob, "business-impacting", p.ImpactingPodDuration, 5 * time.Second},
{p.CriticalQueuesSize, p.CriticalJobsSizePerQueue, p.CriticalPodsSizePerJob, "human-critical", p.CriticalPodDuration, 5 * time.Second},
}

for _, step := range steps {
if step.delay > 0 {
time.Sleep(step.delay) // 模拟业务场景:优先级作业延迟到达
}
for i := range step.queueSize {
for range step.jobsPerQueue {
err := p.addSingleJobs(ctx, step.podsPerJob, i, step.priority, step.duration)
if err != nil {
return err
}
}
}
}
return nil
}

策略解析:通过 三阶段提交 模拟真实多租户场景:

  1. 第一波long-term-research 队列,立即提交(delay=0)
  2. 第二波business-impacting 队列,5秒后提交(模拟业务高峰)
  3. 第三波human-critical 队列,再5秒后提交(模拟紧急任务)

每个阶段都有独立的 queueSize × jobsPerQueue × podsPerJob 三维参数,可灵活调整压测规模。

Volcano Job 模板解析

使用的是 Volcano CRD batch.volcano.sh/v1alpha1/Job,关键字段:

test/volcano/batch_job.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
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
name: volcano-job-#{{ .name }}-#{{ .index }}
spec:
#{{ if .gang }}
minAvailable: #{{ .size }}
#{{ else }}
minAvailable: 1
#{{ end }}
schedulerName: volcano
queue: #{{ .queue }}
tasks:
- replicas: #{{ .size }}
template:
spec:
containers:
- name: sleep
image: hello-world
resources:
requests:
cpu: #{{ .cpuRequestPerPod }}
memory: #{{ .memoryRequestPerPod }}
nodeSelector:
"type": kwok

模板机制解析

  • 模板语法#{{ .variable }}utils.YamlWithArgs() 在运行时替换
  • Gang 调度minAvailable: #{{ .size }} 要求所有 Pod 同时就绪,模拟 MPI/AI 训练场景
  • 资源隔离queue: #{{ .queue }} 指定队列,schedulerName: volcano 确保由 Volcano 调度
  • 假负载nodeSelector: "type": kwok 强制调度到假节点,image: hello-world 秒级启动

实际执行过程addSingleJobs() 调用 utils.YamlWithArgs() 将参数注入模板,再通过 decoder.DecodeEach() 解析 YAML 并调用 Kubernetes API 创建 Job。


技术要点总结:整个测试框架通过 sigs.k8s.io/e2e-framework 与真实 API Server 通信,产生的所有 API 调用都会被 audit-policy.yaml 记录,为后续指标分析提供数据源。


5️⃣ hack 脚本的幕后协同

脚本触发时机作用
hack/kind-with-local-registry.shup-volcano启动本地 5001 registry + 注入 containerd 配置(**在每个 Kind 节点的 /etc/containerd/certs.d/ 写入 hosts.toml**,指向本地仓库),实现“边拉边推、本地秒级拉取”
hack/local-registry-with-load-images.shup-volcano 期间将远端镜像拉取后重新打 tag 推到本地 registry,Kind 节点下载极快
hack/replace-qps.sh可选修改 APIServer QPS,一键替换所有带 # <--QPS 注释的 YAML 中的数值,从而把 --kube-api-qps 或 webhook QPS 提到 1000+,缓解 API Server 限流
hack/save-result-images.sh测试结束后调用 Grafana API 截图面板,写入 output/ 归档

containerd 配置注入示例

hack/kind-with-local-registry.sh 的核心逻辑:

hack/kind-with-local-registry.sh
1
2
3
4
5
6
7
8
9
10
# Create kind cluster with containerd registry configuration
kind create cluster --config "${KIND_CONFIG:-}" --name "${KIND_CLUSTER_NAME:-kind}"

# Configure containerd registry hosts on all nodes
for node in $(kind get nodes); do
docker exec "${node}" mkdir -p "${registry_dir}"
docker exec -i "${node}" tee "${registry_dir}/hosts.toml" >/dev/null <<EOF
[host."http://${in_cluster_registry}"]
EOF
done

核心原理:脚本为每个 Kind 节点在 /etc/containerd/certs.d/kind-registry:5000/hosts.toml 写入配置,告诉 containerd 优先从本地 registry 拉取镜像,实现离线加速。

修改 APIServer QPS 示例

当测试遇到 API Server QPS 瓶颈时,可以手动调用脚本,批量调整:

1
2
# 把所有标记为 "# <--QPS" 的数值统一改为 2000
hack/replace-qps.sh 2000

脚本核心逻辑:

hack/replace-qps.sh
1
2
3
4
5
find . \
-iname "*.yaml" \
-not \( -path ./vendor/\* -o -path ./tmp/\* \) \
-type f \
-exec sed -i 's|\([0-9]\+\)\(.\+\)# <--QPS|'${QPS}'\2# <--QPS|g' {} +

工作原理:遍历项目内所有 .yaml 文件,将包含 # <--QPS 标记行的数字替换为指定值。项目中共有 12 个文件、20+ 处配置受影响,包括调度器组件的 --kube-api-qps/--kube-api-burst 和 Kind 集群的 kube-api-qps 参数。


6️⃣ 结语

至此,我们已经串起了 Volcano 性能测试的最小可运行链路,并定位了关键 Makefile 目标与 YAML / Go 源码。下一篇将深入 指标采集与可视化,详解 audit-exporter 如何把 CREATED / SCHEDULED / RUNNING 三条曲线绘制到同一张 Grafana 面板。

📌 实践练习:如果感兴趣,可以尝试把 JOBS_SIZE_PER_QUEUE 改为 2,再次运行测试,观察 logs/results/ 目录下是否出现新的时间戳文件夹,并查看面板截图差异。



  • 希望这篇博客对你有帮助!如果你有任何问题或需要进一步的帮助,请随时提问。
  • 如果你喜欢这篇文章,欢迎动动小手给我一个follow或star。

🗺参考文献

[1] Github - kube-scheduling-perf

[2] A Comparative Analysis of Kueue, Volcano, and YuniKorn - Wei Huang, Apple & Shiming Zhang, DaoCloud

  • 标题: 【集群】云原生批调度实战:Volcano 测试流程拆解
  • 作者: Fre5h1nd
  • 创建于 : 2025-07-27 16:21:35
  • 更新于 : 2025-08-24 15:22:19
  • 链接: https://freshwlnd.github.io/2025/07/27/k8s/k8s-scheduler-performance-volcano-process/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论