【集群】云原生批调度实战:Volcano 深度解析(五):CREATE/SCHEDULE 阶段“卡顿”现象解析与协程数优化实验

【集群】云原生批调度实战:Volcano 深度解析(五):CREATE/SCHEDULE 阶段“卡顿”现象解析与协程数优化实验

Fre5h1nd Lv6

本系列《云原生批调度实战:Volcano 深度解析》计划分为以下几篇,点击查看其它内容。

  1. 云原生批调度实战:Volcano 深度解析(一)批处理背景需求与Volcano特点
  2. 云原生批调度实战:Volcano 深度解析(二)Volcano调度流程与调度状态
  3. 云原生批调度实战:Volcano 安装与初试
  4. 云原生批调度实战:Volcano 深度解析(三)核心流程解析与架构设计
  5. 云原生批调度实战:Volcano 深度解析(四)Webhook 机制深度解析
  6. 云原生批调度实战:Volcano 深度解析(五)CREATE/SCHEDULE 阶段“卡顿”现象解析与协程数优化实验

本文承接《CREATE 阶段瓶颈追踪与优化思考》,基于大量实验数据和源码深度分析,重新梳理 CREATE/SCHEDULE 卡顿现象 的根本原因。上篇博客中我们推测协程数不足是导致卡顿的主要原因,但实验结果却呈现了一些反直觉的现象,本文将结合源码分析给出更准确的解释。

0️⃣ 引言

在上一篇博客 CREATE 阶段瓶颈追踪 中,我们通过代码分析推测,Controller 的 worker 协程数不足可能是导致 CREATE/SCHEDULE 阶段出现卡顿(表现为“突增、突停”)现象的原因。然而,后续大量的性能测试揭示了一些反直觉的现象,挑战了这一初步结论。

  • 协程数增加并不总是提升性能:在某些配置下,增加协程数反而会降低性能

这些现象促使我们重新审视问题的根本原因。
本次,我们将结合最新的实验结果以及对 Volcano 源码的进一步分析,重新梳理 CREATE/SCHEDULE 卡顿现象的深层原因,并探讨其背后的 K8s API-Server 争用机制。

1️⃣ 背景回顾

1.1 卡顿现象描述

根据前期测试(无论是 KubeCon 的分享还是我们本地的复现),我们发现 Volcano 在处理大量 Pod 的 CREATESCHEDULE 过程中,性能曲线并非平滑上升,而是会出现卡顿现象,具体体现为:

  • “完成 CREATE 的 Pod 数量”“完成 SCHEDULE 的 Pod 数量” 指标各自出现持续增长一段时间后突停一段时间
  • 然后再持续增长一段时间后又突停一段时间,循环往复

CREATE/SCHEDULE 卡顿现象示意图1
CREATE/SCHEDULE 卡顿现象示意图1

CREATE/SCHEDULE 卡顿现象示意图2
CREATE/SCHEDULE 卡顿现象示意图2

这种现象引起了我们的担忧:这些停顿的平台期是否意味着时间的浪费?如果我们能消除这些停顿,把这些时间段用起来,是不是我们就能够达到更高的效率呢?

1.2 测试环境说明

前期我们保持 Node 数量和 Pod 数量不变,调整 Job 数量(以及对应的每 Job 内 Pod 数量)进行测试。测试配置如下:

BenchmarkJob×Pod总 Pod 数现象特征
benchmark-110K×110,000CREATESCHEDULE 几乎重叠,整体速度最慢
benchmark-2500×2010,000阶梯最清晰,CREATE/SCHEDULE 交替性卡顿
benchmark-3200×5010,000CREATE 有阶梯但速度明显快于 SCHEDULE
benchmark-420×50010,000CREATE 有阶梯,且与 SCHEDULE 有交点
benchmark-51×10K10,000CREATE 快速完成,SCHEDULE 成为瓶颈

2️⃣ 现象观察:三个层次的卡顿模式

仔细观察后,我们发现卡顿现象随 Job 数量有三个层次:

2.1 不卡顿:Job 数量少,每 Job 内 Pod 数量多

典型场景:benchmark-4(20×500)、benchmark-5(1×10K)

现象特征

  • CREATE 快速完成,SCHEDULE 慢慢完成
  • 两条曲线基本平滑,无明显卡顿
  • CREATE 始终比 SCHEDULE 更快

不卡顿现象示例
不卡顿现象示例

2.2 卡顿但互不影响:Job 数量较多,每 Job 内 Pod 数量较少

典型场景:benchmark-3(200×50)

现象特征

  • CREATE/SCHEDULE 交替性卡顿
  • CREATE 始终比 SCHEDULE 更快,两条曲线没有交点
  • 卡顿期间,另一条曲线仍在工作

