惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

腾讯CDC
Hacker News: Ask HN
Hacker News: Ask HN
S
Securelist
Security Latest
Security Latest
S
Schneier on Security
T
Threat Research - Cisco Blogs
Latest news
Latest news
Cyberwarzone
Cyberwarzone
A
Arctic Wolf
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
NISL@THU
NISL@THU
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
I
Intezer
T
The Exploit Database - CXSecurity.com
N
News and Events Feed by Topic
Simon Willison's Weblog
Simon Willison's Weblog
T
Tor Project blog
Blog — PlanetScale
Blog — PlanetScale
C
Cyber Attacks, Cyber Crime and Cyber Security
C
CERT Recently Published Vulnerability Notes
The Hacker News
The Hacker News
月光博客
月光博客
WordPress大学
WordPress大学
博客园 - 叶小钗
Hugging Face - Blog
Hugging Face - Blog
美团技术团队
量子位
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cisco Blogs
博客园 - 三生石上(FineUI控件)
Google DeepMind News
Google DeepMind News
Project Zero
Project Zero
Webroot Blog
Webroot Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Application and Cybersecurity Blog
Application and Cybersecurity Blog
云风的 BLOG
云风的 BLOG
L
LINUX DO - 最新话题
Schneier on Security
Schneier on Security
Engineering at Meta
Engineering at Meta
www.infosecurity-magazine.com
www.infosecurity-magazine.com
aimingoo的专栏
aimingoo的专栏
D
Docker
有赞技术团队
有赞技术团队
Google DeepMind News
Google DeepMind News
宝玉的分享
宝玉的分享
T
Troy Hunt's Blog
L
Lohrmann on Cybersecurity
T
The Blog of Author Tim Ferriss
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
L
LangChain Blog

博客园 - 左扬

Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析: Controller 调试与诊断工具:从日志分析到问题定位 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DynamicClient 操作 CRD:无需代码生成的动态操作 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:控制器与 APIServer 完整交互流程:从 Watch 到缓存同步 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:错误处理与重试机制:WorkQueue 限速器详解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Leader 选举机制:高可用控制器的必备技能 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Controller 开发模式完整实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:SharedInformerFactory 与等待缓存同步 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:从认证配置到 Deployment 操作 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:版本对应、架构组件与组件关系 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Informer 源码深度解析:从底层原理到实战应用 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Reflector 源码深度解析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:ListWatcher 源码深度解析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Indexer 与 ThreadSafeStore 核心原理与源码深度剖析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DeltaFIFO 核心原理与源码深度剖析 Kubernetes 编程 / Operator 专题【左扬精讲】—— workqueue 核心原理与实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— runtime.Codec 资源编解码:serializer 与 codec 差异、编解码数据结构、codec 核心调用链路 Kubernetes 编程 / Operator 专题【左扬精讲】—— Scheme 资源注册机制全解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 自定义资源的内部版本与外部版本:从源码看版本定义机制 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 1.36.1 核心 API 数据结构全解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 构建过程 【AIOPS】一文读懂LLM【左扬精讲】:从诞生到普及,解锁大语言模型的核心密码 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Hooks 源码精讲:Hooks 可观测性的无侵入式实现 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Golang 配置解析源码精讲 ——SRE 自定义 Agent 核心技巧 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Golang 并发模型解析 ——SRE 应对高并发采集的调优思路 【AIOPS】AI Agent 专题【左扬精讲】基础架构篇:MCP-VictoriaMetrics Golang 源码整体架构拆解 ——SRE 必懂的核心模块与数据流 OpenTelemetry 开发实战【左扬精讲】—— 云原生可观测体系构建与分布式追踪二次开发 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 7 —— 基于流量预测模型的智能弹性扩缩容 Operator 实战(AIOps 模型训练与智能扩容(下篇)—— 预测式弹性扩缩容 Operator 落地实现) Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 7 —— 基于流量预测模型的智能弹性扩缩容 Operator 实战(AIOps 模型训练与智能扩容(上篇)—— 时序预测模型构建与离线训练) Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 6 —— 基于运维专家知识库的智能故障诊断与排查 Operator 实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 5 —— 基于大语言模型(LLM)的实时日志流智能监测 Operator 实现 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 4 —— 基于 Operator 实现大模型私有化部署与管理 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 3 —— 面向 AI / 算力调度场景:GPU 竞价实例资源池统一调度管理 Operator 开发 Kubernetes编程 / Operator专题【左扬精讲】—— Operator 开发实战项目 2 —— 面向零售 / 电商潮汐流量难题:多云多集群数据中心级全链路弹性伸缩 DataCenter Scaler Operator 从 0 到 1 全链路开发 Kubernetes编程 / Operator专题【左扬精讲】—— 深入理解Kubebuilder注解:为什么Operator开发离不开这些特殊注释 Kubernetes编程 / Operator专题【左扬精讲】—— Operator 开发实战项目1 —— Applicaion Operator(通用应用生命周期管理 Operator 实战) Pod 镜像拉取失败?kubectl edit pods修改镜像地址的底层原理与实操 (该方法仅为临时应急方案,并非长期解决方案) Kubernetes编程/Operator专题精讲—— 理解控制器模式 —— 控制器模式的核心原理与实现逻辑(从原理到实践) 【AIOPS】AI Agent 专题【左扬精讲】模型微调实战:一站式平台 LLaMA-Factory 【AIOPS】AI Agent 专题【左扬精讲】基于 k8s+vLLM+Ray 分布式部署全指南:架构设计、资源调度与性能优化 【AIOPS】AI Agent专题【左扬精讲】非量化版DeepSeek分布式部署全指南:精度保障、显存规划与Ollama/vLLM选型 【AIOPS】AI Agent 专题【左扬精讲】零开发框架实现 ReAct Agent(Go SRE友好)
Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:生产级 Controller 实践:并发安全、资源清理与高可用设计
左扬 · 2026-06-13 · via 博客园 - 左扬

Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:生产级 Controller 实践:并发安全、资源清理与高可用设计

