Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Controller 开发模式完整实战
Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Controller 开发模式完整实战
当我们理解了 Informer、WorkQueue 的工作原理之后,最重要的一步就是把它们串联起来,写出一个真正能跑的生产级 Controller。
这一篇文章,我们从头到尾手把手实现一个完整的 Kubernetes Controller,包括:如何组织代码结构、如何处理 Add/Update/Delete 事件、如何实现 Reconcile 逻辑、如何优雅关闭。看完这篇,你就能从"看懂源码"跨越到"自己写 Controller"了。
Kubernetes Controller Operator Reconcile v1.36.1 🔓 学习重点提示 — 建议先通读全文,再重点回顾标注内容
★ 重点掌握(必须)
• Controller 标准结构:Informer → WorkQueue → Reconcile 的完整闭环
• ResourceEventHandler 的三种写法:Funcs、FilteringResourceEventHandler、自定义
• Reconcile 模式:比较期望状态和实际状态,做必要的修正
☆ 次重点(了解即可)
• 多 Worker 并发处理
一、先看全局:Controller 的标准架构
在开始写代码之前,我们先搞清楚 Controller 的整体架构。Kubernetes 的 Controller 遵循一个标准的"观察-决策-执行"模式:
┌──────────────────────────────────────────────────────────────────┐ │ Controller 标准架构 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Informer │ ───→ │ WorkQueue │ ───→ │ Reconcile │ │ │ │ (观察) │ 事件 │ (去重/限速)│ 取任务 │ (执行) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ ↑ │ │ │ │ ↓ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ └──────────── │ APIServer │ ←── │ 更新资源 │ │ │ │ └─────────────┘ │ │ │ │ │ ↓ │ │ 获取最新状态 │ └──────────────────────────────────────────────────────────────────┘
这个架构的核心逻辑是:
- 1观察(Informer):通过 Informer 监听集群资源的变化,当资源发生变化时收到事件
- 2去重(WorkQueue):将事件中的资源 key 放入 WorkQueue,去除重复事件,避免并发问题
- 3执行(Reconcile):从 WorkQueue 中取出任务,比较期望状态和实际状态,做必要的修正
- 4反馈:Reconcile 执行后,Controller 会主动更新资源状态,这个状态变化又会触发 Informer,形成闭环
二、Controller 的核心数据结构
我们先定义 Controller 的核心数据结构。所有的状态都存在这个结构体里:
// 示例:自定义 Controller 的核心结构体
type Controller struct {
// 客户的 client,用于操作 Kubernetes 资源
kubeclientset kubernetes.Interface
// Informer 的 Lister,用于从本地缓存读取资源
deploymentLister appsv1listers.DeploymentLister
podLister corev1listers.PodLister
// 标记 Informer 的缓存是否已经同步完成
deploymentSynced cache.InformerSynced
podSynced cache.InformerSynced
// 限速工作队列,用于存储待处理的资源 key
workqueue workqueue.TypedRateLimitingInterface[WorkItem]
// Reconcile 需要的辅助工具
recorder record.EventRecorder // 用于记录 Events
}
// WorkItem 定义了工作队列中存储的任务类型
type WorkItem struct {
Namespace string
Name string
Key string // 格式:namespace/name
}
这里的关键点是:Lister 是只读的,用于从本地缓存读取数据;而 KubeClient 是写操作的入口,用于向 APIServer 发送修改请求。这样的设计确保了读操作从本地缓存获取(快),写操作直接走 APIServer(保证一致性)。
三、NewController:Controller 的初始化
NewController 是 Controller 的构造函数,负责把所有组件组装起来:
// Controller 构造函数
func NewController(
kubeclientset kubernetes.Interface,
deploymentInformer appsv1informers.DeploymentInformer,
podInformer corev1informers.PodInformer,
) *Controller {
// 创建限速工作队列
queue := workqueue.NewTypedRateLimitingInterval[string](
workqueue.ItemFastSlowRateLimiter( // 快速重试 3 次,然后慢速
1*time.Second, // 快速重试间隔
10*time.Second, // 慢速重试间隔
3, // 快速重试次数
),
workqueue.WithMaxInterval(60*time.Second), // 最大间隔
workqueue.WithName("example-controller"), // 队列名称(用于监控)
)
// 创建事件记录器,用于在资源上记录 Events
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{
Interface: kubeclientset.CoreV1().Events(""),
})
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{
Component: "example-controller",
})
c := &Controller{
kubeclientset: kubeclientset,
deploymentLister: deploymentInformer.Lister(),
podLister: podInformer.Lister(),
deploymentSynced: deploymentInformer.Informer().HasSynced,
podSynced: podInformer.Informer().HasSynced,
workqueue: queue,
recorder: recorder,
}
// 注册事件处理函数:当 Informer 收到事件时,调用这些函数
deploymentInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.enqueueDeployment, // 添加事件
UpdateFunc: c.updateDeployment, // 更新事件
DeleteFunc: c.deleteDeployment, // 删除事件
})
return c
}
四、ResourceEventHandler:三种写法
ResourceEventHandlerFuncs 是 Informer 的核心扩展点。Informer 在收到集群事件后,会调用我们注册的 Handler 来处理。client-go 提供了三种写法:
方式一:ResourceEventHandlerFuncs(最常用)
这种方式最简单,我们需要实现 AddFunc、UpdateFunc、DeleteFunc 三个回调函数:
// AddFunc:资源被创建时的处理
func (c *Controller) enqueueDeployment(obj interface{}) {
deployment, ok := obj.(*appsv1.Deployment)
if !ok {
utilruntime.HandleError(fmt.Errorf("expected *appsv1.Deployment but got %T", obj))
return
}
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(deployment)
if err != nil {
utilruntime.HandleError(fmt.Errorf("could't get key for Deployment %s: %v", deployment.Name, err))
return
}
// 将 key 放入工作队列
c.workqueue.Add(key)
}
// UpdateFunc:资源被更新时的处理(oldObj 是更新前的对象,newObj 是更新后的对象)
func (c *Controller) updateDeployment(oldObj, newObj interface{}) {
// 这里我们简单处理:只要有更新就重新入队
// 实际场景可以比较新旧对象的差异,决定是否入队
c.enqueueDeployment(newObj)
}
// DeleteFunc:资源被删除时的处理
func (c *Controller) deleteDeployment(obj interface{}) {
deployment, ok := obj.(*appsv1.Deployment)
if !ok {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
utilruntime.HandleError(fmt.Errorf("could't get object from tombstone %T", obj))
return
}
deployment, ok = tombstone.Obj.(*appsv1.Deployment)
if !ok {
utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a Deployment %T", obj))
return
}
}
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(deployment)
if err != nil {
utilruntime.HandleError(fmt.Errorf("could't get key for Deployment %s: %v", deployment.Name, err))
return
}
c.workqueue.Add(key)
}
方式二:FilteringResourceEventHandler(过滤版)
有时候我们只关心特定条件的资源,比如只关心带有某个标签的 Pod。这时可以用 FilteringResourceEventHandler 先过滤:
// 只处理带有 app=myapp 标签的资源
deploymentInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{
FilterFunc: func(obj interface{}) bool {
deployment, ok := obj.(*appsv1.Deployment)
if !ok {
return false
}
// 只关心带有 app=myapp 标签的 Deployment
_, exists := deployment.Labels["app"]
return exists
},
Handler: cache.ResourceEventHandlerFuncs{
AddFunc: c.enqueueDeployment,
UpdateFunc: c.updateDeployment,
DeleteFunc: c.deleteDeployment,
},
})
方式三:自定义结构体(最灵活)
如果要实现更复杂的逻辑,可以直接实现 ResourceEventHandler 接口:
// 自定义 Handler,实现 ResourceEventHandler 接口
type customHandler struct {
controller *Controller
}
func (h *customHandler) OnAdd(obj interface{}) {
// 自定义处理逻辑
h.controller.enqueueDeployment(obj)
}
func (h *customHandler) OnUpdate(oldObj, newObj interface{}) {
oldDep := oldObj.(*appsv1.Deployment)
newDep := newObj.(*appsv1.Deployment)
// 只在特定字段变化时才入队
if oldDep.Spec.Replicas != newDep.Spec.Replicas {
h.controller.enqueueDeployment(newObj)
}
}
func (h *customHandler) OnDelete(obj interface{}) {
h.controller.deleteDeployment(obj)
}
// 使用
deploymentInformer.Informer().AddEventHandler(&customHandler{controller: c})
五、Reconcile:核心业务逻辑
Reconcile 是 Controller 的核心,它负责把实际状态调整为期望状态。这是 Kubernetes Controller 设计的精髓:声明式控制循环。
我们用一个具体的例子来说明:假设我们要实现一个 Controller,它的功能是"确保每个 Deployment 的每个 Pod 都带有 version 标签"。
// Reconcile 实现声明式控制循环
func (c *Controller) reconcile(ctx context.Context, key string) error {
// 第一步:从 key 解析出 namespace 和 name
// key 格式是 "namespace/name"
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
return fmt.Errorf("invalid key %s: %v", key, err)
}
// 第二步:从本地缓存获取 Deployment
deployment, err := c.deploymentLister.Deployments(namespace).Get(name)
if err != nil {
if errors.IsNotFound(err) {
// Deployment 已被删除,我们不需要做任何事
klog.V(4).Infof("Deployment %s/%s has been deleted, skipping", namespace, name)
return nil
}
// 其他错误,重试
return fmt.Errorf("failed to get Deployment %s/%s: %w", namespace, name, err)
}
// 第三步:获取这个 Deployment 管理下的所有 Pod
pods, err := c.podLister.Pods(namespace).List(labels.Everything())
if err != nil {
return fmt.Errorf("failed to list Pods: %v", err)
}
// 筛选出属于这个 Deployment 的 Pod(通过 OwnerReference 判断)
ownedPods := filterOwnedPods(deployment, pods)
// 第四步:比较期望状态和实际状态
podsNeedingUpdate := []string{}
for _, pod := range ownedPods {
if pod.Labels["version"] != deployment.Labels["version"] {
podsNeedingUpdate = append(podsNeedingUpdate, pod.Name)
}
}
// 第五步:如果有 Pod 需要更新,执行更新
for _, podName := range podsNeedingUpdate {
pod, err := c.podLister.Pods(namespace).Get(podName)
if err != nil {
return fmt.Errorf("failed to get Pod %s/%s: %w", namespace, podName, err)
}
// 更新 Pod 标签
pod = pod.DeepCopy()
pod.Labels["version"] = deployment.Labels["version"]
_, err = c.kubeclientset.CoreV1().Pods(namespace).Update(ctx, pod, metav1.UpdateOptions{})
if err != nil {
c.recorder.Eventf(deployment, v1.EventTypeWarning, "FailedUpdate",
"Failed to update Pod %s/%s: %v", namespace, podName, err)
return fmt.Errorf("failed to update Pod %s/%s: %w", namespace, podName, err)
}
klog.V(4).Infof("Successfully updated Pod %s/%s with version=%s",
namespace, podName, deployment.Labels["version"])
}
return nil
}
// filterOwnedPods 筛选出属于指定 Deployment 的 Pod
func filterOwnedPods(deployment *appsv1.Deployment, pods []*v1.Pod) []*v1.Pod {
var owned []*v1.Pod
for _, pod := range pods {
for _, ref := range pod.OwnerReferences {
if ref.UID == deployment.UID {
owned = append(owned, pod)
break
}
}
}
return owned
}
Reconcile 函数的设计原则:
- 幂等性:同一个资源被 Reconcile 多次,结果应该一致
- 不要阻塞:Reconcile 应该尽快完成,不要做耗时操作
- 返回错误:如果处理失败,应该返回错误,让 WorkQueue 自动重试
- 读取用缓存,写作用 client:这是 Kubernetes 的标准模式
六、Worker:并发处理循环
Worker 是实际执行 Reconcile 的 goroutine。通常我们会启动多个 Worker 来并发处理任务:
// Run 启动 Controller
func (c *Controller) Run(ctx context.Context, workers int) error {
// 第一步:等待所有 Informer 的缓存同步完成(关键!)
klog.Info("Waiting for caches to sync...")
if !cache.WaitForCacheSync(ctx.Done(), c.deploymentSynced, c.podSynced) {
return fmt.Errorf("failed to wait for caches to sync")
}
klog.Info("Caches synced, starting workers")
// 第二步:启动指定数量的 Worker goroutine
for i := 0; i < workers; i++ {
go c.worker(ctx, i)
}
// 第三步:等待 context 取消
<-ctx.Done()
klog.Info("Shutting down workers")
return nil
}
// worker 是单个 Worker 的处理循环
func (c *Controller) worker(ctx context.Context, workerID int) {
klog.Infof("Worker %d started", workerID)
// 不断从队列中取任务,直到 context 取消
for c.processNextWorkItem(ctx) {
}
klog.Infof("Worker %d stopped", workerID)
}
// processNextWorkItem 从队列中取出一个任务并处理
func (c *Controller) processNextWorkItem(ctx context.Context) bool {
// GetWithContext 会阻塞等待,直到队列中有任务或 context 取消
key, quit := c.workqueue.GetWithContext(ctx)
if quit {
return false
}
// defer 确保在处理完成后调用 Done
defer c.workqueue.Done(key)
// 调用 Reconcile 处理任务
if err := c.reconcile(ctx, key); err != nil {
// 如果处理失败,将任务重新放回队列(带重试延迟)
c.workqueue.AddRateLimited(key)
klog.Errorf("Reconcile for %s failed: %v, requeued", key, err)
return true
}
// 成功处理,从限速器中移除(下次同一任务会从头开始计数)
c.workqueue.Forget(key)
return true
}
Worker 的设计要点:
- 多个 Worker 并发处理:可以显著提高吞吐量,但要注意并发安全问题
- GetWithContext 支持取消:当 context 取消时,Worker 会优雅退出
- Done 必须调用:标记任务已处理完,允许同一任务再次入队
- AddRateLimited vs Forget:失败时用 AddRateLimited,成功时用 Forget
七、完整的 main 函数
把以上所有部分组合起来,就是一个完整的 Controller main 函数:
func main() {
// 第一步:解析命令行 flags
flag.Parse()
// 第二步:初始化 klog(Kubernetes 的日志库)
klog.InitFlags(nil)
defer klog.Flush()
// 第三步:创建 context,用于控制生命周期
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 第四步:构建 config(从这里开始使用 client-go 标准模式)
config, err := clientcmd.BuildConfigFromFlags(
flag.Lookup("kubeconfig").Value.String(),
flag.Lookup("master").Value.String(),
)
if err != nil {
klog.Fatalf("Failed to build config: %v", err)
}
// 第五步:创建 Kubernetes clientset
kubeclientset, err := kubernetes.NewForConfig(config)
if err != nil {
klog.Fatalf("Failed to create kubeclientset: %v", err)
}
// 第六步:创建 SharedInformerFactory
factory := informers.NewSharedInformerFactory(kubeclientset, 30*time.Second)
// 第七步:创建 Controller(传入 Informer)
controller := NewController(kubeclientset,
factory.Apps().V1().Deployments(),
factory.Core().V1().Pods(),
)
// 第八步:启动 Informer Factory
factory.Start(ctx.Done())
// 第九步:运行 Controller(指定 2 个 Worker)
if err := controller.Run(ctx, 2); err != nil {
klog.Fatalf("Controller exited with error: %v", err)
}
}
完整的启动流程是:
创建 ClientSet → 创建 InformerFactory → 注册 EventHandler → 启动 Factory → 等待缓存同步 → 启动 Workers
八、错误处理与优雅关闭
生产环境的 Controller 需要优雅关闭。当程序收到 SIGTERM 或 SIGINT 时,应该:停止接受新任务、等待正在处理的任务完成、关闭 Informer、退出程序。
import (
"os"
"os/signal"
"syscall"
"k8s.io/apimachinery/pkg/util/wait"
)
func main() {
// ... 初始化代码省略 ...
// 创建一个可取消的 context
ctx, cancel := context.WithCancel(context.Background())
// 启动信号监听 goroutine
stopCh := make(chan os.Signal, 1)
signal.Notify(stopCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
⚠️ 警告
如果在 Reconcile 执行过程中 Controller 被强制关闭,可能导致资源状态不一致。建议在 Reconcile 中使用 Finalizer(最终一致性机制),确保关键操作能够完成。
九、常见问题与排查
在实际开发中,Controller 常见的问题有:
问题 1:事件没有被处理
排查步骤:
- 确认 Informer 的 Handler 是否正确注册
- 确认事件处理函数中是否正确调用了 workqueue.Add()
- 查看 Controller 日志中是否有错误信息
问题 2:任务被重复处理
这是正常现象。WorkQueue 使用"at-least-once"语义,同一个 key 可能会被多次处理。Reconcile 函数应该是幂等的:无论被调用多少次,结果应该一致。
十、总结
这一节我们完整实现了一个 Kubernetes Controller:
- Controller 结构体:保存所有状态,包括 Lister、Synced 函数、WorkQueue
- NewController:组装所有组件,注册事件处理函数
- ResourceEventHandler:三种写法,最常用的是 ResourceEventHandlerFuncs
- Reconcile:核心业务逻辑,声明式控制循环,读取用缓存,写作用 client
- Worker:并发处理循环,从 WorkQueue 取任务,调用 Reconcile
- 优雅关闭:监听信号、等待队列清空、关闭 Informer
下一节我们将学习 Leader 选举机制,这是实现高可用 Controller 的必备技能。敬请期待!
Kubernetes 编程 / Operator 专题【左扬精讲】—— Controller 开发模式完整实战 · 来源:Kubernetes v1.36.1 client-go 源码分析

浙公网安备 33010602011771号