深入解析 Kubernetes DRA 端到端测试驱动 dra-test-driver:架构、部署与实现原理
导读
本文以 test/e2e/dra/test-driver/README.md 为主体,系统讲解 Kubernetes 官方用于端到端验证动态资源分配(Dynamic Resource Allocation,DRA)机制的参考测试驱动 dra-test-driver:它如何在单个二进制中同时扮演"资源驱动控制器"与"kubelet 资源插件",如何通过 ConfigMap 参数控制设备注入行为并模拟网络附加/主机本地、共享/独占、受限/无限等多种资源供给模型。读完本文,你将掌握该驱动的完整命令行参数、单机 local-up-cluster.sh 上的端到端部署与验证步骤,并能对照 app 目录源码理解 DRA 从 ResourceClaim 分配到 CDI 设备注入的完整调用链,为编写自己的 DRA 驱动提供可直接借鉴的参考实现。
DRA 与测试驱动:它解决什么问题
动态资源分配(DRA)是 Kubernetes 中"让 Pod 声明式地申请并消耗非 CPU/内存类资源(GPU、FPGA、加速卡、网络设备等)"的基础设施机制,由 DynamicResourceAllocation feature gate 控制。该门控定义于 pkg/features/kube_features.go,并且它是一系列后续 DRA 子特性(DRAAdminAccess、DRAConsumableCapacity、DRAHealth 相关、DRAResourceClaimDeviceStatus 等)的依赖前置门控,在 pkg/features/kube_features.go 中可见其版本门控矩阵。
要验证这样一套跨 kube-apiserver、调度器、kubelet 与运行时(通过 CDI 注入)的复杂机制,Kubernetes 官方在 test/e2e/dra 中内置了一个名为 dra-test-driver 的测试驱动。它的设计哲学很明确:功能刻意保持最小化,但工程实践对齐真实驱动——支持 leader election、metrics、与 Kubernetes 一致的日志体系,作为 DRA 驱动开发的"最小可行样例"。其文档与源码即位于仓库的 test/e2e/dra/test-driver 目录。
架构与设计:单二进制、双角色
一个二进制承载两类组件
README 明确指出,本驱动在同一二进制中同时实现 DRA 架构中的两类核心组件:
- controller(驱动控制器):负责与
ResourceClaim/DeviceClass交互、执行分配决策、回写分配结果; - kubelet 资源插件(kubelet plugin):运行在节点上,负责向 kubelet 汇报资源可用性、在 Pod 启动前"准备"资源。
之所以放进单一二进制,是为了把样板代码量降到最低;真实驱动完全可以按需拆成两个独立二进制(例如控制器走 Deployment,节点插件走 DaemonSet)。README 还补充了两者的运行形态:控制器可以作为 Deployment 运行并配合 leader election 保证多副本互斥;kubelet 插件可用 DaemonSet 铺到所有节点。此外两者都能以集群外 Kubernetes client 方式运行——kubelet 插件在配合 port-forward 时也支持这种模式,这正是测试期间的使用方式。
从当前仓库源码快照看,app/server.go 中实际注册并完整暴露的子命令为
kubelet-plugin;控制器的分配逻辑则沉淀在k8s.io/dynamic-resource-allocation库族(见下节)以及 driver 自身的接口实现中。阅读源码时应以代码现状为准。
分层解耦:通用库 + 驱动接口
架构上驱动被刻意分成三层(见 Design 一节):
- 通用交互层:
k8s.io/dynamic-resource-allocation库族负责与 Kubernetes 各组件通信。当前仓库快照中该库位于 staging/src/k8s.io/dynamic-resource-allocation,包含kubeletplugin、resourceclaim、resourceslice、client、deviceclass、structured等子包。这些包"只依赖接口",与具体驱动逻辑解耦; - 驱动适配层:
test-driver自己实现库定义的接口,把"如何模拟设备"这类领域逻辑放进来; - 入口编排层:Cobra 命令与各类 flag 组装,见下文。
README 同时提到,与 ResourceClaim 交互的通用 controller 部分长期来看可以被拆成可复用的独立工具包,体现了其"测试代码也按可演进标准组织"的取向。
入口与命令组装
二进制入口为 dra-test-driver.go,它调用 app.NewCommand() 构造 Cobra 命令,并用 k8s.io/component-base/cli.Run 启动。入口文件通过匿名导入一次性注册了:
- JSON 日志输出支持(
component-base/logs/json/register); - Prometheus 默认注册表中的 leader election、workqueue、restclient client 与版本 metrics(
component-base/metrics/prometheus/...系列)。
这意味着驱动在启动之初就复用了 Kubernetes 控制面组件标配的日志、metrics 与 leader election 设施——这正是 README 所说的"与 Kubernetes 相同的日志选项与 best practices"。
在 server.go 的 NewCommand() 中可以看到标志分组(flag sets)的完整组织方式:
| Flag 组 | 关键 flag | 默认值 | 说明 |
|---|---|---|---|
| logging | -v、--logging-format 等 |
— | 复用 k8s.io/component-base/logs 的完整日志体系 |
| Kubernetes client | --kubeconfig |
空(自动尝试 in-cluster) | 集群外运行时的 kubeconfig 路径;也读 KUBECONFIG 环境变量 |
--kube-api-qps |
50 | 与 apiserver 通信的 QPS | |
--kube-api-burst |
100 | 与 apiserver 通信的突发上限 | |
| http server | --http-endpoint |
空(禁用) | 诊断 HTTP 服务监听地址,如 :8080;开启后可提供 metrics/pprof/leader election 健康检查 |
--metrics-path |
/metrics |
Prometheus 指标路径,空则关闭 | |
--pprof-path |
空(禁用) | pprof 性能剖析路径 | |
| CDI | --drivername |
test-driver.cdi.k8s.io |
资源驱动名,全局唯一标识 |
| other | --feature-gates |
— | 日志相关的 feature gate(含 JSON 日志等) |
日志体系与 Kubernetes 完全同源(klog + logsapi),PersistentPreRunE 会在启动最早阶段 ValidateAndApply 日志配置并据此输出最终生效的 flags。
参数注入模型:从 ConfigMap 键值对到容器环境变量
README 用一小节概括了本驱动的"业务协议",而源码把它落得相当具体,是理解 DRA 数据流的最佳入口:
- 参数来源是键值对:驱动接受的"有效参数"是存放在 ConfigMap 中的 string→string 键值对(管理员与用户均可提供);
- 按来源加前缀写入状态:参数随分配结果回写到
ResourceClaimStatus,用户侧参数加user_前缀、来自DeviceClass的管理员侧参数加admin_前缀; - 以 JSON map 形式存入
ResourceHandle:控制器负责把这些参数序列化为 JSON map 放进分配结果; - kubelet 插件转成环境变量:节点插件在准备资源时读取这些参数,最终以环境变量形式注入到每个使用该资源的容器中。
源码对照:extractParameters 与 nodePrepareResource
前缀与"参数→环境变量"的转换逻辑实现在 kubeletplugin.go:
func extractParameters(parameters runtime.RawExtension, env *map[string]string, admin bool) error {
kind := "user"
if admin {
kind = "admin"
}
...
for key, value := range data {
(*env)[kind+"_"+key] = value
}
return nil
}
调用点位于 nodePrepareResource(kubeletplugin.go),其完整处理链路为:
- 遍历
claim.Status.Allocation.Devices.Results,只处理driverName匹配本驱动的结果; - 通过
resourceclaim.ConfigForResult取回对应本次分配使用的config(含AllocationConfigSourceClass标记,用于区分来源是 DeviceClass 还是 ResourceClaim),并依次调用extractParameters合并进 env map(后者覆盖前者,"last one wins"); - 额外注入一个标识变量:
claim_<claimName>_<requestName>=true,容器可通过它反查"哪个设备被映射进了本容器",其中非字母数字字符会被替换为下划线(见 kubeletplugin.go); - 为每个设备构造 CDI 设备 ID,形如
test-driver.cdi.k8s.io/test=claim-<claimUID>-<requestName>,并把 env 拼成排序后的字符串列表写进 CDI spec 的ContainerEdits.Env; - 将 CDI spec 落盘为 JSON 文件,路径为
<cdi-dir>/<driverName>-<claimUID>-<requestName>.json(getJSONFilePath)。
一个值得注意的健壮性细节:如果 env 列表为空,代码会跳过注入——因为 CDI 不支持空的 ContainerEdits,强行写入会导致 CDI device injection failed(kubeletplugin.go),这体现了 DRA 与 CDI 交互的一个真实边界条件。
模拟不同资源供给模型
README 指出,驱动的资源可用性是"可配置的",用于在测试中模拟不同场景:
- 网络附加型 vs 主机本地型:前者在所有运行了节点驱动的节点上均可用,后者只在分配发生的节点上可用;
- 共享 vs 非共享分配:一个 claim/设备是否可被多个 Pod 同时引用;
- 无限 vs 有限资源:有限时按"整个集群或单个节点上的分配次数"这一简单计数来约束上限。
在单节点模拟层面,--num-devices(默认 4)控制每个节点上报的模拟设备数量,代码会按 device-%02d 的命名规则生成设备并包装成 resourceslice.DriverResources 经插件发布(server.go)。
kubelet 插件运行细节:准备、清理与测试钩子
ExamplePlugin 通过断言 var _ kubeletplugin.DRAPlugin = &ExamplePlugin{} 在编译期保证完整实现插件接口(kubeletplugin.go)。
NodePrepareResource:幂等的设备准备
nodePrepareResource 用 prepared map[ClaimID][]kubeletplugin.Device 记录已准备的 claim,因此对同一 claim 的重复调用是幂等的——文件会被"再写一遍"而不会出错。unprepare 侧同样幂等:文件不存在时直接返回 nil(kubeletplugin.go)。这种"确定性文件名 + 幂等写/删"的设计让 prepare/unprepare 无需维护复杂状态机。
设备元数据与健康状态上报
面向较新的 DRA 子特性,插件还支持:
- 设备元数据(device metadata):
--enable-device-metadata开启后,每个注入设备附带driverName/pool/device三个属性(kubeletplugin.go),API 版本固定使用最新的metadataapi(v1beta1); - 设备健康状态流(
WatchHealthStatus):插件实现版本中立的健康上报辅助 API,由kubeletpluginhelper 自动翻译成 kubelet 支持的 gRPC 版本(DRAResourceHealthv1/v1alpha1 可通过Options.DisableHealthV1/DisableHealthV1alpha1单独禁用以测试降级路径);上报内容来自一个可注入的控制通道HealthControlChan,测试可据此动态把某设备标记为 Healthy/Unhealthy/Unknown(kubeletplugin.go); - 故障注入钩子:
SetNodePrepareResourcesFailureMode/SetNodeUnprepareResourcesFailureMode/SetGetInfoError/SetNotifyRegistrationStatusError等方法让 e2e 测试能模拟 prepare 失败、注册失败、重试等异常路径(kubeletplugin.go); - gRPC 调用记录:通过 unary/stream 拦截器把所有入站调用的方法名、请求、响应与错误记录下来(
GetGRPCCalls/CountCalls),供测试断言 kubelet 与插件之间的交互时序(kubeletplugin.go)。
插件启动时通过 StartPlugin(kubeletplugin.go)统一初始化:Unix socket 目录、CDI 目录(默认 /var/run/cdi)、插件注册目录、以及发布 ResourceSlice。收到 SIGINT/SIGTERM 后走优雅退出,删除 Unix socket。
端到端部署与验证:在 local-up-cluster 上跑起来
第一步:编译并以单机集群模式启动
按 README 指引,先构建好 Kubernetes,然后在第一个终端运行 hack/local-up-cluster.sh,并开启 DRA 特性:
RUNTIME_CONFIG="resource.k8s.io/v1alpha3" FEATURE_GATES=DynamicResourceAllocation=true ALLOW_PRIVILEGED=1 ./hack/local-up-cluster.sh -O
RUNTIME_CONFIG需要与当前二进制所支持的resource.k8s.ioAPI 版本匹配。README 写作时使用v1alpha3,而本仓库 deploy/example 下的 YAML 示例已使用稳定化的resource.k8s.io/v1API(对应k8s.io/api/resource/v1)。请以你实际构建的 Kubernetes 版本所暴露的 API 版本为准调整。
第二步:准备目录并以 kubelet 插件模式运行驱动
在另一个终端中,先创建驱动运行所需的目录并放开权限,然后以集群外客户端方式运行 kubelet 插件(KUBECONFIG 指向 local-up 生成的 admin.kubeconfig):
sudo mkdir -p /var/run/cdi
sudo mkdir -p /var/lib/kubelet/plugins/test-driver.cdi.k8s.io
sudo mkdir -p /var/lib/kubelet/plugins_registry
sudo chmod a+rx /var/lib/kubelet /var/lib/kubelet/plugins
sudo chmod a+rwx /var/run/cdi /var/lib/kubelet/plugins_registry /var/lib/kubelet/plugins/test-driver.cdi.k8s.io
KUBECONFIG=/var/run/kubernetes/admin.kubeconfig go run ./test/e2e/dra/test-driver -v=5 kubelet-plugin --node-name=127.0.0.1
其中各目录的用途如下(对应 kubelet-plugin 子命令的 flags):
| 路径 | 对应 flag | 用途 |
|---|---|---|
/var/run/cdi |
--cdi-dir(默认 /var/run/cdi) |
存放插件动态生成的 CDI JSON 文件,运行时(CRI)据此注入设备 |
/var/lib/kubelet/plugins/test-driver.cdi.k8s.io |
--datadir(<plugins目录>/<driverName>) |
创建 DRA 驱动专属 Unix domain socket 的目录 |
/var/lib/kubelet/plugins_registry |
--plugin-registration-path |
kubelet 插件注册机制扫描 socket 的目录 |
--node-name=127.0.0.1 声明本插件负责的节点名(local-up 单机模式下节点即为 127.0.0.1);-v=5 提高日志级别便于观察 NodePrepareResource 等调用过程。
第三步:创建 DeviceClass 与使用内联 claim 的 Pod
导出 kubeconfig 后依次 apply 驱动示例清单:
$ export KUBECONFIG=/var/run/kubernetes/admin.kubeconfig
$ kubectl create -f test/e2e/dra/test-driver/deploy/example/deviceclass.yaml
resourceclass/example created
$ kubectl create -f test/e2e/dra/test-driver/deploy/example/pod-inline.yaml
configmap/pause-claim-parameters created
pod/pause created
$ kubectl get resourceclaims
NAME CLASSNAME EXAMPLE AGE
pause-resource example allocated,reserved 19s
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
pause 1/1 Running 0 23s
两点说明(以当前仓库 YAML 为准):
- deviceclass.yaml 定义一个名为
example的DeviceClass,用 CEL 选择器device.driver == "test-driver.cdi.k8s.io"圈定由本驱动管理的设备; - pod-inline.yaml 展示 DRA 的 inline claim(内联申请) 写法:定义
ResourceClaimTemplate,Pod 通过spec.resourceClaims[].resourceClaimTemplateName引用,并在容器resources.claims声明使用;模板中通过config[].opaque.parameters(如a: b)传入驱动参数。
看到 ResourceClaim 进入 allocated,reserved 状态且 pause Pod 进入 Running,即说明整套链路(调度分配 → 插件准备资源 → CDI 文件生成 → 运行时注入)已经打通。
更多场景示例
deploy/example 目录还提供了覆盖其他场景的清单,可在理解上述基础流程后逐一尝试:
- pod-external.yaml:外部 claim 写法——预创建独立
ResourceClaim,Pod 通过resourceClaimName引用(一个 Pod 两个容器,仅一个使用资源); - pod-shared.yaml:共享分配——两个 Pod 引用同一个外部
ResourceClaim,演示 DRA 的共享资源模型; - pod-inline-multiple.yaml:多 Pod / 多 claim 组合场景;
- resourceclaim.yaml:最小化
ResourceClaim定义(exactly.deviceClassName精确匹配一台设备); - devicetaintrule.yaml 与 resourcepoolstatusrequest.yaml:面向设备污点规则、资源池状态等较新子特性的演示清单;
- plugin-permissions.yaml:驱动所需的 RBAC 权限;
- pod-external-toleration.yaml:外部 claim + 容忍设备污点的示例。
这些清单通过 embed.go 被嵌入测试二进制,供 e2e 测试直接读取使用,这也是 deploy/example 既是"文档示例"又是"测试资产"的原因。
多节点集群的限制
README 特别提醒:截至其写作时点,还没有包含该测试驱动的容器镜像,因此它无法直接部署到"普通(多节点)集群"上跑 Deployment/DaemonSet。它在多节点场景下主要服务于 local-up-cluster.sh 与仓库内 e2e 测试框架(测试以集群外 client + port-forward 的方式驱动它,正如 README 的 Usage 一节 所述)。
在端到端测试中的角色
作为 test/e2e/dra 的测试支柱,该驱动提供了大量面向断言的测试面(上文已逐一列出):gRPC 调用记录(验证 kubelet 何时调用插件的 Prepare/Unprepare)、故障注入(验证失败与重试路径)、健康控制通道(验证健康上报与驱逐联动)。配套的 gomega/matchers.go 提供面向测试的匹配器,方便用 Ginkgo/Gomega 风格断言复杂对象状态。app/options.go 与 options.go 中的函数式选项(TestOption/CancelMainContext)则是把"进程内直接启动插件"做成可测试组件的关键——e2e 框架可以不经 CLI,直接以库方式嵌入启动插件并注入取消钩子。
设计来源与先例
README 的 Prior art 一节说明,本驱动部分代码派生自 Kubernetes CSI 生态的 external-resizer,其 controller 逻辑又与 sig-storage-lib-external-provisioner 一脉相承。这解释了为何驱动的控制器侧会复用一套"通用外部控制器 + 驱动接口"的模式:DRA 的资源驱动本质上和 CSI 的外部控制器/provisioner 一样,都是"围绕特定 Kubernetes API 对象做状态机的独立进程",把与 API 交互的通用部分沉淀进库、把设备语义留给具体实现,正是这一族代码长期演化的最佳实践。
结语
dra-test-driver 规模不大,却是一份信息密度极高的"活文档":它演示了 DRA 驱动应有的分层(通用库 vs 接口实现)、生产级附属能力(logging/metrics/leader election)、以及测试友好设计(幂等、故障注入、调用记录、进程内启动)。无论你的目标是调试 Kubernetes 自身的 DRA 链路,还是参考它编写面向 GPU/加速器的真实驱动,都可以从 test/e2e/dra/test-driver/README.md 出发,对照 app/server.go(命令与参数)、app/kubeletplugin.go(插件核心逻辑)与 deploy/example(可运行清单)三份材料层层深入。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00