Volcano HDRF 深度解析:用层级主导资源公平算法实现树状队列资源调度
2026/9/17 11:48:32 网站建设 项目流程

Volcano HDRF 深度解析:用层级主导资源公平算法实现树状队列资源调度

【免费下载链接】volcanoA Cloud Native Batch System (Project under CNCF)项目地址: https://gitcode.com/GitHub_Trending/vol/volcano

本篇技术指南围绕 Volcano 调度器中的 HDRF(Hierarchical Dominant Resource Fairness,层级主导资源公平)设计与实现展开,讲解为何扁平化的 DRF 权重模型无法表达树状共享结构、HDRF 如何通过"缩放子节点、忽略饱和节点"解决互补资源饿死与饱和节点阻塞两大经典问题,以及如何通过 Queue 注解与 drf 插件选项在 pkg/scheduler/plugins/drf/drf.go 中落地。读完本文,你将掌握 HDRF 的注解配置方式、enableHierarchy插件选项的用法、allocate/preempt/reclaim 三个调度动作在层级队列下的行为差异,以及该特性与 proportion 插件的冲突边界。

背景:从 DRF 到 HDRF,为什么需要树状层级公平

DRF(Dominant Resource Fairness,主导资源公平)是 Kubernetes 生态中多资源调度最常用的公平算法之一。在 Volcano 调度器中,资源共享的公平性通常靠为队列(Queue)和命名空间(Namespace)设置权重来保证——drf插件通过比较各作业的主导份额(dominant share)决定调度顺序,proportion插件则按队列权重比例分配资源。然而,这种"一个大的扁平化加权共享"模型无法表达树状的层级共享结构(tree-like hierarchy)。

当需要表达比"命名空间/队列的扁平加权共享"更复杂的层级分组共享时,例如部门 → 小组 → 项目、或者业务线 → 环境 → 应用这样的多级结构,就需要引入 HDRF 算法。HDRF 源自 Berkeley 团队关于多资源公平分配的经典论文(H-DRF),论文指出了朴素 DRF 无法适应的两个典型场景,也是 HDRF 需要解决的两个核心问题。

问题一:互补主导资源子节点引发的饿死(Starvation)

考虑下面这个层级结构:

n / \ n1 w=50% n2 w=50% (0 CPU,1 GPU) / \ n2,1 w=50% n2,2 w=50% (1 CPU,0 GPU) (0 CPU,1 GPU)

节点 n1 的请求是 GPU 型(0 CPU, 1 GPU),n2 下有 n2,1(CPU 型)与 n2,2(GPU 型)两个子节点。假设 n2,1 组拥有更高的主导份额(例如 CPU 100%),这会给它的父节点 n2 带来 100% 的份额。此时若有作业被移除、新作业加入,n2 的兄弟节点 n2,2 会因为主导资源不同(例如 GPU 50%)而持续受到惩罚——因为 GPU 资源会一直被分配给主导份额更小的 n1 组,n2,2 始终无法获得 GPU。

HDRF 的解法是:把子节点缩放到最小节点(rescale children to the minimum node)。在本例中,n2,1 组被缩放为(10,0) * (0.5/1) = (5,0),再累加到父节点上,于是父节点 n2 的份额变为 50%,这正是我们期望的公平状态——n2 与 n1 各占一半,而不是让 GPU 全部流向 n1。

问题二:饱和节点引起的资源阻塞(Blocking)

再看第二个场景:

n / | | \ n1 n2 n3 n4 (1 CPU,0 GPU) (1 CPU,0 GPU) / \ (0 CPU,1 GPU) n3,1 n3,2 (1 CPU,0 GPU) (0 CPU,1 GPU)

假设某个时刻,每个叶子节点都分配到了其主导资源的 1/3。此时只要有新任务被分配到 n4 组,n4 的主导份额就会高于 n3,导致所有剩余的 GPU 资源被反复分配给 n3,2。最终结果:n3,2 拿到了 2/3 的 GPU,而 n4 只拿到 1/3 的 GPU——n4 被"饱和"的 n3 阻塞了。

HDRF 的解法分三步:

  1. 在所有未被阻塞(non-blocked)的节点中,选取最小的主导份额 M;
  2. 把每个非阻塞节点的资源消耗向量(resource consumption vector)重新缩放,使其主导份额等于 M;
  3. 将所有节点(阻塞与非阻塞)的向量累加,得到父节点的资源消耗向量。

此外,HDRF 在计算任意内部节点的主导份额时会忽略已饱和(saturated)的资源,从而避免饱和节点对整体公平性的干扰。

Volcano 中的 HDRF 实现

层次结构与权重的声明:Queue 注解

HDRF 所关心的层级结构与权重,通过 Queue 的注解(annotations)声明,使用两个固定的注解键:

  • volcano.sh/hierarchy:声明队列所处的层级路径,例如root/eng/prod
  • volcano.sh/hierarchy-weights:声明与该路径各级对应的权重,例如100/50/50

