跳到主要内容
约 14 分钟8 张卡

client-go

client-go 是 Kubernetes 的 Go 客户端库,既能直接访问 API,也提供维护本地缓存和组织控制器工作的组件。编写控制器时,关键是根据当前状态反复作出正确决定,而不是假设每个事件只出现一次、回调里的对象永远最新。

本文以 Go 1.22.12、client-go v0.30.5 的实现说明机制。Kubernetes 的组件关系见 Kubernetes 架构,这里聚焦客户端的数据流与并发边界。

1. 面试核心​

先记住这几条,再读正文
  • client-go 通过 API Server 访问资源,不绕过它直接 Watch etcd。类型化客户端、动态客户端和底层 REST 客户端提供不同层次的访问方式。卡 · 客户端与权限
  • List 建立资源集合的起点,Watch 从相应版本继续观察变化。resourceVersion 用于版本与观察位置,客户端不把它当时间戳或可自行运算的整数。卡 · List、Watch 与 resourceVersion
  • Informer 把资源变化同步到本地缓存,再通知处理器。缓存可能滞后,回调发生时也可能已经有更新的缓存状态。卡 · Informer 数据流
  • DeltaFIFO 缓冲对象变化,工作队列安排业务调谐,两者不承担同一职责。前者保存带变化类型的对象序列,后者通常只保存资源键。卡 · Reflector 与 DeltaFIFO
  • 同一处理器按顺序接收通知,不同处理器有各自的执行过程。慢处理器会积压通知并消耗内存,回调应尽快把键交给工作队列。卡 · 缓存与处理器
  • 工作队列会合并重复键,并避免同一个键在同一队列内同时交给多个 worker。这不保证不同键的业务完成顺序,也不提供跨进程互斥。卡 · 键、去重与顺序
  • 每次 Get 后都要 Done,成功后清除重试历史,失败按策略重试。业务需要幂等,不能把队列当成恰好一次交付系统。卡 · 重试与完成
  • 控制器持续调谐,准入 Webhook 在请求进入保存流程时检查或修改对象。两者处在不同阶段,不能用慢 Webhook 替代长期调谐。卡 · 控制器与准入 Webhook

2. API 访问与版本​

2.1 客户端与权限​

API Server 是资源读写与观察的入口。它处理认证、授权、准入和资源版本等语义;client-go 发出的 List、Watch、Get、Update 等请求都通过这个入口。etcd 的存储细节不会把客户端变成 etcd 直连程序。

访问方式对象形式适用情况
类型化客户端编译时已知的 Go 结构体Pod、ConfigMap 等已有类型的资源
动态客户端unstructured.Unstructured运行时决定资源类型或处理自定义资源
RESTClient组织 API 路径、参数、编码与请求需要在较低层控制调用,或由生成客户端封装

客户端配置包括地址、凭据、证书信任和请求限速等。集群外通常读取明确指定的 kubeconfig;集群内可以使用工作负载对应的身份配置。凭据有效不代表有权访问全部资源,List 与 Watch 也需要对应授权,不能通过关闭 TLS 校验来解决配置问题。

客户端和传输应按服务生命周期复用,而不是每次事件都重新建立。请求继续传递 context,并给可能长时间等待的业务调用设置合适预算;Watch 则是一条需要持续管理的观察流。

2.2 List、Watch 与 resourceVersion​

List 返回匹配条件的资源集合,集合的 resourceVersion 标识这次观察位置。随后以相应版本启动 Watch,可以继续接收新增、修改和删除等通知,避免把“列出已有对象”和“观察后续变化”当成两个没有关联的动作。

Watch 观察的是 API 资源对象变化,不是 YAML 文件内容的逐字符修改,也不是直接订阅某个字段的补丁流。普通资源 Watch 的事件携带事件类型和相应对象表示;具体返回内容受请求形式影响。

resourceVersion 是客户端应视为不透明的版本字符串。它可用于续接观察和写入时的并发检查,但不应当被解释为业务时间,也不能凭数值差推算漏了几个事件。服务端不能提供请求版本之后的历史时,需要重新获取集合并恢复观察。

网络中断、版本过期和重连都是正常设计范围。因此控制器要能从当前资源重新计算结果;需要审计每一次中间状态的业务,不应只依赖 Informer 通知。

3. Informer 数据流​

Informer 将资源集合同步到本地缓存,并向注册的处理器发送变化通知。SharedInformer 让多个使用者共享这条观察链,减少重复 List/Watch 和重复缓存。

3.1 Reflector 与 DeltaFIFO​

Reflector 管理 List/Watch 的读取与恢复,把观察到的对象变化送入后续存储。DeltaFIFO 是这条链中的变化缓冲:按对象键组织等待处理的变化,例如 Added、Updated、Deleted。

