Kubernetes 如何增加一种可调度资源:从 Device Plugin 到 extended resource
这里的「自定义资源」指 extended resource:它像 GPU 一样,可以写入 Pod 的 resources.requests 和 resources.limits,并参与节点调度。它不是 CRD。
本文用两个模拟的 FPGA 设备做实验:Kind 节点注册 example.com/fpga: 2,两个 Pod 各申请一个设备并成功运行;第三个 Pod 申请三个设备时保持 Pending。
先区分 CRD 和 extended resource
这两个概念解决的是不同问题:
| 名称 | 作用 | 本文是否使用 |
|---|---|---|
| Custom Resource / CRD | 给 API Server 增加一种 API 对象,例如 WebApp | 否 |
| Extended Resource | 给节点增加一种可调度的资源,例如 example.com/fpga | 是 |
CPU、memory 是 Kubernetes 内置资源。GPU、FPGA、加速卡数量等设备类资源,可以通过 Device Plugin 向 kubelet 注册为 extended resource。资源名称使用带域名的形式,例如 example.com/fpga,避免和 Kubernetes 内置资源冲突。
CRD 扩展的是 API 对象;extended resource 扩展的是节点上的可调度资源。Device Plugin 通过 Unix socket 接入 kubelet,报告设备列表和健康状态,并在容器创建时响应 Allocate。调度器读取节点的 capacity / allocatable,据此处理这个资源。
从注册到容器启动
flowchart LR
P[Pod 请求 example.com/fpga: 1] --> S[调度器读取 Node allocatable]
S --> K[kubelet Device Manager]
K -->|ListAndWatch| D[Device Plugin]
K -->|Allocate| D
D --> C[容器获得设备分配结果]
这条链路分成两步:
ListAndWatch建立节点资源视图。插件报告两个健康设备后,节点出现example.com/fpga: 2。- Pod 被调度后,kubelet 调用
Allocate。插件可以返回设备节点、环境变量、挂载和 CDI 配置等容器运行时需要的信息。
因此,节点报告 2 个资源只表示总量;调度器还要扣除已分配的数量,才能判断新的 Pod 是否可以调度。
实现一个最小 Device Plugin
完整实现位于 k8sdev 502068b。代码不接入真实硬件,只模拟两个 FPGA ID,用于观察 Kubernetes 的资源注册、调度和分配链路。
1. 构造设备清单
newDevicePlugin 把设备交给 resourceManager 管理。管理器负责保存设备顺序、保护健康状态,并为 RPC 层提供快照:
| |
真实 GPU 插件通常会使用设备 UUID,并在设备异常时更新设备状态。Kubernetes 只会把健康设备计入可分配资源。当前示例用 fakeResourceManager.UpdateHealth 模拟这类状态变化,并通过通知通道触发新的 ListAndWatchResponse。
2. 监听 kubelet 的插件目录
插件和 kubelet 通过同一个 hostPath 目录发现 Unix socket:
| |
进程启动时先删除上一次留下的 socket,再监听按资源名生成的 socket、启动 gRPC server,最后连接 kubelet.sock 完成注册。目录监听器会关注本插件 socket 的删除或重命名:kubelet 重启时删除该 socket,插件会停止当前 server、重新创建 socket,并重试注册。
| |
DaemonSet 只挂载 kubelet 的 device-plugin 目录;它不需要访问 Kubernetes API,也不需要 RBAC:
| |
3. 实现 Device Plugin RPC
Device Plugin API 涉及 GetDevicePluginOptions、ListAndWatch、GetPreferredAllocation、Allocate 和 PreStartContainer。本例没有设备初始化动作,因此 PreStartContainer 直接返回;GetPreferredAllocation 会优先保留 kubelet 指定的 MustIncludeDeviceIDs,再从可用列表中补足数量,它不负责最终占用设备。
| |
Allocate 在 Pod 容器创建前执行。本例返回一个环境变量,容器可以用它观察 kubelet 分配的设备 ID:
| |
真实设备通常还会在这里返回 ContainerAllocateResponse.Devices,例如 /dev/nvidia0 对应的 DeviceSpec;也可以返回 CDI device 名称。本文不返回虚假的设备文件,避免把「调度成功」误解为「硬件隔离已经实现」。
并发分配:kubelet 选择设备,插件负责校验和准备
gRPC 服务端可以同时处理多个 Allocate 请求。这里要区分两类状态:
- 哪些设备已经分配:由 kubelet Device Manager 维护。它先从健康且未分配的设备中选出 ID,再把这些 ID 传给插件;插件不应在
Allocate中再次扫描设备池。Kubernetes 的 Device Manager 源码还明确把插件 RPC 放在 Device Manager 的互斥锁之外执行,因为 RPC 可能比较耗时。 - 如何让设备可用:由插件或设备管理器负责。例如绑定 VFIO、生成 CDI 配置、修改设备状态等。如果这些操作共享可变状态,就必须在插件侧串行化,并让失败和重试保持幂等。
NVIDIA kubevirt-gpu-device-plugin 的 Allocate 采用的也是这个边界:先验证 kubelet 传入的设备 ID,再根据 ID 生成环境变量和 DeviceSpec,而不是在插件里重新挑选 GPU。它没有单独的 Unallocate RPC,因此插件不应仅凭 Allocate 调用维护一份永久占用表。
本实验的插件虽然只返回环境变量,仍用互斥锁保护插件侧的分配阶段,并拒绝同一个 AllocateRequest 中重复出现的设备 ID。下面只展示相关字段和方法,其他 import 省略:
| |
这个锁不替代 kubelet 的设备分配状态;它只保证插件自己的硬件准备逻辑不会并发修改同一份状态。对应测试位于 main_test.go。在 k8sdev 根目录运行 go test -race ./extended-resource,结果为:
| |
Pod 如何申请这种资源
extended resource 使用和 GPU 一样的 resources 字段。数量是整数,不能写 500m 这样的 CPU quantity;通常同时设置 requests 和 limits:
| |
节点报告 example.com/fpga: 2 时,两个这样的 Pod 可以同时被调度;第三个 Pod 申请 3 个时,调度器会因为 Insufficient example.com/fpga 让它保持 Pending。
在 Kind 中实测
实验文件、脚本和结果都在 k8sdev 的 extended-resource/。下面的命令固定使用该提交:
| |
setup.sh 按以下顺序执行:
- 创建
kind-extended-resource-lab,默认节点镜像为kindest/node:v1.35.0。 - 在主机上编译静态 Device Plugin,构建镜像并加载到 Kind 节点。
- 创建
fpga-device-pluginDaemonSet,等待它完成 rollout。 - 把
alpine:3.22一起加载到节点,避免 workload 依赖节点直接访问 Docker Hub。
检查插件注册和两个成功申请:
| |
下面是关键输出,完整原始记录见 verification-m6-20260914.txt:
| |
输出表明:节点状态包含扩展资源,两个 Pod 都已调度并运行,kubelet 触发了 Allocate,两个请求拿到了不同的设备 ID。
容量不足时会怎样
再创建一个申请三个设备的 Pod:
| |
调度事件为:
| |
这与 CPU、memory 的调度结果一致:资源不足时 Pod 会保持 Pending,直到其他工作负载释放资源或请求被修改。
为了验证 kubelet 重启后的恢复路径,我在 Kind 节点中删除插件 socket,模拟 kubelet 清理旧 socket:
| |
插件检测到 socket 被删除后会重新启动 server 并注册:
| |
Kind 验证结束后删除本次独立集群:
| |
实验边界
这次实验验证的是 Device Plugin、kubelet、节点 allocatable、调度器和 Allocate 之间的控制面链路,不包括以下生产能力:
- 真实 FPGA/GPU 的驱动、设备文件或 CDI 配置正确;
- 多容器、多进程对同一硬件的隔离策略正确;
- 设备故障上报、热插拔、NUMA 拓扑和健康检查符合生产要求;
- 本次 m6 实验没有覆盖多副本切换、设备插件升级和真实 kubelet 重启。
生产实现还应使用真实设备 ID,在 ListAndWatch 中上报健康变化,在 Allocate 中返回实际的 DeviceSpec 或 CDI 配置,并测试 kubelet 重启、插件重启、设备异常和节点扩缩容。
结论
要增加一种像 GPU 一样参与调度的资源,应从 Device Plugin 入手:插件向 kubelet 注册设备,kubelet 更新 Node 的 allocatable,调度器据此选择节点,最后由 kubelet 在 Allocate 阶段把设备访问配置交给容器。
CRD 适合扩展 Kubernetes API 对象;Device Plugin 适合扩展节点上可调度、可分配的设备类资源。先区分要扩展的是 API 层还是节点资源层,就能选对实现入口。