当我们把 Controller 部署到生产环境时,会遇到很多开发环境不会出现的问题:多线程并发安全、内存泄漏、资源清理、高可用部署……这些问题如果不在设计阶段就考虑清楚,等上线后再修复代价会非常大。

这一篇文章,我们来系统地学习生产级 Controller 的最佳实践。

Kubernetes 生产实践 高可用 资源泄漏 v1.36.1

🔓 学习重点提示  — 建议先通读全文,再重点回顾标注内容

★ 重点掌握(必须)
   • 并发安全:为什么 informer.eventHandlers 的注册必须在锁保护下
   • 资源清理:Finalizer 如何保证删除安全
   • 优雅关闭:WaitGroup 和 channel 的正确使用

☆ 次重点(了解即可)
   • 监控指标设计


一、并发安全:Controller 编程的核心挑战

Controller 天然是多线程并发执行的:Informer 的事件处理在 goroutine 中运行,Worker 处理任务也在多个 goroutine 中运行。如果不处理好并发安全,就会出现数据竞争、状态不一致等问题。

1. WorkQueue 的并发安全保证

WorkQueue 本身是并发安全的,Get() 返回同一个 key 时,只有一个 goroutine 能获取到:

// WorkQueue 的并发安全由其内部锁保证
// 同一时间只有一个 worker 能 Get 到同一个 key
key1, _ := queue.Get()  // goroutine A 获取了 "deploy-1"
key2, _ := queue.Get()  // goroutine B 获取了 "deploy-1"(如果 A 没有 Done)
// 实际上,A 调用 Get() 后,key1 就从队列中移除了
// B 只能获取到其他 key,或者等待

2. Reconcile 的幂等性设计

WorkQueue 保证同一时间只有一个 worker 处理同一个 key,但不同时间可能有多个 worker 处理同一个 key(因为失败重试)。所以 Reconcile 必须设计成幂等的:

