ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

kOps 中的 controller-runtime 控制器开发 FAQ 实践指南:从事件映射、幂等调和到测试与 Scheme 排查

kOps 中的 controller-runtime 控制器开发 FAQ 实践指南:从事件映射、幂等调和到测试与 Scheme 排查 kOps 中的 controller-runtime 控制器开发 FAQ 实践指南从事件映射、幂等调和到测试与 Scheme 排查【免费下载链接】kopsKubernetes Operations (kOps) - Production Grade k8s Installation, Upgrades and Management项目地址: https://gitcode.com/gh_mirrors/kop/kopskOpsKubernetes Operations在其控制面组件 kops-controller 中深度使用sigs.k8s.io/controller-runtime当前锁定版本 v0.24.1见 go.mod来实现基于 Kubernetes API 的控制器逻辑例如 Node 标签调和与 Cluster API 控制器。本文以 controller-runtime 官方 FAQ.md 为主体逐一解答控制器开发中最常见的六个问题——对象类型映射、事件类型区分、缓存一致性、fake client 与 envtest 选型、测试编写方法以及 Scheme 注册报错——并结合 kOps 仓库中 kops-controller 的真实源码给出可落地的实践参考。读完本文你将掌握编写健壮、幂等、可测试的 controller-runtime 控制器的完整思路并能在 kOps 源码中直接找到对应实现。背景controller-runtime 在 kOps 中的定位kOps 的 kops-controller 进程以 manager 模式运行多个控制器其启动入口在 cmd/kops-controller/main.go。从源码可以看到它完整遵循 controller-runtime 的标准生命周期ctrl.SetLogger(klogr.New()) scheme, err : buildScheme(opt) // ... mgr, err : ctrl.NewManager(kubeConfig, ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{BindAddress: metricsAddress}, LeaderElection: true, LeaderElectionID: kops-controller-leader, }) // ... if err : mgr.Start(ctrl.SetupSignalHandler()); err ! nil { ... }其中ctrl.NewControllerManagedBy(mgr)、manager.Manager、client.Client、ctrl.Request/ctrl.Result等正是 FAQ 中所有讨论的载体。下面逐条深入 FAQ 中的问题。一、如何判断一个控制器引用了哪种类型的对象FAQ 给出的核心原则是每个控制器只负责调和reconcile一种对象类型。其它被影响的次要对象应当通过事件处理器映射到唯一的根对象类型上再在Reconcile方法中一次性调和该根对象关联的全部状态。controller-runtime 为此提供了两类开箱即用的事件处理器位于 vendor/sigs.k8s.io/controller-runtime/pkg/handlerhandler.EnqueueRequestForOwnerenqueue_owner.go当子对象发生变化时把其 Owner通过OwnerReferences关联的父对象加入调和队列handler.EnqueueRequestsFromMapFuncenqueue_mapped.go通过自定义映射函数把任意类型的事件映射到需要调和的目标对象必要时可配合索引indices加速查询。在 kOps 源码中一个控制器调和一种对象这一原则体现得十分清晰NodeReconciler 只负责调和corev1.Node一种类型通过For(corev1.Node{})声明主对象func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). Named(node). For(corev1.Node{}). Complete(r) }KopsConfigReconciler 只调和api.KopsConfig一种类型同样用For(api.KopsConfig{})声明。这种设计的收益在于事件来源Node 的增删改、KopsConfig 的变更等与调和目标解耦队列中永远只有根对象的调和请求Reconcile内部再去读取它需要的所有关联状态。二、如何在 reconciler 中针对不同事件create/update/delete执行不同逻辑FAQ 的答案非常明确你不应该这样做。Reconcile函数必须是幂等的idempotent每次执行时都应当读取它所需的全部当前状态与期望状态比对写出必要的更新。这样设计后控制器可以正确处理泛化事件、容忍事件被跳过或合并coalesced events也能在应用启动时无事件预热期自动收敛状态。FAQ 特别提醒当映射关系变化时控制器会同时为旧对象和新对象入队调和请求因此你必须在调和逻辑中保证不再被引用的旧状态也能被清理。kOps 的 NodeReconciler 是幂等调和的典型范例node_controller.gonode : corev1.Node{} if err : r.client.Get(ctx, req.NamespacedName, node); err ! nil { if apierrors.IsNotFound(err) { return ctrl.Result{}, nil // 删除场景忽略 not-found } return ctrl.Result{}, err } // 每次调和都重新计算期望标签与当前标签比对只 patch 差异 updateLabels : make(map[string]string) for k, v : range labels { actual, found : node.Labels[k] if !found || actual ! v { updateLabels[k] v } } // 清理不再期望的托管标签 deleteLabels : make(map[string]struct{}) for k : range node.Labels { switch k { case nodelabels.RoleLabelAPIServer16, nodelabels.RoleLabelNode16, nodelabels.RoleLabelControlPlane20: if _, found : labels[k]; !found { deleteLabels[k] struct{}{} } } } if len(updateLabels) 0 len(deleteLabels) 0 { return ctrl.Result{}, nil // 状态已收敛无需操作 }注意几点与 FAQ 呼应的实现细节删除事件不会直接调用一个删除分支而是表现为Get返回NotFound此时直接返回空Result即可——这正是以最终状态为准的体现每次调和都重新计算期望标签集因此无论事件是 create、update 还是重复调和结果都一致幂等性使得控制器启动后无需任何事件也能通过若干次自愈调和收敛到正确状态。三、缓存可能过期如何应对controller-runtime 默认通过 informer 缓存读取对象缓存数据与 API server 之间可能存在短暂延迟。FAQ 给出了分层应对策略优先使用乐观锁optimistic locking为创建的对象使用确定性的名字。这样如果对象已存在API server 会直接报已存在错误控制器即可转为读取并修正。Kubernetes 内建控制器广泛采用此模式StatefulSet 控制器给每个 Pod 追加编号Deployment 控制器对 Pod 模板做哈希后拼接命名。无法使用确定性名字时例如使用generateName跟踪自己执行过的动作如果在给定时间内没有观察到预期结果就通过 requeue 结果Result{Requeue: true}或Result{RequeueAfter: ...}重试。ReplicaSet 控制器即采用此策略。兜底手段直接构造一个读取 API server 的 client绕过缓存。FAQ 明确表示这是最后手段last resort前两种方案通常已覆盖绝大多数场景。总的原则是以信息最终会正确但可能稍有滞后为前提编写控制器并让调和函数每次执行时都强制校验世界的完整状态。从源码层面看kOps 也实践了必要时绕过缓存的思路在 main.go 中bootstrap 请求路径刻意创建了一个不经过缓存的client.New(...)uncachedClient并在注释中说明每个/bootstrap请求都会做一次 uncached 的单键 Node GET。这正是 FAQ 所述构造直接读取 API server 的 client在真实场景中的合理应用——低频、强一致性要求高的路径绕过缓存常规调和路径继续享受缓存带来的性能收益。四、fake client 在哪里该如何使用FAQ 指出fake client位于 controller-runtime 的pkg/client/fake确实存在但官方通常推荐使用envtest.Environment针对真实的 API server 编写测试。理由是长期使用 fake client 的测试往往逐渐重新实现一个写得很烂的 API server 仿制品导致测试代码复杂且难以维护。两种方案各自的适用场景可以这样理解fake client轻量、无需外部依赖适合快速验证纯逻辑如标签计算、patch 构造但不会真正执行 admission、默认值填充、校验等 API server 行为envtest启动一个真实的嵌入式API server行为与生产一致适合验证控制器与 API server 的完整交互但需要测试环境具备相应二进制。FAQ 对测试还给出了两条重要建议断言世界的状态而非调用了哪些 API在涉及 Kubernetes API 时应检查最终状态是否符合预期而不是断言某组 API 调用确实发生过。这样重构控制器内部实现时无需改动测试。记住写入与调和之间存在延迟任何时候与 API server 交互从写入完成到调和生效之间都可能存在时间差测试需要容忍这种异步性例如等待状态收敛后再断言。五、遇到 no Kind is registered for the type 错误怎么办FAQ 指出这个错误几乎总是因为缺少一个完整配置的 Scheme。Scheme 记录了 Go 类型与 Kubernetes GroupVersionKindGVK之间的映射关系。每个应用一般都应拥有自己的 Scheme包含它所依赖的所有 API 组的类型——无论是 Kubernetes 内建类型还是自定义类型。kOps 的buildScheme函数main.go是标准做法可作直接参考func buildScheme(opt *config.Options) (*runtime.Scheme, error) { scheme : runtime.NewScheme() if err : corev1.AddToScheme(scheme); err ! nil { return nil, fmt.Errorf(error registering corev1: %v, err) } if err : v1alpha2.AddToScheme(scheme); err ! nil { return nil, fmt.Errorf(error registering kops/v1alpha2 API: %v, err) } // Needed so that the leader-election system can post events if err : coordinationv1.AddToScheme(scheme); err ! nil { return nil, fmt.Errorf(error registering coordinationv1: %v, err) } if opt.CAPI.IsEnabled() { // 注册 kops bootstrap 与 control-plane 的 Cluster API 类型 if err : bootstrapapi.AddToScheme(scheme); err ! nil { return nil, fmt.Errorf(error registering kops bootstrap cluster API: %v, err) } if err : controlplaneapi.AddToScheme(scheme); err ! nil { return nil, fmt.Errorf(error registering kops control-plane cluster API: %v, err) } } return scheme, nil }这份实现示范了几个要点Scheme 必须覆盖所有会与 manager/client 交互的类型不仅包括调和对象如 Node、KopsConfig还包括 leader election 发事件所需的coordinationv1自定义 API 组如 kOps 自身的v1alpha2、Cluster API 的v1beta1通过各自的AddToScheme注册该 Scheme 随后被传入ctrl.NewManager(..., ctrl.Options{Scheme: scheme, ...})成为整个 manager 统一使用的类型注册表。当你的控制器报 no Kind is registered 时对照这份代码检查你的 Scheme 是否包含该类型所在 API 组是否在创建 manager/client 之前就完成了注册六、kOps 中的综合实践把这些 FAQ 答案串起来在 kOps 中以上 FAQ 原则被组合使用构成了一个完整的生产级控制器体系1. Manager 统一装配main.go单进程单 manager开启 leader electionLeaderElectionID: kops-controller-leader保证多副本下只有一个控制器在工作默认将 metrics 端口绑定为:0禁用避免与宿主机网络端口冲突——这是对 FAQ 未涉及但实战中同样关键的配置细节。2. 控制器注册addNodeController根据云厂商AWS/GCE/OpenStack/DigitalOcean/Hetzner/Azure/Scaleway/metal/Linode选择不同的nodeidentity.Identifier实现再注入 NodeReconciler——说明 FAQ 中每次调和读取全部所需状态的所需状态可以来自外部依赖注入。3. Cluster API 控制器族pkg/controllers/clusterapicluster_controller.go、kopsconfig_controller.go、kopscontrolplane_controller.go各自用ctrl.NewControllerManagedBy(mgr)注册、各自调和一种根对象互不混叠严格符合 FAQ 第一条原则。4. IPAM 控制器setupCloudIPAMAWS/GCE/metal 各有一套 IPAM reconciler通过统一的Reconciler接口SetupWithManager(mgr) error抽象体现了控制器应自包含、可独立注册的设计。总结一套可复用的控制器开发清单结合 FAQ.md 与 kOps 源码编写高质量 controller-runtime 控制器时可对照以下清单对象类型一个控制器只调和一种根对象次要对象用EnqueueRequestForOwner/EnqueueRequestsFromMapFunc映射必要时配 indices事件逻辑不在 Reconcile 中区分 create/update/delete写幂等函数每次读取全量状态、比对、只写差异并确保能清理不再引用的状态缓存一致性优先确定性命名 乐观锁generateName场景跟踪动作并 requeue 重试强一致路径可构造 uncached clientkOps bootstrap 请求即如此但作为最后手段测试优先 envtest 打真实 API server断言世界状态而非 API 调用序列容忍写入与调和之间的延迟Scheme为所有涉及的类型含 leader election 等辅助类型建立完整 Scheme并在创建 manager 前注册完毕参照buildScheme装配通过ctrl.NewManagerctrl.NewControllerManagedBy(mgr).For(...).Complete(r)的标准流水线注册控制器必要时用接口抽象多种实现。按此清单开发你的控制器将具备 kOps kops-controller 同等的健壮性、幂等性与可测试性。【免费下载链接】kopsKubernetes Operations (kOps) - Production Grade k8s Installation, Upgrades and Management项目地址: https://gitcode.com/gh_mirrors/kop/kops创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表