这两个注解键在 staging/src/volcano.sh/apis/pkg/apis/scheduling/v1beta1/labels.go 中定义为常量KubeHierarchyAnnotationKeyKubeHierarchyWeightAnnotationKey。调度器在构建QueueInfo时从 Queue 注解中解析出这两个字段(见 pkg/scheduler/api/queue_info.go):其中Weights是一串用斜杠分隔的浮点数,每个数值对应层级路径上的一级权重;Hierarchy是从根到该节点路径上的节点名列表。

一个带层级声明的 Queue 示例:

apiVersion: scheduling.volcano.sh/v1beta1 kind: Queue metadata: name: prod annotations: volcano.sh/hierarchy: "root/eng/prod" # 路径:root → eng → prod volcano.sh/hierarchy-weights: "100/50/50" # 每级权重:root=100, eng=50, prod=50 spec: weight: 1 reclaimable: true

注意:spec.weight是队列自身的基础权重,与注解中的层级权重(volcano.sh/hierarchy-weights)是两套独立的概念,后者专门服务于 HDRF 的树状公平计算。

启用开关:drf 插件新增的 hierarchyEnable 选项

HDRF 作为 drf 插件的一个可选能力,通过插件配置选项enableHierarchy(对应代码中的EnabledHierarchy *bool,yaml 键名为enableHierarchy)开启。该字段定义在 pkg/scheduler/conf/scheduler_conf.go 的PluginOption结构中。drfPlugin.HierarchyEnabled()(见 drf.go)会在调度会话(Session)的 tiers 配置中检索 drf 插件,返回enableHierarchy是否被显式置为 true。

在调度器配置(例如 benchmark/testcases/gang/config/scheduler-config.yaml 所使用的 ConfigMap 格式)中启用方式如下:

actions: "enqueue, allocate, backfill, preempt, reclaim" tiers: - plugins: - name: drf enableHierarchy: true # 开启 HDRF 层级公平 enableQueueOrder: true # 按层级份额参与队列排序 enableReclaimable: true # 按层级份额参与抢占回收

核心数据结构

drf 插件内部为 HDRF 维护了一棵层级树,根节点名为root、权重固定为 1。树中每个节点由hierarchicalNode表示(drf.go),包含以下关键字段:

  • parent/children:父子关系,叶子节点没有子节点;
  • attr *drfAttr:该节点的 DRF 属性,含share(主导份额)、dominantResource(主导资源名)、allocated(已分配资源量);
  • request *api.Resource:仅叶子节点有意义,表示该作业的总请求量;
  • weight float64:该节点在层级中的权重;
  • saturated bool:该节点是否已饱和(无法再获得资源);
  • hierarchy string:节点在路径中的名字。

树通过Clone()方法支持整树深拷贝,这在 reclaim 动作的模拟计算中至关重要(详见后文)。

更新流程:三步递归算法

启用 HDRF 后,在任务分配(allocate)和释放(deallocate)事件发生时,drf 属性按照以下三个步骤更新(对应OnSessionOpen中注册的AllocateFunc/DeallocateFunc事件处理器,见 drf.go):

  1. 构建层级路径:根据作业所属 Queue 的hierarchy注解,沿路径构建层级节点。例如层级root/eng/prod加权重100/50/50会构造出三级层级结构(root → eng → prod)。此逻辑由buildHierarchy()实现(drf.go):逐级解析以/分隔的路径与权重,已存在的节点直接复用,不存在的节点按权重创建(权重解析失败或小于 1 时被强制为 1),最后把作业作为叶子节点挂到路径末端。

  2. 计算作业的 DRF 属性并标记饱和:作业叶子节点按照普通 DRF 算法计算主导份额与主导资源;如果其任一资源已满足(已分配量达到请求量),或者它请求的资源已全部被分配完毕(不再是 demanding resource),则标记为饱和。饱和判定由resourceSaturated()实现(drf.go)。

  3. 自底向上递归更新内部节点:从根开始递归,先计算所有非阻塞子节点的主导资源,找出其中最小的主导份额;将非阻塞子节点的资源向量按最小份额缩放(饱和子节点不缩放、直接累加原始向量),累加所有子节点(阻塞与非阻塞)的资源得到父节点的资源消耗向量;在"有需求"(demanding)的资源集合中计算父节点的主导份额;若所有子节点都已饱和,则父节点也标记为饱和。该逻辑由updateHierarchicalShare()实现(drf.go)。

入口函数UpdateHierarchicalShare()(drf.go)还会先做一步"需求资源过滤":只有当前总分配量(totalAllocated)小于集群总资源(totalResource)的资源才被视为 demanding resource,饱和资源不参与内部节点主导份额的计算——这正是文档中"HDRF 忽略饱和资源"原则的代码落地。