// 幂等的 Reconcile 示例:确保 Pod 带有特定标签
func (c *Controller) reconcile(ctx context.Context, deployment *appsv1.Deployment) error {
    // 获取当前 Pod 列表(从 Lister,可能不是最新)
    pods, err := c.podLister.Pods(deployment.Namespace).List(labels.Everything())
    if err != nil {
        return err
    }

    // 筛选属于这个 Deployment 的 Pod
    ownedPods := filterOwnedPods(deployment, pods)

    // 比较期望状态和实际状态
    for _, pod := range ownedPods {
        if pod.Labels["app"] != deployment.Labels["app"] {
            // 更新 Pod(使用 Patch 而不是 Replace,避免冲突)
            patch, _ := json.Marshal([]map[string]interface{}{
                {
                    "op":    "add",
                    "path":  "/metadata/labels/app",
                    "value": deployment.Labels["app"],
                },
            })
            _, err := c.kubeclientset.CoreV1().Pods(pod.Namespace).Patch(
                ctx, pod.Name, types.JSONPatchType, patch, metav1.PatchOptions{},
            )
            if err != nil {
                return err
            }
        }
    }

    return nil  // 幂等:重复执行结果一致
}

3. 避免读取过期数据

Lister 返回的是本地缓存的数据,可能不是最新状态。当需要最新状态时,应该直接查询 APIServer:

// 当需要最新状态时,直接从 APIServer 获取
func (c *Controller) reconcile(ctx context.Context, deployment *appsv1.Deployment) error {
    // Lister 可能有过期数据
    pods, err := c.podLister.Pods(deployment.Namespace).List(...)
    
    // 某些情况下需要最新状态
    latestDeployment, err := c.kubeclientset.AppsV1().Deployments(
        deployment.Namespace).Get(ctx, deployment.Name, metav1.GetOptions{})
    if err != nil {
        return err
    }
    
    // 检查是否有其他 Controller 修改过这个资源
    if latestDeployment.ResourceVersion != deployment.ResourceVersion {
        klog.V(4).Infof("Deployment %s/%s has been modified, will retry",
            deployment.Namespace, deployment.Name)
        return fmt.Errorf("deployment was modified")
    }
    
    // ... 继续处理 ...
}

二、资源清理:Finalizer 的正确使用

当 Controller 管理的资源被删除时,我们需要确保清理逻辑能够执行。Finalizer(最终一致性机制)就是来解决这个问题的。

什么是 Finalizer?

Finalizer 是 Kubernetes 的一种机制,它告诉 APIServer:在删除对象之前,必须先移除某个 finalizer。这个机制常用于:

  • 清理外部资源(如云存储卷、外部数据库)
  • 等待相关资源删除完成(如等待 Pod 删除)
  • 完成数据同步或备份

Finalizer 的工作流程

┌──────────────────────────────────────────────────────────────────────────┐
│                      Finalizer 工作流程                                      │
├──────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  1. 创建资源时,添加 finalizer                                            │
│     metadata.finalizers = ["example.com/cleanup"]                         │
│                                                                          │
│  2. 用户发起删除请求                                                      │
│     kubectl delete foo my-foo                                            │
│                                                                          │
│  3. APIServer 设置 deletionTimestamp,但不立即删除                         │
│     metadata.deletionTimestamp = "2024-01-01T12:00:00Z"                  │
│     → Controller 收到 Update/Delete 事件                                  │
│                                                                          │
│  4. Controller 执行清理逻辑                                               │
│     - 清理关联资源                                                       │
│     - 清理外部资源                                                       │
│     - 完成必要的清理工作                                                   │
│                                                                          │
│  5. Controller 移除 finalizer                                             │
│     PATCH metadata.finalizers = []                                       │
│                                                                          │
│  6. APIServer 完成删除                                                   │
│     ✓ 资源被真正删除                                                     │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

Finalizer 实现示例

const finalizerName = "example.com/cleanup"

func (c *Controller) reconcileFoo(ctx context.Context, foo *examplev1.Foo) error {
    // 检查是否需要添加 finalizer
    if !hasFinalizer(foo, finalizerName) {
        foo = foo.DeepCopy()
        foo.Finalizers = append(foo.Finalizers, finalizerName)
        _, err := c.exampleClient.ExampleV1().Foos(foo.Namespace).Update(ctx, foo, metav1.UpdateOptions{})
        return err
    }

    // 检查是否正在被删除
    if foo.DeletionTimestamp != nil {
        // 执行清理逻辑
        if err := c.cleanupResources(ctx, foo); err != nil {
            return err
        }

        // 移除 finalizer,允许资源被删除
        foo = foo.DeepCopy()
        foo.Finalizers = removeFinalizer(foo.Finalizers, finalizerName)
        _, err := c.exampleClient.ExampleV1().Foos(foo.Namespace).Update(ctx, foo, metav1.UpdateOptions{})
        return err
    }

    // 正常处理逻辑
    // ...
    return nil
}

