controller注册及调用时机

Controller 注册后的完整调用时序

整个流程分五个阶段:注册 → 构造 → 启动 Informer → 并发启动 Controller → 阻塞运行


完整时序图

scss 复制代码
kube-controller-manager 进程启动
│
├─ 阶段 0: 注册 (启动时立即执行,一次性)
│   NewControllerDescriptors()
│   [controller_descriptor.go:138]
│   │
│   └─ 返回 map[string]*ControllerDescriptor  ← 仅元数据,不实例化
│      (49 个 Descriptor,包含 name + constructor)
│
├─ 阶段 1: Leader Election (阻塞等待)
│   [controllermanager.go:397]
│   │
│   └─ leaderElectAndRun() → 竞争 Lease 锁
│       │
│       ├─ 未获取到锁 → 阻塞在 acquire(),每 2s 重试
│       │
│       └─ 获取到锁 → OnStartedLeading 回调
│           │
│           ▼
│   ┌─────────────────────────────────────────────────────────┐
│   │  阶段 2: 构造所有 Controller (单线程,顺序执行)            │
│   │  [controllermanager.go:268-298]                         │
│   │                                                         │
│   │  run := func(ctx, controllerDescriptors) {              │
│   │      ① CreateControllerContext()  ← 创建共享上下文       │
│   │      ② BuildControllers()         ← 逐个构造            │
│   │      ③ InformerFactory.Start()    ← 启动 Informer       │
│   │      ④ RunControllers()           ← 并发启动            │
│   │  }                                                      │
│   └─────────────────────────────────────────────────────────┘
│           │
│           ▼
│   ① CreateControllerContext()  [controllermanager.go:530]
│       │
│       ├─ 创建 SharedInformerFactory (共享 Informer)
│       ├─ 创建 metadata Informer Factory
│       ├─ 等待 API Server 就绪 (10s 超时)
│       └─ 创建 RESTMapper (用于 GC Controller)
│
│   ② BuildControllers()  [controllermanager.go:626]
│       │
│       ├─ 第 1 个: SA Token Controller (特殊处理,必须最先)
│       │   └─ constructor() → NewServiceAccountTokenController()
│       │
│       └─ 第 2~49 个: 按注册顺序构造
│           for _, desc := range controllerDescriptors {
│               if desc.RequiresSpecialHandling() → skip
│               if !IsControllerEnabled(desc)       → skip
│               if FeatureGate 未启用                → skip
│               desc.BuildController()               → constructor()
│           }
│
│   ③ InformerFactory.Start(stopCh)  [controllermanager.go:284]
│       │
│       └─ 启动所有 Informer 的 Watch 连接
│          此时 Controller 还没开始 Run,但 Informer 已在缓存数据
│
│   ④ close(controllerContext.InformersStarted)  [controllermanager.go:287]
│       │
│       └─ 广播信号: "Informer 已就绪,Controller 可以开始消费数据了"
│
│   ⑤ RunControllers()  [controllermanager.go:706]
│       │
│       └─ 并发启动所有 Controller (详见下方)
│
└─ 阶段 3: 阻塞运行,直到 ctx 被取消

RunControllers 的并发启动细节

controllermanager.go#L710-L795

Go

go 复制代码
func RunControllers(ctx, controllerCtx, controllers, jitter, shutdownTimeout) bool {
    // ① 每个 Controller 一个 goroutine,全部并发启动
    for _, controller := range controllers {
        go func(ctrl Controller) {
            // ② Jitter: 随机延迟后启动,防止所有 Controller 同时发起 List 请求
            time.Sleep(wait.Jitter(
                controllerCtx.ComponentConfig.Generic.ControllerStartInterval.Duration,  // 默认 0s
                controllerStartJitterMaxFactor,  // 默认 1.0
            ))

            // ③ 调用 Controller.Run(ctx),阻塞直到 ctx 被取消
            ctrl.Run(ctx)
        }(controller)
    }

    // ④ 等待 ctx 被取消(SIGTERM / Leader Election 丢失)
    <-ctx.Done()

    // ⑤ 等待所有 Controller 在 shutdownTimeout 内优雅退出
    //    超时则强制返回 false
}