同一键可以累积多个 delta,队列中则不为每次变化重复放置同一个键。它不是业务任务队列,也不能笼统理解成“只保存最后一个对象”;消费端需要知道对象经历了哪些待处理变化,才能正确更新缓存和发出通知。

重新 List 后,DeltaFIFO 还会结合已知对象识别本地原先存在、当前集合已经没有的对象。由于可能错过直接删除通知,删除事件有时用 DeletedFinalStateUnknown 包装最后已知对象,俗称 tombstone。处理删除时应使用支持该包装的取键函数,而不是直接断言它一定是 Pod 或 ConfigMap。

3.2 缓存与处理器​

Indexer 在缓存存储之外增加索引,例如按 namespace 查找对象;Lister 则在这些缓存之上提供读取接口。缓存读取减少 API 请求,但不会自动变成强一致读取。

Informer 先更新缓存和索引,再通知处理器。处理器收到某次更新时,缓存可能已经推进到同一对象的更晚版本。因此事件通常用于提示“这个键需要重新检查”,worker 再按键读取当前缓存,而不把旧回调对象长期当作权威状态。

同一注册处理器的通知顺序被保留,不同处理器之间没有统一的回调完成顺序。本文版本为每个处理器安排通知传递与执行过程,并使用可增长的待处理缓冲。一个处理器暂时阻塞,不意味着其他处理器立即同步阻塞;但持续积压会占用内存并影响整个进程,不能据此认为慢处理器没有代价。

共享缓存返回的对象应当只读。需要修改对象时先 DeepCopy,再按 API 的并发更新规则提交;直接改缓存中的 map 或字段,会污染其他使用者观察到的状态,还可能产生数据竞争。

3.3 初始同步与 resync​

启动后应先等待需要的 Informer 完成初始缓存同步,再开始依赖缓存的业务。WaitForCacheSync 成功说明初始同步条件已经满足,不证明网络以后永不失败,也不证明所有处理器都完成了初始通知或所有业务已经调谐成功。

resync 是把本地已有对象再次交给处理器检查的机制。它不等于重新向 API Server List 全部数据,更不等于强制刷新成最新状态。断线后的重新 List 是观察恢复的一部分,与定期重新处理本地对象要分开解释。

4. 工作队列与调谐​

4.1 键、去重与顺序​

调谐(reconcile)是读取当前状态,与业务希望达到的状态比较,再执行必要动作。工作队列通常存放 namespace/name 这样的键,表示这个对象需要重新检查。

本文版本的普通队列维护待处理、正在处理和重新变脏等状态:

  1. 同一个键重复 Add,尚未开始处理时只保留一次待处理机会。
  2. Get 把键移到正在处理的集合中;此时再 Add 同一键,不会把它立即交给另一个 worker。
  3. 当前 worker 调用 Done 后,如果处理期间这个键又变脏,就再次放入待处理队列。

这个机制避免同一队列内同一键并行调谐,但不同键可以并行,完成时间不保证按入队顺序排列。延迟重试、事件合并和多 worker 也意味着不能用工作队列还原每个事件的原始时间线。

不同进程有各自的队列,不因此获得跨进程锁。多个控制器副本是否允许同时执行、如何选主、外部写入如何幂等,必须另外设计。相同 namespace/name 删除后再创建还可能对应不同 UID,需要区分“同名资源”和“同一个对象实例”。

4.2 重试与完成​

Get 取得的每个条目都要调用 Done,即使处理失败。Done 结束本次占用,Forget 清除该键的限速重试历史;这两个动作不能互相代替。

失败时可用 AddRateLimited 安排带退避的重试,成功后调用 Forget。还应区分暂时故障与不可恢复错误,设置可观察的失败处理和重试策略。永久丢弃一个键以后,如果没有新事件、定期检查或其他触发,它不会凭空重新出现。

调谐应尽量幂等。例如设置某个受控对象达到期望配置,比每收到一次通知就无条件追加一个对象更容易重复执行。即使队列不同时交付同一键,进程重启、重试和结果不确定的远程调用仍可能使业务执行多次。

5. ConfigMap 观察示例​

下面创建一个 namespace 内的 ConfigMap Informer。调用方提供已配置的类型化客户端和控制器生命周期 context;List/Watch 保留 Informer 传入的选项,不丢弃 resourceVersion 等参数。

informer.go
package controller

import (
"context"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
coreclient "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/cache"
)

func NewInformer(ctx context.Context, client coreclient.CoreV1Interface, namespace string) cache.SharedIndexInformer {
resource := client.ConfigMaps(namespace)
source := &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
return resource.List(ctx, options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return resource.Watch(ctx, options)
},
}
return cache.NewSharedIndexInformer(source, &corev1.ConfigMap{}, 0,
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
}

