- 云原生
- 运维
【免费下载链接】descheduler
Descheduler for Kubernetes
Descheduler 作为 Kubernetes 官方的 Pod 驱逐/重调度工具,其代码库演进过程中沉淀了一套明确的设计约束与代码规范。本文以仓库根目录下的 CONTRIBUTING-descheduler.md 为骨架,结合pkg/framework、test、Makefile等源码证据,系统讲解:提交前必须执行的格式化检查、单元测试中对象构建的四大约定,以及框架层"暂不提供插件索引器注册/获取帮助函数"这一设计决策背后的深层考量。读完本文,你将能够按照社区规范编写风格一致、低认知负担的 Descheduler 单元测试,并理解框架 Handle 与 PluginInstanceID 机制如何为未来的索引器扩展预留空间。
文档定位:一份"缓慢生长"的设计约束清单
CONTRIBUTING-descheduler.md在开头就明确了自己的定位:它是一份**缓慢增长(slowly growing)**的文档,用于沉淀 Descheduler 代码库中的良好实践(good practices)、约定(conventions)与设计决策(design decisions)。它不追求一次写完整,而是随着代码演进、社区讨论逐步补充。目前文档包含三大板块:
- Code convention(代码约定):目前仅有一条核心约定——提交前运行
make fmt,避免 CI 失败; - Unit Test Conventions(单元测试约定):四条围绕
test.BuildTestPod/test.BuildTestNode/test.BuildXXX的对象构建纪律; - Design Decisions FAQ(设计决策问答):当前收录了一个经典问题——"为什么框架不提供用于插件注册和获取索引器的帮助函数?"及其完整解答。
文档的## Overview部分目前仍标注为TBD,这说明该文档仍在演进中,属于开放的补充空间,而非完整的技术手册。下面逐一展开解读。
代码约定:提交前先运行make fmt
约定内容
文档给出的代码约定非常简短但极为实用:
- formatting code: running
make fmtbefore committing each change to avoid ci failing
即:每次提交变更前,先运行make fmt,以避免 CI 失败。这背后是 Descheduler 的 CI 验证链路——Makefile中verify目标聚合了verify-govet、verify-spelling、verify-gofmt、verify-vendor、lint与lint-chart等检查,其中verify-gofmt与lint(内部含 golangci-lint 的fmt子命令)都会对代码格式做硬性校验,格式不符合会直接导致 CI 红灯。
make fmt到底做了什么
查看仓库根目录的 Makefile,fmt目标的实现如下:
fmt: ifndef HAS_GOFUMPT go install mvdan.cc/gofumpt@$(GOFUMPT_VERSION) endif gofumpt -w -extra . goimports -w pkg metrics test它做了三件事:
- 安装 gofumpt:版本由
GOFUMPT_VERSION := v0.10.0固定,保证开发者本地与 CI 使用同一格式化工具版本; - 执行
gofumpt -w -extra .:对全仓库 Go 代码做更严格的格式化(gofumpt 是 gofmt 的加强版,-extra还会处理更多边缘格式场景); - 执行
goimports -w pkg metrics test:仅针对pkg、metrics、test三个目录统一 import 分组与排序。
与之配套的lint目标会先按GOLANGCI_VERSION := v2.8.0安装 golangci-lint,再运行golangci-lint run -v与golangci-lint fmt -v,从静态分析与格式两个维度把关。因此文档这条约定的实际含义是:本地make fmt一次性解决了 gofumpt 格式与 goimports 排序问题,开发者无需手工对齐 import 顺序,自然也就不会触发 CI 的格式校验失败。
与验证脚本的关系
仓库的 hack/verify-gofmt.sh 是 CI 侧的对应物,verify-gofmt目标直接调用它。提交前先make fmt、再本地跑一遍make verify,即可与 CI 行为对齐,把格式问题消灭在提交之前。
单元测试约定:用四条规定降低测试代码的认知负担
文档中的 Unit Test Conventions 部分说明了这些约定的适用对象与目的:
These are the known conventions that are useful to practice whenever reasonable
即"在合理的情况下尽量实践"。四条规定全部围绕test.BuildXXX系列辅助函数展开,其最终目标是:
The aim is to reduce cognitive load when reading and debugging the test code.
(降低阅读与调试测试代码时的认知负担。)以下结合源码逐一解读。
约定一:单一 Pod 创建——只用apply参数修改BuildTestPod构建出的 Pod
- single pod creation: each pod variable built using
test.BuildTestPodis updated only through theapplyargument ofBuildTestPod
这条规定要求:凡是经由test.BuildTestPod创建的 Pod 变量,后续任何修改都必须通过BuildTestPod的apply函数参数完成,而不是在返回后对 pod 变量做散落的、非结构化的赋值。
查看 test/test_utils.go 中BuildTestPod的实现:
// BuildTestPod creates a test pod with given parameters. func BuildTestPod(name string, cpu, memory int64, nodeName string, apply func(*v1.Pod)) *v1.Pod { pod := &v1.Pod{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Pod", }, ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: name, SelfLink: fmt.Sprintf("/api/v1/namespaces/default/pods/%s", name), UID: uuid.NewUUID(), }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Resources: v1.ResourceRequirements{ Requests: v1.ResourceList{}, Limits: v1.ResourceList{}, }, }, }, NodeName: nodeName, }, } if cpu >= 0 { pod.Spec.Containers[0].Resources.Requests[v1.ResourceCPU] = *resource.NewMilliQuantity(cpu, resource.DecimalSI) } if memory >= 0 { pod.Spec.Containers[0].Resources.Requests[v1.ResourceMemory] = *resource.NewQuantity(memory, resource.DecimalSI) } if apply != nil { apply(pod) } return pod }可以看到该函数签名中apply func(*v1.Pod)是唯一的"扩展点":调用者可以通过闭包注入任意 Pod 字段的修改(例如设置 ownerRef、优先级、亲和性、注解等),函数内部在最后统一执行apply(pod)。把"额外定制"全部收拢到apply里,能让测试用例的 Pod 定制逻辑在函数调用处一眼可见、集中可读,而不是散落在后续十几行赋值语句中。
同一目录下还提供了一批可组合进apply的辅助函数,例如SetRSOwnerRef、SetSSOwnerRef、SetDSOwnerRef、SetMirrorPodAnnotation、SetPodPriority、SetPodAntiAffinity、MakeBestEffortPod/MakeBurstablePod/MakeGuaranteedPod(见 test/test_utils.go),它们正是为"通过 apply 定制 Pod"这一约定准备的现成积木。
约定二:单一 Node 创建——只用apply参数修改BuildTestNode构建出的 Node
- single node creation: each node variable built using
test.BuildTestNodeis updated only through theapplyargument ofBuildTestNode
与约定一完全对称,Node 的定制同样只允许通过BuildTestNode的apply参数完成。BuildTestNode的实现(test/test_utils.go)会预置一套完整的 Node 状态:Capacity 与 Allocatable(CPU、内存、Pod 数)、NodeRunningPhase、以及NodeReadyCondition:
// BuildTestNode creates a node with specified capacity. func BuildTestNode(name string, millicpu, mem, pods int64, apply func(*v1.Node)) *v1.Node { node := &v1.Node{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Node", }, ObjectMeta: metav1.ObjectMeta{ Name: name, SelfLink: fmt.Sprintf("/api/v1/nodes/%s", name), Labels: map[string]string{}, }, Status: v1.NodeStatus{ Capacity: v1.ResourceList{ v1.ResourcePods: *resource.NewQuantity(pods, resource.DecimalSI), v1.ResourceCPU: *resource.NewMilliQuantity(millicpu, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(mem, resource.DecimalSI), }, Allocatable: v1.ResourceList{ /* 与 Capacity 一致 */ }, Phase: v1.NodeRunning, Conditions: []v1.NodeCondition{ {Type: v1.NodeReady, Status: v1.ConditionTrue}, }, }, } if apply != nil { apply(node) } return node }配套的定制辅助函数同样存在于 test/test_utils.go:例如SetNodeUnschedulable(设置node.Spec.Unschedulable = true)、SetNodeExtendedResource(写入扩展资源 Capacity 与 Allocatable)等,均可直接嵌入apply闭包。
约定三:不共享对象实例——每个用例都新建对象,避免意外的对象变更
- no object instance sharing: each object built through
test.BuildXXXfunctions is newly created in each unit test to avoid accidental object mutations
Go 中对象以指针传递,若多个测试用例共享同一个*v1.Pod/*v1.Node实例,前一个用例对字段的修改会"污染"后续用例,导致测试间隐性耦合、出现难以排查的随机失败。因此约定要求:每个单元测试中都通过test.BuildXXX新建对象。
值得注意的是,BuildTestPod内部使用uuid.NewUUID()为每个对象生成独立的UID(test/test_utils.go),这正是"每次构建都是全新对象"的体现之一。
约定四:不重复对象实例——避免用相同参数在两处创建等价对象,改用封装函数
- no object instance duplication: avoid duplication by no creating two objects with the same passed values at two different places. E.g. two nodes created with the same memory, cpu and pods requests. Rather create a single function wrapping test.BuildTestNode and invoke this wrapper multiple times.
这条约定与约定三互补:共享实例是禁止的,但"重复造相同对象"也是要避免的。典型反例是:在两个不同测试位置各自写一遍test.BuildTestNode("n1", 2000, 3000, 10, nil),参数完全一致;正确做法是抽出一个薄封装函数,再在多个用例中调用它。
仓库中有大量现成范例。例如 pkg/framework/plugins/removepodsviolatingnodetaints/node_taint_test.go 正是按约定四组织:
func buildTestNode(name string, apply func(*v1.Node)) *v1.Node { return test.BuildTestNode(name, 2000, 3000, 10, apply) } func buildTestPod(name, nodeName string, apply func(*v1.Pod)) *v1.Pod { return test.BuildTestPod(name, 100, 0, nodeName, apply) }测试用例内部统一通过buildTestNode("n1", withPreferNoScheduleTestTaint1)之类的方式创建节点,既保证每个用例都拿到全新实例(约定三),又避免在 621 行的测试文件中反复书写2000, 3000, 10这样的魔数(约定四)。nodeutilization包中的BuildTestNodeInfo(pkg/framework/plugins/nodeutilization/nodeutilization_test.go)同样遵循该模式,默认预置 2000m CPU / 3977868×1024 内存 / 29 个 Pod 的容量,并通过apply覆盖 usage 数据。
四条约定共同保证的东西
把四条约定连起来看,它们其实是同一目标(降低认知负担)的三个维度:
- 可读性(约定一、二):定制逻辑集中、可见、结构化,而非散落赋值;
- 隔离性(约定三):用例之间互不污染,测试结果可重复;
- 简洁性(约定四):消除重复魔数,公共构造逻辑收敛为单一封装点。
这也让新贡献者读测试时不必在"这个对象从哪来、被谁改过"上花太多脑力。
设计决策 FAQ:为什么框架不提供索引器的注册/获取帮助函数
Design Decisions FAQ部分当前收录的问题,是理解 Descheduler 框架扩展点设计思想的关键入口:
Why doesn't the framework provide helpers for registering and retrieving indexers for plugins?(为什么框架不提供用于插件注册和获取索引器的帮助函数?)
背景:插件与索引器
在 Kubernetes client-go 的 informer 机制中,**indexer(索引器)**负责按自定义 key 为缓存对象建立索引,从而把"按条件查询"从 O(n) 遍历降为 O(1) 哈希查找。Descheduler 框架中的每个插件都可能为节点、命名空间、Pod 等资源建立多个索引器,且不同插件可能选择不同的索引函数(indexing function)以实现内部优化。
文档给出的设计决策
文档明确记录了当时的决策结论与理由(原文为作者口吻的第一人称阐述):
- 索引器目前使用频率极低:无论框架本身还是默认插件,对索引器的使用都非常稀少("Indexers are currently used very rarely in the framework and default plugins");
- 过早抽象会引入不必要的限制层:在不清楚索引器将来如何被使用的情况下,贸然在框架接口中增加"注册/获取索引器"的帮助函数,可能带来一个既多余又过度约束的抽象层;
- 当下的务实做法是"不加限制 + 防冲突":现阶段不限制"能注册多少个索引器、注册哪些索引器";取而代之的是,扩展框架 handle,为每个 profile 提供唯一 ID(unique ID),让同一 profile 内的索引器共享唯一前缀(unique prefix),从而避免同一 profile 被多次实例化时发生 key 冲突;
- 留待未来演进:等到对索引器实际用法有了更多认识后,再评估是否值得施加额外限制("Later, once we learn more about indexer usage, we can revisit whether it makes sense to impose additional restrictions")。
简言之,这是一个典型的"YAGNI(你不会需要它)"式决策:在真实需求出现前,不为框架接口增加可能束缚未来的辅助函数,而是先解决已能预见的冲突问题(profile 多次实例化时的 key 碰撞)。
源码证据:PluginInstanceID 机制正是这一决策的实现
"为每个 profile 提供唯一 ID,使同一 profile 内的索引器共享唯一前缀"这条决策,已在当前代码库中落地为PluginInstanceID机制。可以从源码链路完整看到它:
- 框架接口层: pkg/framework/types/types.go 中
Handle接口定义了:
type Handle interface { // ClientSet returns a kubernetes clientSet. ClientSet() clientset.Interface PrometheusClient() promapi.Client Evictor() Evictor GetPodsAssignedToNodeFunc() podutil.GetPodsAssignedToNodeFunc SharedInformerFactory() informers.SharedInformerFactory MetricsCollector() *metricscollector.MetricsCollector // PluginInstanceID returns a unique identifier for this plugin instance. // The ID is unique across all plugin instances in a configuration. PluginInstanceID() string }注意接口注释直接写明:该 ID在整个配置的所有插件实例中唯一("unique across all plugin instances in a configuration")——这正是为索引器前缀防冲突预留的语义。
测试替身层: pkg/framework/fake/fake.go 中的
HandleImpl提供PluginInstanceIDImpl string字段,并通过PluginInstanceID()方法返回它,供单元测试注入。profile 构建层(核心实现): pkg/framework/profile/profile.go 中,每个 profile 内的每个唯一插件都得到一个
pluginHandle,其 instance ID 由profileInstanceID与插件序号拼接而成:
// Build each unique plugin only once with a unique plugin instance ID plugins := make(map[string]frameworktypes.Plugin) for idx, pluginName := range sets.New(pluginNames...).UnsortedList() { ph := &pluginHandle{ handleImpl: handle, pluginInstanceID: fmt.Sprintf("%s-%d", hOpts.profileInstanceID, idx), } pg, err := buildPlugin(ctx, config, pluginName, ph, reg) ... plugins[pluginName] = pg }基础 handle 的PluginInstanceID()则直接panic("not implemented")(pkg/framework/profile/profile.go),确保只有真正的插件 handle 才会携带 instance ID——从类型层面杜绝了"拿错 handle"的可能。
- 上层接线: pkg/descheduler/descheduler.go 在创建每个 profile 时注入唯一实例 ID:
for idx, profile := range deschedulerPolicy.Profiles { ... currProfile, err := frameworkprofile.NewProfile( ctx, profile, pluginregistry.PluginRegistry, frameworkprofile.WithClientSet(desch.client), ... // Generate a unique instance ID using just the index to avoid long IDs // when profile names are very long frameworkprofile.WithProfileInstanceID(fmt.Sprintf("%d", idx)), ) ... }这里还有一处与文档精神一致的工程细节:用 profile 的索引序号(idx)而非 profile 名作为 instance ID 前缀,注释说明是为了避免 profile 名过长时生成过长的 ID。WithProfileInstanceID选项的实现见 pkg/framework/profile/profile.go。
- 测试验证: pkg/framework/profile/profile_test.go 中用多组用例专门验证了 instance ID 的唯一性与格式(例如"三个 profile 各自包含相同的两个插件名时,每个 profile 内的插件仍获得不同的 instance ID"),并有
verifyInstanceIDsMatch、verifyInstanceIDFormat两个辅助函数逐项断言(pkg/framework/profile/profile_test.go)。
从这条实现链路可以看出:文档中"为 profile 提供唯一 ID、用唯一前缀避免索引器 key 冲突"的设计决策并非停留在纸面,而是已经通过PluginInstanceID完整落地——未来若引入索引器帮助函数,插件可以拿PluginInstanceID()作为自己的索引 key 前缀,实现即插即用的隔离。
给贡献者的实操清单
综合以上分析,向 Descheduler 提交代码变更时,可以按如下清单自查(对应文档的 Code convention 与 Unit Test Conventions 部分):
- 提交前:在仓库根目录执行
make fmt,确保 gofumpt 格式与 goimports import 排序通过;有条件时再跑make verify与make lint对齐 CI; - 写测试时:
- 用
test.BuildTestPod/test.BuildTestNode构建对象,定制一律放进apply闭包; - 每个测试用例新建对象,绝不跨用例共享指针;
- 若多个用例需要"相同参数"的对象,先封装一个薄包装函数(如
buildTestNode(name, apply)),再在用例中复用;
- 用
- 涉及框架扩展时:如果要为插件注册索引器,先通过
Handle.PluginInstanceID()获取当前插件实例的唯一 ID,并以其作为索引 key 的前缀,避免多 profile 实例化时的冲突;不要在框架接口中随意新增"索引器注册/获取"帮助函数——按当前设计决策,这层抽象应等真实需求出现后再讨论。
结语
CONTRIBUTING-descheduler.md虽然篇幅简短、仍在缓慢生长(Overview还是 TBD),但它浓缩了 Descheduler 社区两条重要的工程准则:用集中、可复用的测试构造方式对抗测试代码的认知负担,以及在缺乏真实需求时克制地不做抽象、只解决当下可预见的冲突。前者体现在test.BuildXXX+apply的整套测试基建中,后者则通过PluginInstanceID机制在框架层落地。对想要参与 Descheduler 贡献的开发者而言,遵循这份文档与其背后的代码约定,是让补丁快速通过 CI、并被维护者顺利接受的第一步。
- 云原生
- 运维
【免费下载链接】descheduler
Descheduler for Kubernetes
相关推荐
提升Quaver游戏体验的10个设置技巧:从画面到音频的全方位优化
提升Quaver游戏体验的10个设置技巧:从画面到音频的全方位优化 作为一款社区驱动的开源节奏游戏,Quaver提供了丰富的自定义选项来优化你的游戏体验。无论你
Ray Data 单元测试规范与实践:解读 `python/ray/data/tests/unit` 目录的隔离性设计与强制约束
Ray Data 单元测试规范与实践:解读 python/ray/data/tests/unit 目录的隔离性设计与强制约束 Ray Data 是 Ray 分布
人工智能分布式训练强化学习任务调度模型推理服务后端Mac Mouse Fix 配置全解:5 步让外接鼠标接近触控板手感
Mac Mouse Fix 配置全解:5 步让外接鼠标接近触控板手感 如果你的鼠标侧键在 Mac 上时好时坏、滚轮一格一格"咔哒咔哒"地跳,Mac Mouse
桌面应用系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考