三个调度动作中的 HDRF 语义

allocate:沿层级路径比较队列

启用层级后,队列的排序顺序是沿层级路径逐级比较的。compareQueues()(drf.go)实现如下语义:两个同层节点的share / weight若相等(落入同一档),则继续下钻比较路径上的子节点;若不相等,则份额小的优先。饱和节点拥有最低优先级——它已无法再获得资源,自然排在队尾。队列排序函数通过ssn.AddQueueOrderFn()注册(drf.go)。

同时,层级模式下存在一条约束:一个队列不能同时包含子队列和作业。例如root/sci队列与root/sci/dev队列互相冲突,因为它们试图把同一层级路径节点既当作作业容器又当作子队列容器。

preempt:纳入层级队列优先级

抢占时,除了原有的命名空间优先级(namespace priority)与作业优先级(job priority),层级队列的优先级也必须被纳入考量。具体规则为:份额(share)更低的队列拥有更高优先级,饱和队列拥有最低优先级。这保证了低份额队列中的作业在被抢占时处于更有利的地位。

reclaim:以 HDRF 份额决定应得资源

回收(reclaim)动作中,队列的"应得份额"(deserved share)由队列的 HDRF 份额决定。实现上,reclaim 函数会先克隆整棵 HDRF 树和 totalAllocated 向量(见 drf.go),模拟"回收者拿到资源"后的树状态;对每个候选被回收对象,再模拟"释放其资源"后的状态,调用compareQueues()比较回收者队列与被回收者队列的层级份额;若回收者份额仍更低,则将其加入受害者列表。模拟结束后恢复现场(把释放的资源加回 totalAllocated 并重新 updateShare),保证不影响真实状态——这正是Clone()存在的意义。

测试验证:rescaling 与 blocking 两个经典场景

HDRF 的单元测试位于 pkg/scheduler/plugins/drf/hdrf_test.go,TestHDRF通过 uthelper 构造真实调度会话(同时启用 drf 与 proportion 插件、仅运行 allocate 动作),用两个用例分别验证文档中描述的两大问题均被修复:

用例一 "rescaling test"(缩放测试):构造root/sci(权重100/50)、root/eng/devroot/eng/prod(权重100/50/50)三个层级队列,分别提交 CPU 型作业(10 个 pod,各 1 CPU)与内存型作业(10 个 pod,各 1G 内存),集群总资源 10 CPU / 10G。最终断言:sci 队列作业获得 5 CPU + 5G(占总量 50%),eng 下的 dev 作业获得 5 CPU、prod 作业获得 5G 内存——互补资源子节点经缩放后各得其所,没有出现 GPU/内存被单边占满的饿死现象。

用例二 "blocking nodes test"(饱和节点阻塞测试):构造root/pg1root/pg2root/pg3/pg31root/pg3/pg32root/pg4五条层级路径(权重100/25100/25/50),混合 CPU 型与内存型作业各 30 个 pod,总资源 30 CPU / 30G。最终断言:CPU 型的 pg1、pg2、pg31 各得 10 CPU,内存型的 pg32、pg4 各得 15G——即便 pg3 子树已趋于饱和,pg4 也不再被阻塞,仍然拿到 50% 的内存份额。

两个用例共同验证了 HDRF 的核心行为:子节点按最小主导份额缩放、饱和节点不缩放不参与最小份额选取、内部节点只在 demanding 资源集合上计算主导份额

局限性与注意事项

  1. 与 proportion 插件冲突:HDRF 与 proportion 插件互斥。启用 HDRF 后应禁用 proportion,同时需要补充一个"按层级份额与权重比较队列"的 reclaim 函数(drf 插件自身的 reclaim 实现已覆盖此职责)。配置时务必注意不要把proportion的队列排序/回收能力与 drf 的enableHierarchy混在同一 tier 中,以免两个公平模型相互干扰。

  2. 递归更新开销:每当有作业加入层级树,HDRF 需要从根开始递归遍历整棵树,并标记所有饱和节点(当某些资源被完全分配时)。在队列层级深、作业数量大的场景下,这一过程可能效率不高。从实现看,每次 allocate/deallocate 事件都会触发一次UpdateHierarchicalShare()的全树递归,调度规模较大时需要评估其性能影响。

延伸阅读

  • DRF 插件设计:HDRF 所依赖的基础 DRF 算法与作业排序逻辑;
  • 队列管理文档:Queue 对象模型与权重语义;
  • proportional 设计:与 HDRF 互斥的比例公平插件,理解两者的边界有助于正确选型;
  • 公平份额插件使用指南:fairshare 插件与 DRF/HDRF 的定位差异。

【免费下载链接】volcanoA Cloud Native Batch System (Project under CNCF)项目地址: https://gitcode.com/GitHub_Trending/vol/volcano

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询