第三个参数零关闭本示例的周期 resync,不会关闭 Watch。调用方需要在结束时取消传入的 ctx,让在途 List/Watch 请求也能退出;不要为每次请求重新创建 Informer。

下面的 Controller 把事件转换成键,读取当前缓存后交给 reconcile。current == nil 表示当前缓存中没有这个键,非 nil 时提供独立副本。示例本身不修改集群资源;具体业务需要决定如何处理缺失、同名重建和写入冲突。

controller.go
package controller

import (
"context"
"errors"
"fmt"
"sync"

corev1 "k8s.io/api/core/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
)

type Reconcile func(context.Context, string, *corev1.ConfigMap) error

type Controller struct {
informer cache.SharedIndexInformer
queue workqueue.RateLimitingInterface
reconcile Reconcile
}

func NewController(informer cache.SharedIndexInformer, reconcile Reconcile) (*Controller, error) {
if informer == nil || reconcile == nil {
return nil, errors.New("informer and reconcile are required")
}
c := &Controller{informer: informer, reconcile: reconcile,
queue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())}
enqueue := func(obj any) {
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
utilruntime.HandleError(err)
return
}
c.queue.Add(key)
}
_, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: enqueue,
UpdateFunc: func(old, current any) { enqueue(current) },
DeleteFunc: enqueue,
})
if err != nil {
c.queue.ShutDown()
return nil, err
}
return c, nil
}

func (c *Controller) process(ctx context.Context) bool {
item, shutdown := c.queue.Get()
if shutdown {
return false
}
defer c.queue.Done(item)
if ctx.Err() != nil {
c.queue.Forget(item)
return false
}
key := item.(string) // 本控制器只把字符串键放入队列。
obj, exists, err := c.informer.GetIndexer().GetByKey(key)
if err == nil {
var current *corev1.ConfigMap
if exists {
current = obj.(*corev1.ConfigMap).DeepCopy()
}
err = c.reconcile(ctx, key, current)
}
if err == nil {
c.queue.Forget(key)
} else if ctx.Err() == nil && c.queue.NumRequeues(key) < 3 {
c.queue.AddRateLimited(key)
} else {
c.queue.Forget(key)
utilruntime.HandleError(fmt.Errorf("reconcile %s: %w", key, err))
}
return true
}

func (c *Controller) Run(ctx context.Context, workers int) error {
if workers < 1 {
c.queue.ShutDown()
return errors.New("workers must be positive")
}
var group sync.WaitGroup
group.Add(1)
go func() {
defer group.Done()
c.informer.Run(ctx.Done())
}()
if !cache.WaitForCacheSync(ctx.Done(), c.informer.HasSynced) {
c.queue.ShutDown()
group.Wait()
return errors.New("initial cache sync interrupted")
}
for i := 0; i < workers; i++ {
group.Add(1)
go func() {
defer group.Done()
for c.process(ctx) {
}
}()
}
<-ctx.Done()
c.queue.ShutDown()
group.Wait()
return nil
}

Run 在初始缓存同步后启动 worker,结束时关闭队列以唤醒等待者,再等待本次启动的 goroutine 退出。它只运行一次;reconcile 必须支持多个键并行,并响应 context,否则等待退出仍可能卡住。构造后应启动控制器,不长期闲置已创建的限速队列。

每轮失败最多安排三次限速重试,成功后重试计数清零;耗尽后记录错误,等待其他触发。这个上限用于展示生命周期,不是适合所有故障的生产策略。关闭路径不承诺处理完全部待办,业务如需排空,应另外定义退出预算与停止接收新任务的顺序。

删除事件可能在 worker 运行前又被同名创建覆盖,因此 worker 读取到的是当前缓存状态。若要清理外部资源,不能只凭一次缓存缺失就删除:还需确认对象身份、所有权和业务协议,必要时结合 API 读取、finalizer 等机制。缓存同步和重复调谐无法替代这些约束。

6. 控制器与准入 Webhook​

控制器持续观察资源并调谐,使实际状态向期望状态靠拢。例如 Deployment 控制器根据副本需求维护下层资源,实际执行可能晚于用户提交配置。

准入 Webhook 在 API 请求的准入阶段参与校验或变更。校验型 Webhook 可以拒绝不符合约束的对象,变更型 Webhook 可以按规则调整对象;它不是一个持续观察资源状态的后台控制循环。

需要在对象接受前检查约束时考虑准入机制,需要持续修复偏差、创建下层资源或处理外部依赖时考虑控制器。Webhook 的延迟和可用性会影响 API 请求,不适合在里面等待一个长时间才能完成的调谐过程。控制器的执行又可能滞后,不能把它当成对每次 API 请求的同步拦截。

7 · 面试题卡片​

本页题目与参考回答

