基于 client-go 构建 Kubernetes Controller:2018 贡献者峰会实战总结与最佳实践 📅 发布时间:2026/9/15 15:26:36 👁 浏览次数: 基于 client-go 构建 Kubernetes Controller2018 贡献者峰会实战总结与最佳实践【免费下载链接】communityKubernetes Community Documentation项目地址: https://gitcode.com/GitHub_Trending/com/community在 Kubernetes 生态中client-go 是连接 API Server 与控制器的核心客户端库而基于它编写 Controller 长期被视为必要但繁琐的工程实践。本文以 2018 年 Kubernetes Contributor Summit EU 上由 munnerzJames Munnelly主持、lavalampDaniel Smith协助的 client-go 专题讨论记录见 clientgo-notes.md为主体系统梳理与会贡献者提出的真实痛点、最佳实践问答与演进方向并对照本仓库中 SIG API Machinery 沉淀的 Controller 编写指南 与 clientset 生成机制 进行纵深解读。读完本文你将掌握 Controller 样板代码的构成、resync/工作队列/status 的设计取舍、clientset 与 lister 的差异以及从测试到部署的完整工程思路。一、讨论背景一次聚焦 Controller 工程化痛点的社区脑暴该讨论是 2018 年 5 月 1 日于丹麦哥本哈根 Bella Center 举行的 Kubernetes Contributor Summit EU 的Current Contributor轨道环节时间轴见 峰会 README与 CRD 专题共用一份幻灯片。会议的目标非常明确归纳构建 Controller 时最让人痛苦的地方收集围绕最佳实践的问题让新人说出哪些概念难以理解让有经验者分享哪些关键信息是必须掌握的。从仓库中 sig-api-machinery/README.md 可以看到client-go 属于SIG API Machinery的管辖范围——该 SIG 覆盖 API Server、API 注册与发现、CRUD 语义、admission control、OpenAPI、CustomResourceDefinition 以及 client 库。因此这场讨论的结论最终也大多沉淀回 SIG API Machinery 的开发者文档与社区实践中。二、构建 Controller 的核心痛点从样板代码到安全与回滚2.1 大量重复的样板代码与会者公认的第一痛点是大量 boilerplateWork queues工作队列去重、限速、重新入队的逻辑几乎每个 Controller 都要重复实现HasSynced 函数启动时必须等待 informer 缓存同步完毕代码千篇一律Re-queuing重新入队失败重试、退避策略需要反复编写。与之配套的是这些领域缺乏深度文档的抱怨——当时已有的一些文档主要面向 kube-controller-manager 中的核心in-tree控制器对如何为扩展资源CRD编写独立 Controller的讲解几乎空白。这一缺口正是后续 Writing Controllers 指南 想要填补的该文档明确给出了一条最朴素的控制器心智模型for { desired : getDesiredState() current : getCurrentState() makeChanges(desired, current) }即控制器就是一个持续的调和reconciliation循环——观察世界的期望状态观察世界的实际状态然后下发指令让当前状态向期望状态靠拢watch 等机制都只是对这个循环的优化。2.2 Webhook、API Server 安全与 TLS 证书第二个痛点集中在安全侧Securing webhooks APIServers如何安全地暴露 Validating/Mutating webhook 与聚合 API ServerValidation schemas验证 schema 的设计与维护TLS 证书数量当时为每个 webhook 管理证书非常痛苦内部 k8s CA 虽被部分使用但体验不佳。会上提到 OpenShift 的 serving cert controller 可以根据 annotation 自动生成证书有可能整合到上游Leader election选主当时的选举机制问题较多且 Scaling API 底层且难用——当资源具有多种规模含义例如多组节点池时表现不佳。2.3 声明式 API 到事务式 API 的翻译不少 Controller如 Ingress面临一端是声明式 API、另一端必须翻译成事务式 API的困境——控制器需要一次性变更很多底层对象。会上给出的应对思路包括可以自建locking锁但它必须由你自己构建对于底层基础设施拒绝了某个操作时如何回滚的问题讨论给出的答案集中在三处使用validating webhook在写入前拦截非法请求使用status字段记录进行中的状态与进度区分两类控制器——kube -- kube纯 Kubernetes 内部与kube -- external与外部系统交互二者的回滚与重试语义不同。最后一致认为需要一个持续记录进行中事项的机制例如 status且社区需要更多关于如何正确解决这一问题的信息。2.4 CRD 的注册时机关于注册 CRD 的最佳方式是什么现场结论是没有唯一的最佳方式但普遍做法是随应用一起部署发言人个人建议先部署 CRD再部署应用主要出于RBAC的考量——先有类型定义权限模型才完整。三、最佳实践问答resync、status、conditions 与测试讨论记录中 Q 代表提问A 代表听众或回答3.1 外部资源同步与 resync periodQ如何让外部资源与 Kubernetes 资源保持同步A最初的设计意图就是把 resync period重同步周期用在你 watch 了外部资源的场景——周期性把所有对象重新推入处理循环借此去轮询/校准外部资源。Q如果不涉及外部资源resync period 应该设为 never 吗A是的。watch 偶尔漏掉事件不是 bug——控制器在连接异常时会自动重新 list而resync interval 只服务于外部资源与 watch 的可靠性无关。有与会者甚至建议把这个参数改名使其语义为外部资源服务更清晰。仓库中的 Writing Controllers 指南 对 resync 给出了补充警示informers 周期性 resync 会对本地缓存中的每个对象触发一次 Update 事件如果你确定没有新变更时无需重新入队可以比较新旧对象的 resourceVersion相同则跳过但必须谨慎——如果因为跳过重新入队而导致失败后永不重试这个对象就再也得不到处理了。3.2 status 更新频率conditions 与 fields 的分工Q每次 sync 应更新几次 statusA把状态信息分成两类用不同机制表达conditions软状态用于向用户传达模糊的状态——提示信息、当前被什么阻塞例如 HPA 中的消息适合描述对象处于什么状态fields硬状态用于传达精确的状态——最近观测到的数字、最近一次指标、之后处理还需要引用的值。conditions 设计动机是兼容混合版本客户端ready这类带 msg 的附加字段表达对象所处的状态对象的可选状态数量是有限的conditions 允许老客户端忽略新增 condition 而新客户端遵循它从而在不破坏已有客户端的前提下更容易扩展 status。会上同时提醒不要过度使用 conditions——部分功能确实强制要求 conditions而 status 字段本身的使用边界当时仍有讨论空间。3.3 验证方式Validating webhook vs OpenAPI schema与会者讨论了验证扩展资源合法性的两条路线通过 Validating webhook 做灵活校验还是通过 CRD 的 OpenAPI 验证 schema 做声明式校验。这场讨论与同峰会的 CRD 专题记录 相互呼应——当时 CRD 的 schema 能力仍在演进1.11 计划引入 pruning、defaulting且验证是否将成为必填项本身也是一个悬而未决的问题因此两条路线并存、各有适用场景。3.4 扩展控制器的测试策略Q能否写一个测试在进程内直接拉起主 API ServerAk/k 的部分测试可以做到但不容易被外部项目复用vendoring依赖固化很难当时还存在一个 bug——聚合 API 必须跑在 443 端口上进一步复杂化了进程内测试。Q大家怎么测试扩展CRD/Controller有人复用上游的dindDocker-in-Docker集群kubebuilder 使用 sig-testing 框架拉起本地 control plane 并针对它做测试pwittrock 提供e2e 层面用 kubeadm 拉起完整集群再跑测试集成测试则引入能构建集群的包。Q用什么 CI常见做法是Circle CI 起新的 VM 来承载测试集群Mirantis 有一个多节点 dind 集群测试工具社区在 stack 上有一份 27 页的 testing-commons 文档链接收录于会议幻灯片可关注#testing-commons频道。3.5 子资源subresources的使用时机Q什么时候该用 subresourcesA目前主要就是status 与 scale两个用途。关于为什么今天不把 scale 做成 subresource的疑问讨论给出的解释是存在多个 replicas 字段且 scale 与多态结构不同资源的规模语义不同不匹配pwittrock 从 kubectl 侧补充希望把kubectl 的特殊动词如 scale推进 subresource使 kubectl 对版本偏差version skew更宽容。这与 CRD 专题中1.11 将 subresource 从 alpha 推进到 beta的路线图一致见 crds-notes.md。四、clientset 与 lister两个接口的差异与取舍Qclient-go 生成的 listers 为什么要提供两套从 client 和从 cache 读取的接口A有历史原因但更本质的是有些事情在本地做比在服务端做更好clientset 接口允许传入特殊 options在 API Server 上执行一些 lister 无法完成的有趣操作两者最初是同一种函数调用后来逐渐分化lister 返回的是指针切片共享同一份缓存对象clientset 返回的是非指针切片独立副本很多人会把 clientset 的返回值再转换成指针切片而 lister 直接免去了这个转换以及随之而来的深拷贝开销——所以两套接口并不等价。这一设计背景可以从 clientset 的生成机制 中得到印证client-gen 工具根据 API 类型上的// genclient系列标记自动生成 clientset 的 CRUD 动词函数create/update/delete/get/list/patch/watch若类型含.Status字段还会生成 updateStatus而 lister 则是面向本地缓存读取的优化接口。两者分工在 Writing Controllers 指南 中体现为一条硬性约束永远不要修改原始对象——缓存是跨控制器共享的如果你修改了自己的副本实际上只是引用或浅拷贝会污染其他控制器最常见的坑是浅拷贝后修改Annotations这类 map。必须用api.Scheme.Copy做深拷贝。五、数据一致性外部数据同步、缓存与 UID5.1 数据陈旧问题的处理Q如何处理数据陈旧让本地数据与外部数据保持同步A在 informer 上指定 sync period把所有对象周期性推入处理循环借此访问外部资源对纯 Kubernetes 场景k8s in、k8s outwatch 应返回一切、应被信赖无需设置 sync period。5.2 缓存跨重启与节点名重复会上记录了一个真实事故节点名重复曾给 AWS 场景和长期缓存带来问题。给出的工程建议非常具体如果要在重启后保留缓存务必存储 UID而不是依赖名字。因为名字可能被删除后复用而 UID 在对象生命周期内唯一且稳定。5.3 其他语言的控制器Q除 Go 外的语言如何写控制器A提到了metacontroller声明式控制器的思路其他语言虽有 client 库但缺少的拼图是 work queue、informer 等基础设施——这正是 Go 生态中 client-go 最核心的价值所在。此外针对 Cluster API 中 deployment 代码被复制到 machineset 等控制器的问题与会者坦言把它抽成库是很大的工作量需要有人来做并建议就核心工作负载 API 的意见咨询 Janet Kuo。六、从讨论走向实践仓库中的 Controller 编写指南上述讨论的许多共识已经沉淀为 SIG API Machinery 维护的正式指南 Writing Controllers。该文档给出了一份可直接复用的 Controller 骨架可作为理解痛点→解决方案的最佳教材type Controller struct { // pods gives cached access to pods. pods informers.PodLister podsSynced cache.InformerSynced // queue is where incoming work is placed to de-dup and to allow easy // rate limited requeues on errors queue workqueue.TypedRateLimitingInterface[cache.ObjectName] } func NewController(pods informers.PodInformer) *Controller { c : Controller{ pods: pods.Lister(), podsSynced: pods.Informer().HasSynced, queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controller-name), } // register event handlers to fill the queue with pod creations, updates and deletions pods.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { ref, err : cache.ObjectToName(obj) if err nil { c.queue.Add(ref) } }, UpdateFunc: func(old interface{}, new interface{}) { ref, err : cache.ObjectToName(new) if err nil { c.queue.Add(ref) } }, DeleteFunc: func(obj interface{}) { // IndexerInformer uses a delta nodeQueue, therefore for deletes we have to use this // key function. ref, err : cache.DeletionHandlingObjectToName(obj) if err nil { c.queue.Add(ref) } }, }) return c }这段代码把痛点清单逐一落地HasSyncedpodsSynced、work queue带限速与去重的RateLimitingInterface、以及重新入队的触发源Add/Update/Delete 事件处理器。其Run与processNextWorkItem则展示了规范化的生命周期与错误处理func (c *Controller) Run(ctx context.Context, threadiness int) error { defer utilruntime.HandleCrash() defer c.queue.ShutDown() logger : klog.FromContext(ctx) logger.Info(Starting NAME controller) // wait for your secondary caches to fill before starting your work if !cache.WaitForCacheSync(ctx.Done(), c.podsSynced) { return fmt.Errorf(failed to wait for caches to sync) } for i : 0; i threadiness; i { go wait.UntilWithContext(ctx, c.runWorker, time.Second) } logger.Info(Started workers) -ctx.Done() logger.Info(Shutting down NAME controller) return nil }func (c *Controller) processNextWorkItem(ctx context.Context) bool { ref, shutdown : c.queue.Get() if shutdown { return false } defer c.queue.Done(ref) err : c.syncHandler(ref) if err nil { // success: stop tracking history, reset failure counts for rate limiting c.queue.Forget(ref) return true } // report error, then requeue with backoff to avoid hotlooping utilruntime.HandleErrorWithContext(ctx, err, Error syncing; requeuing for later retry, objectReference, ref) c.queue.AddRateLimited(ref) return true }指南中还包含一批与峰会讨论直接呼应的工程准则值得逐条对照峰会讨论要点正式指南中的对应准则工作队列的重复样板一次只处理一个对象用workqueue.Interface保证同一对象不会被两个 worker 同时处理watch 可靠性、relistlevel driven而非 edge driven即使有 watch也不能依赖看到从 false 变为 true只能相信现在观察到它是 true并在 status 中记录上次决策依据informer 共享缓存使用 SharedInformers共享同一份缓存实例节省 API Server 连接、服务端序列化与控制器侧反序列化/缓存开销旧式 reflector/deltafifo 机制不应在新控制器中使用数据陈旧外部资源等待辅助缓存就绪用cache.WaitForCacheSync等次级缓存填充完成再启动主 sync避免基于过期信息造成抖动状态跟踪、进行中记录若资源 status 支持ObservedGeneration务必在metadata.Generation与观测值不一致时正确设置让客户端知道控制器已处理该资源多副本与故障用k8s.io/client-go/tools/leader-election运行多副本一个 active、其余待命但要注意选举也非绝对可靠极端情况下仍可能多副本并存错误重试把错误上抛到顶层做统一重新入队需要重试就返回 error不需要就utilruntime.HandleErrorWithContext并返回 nil方便评审者审查错误处理路径子资源status/scale使用 owner references 保证子资源如 ReplicaSet 创建的 Pod随父资源被垃圾回收收养子资源时若观察到 owner 引用更新应绕过缓存直接读 API避免与 GC 竞争七、后续演进知识沉淀与社区方向讨论的最后一部分展望了如何让这些经验不再依赖走廊对话知识共享渠道多数 SIG 自己维护控制器而现有文档集中在 in-tree 开发——与会者提议建设专门的 extending kubernetes 文档板块并讨论 Wiki、Developer Docs working group 或专门的SIG PlatformDev / 平台开发工具包 working groupkubebuilder 文档生成针对如何生成漂亮的文档markdown 而非 swagger答案是kubebuilder 开箱即用地从类型定义生成文档社区还希望有一个对原生类型与 CRD 通用的 IDL 管线实时交流渠道控制器相关问题目前主要流向#sig-apimachinery工作负载类控制器话题则更适合#sig-apps或对应邮件列表。从今天回看这些讨论中缺少 work queue/informer 基础设施样板代码太多文档不足的诉求很大程度上推动了 controller-runtime / kubebuilder 生态的发展——本仓库中 sig-api-machinery/README.md 已能看到定期举行的Kubebuilder Meeting与相关 subprojects 的沉淀而 Writing Controllers 指南 至今仍是理解控制器心智模型与工程骨架的最佳入口。结语2018 年这场 client-go 专题讨论的价值不在于它给出了多少终极答案而在于它系统性地暴露了基于 client-go 构建控制器的真实工程全貌从样板代码、TLS 与 webhook 安全、声明式到事务式的翻译难题到 resync 语义、conditions/fields 分工、clientset 与 lister 的差异、缓存与 UID 陷阱再到测试与 CI 策略。这些讨论成果大多已沉淀为本仓库 contributors/devel/sig-api-machinery 下的正式指南成为后续所有扩展控制器开发者的共同起点。无论是初学 Controller 的新人还是维护生产级扩展组件的资深工程师都可以从这份现场实录 官方指南的组合中获得可复用的设计判断与避坑清单。【免费下载链接】communityKubernetes Community Documentation项目地址: https://gitcode.com/GitHub_Trending/com/community创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考