



























当我们刚开始学习 Kubernetes Operator 开发时,往往会被 client-go 里一大堆概念砸得晕头转向:Clientset、DynamicClient、RESTClient 有什么区别?SharedInformer、SharedIndexInformer、SharedInformerFactory 三个" Informatin"到底是什么关系?为什么 Reflector 要和 DeltaFIFO 配合使用?更重要的是,client-go 的版本号和 Kubernetes 的版本号到底怎么对应?用错了版本会有什么后果?这些问题如果不搞清楚,后面写代码的时候就会处处踩坑。这篇文章我们就从源码出发,把这些问题全部讲清楚。
Kubernetes client-go Operator v1.36.1 Go 1.26
🔓 学习重点提示 — 建议先通读全文,再重点回顾标注内容
★ 重点掌握(必须)
• 版本对应关系:client-go v0.32.x 对应 Kubernetes 1.32.x,这是外部项目引用的基础
• 三种客户端差异:Clientset(typed、强类型)、DynamicClient(unstructured、动态类型)、RESTClient(底层 HTTP)
• Informer 体系:SharedInformer → SharedIndexInformer → SharedInformerFactory 的继承关系和职责分工
• 数据流:Reflector → DeltaFIFO → Indexer → 事件处理器 的完整数据流动路径
☆ 次重点(了解即可)
• CachedDiscoveryClient 和 MemCacheClient 的缓存机制
• WorkQueue 的限速和重试机制
很多初学者被 client-go 的版本号搞糊涂了——Kubernetes 都发布到 1.36 了,为什么 client-go 的版本号才到 0.32?它们之间到底是什么关系?
这个规则的背后有一个明确的设计决策:Kubernetes 官方保证"主版本号 - 4"的 client-go 版本能够兼容当前 Kubernetes 版本。也就是说,Kubernetes 1.36 对应 client-go 0.32,Kubernetes 1.35 对应 client-go 0.31,以此类推。为什么要减 4?因为 Kubernetes 采用"主版本号减 4"的策略来给外部依赖留出兼容空间。
我们来从源码中验证这个规则。先看 Kubernetes 1.36.1 的 go.mod 文件:
// staging/src/k8s.io/client-go/go.mod
module k8s.io/client-go
go 1.26.0
require (
k8s.io/api v0.0.0
k8s.io/apimachinery v0.0.0
k8s.io/klog/v2 v2.140.0
...
)
replace (
k8s.io/api => ../api
k8s.io/apimachinery => ../apimachinery
k8s.io/streaming => ../streaming
)
在 Kubernetes 源码仓库内部,client-go、api、apimachinery 这些 staging 包都通过 replace 指令指向本地目录,所以版本号显示为 v0.0.0。但当我们把这些包单独发布(publish)到 GitHub 时,版本号就会遵循 "Kubernetes 主版本号 - 4" 的规则。
| Kubernetes 版本 | client-go 版本 | 适用场景 |
|---|---|---|
| v1.36 | v0.32 | 最新 Kubernetes Operator 开发 |
| v1.35 | v0.31 | 生产环境推荐 |
| v1.29 | v0.29 | 稳定版本 |
| v1.28 | v0.28 | 长期维护版本 |
| v1.26 | v0.26 | 经典版本 |
🌟 实用技巧
在 go.mod 中使用 client-go 时,推荐写法是:
require k8s.io/client-go v0.32.0
注意使用精确版本号(v0.32.0),而不是 v0.32.x 或 v0.32/latest,这样能确保每次构建的可重复性。
如果你的 Operator 使用了 client-go v0.28,但连接的是 Kubernetes 1.35 集群,会发生什么?主要有三类问题:
反过来,如果用 client-go v0.32 连接 Kubernetes 1.29 集群,一般情况下是兼容的——client-go 通常能支持"更旧"的 Kubernetes 版本。但如果用到新版本独有的 API 字段,还是会报错。所以最安全的做法是让 client-go 版本和集群版本保持一致,或者让 client-go 版本略高于集群版本。
在 Kubernetes 源码仓库中,client-go 位于 staging/src/k8s.io/client-go 目录。理解这个目录结构,是后续理解各组件关系的基础。
staging/src/k8s.io/client-go/ ├── kubernetes/ # typed 客户端集合(Clientset) │ ├── typed/ # 按 API 组和版本分类的强类型客户端 │ │ ├── core/v1/ # Pod、Service、Namespace 等核心资源 │ │ ├── apps/v1/ # Deployment、StatefulSet、DaemonSet │ │ ├── networking/v1/ # Ingress、NetworkPolicy │ │ └── ... # 其他 API 组 │ └── clientset.go # Interface 接口定义,组合所有 typed 客户端 ├── dynamic/ # DynamicClient(动态客户端) │ ├── simple.go # DynamicClient 主实现 │ └── dynamicinformer/ # DynamicInformer 相关 ├── rest/ # RESTClient(底层 REST 客户端) │ ├── config.go # Config 配置结构体 │ ├── client.go # RESTClient 实现 │ └── request.go # 请求构建器 ├── discovery/ # DiscoveryClient(服务发现) │ ├── client.go # DiscoveryClient 主实现 │ └── cached/ # 缓存实现(磁盘缓存、内存缓存) ├── informers/ # SharedInformerFactory + 各种资源 Informer │ ├── factory.go # SharedInformerFactory 主实现 │ ├── generic.go # GenericInformer 通用接口 │ └── internalinterfaces/ # 工厂内部接口 ├── listers/ # Lister(本地缓存读取器) │ ├── core/v1/ # 按资源类型分类 │ ├── apps/v1/ │ └── ... ├── tools/ # 核心工具:Informer、Reflector、Cache 等 │ ├── cache/ # 缓存层核心实现 │ │ ├── shared_informer.go # SharedInformer 接口 │ │ ├── shared_index_informer.go # SharedIndexInformer │ │ ├── reflector.go # Reflector:ListAndWatch │ │ ├── delta_fifo.go # DeltaFIFO:增量队列 │ │ ├── fifo.go # FIFO 队列 │ │ ├── index.go # Indexer + ThreadSafeStore │ │ └── store.go # Store 接口 │ ├── clientcmd/ # kubeconfig 解析 │ └── record/ # 事件记录器 ├── util/ # 工具函数 │ ├── workqueue/ # 工作队列(限速、重试) │ ├── apply/ # Server-Side Apply │ └── flowcontrol/ # 限速器 ├── scale/ # Scale 子资源客户端 ├── openapi/ # OpenAPI 客户端 ├── plugin/ # 插件(client auth exec 等) └── examples/ # 示例代码
这个目录结构本身就透露了很多信息。比如,kubernetes/typed 目录下为什么有那么多子目录?因为 Kubernetes 的 API 就是按 API 组(Group)和版本(Version)组织的。apps/v1 里面有 Deployment、StatefulSet、DaemonSet,它们都属于 apps 这个 API 组,版本是 v1。而 tools/cache 目录下集中了所有缓存相关的核心实现,这正是 Informer 机制的心脏。
这是最容易混淆的知识点。client-go 提供了三种客户端,它们的抽象层次从高到低分别是:Clientset(最高)、DynamicClient(中等)、RESTClient(最低)。理解它们之间的差异,是选择正确客户端的基础。
Clientset 是抽象层次最高的客户端,它的每一个方法都对应一个具体的 Kubernetes 资源类型。我们先看它的 Interface 接口定义:
// staging/src/k8s.io/client-go/kubernetes/clientset.go
// kubernetes.Interface 是所有 Kubernetes 客户端的顶层接口
type Interface interface {
Discovery() discovery.DiscoveryInterface
AdmissionregistrationV1() admissionregistrationv1.AdmissionregistrationV1Interface
AppsV1() appsv1.AppsV1Interface
AutoscalingV1() autoscalingv1.AutoscalingV1Interface
CoreV1() corev1.CoreV1Interface
NetworkingV1() networkingv1.NetworkingV1Interface
PolicyV1() policyv1.PolicyV1Interface
RbacV1() rbacv1.RbacV1Interface
// ... 其他 API 组
}
这个接口的每个方法都返回一个对应 API 组的客户端接口。比如 CoreV1() 返回 CoreV1Interface,里面包含了 Pods()、Services()、Namespaces() 等方法。以 Pods() 为例,它返回 PodInterface,PodInterface 里定义了 Create、Update、Delete、Get、List、Watch 等方法——每一个方法签名都接收或返回强类型的 Pod 结构体。
// staging/src/k8s.io/client-go/kubernetes/typed/core/v1/interface.go
// CoreV1Interface 是 core/v1 API 组的客户端接口
type CoreV1Interface interface {
RESTClient() rest.Interface
Pods(namespace string) PodInterface
PodTemplates(namespace string) PodTemplateInterface
Services(namespace string) ServiceInterface
Namespaces() NamespaceInterface
Nodes() NodeInterface
Secrets(namespace string) SecretInterface
ConfigMaps(namespace string) ConfigMapInterface
// ... 更多资源
}
使用 Clientset 的典型场景是:你的代码需要操作已知的、固定的 Kubernetes 资源类型,比如 Deployment、Pod、Service。在这种情况下,Clientset 提供了最好的类型安全——编译器会在编译期就检查你是否传入了正确的参数、是否正确处理了返回值。
DynamicClient 是为操作 CRD(Custom Resource Definition,自定义资源)设计的。它的核心特点是"动态"——不需要在编译期知道你要操作的是什么资源类型,运行期通过 GVR(GroupVersionResource)来指定。
// staging/src/k8s.io/client-go/dynamic/simple.go
// DynamicClient 内部持有的是一个通用的 REST Interface
type DynamicClient struct {
client rest.Interface
}
var _ Interface = &DynamicClient{}
// Resource 方法接收一个 GroupVersionResource,返回可操作该资源的客户端
func (c *DynamicClient) Resource(resource schema.GroupVersionResource) NamespaceableResourceInterface {
return &dynamicResourceClient{client: c, resource: resource}
}
// dynamicResourceClient 持有了资源的 GVR 信息
type dynamicResourceClient struct {
client *DynamicClient
namespace string
resource schema.GroupVersionResource
}
注意看,dynamicResourceClient 里的 Create、Update、Delete 等方法,接收和返回的类型都是 unstructured.Unstructured——这是一个通用的 map[string]interface{} 结构,所有的 K8s 对象都可以用它来表示。这就是 DynamicClient 的核心设计:用通用的数据结构,承载任意的资源类型。
RESTClient 是最底层的客户端,它本质上就是一个 HTTP 客户端,提供了 Get()、Post()、Put()、Patch()、Delete() 等方法,构建和发送 HTTP 请求到 API Server。
// staging/src/k8s.io/client-go/rest/config.go
// Config 保存了连接 API Server 所需的所有配置信息
type Config struct {
Host string // API Server 地址,如 "https://192.168.1.10:6443"
APIPath string // API 路径,如 "/api" 或 "/apis/apps/v1"
ContentConfig ContentConfig // 内容配置(序列化相关)
Username string // Basic Auth 用户名
Password string // Basic Auth 密码
BearerToken string // Bearer Token
TLSClientConfig // TLS 配置
UserAgent string // User-Agent 头
// ... 更多字段
}
RESTClient 的使用方式非常底层,你需要手动构建 URL、设置参数、处理响应。通常不推荐直接使用 RESTClient,除非你有非常特殊的需求,比如需要精确控制 HTTP 请求的每个细节。
| 对比维度 | Clientset | DynamicClient | RESTClient |
|---|---|---|---|
| 类型安全 | ★★★★★ 编译期检查 | ★★ 运行期检查 | ★ 无类型检查 |
| 适用场景 | 内置资源(Pod/Svc/Deploy) | CRD / 未知类型 | 底层 HTTP 控制 |
| 使用难度 | 简单 | 中等 | 复杂 |
| 代码提示 | 完整(IDE 自动补全) | 有限(需查文档) | 无(手写参数) |
| 典型使用 | Operator 开发 | 通用控制器 | kubectl 内部 |
💡 提示
实际开发中,最常见的组合是:Clientset 用于操作内置资源(Deployment、Pod),DynamicClient 用于操作 CRD 自定义资源。RESTClient 基本上是 client-go 内部的实现细节,开发者很少直接用到。
Informer 是 Kubernetes 控制器开发中最核心的概念。理解它的体系结构,是写出高效控制器的关键。很多初学者分不清 SharedInformer、SharedIndexInformer、SharedInformerFactory 之间的关系,这一节我们从源码出发,把它们讲清楚。
SharedInformer 是整个体系的顶层接口,它定义了"事件监听器"的基本行为。先看接口定义:
// staging/src/k8s.io/client-go/tools/cache/shared_informer.go(行 144-220)
// SharedInformer 提供最终一致的方式来追踪某个资源集合的变化
// 关键保证:
// 1. 本地缓存最终会与权威状态一致
// 2. 缓存中对象的 key 格式:namespace/name(命名空间资源)或 name(集群级资源)
// 3. 对同一 informer 添加的多个事件处理器,共享同一份缓存
type SharedInformer interface {
// AddEventHandler 添加一个事件处理器,接收 Added/Updated/Deleted 事件
// 返回一个 registration handle,可用于移除处理器
AddEventHandler(handler ResourceEventHandler) (ResourceEventHandlerRegistration, error)
// AddEventHandlerWithResyncPeriod 添加带重同步周期的事件处理器
// resyncPeriod=0 表示不重同步
AddEventHandlerWithResyncPeriod(handler ResourceEventHandler, resyncPeriod time.Duration) (ResourceEventHandlerRegistration, error)
// GetStore 返回本地缓存(Store 接口)
GetStore() Store
// Run 启动 informer,stopCh 关闭时停止
Run(stopCh
SharedInformer 的核心设计理念是"共享"(Shared)。在一个复杂的控制器中,可能有多个组件都需要监听同一个资源的变化。如果没有共享机制,每个组件都会独立地向 API Server 发起 List + Watch 请求,造成大量重复流量。SharedInformer 确保所有的事件处理器共享同一份缓存、同一份到 API Server 的 Watch 连接。
SharedIndexInformer 在 SharedInformer 的基础上增加了索引能力。我们看它的接口定义:
// staging/src/k8s.io/client-go/tools/cache/shared_informer.go(行 284-288)
// SharedIndexInformer 在 SharedInformer 基础上增加了索引能力
// 索引可以加速按标签或字段查询对象的速度
type SharedIndexInformer interface {
SharedInformer
// AddIndexers 添加自定义索引函数
AddIndexers(indexers Indexers) error
// GetIndexer 返回索引器,用于按标签或字段快速查找对象
GetIndexer() Indexer
}
两者的继承关系非常清晰:SharedIndexInformer extends SharedInformer。也就是说,所有的 SharedIndexInformer 都是 SharedInformer,但反过来不一定成立——普通的 SharedInformer 没有索引能力。
SharedInformerFactory 是工厂类,负责创建和管理多个 SharedIndexInformer 实例。它的设计目标是:在同一个进程内,同一种资源类型只创建一个 Informer 实例,所有需要监听该资源的组件都使用这个共享的 Informer。
// staging/src/k8s.io/client-go/informers/factory.go(行 125-140)
// SharedInformerFactory 主实现
type sharedInformerFactory struct {
client clientset.Interface // Kubernetes 客户端
namespace string // 监控的命名空间(v1.NamespaceAll 表示所有)
defaultResync time.Duration // 默认重同步周期
informers map[reflect.Type]cache.SharedIndexInformer // 按类型缓存的 informer
startedInformers map[reflect.Type]bool // 已启动的 informer
customResync map[reflect.Type]time.Duration // 自定义重同步周期
}
// 工厂的核心方法:根据对象类型获取或创建 informer
func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informerType := reflect.TypeOf(obj)
informer, exists := f.informers[informerType]
if exists {
return informer // 命中缓存,直接返回
}
// 不存在则创建新的
resyncPeriod, exists := f.customResync[informerType]
if !exists {
resyncPeriod = f.defaultResync
}
informer = newFunc(f.client, resyncPeriod)
f.informers[informerType] = informer
return informer
}
SharedInformerFactory 的 InformerFor 方法就是"单例模式"的实现。第一次请求某个资源的 Informer 时,它会创建新实例并缓存起来;后续请求直接返回缓存的实例。这就保证了同一进程内同一资源只有一个 Informer。
┌─────────────────────────────────────────────────────────────┐
│ SharedInformerFactory │
│ 角色:工厂模式,生产和管理 SharedIndexInformer 实例 │
│ 关键方法: │
│ - InformerFor(obj, newFunc) -> SharedIndexInformer │
│ - Start(stopCh) -> 启动所有 informers │
│ - WaitForCacheSync(stopCh) -> 等待缓存同步完成 │
└───────────────────────┬─────────────────────────────────────┘
│ 创建/获取
▼
┌─────────────────────────────────────────────────────────────┐
│ SharedIndexInformer │
│ 角色:带索引的共享事件监听器 │
│ 继承自:SharedInformer │
│ 扩展能力: │
│ + AddIndexers() 添加自定义索引 │
│ + GetIndexer() 获取索引器 │
│ 内部组件: │
│ - Indexer(本地缓存,存储所有对象) │
│ - Controller(控制循环,驱动数据流动) │
│ - Processor(事件分发,通知所有事件处理器) │
└───────────────────────┬─────────────────────────────────────┘
│ 继承
▼
┌─────────────────────────────────────────────────────────────┐
│ SharedInformer │
│ 角色:共享事件监听器的基础接口 │
│ 核心职责: │
│ - 管理事件处理器(AddEventHandler) │
│ - 提供本地缓存(GetStore) │
│ - 控制生命周期(Run/HasSynced) │
└─────────────────────────────────────────────────────────────┘
Informer 背后的四大核心组件:Reflector 负责从 API Server 拉取数据,DeltaFIFO 负责增量事件的排序和管理,Indexer 负责本地缓存的存储和索引,WorkQueue 负责接收事件后的去重和限速处理。这四个组件串起来,就构成了完整的 Informer 数据流。
Reflector 是整个 Informer 体系的数据源。它封装了"先 List 再 Watch"的逻辑,持续不断地从 API Server 获取最新数据。我们看它的核心结构:
// staging/src/k8s.io/client-go/tools/cache/reflector.go(行 105-171)
// Reflector 监听指定资源的所有变化,并反映到给定的 Store 中
type Reflector struct {
logger klog.Logger
name string // 用于日志标识,如 "deployment-controller"
// expectedType 是期望放入 store 的对象类型
expectedType reflect.Type
// store 是目标 store,通常是 DeltaFIFO
store ReflectorStore
// listerWatcher 负责执行 List 和 Watch 操作
listerWatcher ListerWatcherWithContext
// resyncPeriod 是重同步周期
resyncPeriod time.Duration
// lastSyncResourceVersion 是最后一次同步的资源版本(用于 Watch)
lastSyncResourceVersion string
// useWatchList 是否使用 WatchList 流式拉取(更高效)
useWatchList bool
// paginatedResult 记录 List 是否支持分页
paginatedResult bool
}
Reflector 的核心方法是 ListAndWatch,它的工作流程是:
DeltaFIFO 是 Informer 体系中最独特的数据结构。它的名字里"FIFO"表示先进先出队列,"Delta"表示它存储的不是完整对象,而是对象的变化(增量)。
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go
// DeltaType 表示变化的类型
type DeltaType string
const (
Added DeltaType = "Added" // 对象被添加
Updated DeltaType = "Updated" // 对象被更新
Deleted DeltaType = "Deleted" // 对象被删除
Replaced DeltaType = "Replaced" // 全量替换(来自 Replace() 调用)
Sync DeltaType = "Sync" // 同步(历史原因,行为同 Replaced)
)
// Delta 包含一个变化类型和变化的对象
type Delta struct {
Type DeltaType // 变化的类型
Object interface{} // 对象本身或 DeletedFinalStateUnknown
}
// Deltas 是某个对象的多个变化的列表(按时间顺序)
type Deltas []Delta
// DeltaFIFO 维护了每个对象的 Deltas 列表
type DeltaFIFO struct {
lock sync.RWMutex
cond sync.Cond
items map[string]Deltas // key -> Deltas(该对象的所有变化)
queue []string // FIFO 顺序
knownObjects KeyListerGetter // 用于判断是否是新增
// ...
}
DeltaFIFO 的关键特性是"去重":同一个对象连续发生多次变化,只会保留最后一次。比如某个 Pod 在 1 秒内被更新了 10 次,DeltaFIFO 只会保留最后一次的 Updated 事件,之前 9 次会被丢弃。这样做的好处是避免事件处理器处理大量无意义的重复事件。
Indexer 是 SharedIndexInformer 的本地缓存层,负责存储所有已知对象,并提供索引查询能力。
Indexer 基于 ThreadSafeStore(线程安全存储)实现,内部维护了一个 key 到对象的 map,以及多个索引函数(Indexers)。通过索引,可以按标签选择器(label selector)或字段选择器(field selector)快速查找对象,而不需要遍历整个缓存。
WorkQueue 在 Informer 体系中扮演的是"事件处理器和 Informer 之间的缓冲层"。当 DeltaFIFO 中的事件被 Pop 出来之后,不会直接调用用户的处理器,而是先写入 WorkQueue。这样做的好处是:用户的处理器可以异步执行,且 WorkQueue 提供了限速、重试、去重的能力,防止事件处理过载。
现在我们来把所有的组件串起来,看一看一个资源变化从 API Server 到用户处理器,经历了怎样的完整数据流:
┌──────────────────────────────────────────────────────────────────┐
│ API Server │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ /api/v1 │ │ /apis/apps │ │ /apis/crd │ │
│ │ /pods │ │ /deployments│ │ /customs │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼─────────────────┼─────────────────┼────────────────────┘
│ │ │
│ List + Watch │ List + Watch │ List + Watch
│ (HTTP/REST) │ (HTTP/REST) │ (HTTP/REST)
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ SharedInformerFactory │
│ 负责创建和管理多个 SharedIndexInformer(同类型只创建一个) │
│ - SharedInformerFactory.InformerFor(obj, newFunc) │
│ - SharedInformerFactory.Start(stopCh) │
│ - SharedInformerFactory.WaitForCacheSync(stopCh) │
└───────────────────────────┬──────────────────────────────────────┘
│ 创建 SharedIndexInformer
▼
┌──────────────────────────────────────────────────────────────────┐
│ SharedIndexInformer │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Controller (Reflector) │ │
│ │ 核心职责: │ │
│ │ 1. List 全量拉取 -> 填充 DeltaFIFO │ │
│ │ 2. Watch 增量监听 -> 持续写入 DeltaFIFO │ │
│ │ 3. 处理 Watch 断开 -> backoff 重连 │ │
│ │ 4. 处理 ResourceVersion 过期 -> 重新 List │ │
│ └─────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ DeltaFIFO │ │
│ │ 存储对象的所有增量变化(Added/Updated/Deleted/Replaced) │ │
│ │ 关键特性: │ │
│ │ - 去重:同对象连续变化只保留最后一次 │ │
│ │ - 先进先出:按事件发生顺序处理 │ │
│ │ - KnownObjects:区分新增和已有对象 │ │
│ └─────────────────────────────┬────────────────────────────────┘ │
│ │ Pop() │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Indexer │ │
│ │ 本地缓存存储层(ThreadSafeStore + 索引函数) │ │
│ │ - Add/Update/Delete/Replace 操作缓存 │ │
│ │ - 按标签/字段索引查询对象 │ │
│ │ - SharedIndexInformer.GetIndexer() 获取 │ │
│ └─────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Processor │ │
│ │ 事件分发器: │ │
│ │ - 接收 DeltaFIFO 的每个事件 │ │
│ │ - 分发给所有注册的 ResourceEventHandler │ │
│ │ - 支持多个 handler 共享同一份缓存 │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│
│ 事件通知
▼
┌──────────────────────────────────────────────────────────────────┐
│ 用户的 ResourceEventHandler │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ OnAdd(obj) │ │ OnUpdate(o,n)│ │ OnDelete(obj) │ │
│ │ 资源新增时调用 │ │ 资源更新时调用 │ │ 资源删除时调用 │ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
└──────────┼─────────────────┼─────────────────┼────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ WorkQueue │
│ 限速工作队列(通常配合 Controller 使用): │
│ - 去重:同一对象的多次变化只入队一次 │
│ - 限速:控制处理速率,防止过载 │
│ - 重试:处理失败后自动重试 │
└──────────────────────────────────────────────────────────────────┘
整个数据流的核心逻辑是:Reflector 持续不断地从 API Server 拉取数据(先 List 再 Watch),每次有变化就写入 DeltaFIFO。DeltaFIFO 负责去重和排序,然后通过 Pop 操作把事件交给 Processor。Processor 更新 Indexer 中的缓存,并通知所有注册的 ResourceEventHandler。Handler 收到通知后,可以直接处理,也可以先把 key 写入 WorkQueue,异步处理。
▼ Q: client-go 一定要和 Kubernetes 源码一起用吗?
A: 不一定。client-go 作为独立的 Go 模块发布在 GitHub(github.com/kubernetes/client-go),你可以单独 import 使用。在 Kubernetes 源码仓库内部,staging/src/k8s.io/client-go 是源码目录,通过 go.work 工作区文件关联到其他 staging 包。外部项目引用时,直接 go get k8s.io/client-go@v0.32.0 即可。内部的 staging 目录主要用于 Kubernetes 自身的开发。
▼ Q: SharedInformer 和 SharedIndexInformer 到底用哪个?
A: 在实际开发中,SharedInformerFactory 返回的类型是 SharedIndexInformer(向上转型为 SharedInformer 使用)。你不需要纠结用哪个——只需要用 SharedInformerFactory 创建 informer,然后注册事件处理器。如果你的控制器需要按标签或字段快速查询对象,就调用 GetIndexer() 获取索引能力;如果不需要查询,直接用 GetStore() 获取缓存即可。
▼ Q: HasSynced() 返回 true 是什么意思?为什么 WaitForCacheSync 必须等它?
A: HasSynced() 返回 true,表示 Informer 已经完成了首次全量 LIST,本地缓存已经和 API Server 的当前状态一致了。在控制器启动时,必须先等所有 informer 的 HasSynced() 返回 true,才能开始 Reconcile 循环。否则,如果缓存还没同步完就开始处理业务逻辑,可能会读到不完整的数据,导致错误的决策。比如 DeploymentController 在启动时等待缓存同步,就是为了避免处理到启动前的历史状态。
▼ Q: DynamicClient 和 Clientset 的 Watch 方法都能监听变化,有什么区别?
A: 关键区别在于:Clientset 的 Watch 返回的是强类型的 Watch Interface(watch.Interface),事件是强类型的结构体(如 watch.Event{Type: watch.Added, Object: *v1.Pod});DynamicClient 的 Watch 返回的是 watch.Interface,但事件对象是 unstructured.Unstructured(map[string]interface{})。如果你需要操作 CRD 且不想生成代码,用 DynamicClient;如果你需要最好的类型安全和 IDE 代码提示,用 Clientset。
▼ Q: DeltaFIFO 的 Replaced 事件什么时候会产生?和 Updated 有什么区别?
A: Replaced 事件在 Reflector 重新 LIST 全量数据后产生。当你调用 DeltaFIFO 的 Replace() 方法(Reflector 会调用)时,会产生 Replaced 事件。相比之下,Updated 来自 Watch 流中的修改事件。两者的语义不同:Updated 是增量变化,Replaced 是全量替换。在处理事件时,如果你的 handler 需要区分"对象被修改"和"对象被全量替换",可以通过 Delta.Type 来判断。在新版 client-go 中,Replace 默认产生 Replaced 事件。
▼ Q: DiscoveryClient 是做什么用的?和 Clientset 有什么关系?
A: DiscoveryClient 用于发现集群支持的 API 资源——它会调用 API Server 的 /apis 和 /api 端点,返回集群中安装了哪些 API 组、哪些版本、哪些资源类型。这在写通用工具时特别有用,比如 kubectl 就是用 DiscoveryClient 来发现"kubectl get xxx" 能支持哪些资源类型。DiscoveryClient 和 Clientset 是两个独立的功能:Clientset 用于操作资源,DiscoveryClient 用于查询"集群支持哪些资源"。
本篇文章我们从版本对应关系出发,深入 client-go 的源代码结构,详细讲解了三种客户端的差异、Informer 体系的组件关系、以及核心数据结构的工作原理。理解这些内容,是后续开发 Kubernetes Operator 和控制器的基础。
本文学到的要点:
Kubernetes 编程 / Operator 专题【左扬精讲】—— client-go 源码全解 · 来源:Kubernetes 1.36.1 源码分析
相关阅读:
• kubernetes/client-go GitHub 仓库
• client-go tools/cache 源码目录
• Kubernetes Informer 设计提案
• sample-controller 示例项目
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。