7.1 client-go 应怎样访问资源并管理客户端?​

考察重点:API 入口、客户端层次、配置与权限、复用和 context。

相关内容:客户端与权限。

参考回答

通过 API Server 访问资源,按对象类型选择类型化、动态或底层 REST 客户端。 使用明确的配置与身份,检查证书信任和资源权限,不直接绕过 API Server 读写 etcd。

客户端在服务生命周期内复用,请求传递 context;共享缓存对象只读,修改前复制。控制器还应检查缓存初始同步、重试和关闭,而不是只证明一次 Get 能成功。

7.2 List/Watch 观察的是什么,resourceVersion 有什么作用?​

考察重点:资源集合与后续变化、API Server、版本位置和观察恢复。

相关内容:List、Watch 与 resourceVersion。

参考回答

观察的是 API 资源对象,入口是 API Server。 List 给出集合及观察位置,Watch 从相应版本继续读取变化,不是监听 YAML 文件或直接监听 etcd。

resourceVersion 作为不透明版本字符串用于续接和并发检查,不当时间戳或整数运算。历史不可用时重新 List 并恢复观察,业务依靠当前状态重新调谐,而不假设必定见到每一个中间事件。

7.3 Informer 从观察变化到业务执行经过哪些步骤?​

考察重点:Reflector、变化缓冲、缓存与索引、通知和业务队列。

相关内容:Informer 数据流、初始同步与 resync。

参考回答

Reflector 通过 List/Watch 取得变化,经 DeltaFIFO 交给缓存更新流程,再通知处理器。 处理器把资源键入队,worker 从缓存读取当前状态并调谐。

先更新缓存再通知,不代表 worker 看到的一定是触发通知时的版本。启动时等待初始同步,运行中仍处理缓存滞后和断线;resync 是重新处理本地对象,不是强制重新 List。

7.4 为什么需要 DeltaFIFO,它和业务工作队列有什么区别?​

考察重点:资源变化与业务任务、同键累积和删除恢复。

相关内容:Reflector 与 DeltaFIFO、键、去重与顺序。

参考回答

DeltaFIFO 缓冲带变化类型的对象,供 Informer 更新缓存和发送通知;业务队列通常保存需要调谐的资源键。 同一个键的多个 delta 可以累积,不能概括为始终只留最后一个事件。

重新 List 时需要识别已知对象的缺失,删除通知也可能带 tombstone。业务队列则关注去重、正在处理的键和失败重试,消费时通常重新读取当前缓存。

7.5 一个 Informer 的处理器阻塞,会怎样影响其他处理器?​

考察重点:单处理器顺序、处理器间隔离和通知积压。

相关内容:缓存与处理器。

参考回答

不同注册处理器有各自的通知执行过程,某个处理器短暂阻塞不等于其他处理器立即一起阻塞。 同一处理器自己的后续通知仍要等前面的回调返回。

本文版本的待处理通知缓冲可以增长,持续阻塞会消耗内存,最终影响进程。回调应尽快取键入队,把慢操作交给有并发控制和重试策略的 worker。

7.6 工作队列能保证怎样的顺序和互斥?​

考察重点:重复键合并、processing 与 dirty、并行及跨进程边界。

相关内容:键、去重与顺序。

参考回答

正确配对 Get 与 Done 时,同一队列内同一键不会同时交给多个 worker。 处理期间再次 Add 会记为需要重做,Done 后再入队;未处理时的重复 Add 会合并。

不同键可以并行,业务完成顺序不等于入队顺序,也不能还原全部原始事件。多个控制器进程的队列彼此独立,外部副作用仍需幂等和并发控制。

7.7 Done、Forget 和 AddRateLimited 各自解决什么问题?​

考察重点:本次占用、重试历史、失败调度与退出。

相关内容:重试与完成、ConfigMap 观察示例。

参考回答

Done 结束本次 Get 的处理占用,Forget 清除该键的限速历史,AddRateLimited 安排退避重试。 失败也要 Done;成功通常 Forget,不能用其中一个替代另一个。

重试有预算和错误分类,丢弃后还要有可观察结果。队列关闭可以唤醒 Get,但不能强制结束已经进入、又不响应取消的业务函数。

7.8 控制器和准入 Webhook 分别适合解决什么问题?​

考察重点:持续调谐、准入阶段、延迟与可用性边界。

相关内容:控制器与准入 Webhook。

参考回答

控制器持续观察并修复状态偏差,准入 Webhook 在请求接受过程中校验或修改对象。 前者适合维护期望状态,后者适合提交时必须执行的约束或变更规则。

Webhook 影响 API 请求的延迟与可用性,不应等待长期调谐;控制器可能滞后,也不能代替同步拒绝不合法请求。两者可以配合,但职责和失败策略应分别定义。

8. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明