卡顿但互不影响示例
卡顿但互不影响示例

2.3 卡顿且相互影响:Job 数量极多,每 Job 内 Pod 数量极少

典型场景:benchmark-2(500×20)、benchmark-1(10K×1)

现象特征

  • CREATE/SCHEDULE 交替性卡顿
  • CREATE 有时会阻塞 SCHEDULE,两条曲线存在交点
  • 整体性能最差

卡顿且相互影响示例1
卡顿且相互影响示例1

卡顿且相互影响示例2
卡顿且相互影响示例2

2.4 关键发现:CREATE 和 SCHEDULE 不会同时卡顿

仔细观察后,我们发现了一个非常有意思且关键的现象:CREATESCHEDULE 不会同时出现卡顿(即从图中看不会同时斜率为 0,而斜率代表着每秒完成对应操作的 Pod 数量)。

CREATE 和 SCHEDULE 不会同时卡顿示意图1:完成 CREATE 和 SCHEDULE 的 Pod 总数数量图
CREATE 和 SCHEDULE 不会同时卡顿示意图1:完成 CREATE 和 SCHEDULE 的 Pod 总数数量图

对照事件趋势图也会发现,CREATE(图中红色虚线)和SCHEDULE(图中亮绿色实线)的波峰和波谷恰恰好重合。

CREATE 和 SCHEDULE 不会同时卡顿示意图2:完成 CREATE 和 SCHEDULE 的 Pod 每秒吞吐图
CREATE 和 SCHEDULE 不会同时卡顿示意图2:完成 CREATE 和 SCHEDULE 的 Pod 每秒吞吐图

即使是在上述第 3 种层次下,两条曲线的交点也是一交即分。这也就意味着,CREATESCHEDULE 两种操作始终是在工作的,但是似乎存在一种严重的“争用”,导致某些时候只有一类操作能够顺利进行。

3️⃣ 原因分析:API-Server 请求排队争用

3.1 初步分析:CREATESCHEDULE 的独立性

根据上述关键现象,我们猜测造成“卡顿”的原因很可能是 CREATESCHEDULE 两种操作之间的“争用”。

然而,经过对 Volcano 源码的仔细分析,我们发现 CREATE 功能属于 Controller 组件,而 SCHEDULE 功能属于 Scheduler 组件,两者在代码层面并没有直接的调用、依赖或其它影响关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// pkg/controllers/job/job_controller_actions.go
func (cc *jobcontroller) syncJob(jobInfo *apis.JobInfo, updateStatus state.UpdateStatusFn) error {
// Controller 负责 Pod 创建
for _, pod := range podToCreateEachTask {
go func(pod *v1.Pod) {
defer waitCreationGroup.Done()
newPod, err := cc.kubeClient.CoreV1().Pods(pod.Namespace).Create(context.TODO(), pod, metav1.CreateOptions{})
// ...
}(pod)
}
waitCreationGroup.Wait()
}

// pkg/scheduler/actions/allocate/allocate.go
func (alloc *Action) Execute(ssn *framework.Session) {
// Scheduler 负责 Pod 调度
for _, task := range tasks {
// 调度决策和绑定
ssn.Bind(task, node)
}
}

3.2 深入分析:API-Server 层面的争用

那么争用发生在哪里?进一步分析会发现,这两个组件在完成各自的操作后,都需要将结果(创建 Pod / 更新 Pod 状态)提交到 K8s API-Server,并由其完成后续的 etcd 写入等操作。真正的争用就发生在这里

换言之,我们观察到的卡顿,是两大批对 K8s API-Server 的请求(CREATE 请求和 SCHEDULE 更新请求)排队导致的宏观表现。

3.2.1 CREATE 请求流

1
2
3
4
5
// Controller 创建 Pod 的请求流
JobController.syncJob()
→ kubeClient.CoreV1().Pods().Create()
→ kube-apiserver
→ etcd write

3.2.2 SCHEDULE 请求流

1
2
3
4
5
6
// Scheduler 调度 Pod 的请求流
VolcanoScheduler.allocate()
→ ssn.Bind()
→ kubeClient.CoreV1().Pods().Update()
→ kube-apiserver
→ etcd write

3.3 排队机制分析

那排队为什么会导致如此泾渭分明的“交替卡顿”呢?这就需要结合我们在上一篇博客中分析的协程阻塞模型

3.3.1 Job 数量对请求模式的影响

