告别 Device Plugin: Kubernetes 下一代异构资源管理利器 —— DRA

在 AI 和 GPU 计算盛行的今天,如何高效地在 Kubernetes 中调度显卡、FPGA 等硬件资源,一直是开发者关注的焦点。过去,我们依赖 Device Plugin 框架,但随着业务复杂化,其局限性(如无法动态共享、缺乏复杂的调度参数)日益凸显。

为了解决这些痛点,Kubernetes 推出了 Dynamic Resource Allocation (DRA)。该特性在 v1.34 版本中已正式进入 GA(General Availability) 阶段,标志着 K8s 进入了异构资源管理的 2.0 时代。

一、 为什么需要 DRA?(痛点分析)

在 DRA 出现之前,Device Plugin 是管理硬件的主力,但它有三个致命伤:

  1. 静态分配:资源请求只能是整数(如 nvidia.com/gpu: 1),难以实现 GPU 分片或复杂的组合请求。
  2. 网络与存储隔离:硬件驱动无法深度参与调度决策,导致调度器可能把 Pod 调度到一个虽然有 GPU 但网络带宽不足的节点上。
  3. API 表达力弱:用户无法指定“我需要一个显存大于 24GB 且支持 NVLink 的 GPU”这类复杂逻辑。

DRA 的核心理念是:像管理存储(PVC)一样管理硬件资源。


二、 DRA 的核心架构:四大支柱

DRA 引入了几个关键的 API 对象,构建了一套声明式的资源分配逻辑:

  1. DeviceClass(设备类):

    类似于存储的 StorageClass。它定义了某种硬件的统称(如 “high-performance-gpu”),并包含了驱动名称和通用选择逻辑。

  2. ResourceClaim(资源申请):

    类似于 PersistentVolumeClaim (PVC)。Pod 不直接请求硬件数量,而是引用一个 ResourceClaim。它描述了具体的资源需求(如:型号、显存大小、是否需要共享)。

  3. ResourceClaimTemplate(申请模板):

    用于 StatefulSet 或 Deployment。每当创建一个 Pod,控制器就会根据模板自动生成一个独立的 ResourceClaim。

  4. ResourceSlice(资源切片):

    这是驱动程序上报的“账本”。它详细列出了节点上每个设备的属性(如 UUID、频率、显存等),供调度器精准匹配。


三、 DRA 的黑科技特性

1. 结构化参数与 CEL 过滤

DRA 支持使用 Common Expression Language (CEL)。你可以写出类似“要求显存 > 16GB 且 驱动版本 >= 535”的表达式。调度器会根据 ResourceSlice 中的属性进行过滤,实现真正的“按需匹配”。

2. 延迟绑定(Late Binding)

这解决了著名的“死锁”问题。调度器会等到 Pod 确定了调度节点后,再触发驱动程序进行实际的硬件分配。这保证了硬件资源和计算节点在空间位置上的完美契合。

3. 设备共享与动态拆分(Partitioning)

DRA 允许驱动动态地将一个物理设备拆分为多个逻辑设备(如 NVIDIA 的 MIG)。多个 Pod 可以共享同一个 ResourceClaim,或者将一个物理 GPU 动态划分为多个小切片分给不同的容器。

4. 优先级列表(Prioritized List)

你可以给调度器一个“备选方案”:首选 A100 GPU,如果没有,给两块 T4 也可以。调度器会按优先级尝试满足你的需求。


四、 使用示例

下面是一个使用 DRA 进行 GPU 资源申请的典型配置示例。

这个例子展示了如何通过 DeviceClassResourceClaimPod 三者联动来请求特定属性的硬件。

1. 定义设备类 (DeviceClass)

这是由集群管理员定义的,用于描述某种类型的资源(类似于定义“黄金级存储”)。

1
2
3
4
5
6
7
8
9
apiVersion: resource.k8s.io/v1alpha3
kind: DeviceClass
metadata:
name: high-vram-gpu
spec:
selectors:
- cel:
# 使用 CEL 表达式筛选:显存大于 20GB 的设备
expression: device.attributes["vendor.com/vram-gb"] > 20

在 DRA 架构中,DeviceClass 到具体物理资源的“对应”过程,实际上是由 DRA 驱动程序(Driver)节点资源切片(ResourceSlice)控制面(Control Plane) 共同完成的。

我们可以把这个过程拆解为以下三个步骤:

1. 资源上报:数据从哪里来?

在每个带有硬件资源的节点上,都会运行一个该硬件厂商提供的 DRA Driver(类似于以前的 Device Plugin Agent)。

  • 驱动扫描硬件:驱动程序启动后,会扫描节点上的物理设备(如 GPU、FPGA)。

  • 创建 ResourceSlice:驱动会为该节点创建一个名为 ResourceSlice 的资源对象。

  • 打标签(Attributes):在 ResourceSlice 中,驱动会详细列出每个设备的属性。例如:

    YAML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 这是一个 ResourceSlice 的简化概念图
    spec:
    nodeName: "node-1"
    devices:
    - name: "gpu-0"
    attributes:
    "vendor.com/model": { stringValue: "A100" }
    "vendor.com/vram-gb": { intValue: 80 }
    "vendor.com/cuda-version": { stringValue: "12.2" }

2. 规则匹配:DeviceClass 扮演什么角色?