关键设计:

  1. Jitter 防惊群 :所有 Controller 不会同时启动,而是随机错开。默认 ControllerStartInterval 为 0,jitter factor 为 1.0,所以实际效果是 0~0 秒的随机延迟(默认无延迟),但生产环境可配置
  2. 每个 Controller 的 Run() 是阻塞的 :以 ReplicaSet Controller 为例,它的 Run() 方法内部启动 worker goroutine 处理队列中的事件,然后阻塞在 <-ctx.Done()

controllerLoop 包装器

每个 Controller 的构造函数返回的是 *controllerLoopcontroller_utils.go#L48-L67):

Go

go 复制代码
type controllerLoop struct {
    name string
    run  runFunc     // func(ctx context.Context)
}

func (loop *controllerLoop) Name() string { return loop.name }
func (loop *controllerLoop) Run(ctx context.Context) { loop.run(ctx) }

以 Deployment Controller 为例(apps.go#L145-L147):

Go

go 复制代码
return newControllerLoop(func(ctx context.Context) {
    dc.Run(ctx, int(controllerContext.ComponentConfig.DeploymentController.ConcurrentDeploymentSyncs))
}, controllerName), nil
//  ↑
//  dc.Run() 内部启动 N 个 worker goroutine,然后阻塞在 <-ctx.Done()
//  所以 controllerLoop.Run() 也是阻塞的

完整时间线总结

scss 复制代码
t=0    进程启动,NewControllerDescriptors() 注册 49 个 Descriptor
       │
t=1    Leader Election 开始,阻塞等待获取 Lease 锁
       │  (可能立即获取,也可能等待数秒到数分钟)
       │
t=N    获取到 Leader 锁,OnStartedLeading 回调触发
       │
       ├─ CreateControllerContext()        ~100ms  (创建 Informer、等 API Server)
       ├─ BuildControllers()               ~500ms  (构造 49 个 Controller)
       │   ├─ SA Token Controller 最先
       │   └─ 其余 48 个按注册顺序
       ├─ InformerFactory.Start()          ~即时    (启动 Watch,开始缓存)
       ├─ close(InformersStarted)          ~即时    (通知 Controller 数据就绪)
       └─ RunControllers()                 ~即时    (49 个 goroutine 并发启动)
           │
t=N+1  所有 Controller 并发运行中
       │  (每个 Controller 内部启动 N 个 worker 处理事件队列)
       │
t=M    收到 SIGTERM 或 Leader Election 丢失
       │
       ├─ ctx 被取消
       ├─ 每个 Controller.Run() 返回
       ├─ 等待 shutdownTimeout (默认 0s = 不限时)
       └─ 进程退出

总结

阶段 时机 操作 并发性
注册 进程启动时 NewControllerDescriptors() 创建 Descriptor map 单次同步
构造 Leader 获取后 BuildControllers() 逐个调用 constructor 单线程顺序
Informer 启动 构造完成后 InformerFactory.Start() 启动所有 Watch 内部并发
Controller 启动 Informer 就绪后 RunControllers() 每个 Controller 一个 goroutine 全部并发
运行 启动后 每个 Controller 内部启动 N 个 worker 全部并发
退出 ctx 取消后 等待所有 goroutine 返回 并发退出
相关推荐
Profile排查笔记23 分钟前
指纹浏览器哪个好?从 Profile、代理、权限和自动化能力判断是否适合
前端·人工智能·后端·自动化
用户9385156350743 分钟前
用 AI 结对编程从 0 搭一个"单词后台管理系统":Next.js + Supabase + Drizzle + shadcn/ui 全记录
后端·postgresql·next.js
爱学习的小邓同学1 小时前
Golang --- (1)第一个Golang程序
开发语言·后端·golang
小灰灰搞电子2 小时前
Rust+Slint 实现的“DNA双螺旋”加载动画源码分享
后端·rust·slint·加载动画
面向Google编程2 小时前
向量库不再囤数据:Milvus 3.0 零拷贝直读数据湖
后端
IT_陈寒4 小时前
Redis内存警告竟是因为这个不起眼的配置项
前端·人工智能·后端
Python私教4 小时前
AI 漫剧角色一进分镜就变脸?把提示词升级成“角色 ID + 镜头合同”
后端
Python私教4 小时前
一次生成 20 个角色却全都撞脸:我用“角色合同”重做了批量生成流程
后端
用户594404103565 小时前
Go 语言高性能 Web 服务开发:基于 Gin + GORM + Redis 构建 RESTful API
后端
Python私教5 小时前
App 第一版该砍什么?用三问法守住 MVP 的工程底线
后端·mvp