仔细分析上述三种层次的现象,其间唯一的区别是 Job 数量。Job 数量多时,会导致:

  1. 请求碎片化:当 Job 数量多时,原本一次性的 10000 个 Pod CREATE 请求,被切割成了许多个小批次的请求段;
  2. 争用与插队:在这些 CREATE 请求的小段之间,存在着时间空隙。Scheduler 发送的 SCHEDULE(Pod Update)请求就会“见缝插针”,填满这些空隙。这就导致了 API-Server 的请求队列中,呈现出一段 CREATE、一段 SCHEDULE 交替进行的局面。当 SCHEDULE 请求占据队列时,CREATE 请求就会排队阻塞,反之亦然。

因此,在不同 Job 数量下会有不同的请求模式

Job 数量少时

  • 所有 Pod 的 CREATE 请求(共 10K)可以在很短时间内同时被发送到 API-Server
  • 由于 SCHEDULE 需要的时间比 CREATE 更多,因此后续 SCHEDULE 请求陆陆续续被发送到 API-Server 时 CREATE 已经几乎完成
  • 不会产生二者间的争用

Job 数量多时

  • Pod 的 CREATE 请求断断续续被发送到 API-Server
  • 此后 SCHEDULE 请求也陆陆续续被发送到 API-Server
  • 由于 Pod 的多批 CREATE 请求间存在时间空隙,导致出现某些时刻 SCHEDULE 占满而使得 CREATE 排队阻塞

3.3.2 协程阻塞的放大效应

简单回顾一下:

Controller 是以 Job 为粒度进行处理的,一个 worker 协程会等待一个 Job 内的所有 Pod 创建完成后,才会开始处理下一个 Job。

在此过程中,由于 Controller 每个协程会等待一个 Job 完成后再进行下一个 Job 的处理,因此一旦一个 Job 中某些 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
// pkg/controllers/job/job_controller_actions.go
func (cc *jobcontroller) syncJob(jobInfo *apis.JobInfo, updateStatus state.UpdateStatusFn) error {
// ...
waitCreationGroup := sync.WaitGroup{}
// 收集所有需要创建的 Pod
for _, ts := range job.Spec.Tasks {
for i := 0; i < int(ts.Replicas); i++ {
// ...
waitCreationGroup.Add(1)
}
}

// 并发创建 Pod
for taskName, podToCreateEachTask := range podToCreate {
go func(taskName string, podToCreateEachTask []*v1.Pod) {
for _, pod := range podToCreateEachTask {
go func(pod *v1.Pod) {
defer waitCreationGroup.Done()
// API-Server 调用
newPod, err := cc.kubeClient.CoreV1().Pods(pod.Namespace).Create(context.TODO(), pod, metav1.CreateOptions{})
}(pod)
}
}(taskName, podToCreateEachTask)
}

// ⚠️ 关键阻塞点:等待所有 Pod 创建完成
waitCreationGroup.Wait()
}

3.4 CREATE 本身成为瓶颈的情况

此外还有一个因素,随着 Job 数量变多,CREATE 过程中 Controller 用于处理 Job 的计算量越来越大,速度也会变得越来越慢并成为瓶颈,这种情况下则无需争用分析,CREATE 本身就是瓶颈,对应上述benchmark-1(10K×1)。

3.5 小结

目前根据 “关键发现: CREATESCHEDULE 不会同时卡顿” 推断 vc-controller-manager 和 vc-scheduler 之间存在某种类型资源的争用,但暂未有更直接的证据,后续还将进一步分析。

但在此之外,有一个更根本的问题值得我们继续思考:“卡顿”问题如果能够被解决(即 CREATE 效率更高、不会“阻塞” SCHEDULE),整体效率就能更高吗?从后续实验会发现,并不一定。

4️⃣ 协程数优化实验:反直觉的结果

前期我们猜想协程数--worker-threads 是很重要的参数,并且猜想协程越多越能够并行、效率越高,但实验发现并非如此。

  1. “协程数” 与 “CREATE 效率” 非线性相关关系,也就是不是“协程越多越好”。
  2. CREATE 效率” 与 “整体效率” 非线性相关关系,也就是不是“CERATE 效率越快越好”。

4.1 实验设计

实验环境说明

  • 协程数分别为:1、5、10、25、50、100、150、200、400、600
  • Job 和 Pod 组合分别为:10000×1、5000×2、2000×5、1000×10、500×20、200×50、100×100、50×200、20×500、1×10000
  • 统计从测试开始到最后一个 CREATESCHEDULE 完成时间
  • 计划每种配置下都重复执行 3 次,但由于时间问题目前某些配置还只有 2 或 1 次

4.2 实验结果分析

实验得出了两个关键结论:

  • 在相同协程数下,随着 Job 数的增加,CREATE 用时整体提高。
  • 在相同 Job 数下,随着 Controller 协程的增加,CREATE 用时呈现出明显的类似 “V”字型:先下降,后提高。