func hasFinalizer(obj metav1.Object, finalizer string) bool {
    for _, f := range obj.GetFinalizers() {
        if f == finalizer {
            return true
        }
    }
    return false
}

func removeFinalizer(finalizers []string, finalizer string) []string {
    var result []string
    for _, f := range finalizers {
        if f != finalizer {
            result = append(result, f)
        }
    }
    return result
}

⚠️ 警告
如果 Controller 因为 bug 无法移除 finalizer,资源会永远无法删除。这时需要手动介入:kubectl edit foo my-foo 并移除 finalizers 数组。

三、优雅关闭:Controller 的生命周期管理

当 Controller 收到 SIGTERM(终止信号)时,应该优雅地关闭,而不是立即退出。

优雅关闭的步骤

  1. 1停止接收新任务:不再接受新的 Reconcile 请求
  2. 2等待正在处理的任务完成:给 worker 一段时间处理完当前任务
  3. 3关闭 Informer:停止 Watch,释放连接
  4. 4释放锁(如使用了 Leader 选举):让其他副本可以接管

优雅关闭实现

func (c *Controller) Run(ctx context.Context, workers int) error {
    // 等待缓存同步
    if !cache.WaitForCacheSync(ctx.Done(), c.deploymentSynced, c.podSynced) {
        return fmt.Errorf("failed to wait for caches to sync")
    }

    // 启动 workers
    for i := 0; i < workers; i++ {
        c.workerGroup.Go(func() error {
            return c.runWorker(ctx, i)
        })
    }

    // 等待 context 取消
    <-ctx.Done()

    // Context 取消后,开始优雅关闭
    klog.Info("Shutdown signal received, gracefully stopping...")

    // 通知所有 worker 停止
    c.workerGroup.Wait()

    // 关闭 Informer Factory
    c.factory.Shutdown()

    klog.Info("Shutdown complete")
    return nil
}

func (c *Controller) runWorker(ctx context.Context, id int) error {
    for {
        select {
        case <-ctx.Done():
            return nil  // Context 取消,退出
        default:
            if !c.processNextWorkItem(ctx) {
                return nil
            }
        }
    }
}

四、内存泄漏:Informer 的陷阱

Informer 是最容易导致内存泄漏的地方。常见的原因有:

1. 没有调用 Shutdown

// 错误示例:没有关闭 Informer
func main() {
    factory := informers.NewSharedInformerFactory(clientset, 30*time.Second)
    factory.Start(ctx.Done())
    
    // ... 使用 factory ...
    
    // 程序退出时没有调用 Shutdown
    // goroutine 继续运行,持有的内存无法释放
}

// 正确示例
func main() {
    factory := informers.NewSharedInformerFactory(clientset, 30*time.Second)
    
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    
    factory.Start(ctx.Done())
    
    // ... 使用 factory ...
    
    // 程序退出前调用 Shutdown
    factory.Shutdown()
}

2. 注册了 Handler 但没有清理

// 错误示例:在 Controller 运行时注册了新的 Handler
func (c *Controller) updateHandler(newHandler cache.ResourceEventHandler) {
    // 每次调用都会添加一个新的 Handler!
    c.informer.AddEventHandler(newHandler)
    // 旧 Handler 永远不会被移除
}

