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. 1观察(Informer):通过 Informer 监听集群资源的变化,当资源发生变化时收到事件
  2. 2去重(WorkQueue):将事件中的资源 key 放入 WorkQueue,去除重复事件,避免并发问题
  3. 3执行(Reconcile):从 WorkQueue 中取出任务,比较期望状态和实际状态,做必要的修正
  4. 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:事件没有被处理

排查步骤:

  1. 确认 Informer 的 Handler 是否正确注册
  2. 确认事件处理函数中是否正确调用了 workqueue.Add()
  3. 查看 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 源码分析

posted @ 2026-06-13 18:15  左扬  阅读(3)  评论(0)    收藏  举报