4.2.1 相同协程数下,Job 数增加的影响

在相同协程数下,随着 Job 数的增加,CREATE 用时整体提高。这验证了我们前面的分析:

  1. Job 数量越多,Controller 用于处理 Job 的计算量越来越大
  2. Job 数量越多,CREATE 请求被切割得越细,与 SCHEDULE 请求的争用越严重。

在相同协程数下,不同 Job 数结果
在相同协程数下,不同 Job 数结果

4.2.2 相同 Job 数下,协程数增加的影响

在相同 Job 数下,随着 Controller 协程的增加,CREATE 用时先下降后提高(类似 V 字型)。

用时“先下降”(V 型左侧)的原因

  • 协程数过少时,瓶颈在 API-Server 通信和远程 etcd 的 IO
  • 计算资源未被充分利用,少量协程快速完成计算后发送 CREATE 请求、此后协程阻塞
  • 此时协程数增加可以充分利用计算资源,消除等待通信/IO 的气泡时间

用时“后提高”(V 型右侧)的原因

  • 协程数过多时,瓶颈在计算资源本身 + 加剧跨 Job 的 Pod 排队阻塞
  • 大量协程已充分利用所有计算资源,此时跨 Job 的 Pod 排队竞争且公平地依次创建
  • 一个 Job 内最后几个 Pod 阻塞整个 Job,导致突变情况更为严重

在相同 Job 数下,不同协程数结果
在相同 Job 数下,不同协程数结果

4.3 实验结论

协程数优化存在一个最优值,既不能太少(无法充分利用计算资源),也不能太多(加剧 API-Server 争用)。这个最优值需要根据具体的 Job 数量和 Pod 数量来动态调整。

5️⃣ 解决思路:从争用角度思考

5.1 问题本质重新认识

基于以上分析我们会发现,减小卡顿其实并不是”治本”。
即使我们通过调优让 CREATE 阶段飞速完成,这些 Pod 依然要等待缓慢的 SCHEDULE 阶段,总时间相差无几,甚至可能因为前期资源占用过猛而变得更长。(例如下图100x100500x20,调整协程后 CREATE 更快完成,但SCHEDULE完成时间反而更长)

  • 从全局最优的角度看,CREATESCHEDULE 的速率接近,实现一种动态平衡,可能是更好的选择

在相同 Job 数下,不同协程数结果2
在相同 Job 数下,不同协程数结果2

5.2 根本解决方案

如果想要彻底解决这个问题,可能需要对 Volcano 的整个架构进行比较大的修改,实现 CREATESCHEDULE 的合理配比、速率接近。可能需要:

  1. 调整 API-Server 的策略:实现更智能的请求排队和优先级机制
  2. 调整 Controller 和 Scheduler 之间的联动速率调整:实现动态的速率匹配
  3. 引入异步处理机制:将同步的批处理改为异步处理

5.3 短期优化方案

在有限时间内,可以考虑以下优化方向:(囊括前期总结的几项内容)

  1. 调整协程数:根据 Job 数量和 Pod 数量动态调整最优协程数
  2. 设置合适的 webhook timeout:避免超时导致的请求重试和性能下降
  3. 优化 webhook 效率:参考 前面的分析,将必要的校验功能转移到 Controller 或 K8s CRD(使用 CEL) 中。

6️⃣ 小结

截至目前,我们已经分析了几项特定的结论,并提出了一些解决方案(解决方案如上所述)。

【结论】

  1. Job数量对性能影响极大,Jobs数量越多性能越差。
  2. Jobs多时,CREATE/SCHEDULE 存在“卡顿”现象。但卡顿只是表象,本质很可能是资源争用导致的轮替;除此之外,不一定要把卡顿消除、而是应该让CREATE和SCHEDULE的卡顿速率保持一致。
  3. 除此之外,webhook本身对性能的影响仍然比较大,需要优化。

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

🗺参考文献

[1] Volcano GitHub 仓库

[2] Volcano 官方文档

[3] Kubernetes API Server 设计

[4] Kubernetes 控制器模式

[5] Volcano 调度器 Actions

[6] Kubernetes 调度器

[7] Volcano 架构设计

[8] Kubernetes Webhook 机制

  • 标题: 【集群】云原生批调度实战:Volcano 深度解析(五):CREATE/SCHEDULE 阶段“卡顿”现象解析与协程数优化实验
  • 作者: Fre5h1nd
  • 创建于 : 2025-09-04 23:44:55
  • 更新于 : 2025-09-05 11:33:19
  • 链接: https://freshwlnd.github.io/2025/09/04/k8s/k8s-volcano-create-schedule-contention-analysis/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论