// 正确做法:Handler 只注册一次,使用 channel 或其他机制传递事件
func (c *Controller) Run() {
    // 只注册一次 Handler
    c.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: func(obj interface{}) {
            c.eventCh

3. Indexer 持有过期对象

Indexer 会缓存所有见过的对象。如果对象被大量创建和删除,Indexer 会持续积累内存。使用带过期时间的索引可以缓解这个问题:

// 使用带 TTL 的 ThreadSafeStore
indexers := cache.Indexers{
    cache.NamespaceIndex:    cache.MetaNamespaceIndexFunc,
    "byUID": func(obj interface{}) ([]string, error) {
        meta, ok := obj.(metav1.Object)
        if !ok {
            return nil, fmt.Errorf("object is not metav1.Object")
        }
        return []string{string(meta.GetUID())}, nil
    },
}

// 或者定期清理不需要的索引
func (c *Controller) cleanupIndexer() {
    items := c.indexer.List()
    for _, item := range items {
        meta, _ := item.(metav1.Object)
        // 删除过期的对象
        if time.Since(meta.GetDeletionTimestamp().Time) > 5*time.Minute {
            c.indexer.Delete(item)
        }
    }
}

五、高可用部署:多副本 + Leader 选举

生产环境的 Controller 通常需要部署多个副本以保证高可用。

Deployment + Leader 选举模式

┌──────────────────────────────────────────────────────────────────────────┐
│                      高可用部署架构                                          │
├──────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │  Deployment (replicas: 3)                                        │   │
│   │                                                                  │   │
│   │   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │   │
│   │   │   Pod-1    │  │   Pod-2    │  │   Pod-3    │          │   │
│   │   │  (Leader)  │  │  (Standby) │  │  (Standby) │          │   │
│   │   └──────┬──────┘  └─────────────┘  └─────────────┘          │   │
│   │          │                                                      │   │
│   │          ▼                                                      │   │
│   │   ┌─────────────┐                                              │   │
│   │   │ Lease Lock │  只允许一个 Pod 处理事件                        │   │
│   │   │(in cluster)│                                              │   │
│   │   └─────────────┘                                              │   │
│   │                                                                  │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

Deployment 配置

apiVersion: apps/v1
kind: Deployment
metadata:
  name: example-operator
  namespace: operators
spec:
  replicas: 3
  selector:
    matchLabels:
      app: example-operator
  template:
    metadata:
      labels:
        app: example-operator
    spec:
      serviceAccountName: example-operator
      containers:
      - name: operator
        image: example/operator:v1.0.0
        args:
        - --leader-elect=true
        - --leader-election-namespace=operators
        # Pod 必须配置优雅终止宽限期
      terminationGracePeriodSeconds: 30

六、监控指标:生产级 Controller 的必备功能

生产环境的 Controller 应该暴露 metrics,方便监控和告警。

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    reconcileTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "controller_reconcile_total",
            Help: "Total number of reconcile attempts",
        },
        []string{"result"},  // result: success, error
    )

    reconcileDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "controller_reconcile_duration_seconds",
            Help:    "Duration of reconcile operations",
            Buckets: prometheus.DefBuckets,
        },
        []string{"result"},
    )

    workqueueDepth = promauto.NewGauge(
        prometheus.GaugeOpts{
            Name: "controller_workqueue_depth",
            Help: "Current depth of the workqueue",
        },
    )
)

func (c *Controller) reconcile(ctx context.Context, key string) error {
    start := time.Now()
    defer func() {
        duration := time.Since(start).Seconds()
        reconcileDuration.WithLabelValues("success").Observe(duration)
        reconcileTotal.WithLabelValues("success").Inc()
    }()

    err := c.doReconcile(ctx, key)
    if err != nil {
        reconcileDuration.WithLabelValues("error").Observe(time.Since(start).Seconds())
        reconcileTotal.WithLabelValues("error").Inc()
        return err
    }

    return nil
}

🌟 实用技巧
建议暴露以下 metrics:reconcile 总次数和成功率、reconcile 耗时分布、workqueue 深度、Leader 选举切换次数。


七、总结

这一节是 client-go 系列的最后一篇,我们总结了生产级 Controller 的最佳实践:

  • 并发安全:Reconcile 必须是幂等的,避免读取过期数据
  • Finalizer:确保清理逻辑能够执行,避免资源泄漏
  • 优雅关闭:正确处理 SIGTERM,给 worker 时间完成当前任务
  • 内存泄漏:正确关闭 Informer,避免注册重复 Handler
  • 高可用:多副本部署 + Leader 选举
  • 监控指标:暴露 metrics,方便监控和告警

至此,client-go 系列的全部文章已经完成。从 Informer 源码、DeltaFIFO、Indexer、WorkQueue,到 SharedInformerFactory、Controller 开发模式、Leader 选举、限速器、Watch 机制、DynamicClient,再到调试工具和生产实践,希望这一系列文章能帮助您真正掌握 Kubernetes 客户端编程。


Kubernetes 编程 / Operator 专题【左扬精讲】—— 生产级 Controller 实践 · 来源:Kubernetes v1.36.1 client-go 源码分析