免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Istio 控制器体系深度解析:从 kube.Client 到 kclient 与 krt 的分层抽象设计

Istio 控制器体系深度解析:从 kube.Client 到 kclient 与 krt 的分层抽象设计 Istio 控制器体系深度解析从 kube.Client 到 kclient 与 krt 的分层抽象设计【免费下载链接】istioConnect, secure, control, and observe services.项目地址: https://gitcode.com/GitHub_Trending/is/istioIstio 内部运行着大量 Kubernetes Controller控制器它们持续监听集群资源变化并写回新对象、向代理下发 XDS 配置。由于手写控制器极易引入并发与同步缺陷Istio 在底层client-go之上构建了三层逐步升高的抽象kube.Client、kclient.Client和krt.Collection并配套controllers包提供队列与事件处理工具。本文基于仓库中的架构文档 architecture/networking/controllers.md 与对应源码实现系统讲解这套分层体系的设计动机、各层能力边界以及 Istio 控制器构建期 运行期两段式的标准写法。为什么需要控制器抽象从架构文档的开篇定义看Istio 的控制器遵循经典的 Kubernetes 控制器模式监听某些输入然后执行动作——典型场景是从 Kubernetes 读取对象再写回其他对象或者通过 XDS 协议把配置写入代理。文档同时指出一个关键痛点编写控制器非常容易出错即便看起来逻辑很简单的场景也不例外。为此 Istio 构建了一组专门降低控制器编写难度的抽象。文档中的抽象层次图展示了如下构建于Builds on关系Kubernetes client-go ↑ builds on Istio kube.Client pkg/kube/client.go ↑ builds on Istio kclient.Client pkg/kube/kclient/ ↑ builds on Istio krt.Collection pkg/kube/krt/实验性下面从底向上逐层展开。第一层kube.Client —— 统一管理多种 Kubernetes 客户端kube.Client是对 Kubernetes 客户端的轻量封装但提供了显著收益。其接口定义见 pkg/kube/client.go核心接口包含以下方法Kube()核心 Kubernetes clientsetDynamic()动态客户端用于 CRD 等无强类型的资源Metadata()仅操作对象 metadata 的轻量客户端Istio()Istio 自身 CRDVirtualService、DestinationRule 等的客户端GatewayAPI()/GatewayAPIInference()Gateway API 及推理扩展相关客户端Ext()API 扩展客户端如 CRD 管理Informers()返回 informer 工厂CrdWatcher()CRD 监听器用于感知 CRD 是否已安装ObjectFilter()基于配置过滤不需要对象的动态过滤器RunAndWait()集中启动全部 informer 并等待缓存同步。从接口注释可以看到它的价值所在Client接口声明这包含多个共用同一份 rest.Config 的不同 Kubernetes 客户端期望整个 Istiod 共享同一组客户端和 informer共享 informer 对减轻 API server / Istiod 自身负载尤为重要。换言之同一集群内所有控制器共享一个kube.Client两个控制器同时 watch Pod 时只会对 API server 建立一条 watch 连接。此外kube.Client还内置了几类便利逻辑Fake 客户端变体单元测试中可用kube.NewFakeClient(...)构造带假数据的客户端行为与真实客户端一致参考 pkg/kube/controllers/example_test.go 中的Example()用法动态对象过滤ObjectFilter()允许在客户端层面屏蔽无关对象。架构文档对此层的强制约定是Istio 中所有 Kubernetes 使用都应经由该库完成不允许直接操作原始 Kubernetes 客户端。第二层kclient.Client —— 泛型化的强类型资源客户端kclient是构建在kube.Client之上的更高一层抽象按资源类型泛型化kclient.Client[*corev1.Pod]。从 pkg/kube/kclient/interfaces.go 可以看到它由三个子接口组成Reader[T]基于缓存的读接口提供Get(name, namespace)不存在时返回 nil和List(namespace, selector)Writer[T]直写接口提供Create、Update、UpdateStatus、Patch、PatchStatus、ApplyStatusserver-side apply、DeleteInformer[T]管理 informer 生命周期包括AddEventHandler、HasSynced、ShutdownHandlers、Start、Index自定义索引等。实践中通常直接使用组合了三者的Client[T]。kclient相比裸用 informer 的额外能力架构文档列举了四点均可在源码中得到印证泛型带来的强类型客户端与更顺手的 API延迟客户端delayed client用于基于可能尚不存在的 CRD构建客户端。Istio 的一般策略是对缺失的 CRD 不报错而是将其视为该客户端下零个资源。实现见 pkg/kube/kclient/delayed.go其中delayedClient结构注释写得很直白以一个空客户端启动之后可切换为真实客户端空客户端对所有读返回空响应所有写均失败。它对外提供与正常kclient.Client完全相同的 API把 CRD 未就绪的复杂性完全屏蔽掉动态过滤dynamic filters除静态过滤外还支持运行时可变的过滤器——当过滤条件变化导致新对象进入/离开匹配集合时框架会自动把差异作为事件处理正确的同步与关闭逻辑判断 informer 是否真正完成初始填充且已对初始状态触发过所有 handler是一件很麻烦的事kclient自动处理这也是它HasSynced与标准 informerHasSynced的语义差别——后者不检查 handler 是否已被调用过。配套地kclient包下的 clienttest 子包提供面向单元测试的同类接口可以便捷地预置和断言资源。文档同样强调Istio 中所有 informer 使用都应经由该库完成不允许直接操作 Kubernetes informer。第三层krt.Collection —— 声明式控制器运行时实验性krtKubernetes Declarative ControllerRuntime是构建在kclient之上的最顶层封装。关于它的详细设计可参阅仓库内文档 pkg/kube/krt/README.md此处按该文档整理其核心内容核心原语是Collection接口本质上是一个不与 Kubernetes 绑定的 informer可操作任意来源、任意类型的对象构造Collection有三种方式基于 informer 的WrapClient/NewInformer、静态数据NewStatic、以及由其他 Collection 派生每个对象T需要一个唯一的Key[T]Kubernetes 对象、Istioconfig.Config、ResourceName()实现均有默认实现可选实现Equals用于变更检测派生集合通过转换函数表达目前支持三种形态NewSingleton单值如全局配置、NewCollection一对一如 Pod → Workload 转换、NewManyCollection一对多如 Service → 一组 Endpoint通过krt.Fetch(ctx, 其他Collection, 过滤器...)声明对其它集合的依赖依赖变化时框架自动重算且只有输出发生变化时基于 Equals 比较才向外产生事件过滤器包括FilterName、FilterNamespace、FilterKey、FilterLabel、FilterSelects、FilterSelectsNonEmpty、FilterGeneric在Fetch内部过滤比在外部过滤更高效转换函数必须无状态且幂等只能通过krt.Fetch查询其他集合禁止查询其他可变存储或发起外部调用如 HTTP因为框架可能在任意时刻、对相同输入多次调用它违反约束将导致未定义行为通常表现为陈旧数据。krt 的 README 中还给出了一组基准数据BenchmarkControllers对比理想的手工控制器与 krt 实现krt 的开销约为 10%time/op 13.4ms vs 11.4msalloc/op 15.2MB vs 12.9MB且这些数字可能会随时间变化。需要注意架构文档中明确的定位krt 目前是实验性的在将其用于新领域之前应先与维护者沟通krt README 也说明它尚未用于 Istio 生产路径计划以风险较低的控制器如 ambient 相关控制器作为首批落地目标。编写控制器构建期与运行期两段式controllers 包提供编写控制器的辅助设施。其设计哲学是与controller-runtime工作方式相似但远更小、抽象更少。除了少数例外Istio 控制器都遵循两段式结构构建期Construction构建阶段应完成三件事且不应启动运行、不做 I/O、不阻塞创建 informer通过kclient.New*corev1.Pod创建队列通过controllers.NewQueue在 informer 上注册事件处理器常见写法是client.AddEventHandler(controllers.ObjectHandler(queue.AddObject))。运行期Running运行阶段真正开始处理事件通常就是运行队列。由于kube.Client集中跟踪所有由其创建的 informer并通过一次RunAndWait统一启动所以每个控制器自身只需等待自己关心的 informer 同步完成然后运行队列。队列提供的关键性质从 pkg/kube/controllers/queue.go 的实现看Queue是对 Kubernetesworkqueue的抽象入队项去重因此不能依赖队列内事件的顺序它带来文档列举的四个性质串行处理来自多种来源的更新避免互斥锁等同步原语启动正确性结合上述时序先等 informer 同步、再跑队列只有全部 informer 同步后条目才会被处理启动阶段的查询不会读到陈旧数据相同事件去重失败自动重试可配置。实现细节值得注意Queue在Run()时会先向队列注入一个内部的defaultSyncSignal假信号——由于 workqueue 保序当处理到该信号时即可判定Run() 之前入队的所有初始条目均已处理完据此置位initialSyncHasSynced()即为 true。重试逻辑由WithMaxAttempts(n)控制处理器返回错误时若重试次数未超过maxAttempts则通过AddRateLimited带退避地重新入队超出预算则记录错误并放弃该条目。队列的其他可选配置包括WithName日志标识、WithRateLimiter自定义限流器、WithReconciler/WithGenericReconciler设置处理函数未调用Run的队列必须调用ShutDownEarly()避免泄漏。controllers包还提供了一批常用工具见 pkg/kube/controllers/common.goObjectHandler/TypedObjectHandler把 Add/Update/Delete 统一为取最新版本对象、触发一次 reconcile的处理器底层用Extract正确处理DeletedFinalStateUnknown墓碑对象FilteredObjectHandler/FilteredObjectSpecHandler带过滤器的变体后者仅在resourceVersion变化即 spec 变更时触发可跳过纯 status 更新EnqueueForParentHandler按ownerReferences把事件入队到父资源是子资源变更 → 调谐父资源这一模式的现成实现IgnoreNotFound把 NotFound 错误视为 nil 的辅助函数。标准示例Example Controller架构文档明确指定 pkg/kube/controllers/example_test.go 是一切控制器开发的关键参照虽然存在少数应当表现不同的例外情况但大部分情况下偏离参照代码都会引入 bug。该示例是一个打印所有 Pod 的极简控制器完整覆盖了上述规范其骨架如下// 构建期注入 kube.Client构建 typed client、队列、注册 handler func NewController(cl kube.Client) *Controller { c : Controller{events: atomic.NewInt32(0)} // 每个集群应只有一个 kube.Client其内部共享 watch // 因此永远不要直接对 api-server 做 List/Get c.pods kclient.New*corev1.Pod // 也可以带过滤条件构建例如 // c.pods kclient.NewFiltered*corev1.Pod}) // kclient.Filter{FieldSelector: metadata.namemy-pod}) c.queue controllers.NewQueue(pods, controllers.WithReconciler(c.Reconcile), controllers.WithMaxAttempts(5)) c.pods.AddEventHandler(controllers.ObjectHandler(c.queue.AddObject)) return c } // 调谐函数入参是对象名应据此驱动对全世界状态的收敛 func (c *Controller) Reconcile(key types.NamespacedName) error { pod : c.pods.Get(key.Name, key.Namespace) if pod nil { log.Infof(pod deleted) } else { log.Infof(pod has IP %v, pod.Status.PodIP) } return nil // 若返回错误会按 WithMaxAttempts 带退避地重试 } // 运行期在 New() 之后、HasSynced() 之前调用且应当阻塞 func (c *Controller) Run(stop -chan struct{}) { // 先等 pods informer 同步完成此时初始状态已全部入队 kube.WaitForCacheSync(pod controller, stop, c.pods.HasSynced) // 再运行队列阻塞直到 stop 关闭 c.queue.Run(stop) // 注销 handler防止控制器退出后 informer 仍在跑时 handler 继续执行 // 主要面向 leader-election 场景常规场景下控制器生命周期与 informer 一致 c.pods.ShutdownHandlers() } // HasSynced我们只检查 queue——因为 Run() 的时序已保证它蕴含 pods 同步 func (c *Controller) HasSynced() bool { return c.queue.HasSynced() }示例中对细节微妙但必须做对的部分给出了大量注释为什么必须先把初始状态灌入队列再跑队列queue.HasSynced的语义是Run() 之前加入的条目已全部处理为什么HasSynced()只查队列就足够Run()的实现已隐含 informer 同步以及测试侧的收尾顺序——先关闭 stop channel 触发 informer/队列关停流程再用queue.WaitForClose(timeout)等待真正关闭。测试中还演示了用 fake client 预置数据kube.NewFakeClient(corev1.Pod{...})、defer c.Shutdown()确保 informer 全部关闭、以及clienttest.Wrap(t, ...)这类测试辅助设施。小结选择哪一层抽象结合架构文档与源码三层抽象的选择依据可以归纳为层次适用场景关键约束kube.Client一切需要访问 Kubernetes 的代码共享 informer 降低 API server 负载必须经此库不得直接操作原生客户端kclient.Client一切需要 informer / 强类型资源读写 / 动态过滤 / CRD 可能缺失delayed client的控制器必须经此库不得直接操作 informerkrt.Collection跨来源、多类型对象间的声明式数据派生与依赖跟踪实验性转换函数须无状态、幂等新领域使用前需与维护者确认而控制器的构建期创建 informer 队列 handler运行期等待同步后跑队列两段式结构加上 示例控制器 作为参照基线构成了 Istio 控制器正确性的核心保障。【免费下载链接】istioConnect, secure, control, and observe services.项目地址: https://gitcode.com/GitHub_Trending/is/istio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表