DeviceClass 就像是一个**“过滤器蓝图”**。它定义了全局通用的筛选规则。

  • Selector(选择器):当你定义 DeviceClass 时,你会使用 CEL (Common Expression Language) 表达式。
  • 逻辑对应:当调度器看到一个 ResourceClaim 引用了 DeviceClass 时,它会拿着 DeviceClass 里的表达式,去遍历集群中所有 ResourceSlice 里的设备属性。

匹配逻辑示例:

  • 用户请求deviceClassName: high-vram-gpu
  • DeviceClass 定义expression: device.attributes["vendor.com/vram-gb"] >= 40
  • 系统动作:K8s 控制面会自动匹配那些在 ResourceSlicevram-gb 属性大于等于 40 的设备。

3. 最终绑定:如何锁定到那个“它”?

这个过程被称为 Structured Parameters(结构化参数) 处理:

  1. 调度决策:K8s 调度器通过 ResourceSlice 发现 node-1gpu-0 符合 DeviceClass 的要求。
  2. 分配 (Allocation):调度器(或专门的资源管理控制器)会更新 ResourceClaim 的状态,将其 status.allocation 指向具体的节点名和设备 ID(例如 node-1 上的 gpu-0)。
  3. 驱动执行:当 Pod 调度到 node-1 时,该节点上的 DRA Driver 收到通知,执行最后的“准备工作”(如挂载驱动、配置环境变量、切分显存等),确保容器启动时能看到对应的硬件。

总结:对应关系的链路

  1. 物理硬件 \rightarrowDriver 抽象为 ResourceSlice(带有属性标签)。
  2. ResourceSlice \rightarrowDeviceClass(CEL 表达式)进行筛选过滤。
  3. DeviceClass \rightarrowResourceClaim 引用,完成意向表达。
  4. ResourceClaim \rightarrow 最终由 调度器 锁定到具体的物理节点和设备。

为什么这种方式比以前高级?

以前的 Device Plugin 只会数数(节点有几个 GPU),它不知道 GPU 是什么型号。而 DRA 的 DeviceClass 配合 ResourceSlice 实现了**“按属性查货”**,这就像从“我要买一件衣服”进化到了“我要买一件 XL 码、蓝色的、纯棉的衬衫”。

2. 创建资源申请 (ResourceClaim)

这是用户定义的,描述具体的 Pod 需要什么资源。

1
2
3
4
5
6
7
8
9
10
apiVersion: resource.k8s.io/v1alpha3
kind: ResourceClaim
metadata:
name: my-gpu-claim
spec:
devices:
requests:
- name: req-1
deviceClassName: high-vram-gpu
count: 1 # 申请 1 个符合上述条件的设备

3. 在 Pod 中引用资源 (Pod)

Pod 不再使用 resources.limits,而是通过 resourceClaims 字段引用上面创建的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: ai-workload
spec:
containers:
- name: cuda-container
image: nvidia/cuda:12.0-base
resources:
claims:
- name: gpu-resource # 这里的名字对应下面的 resourceClaims.name
resourceClaims:
- name: gpu-resource # Pod 内部逻辑名称
resourceClaimName: my-gpu-claim # 指向外部创建的 ResourceClaim 对象

进阶场景:在 Deployment 中使用模板 (ResourceClaimTemplate)

如果你需要扩容多个副本(Replica),手动创建 ResourceClaim 显然不现实。这时可以使用模板,让 K8s 为每个 Pod 自动创建申请。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpu-worker
spec:
replicas: 3
selector:
matchLabels:
app: gpu-worker
template:
metadata:
labels:
app: gpu-worker
spec:
containers:
- name: worker
image: my-ai-app:v1
resources:
claims:
- name: ephemeral-gpu
resourceClaims:
- name: ephemeral-gpu
resourceClaimTemplateName: gpu-template # 引用下面的模板
---
apiVersion: resource.k8s.io/v1alpha3
kind: ResourceClaimTemplate
metadata:
name: gpu-template
spec:
spec:
devices:
requests:
- name: gpu
deviceClassName: high-vram-gpu

关键点解析:

  1. 解耦:Pod 不再关心具体哪个节点有 GPU,它只声明“我要一个符合 high-vram-gpu 条件的资源”。
  2. CEL 过滤:在 DeviceClass 中你可以看到 device.attributes,这允许你根据厂商提供的标签(如显存、架构、驱动版本)进行非常精细的筛选。
  3. 动态生成:使用 ResourceClaimTemplate 时,当 Pod 销毁,对应的 ResourceClaim 也会自动清理,像管理临时磁盘一样方便。

这种方式比旧版的 nvidia.com/gpu: 1 提供了更强大的表达能力,支持复杂的资源拓扑和参数。

五、 迁移与未来:Device Plugin 还会存在吗?

虽然 DRA 非常强大,但 Kubernetes 社区并没有打算立即废除 Device Plugin

  • 兼容性桥接:DRA 支持将资源暴露为旧版的“扩展资源”,方便旧工作负载平滑过渡。
  • 现状:目前 NVIDIA、AMD 等头部厂商已开始推出原生的 DRA 驱动。

六、 总结

DRA 的出现,将 Kubernetes 从简单的“容器编排器”推向了“数据中心级硬件资源调度器”的高度。对于大模型训练、自动驾驶仿真等重资产业务,DRA 提供的灵活性和高利用率将直接转化为成本的节省

一句话总结:

如果你还在为 GPU 调度不均、利用率低而苦恼,那么基于 DRA 的新一代基础设施建设已经可以提上日程了!


参考文档:Kubernetes 官方文档 - Dynamic Resource Allocation