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 返回 并发退出
相关推荐
edwarddamon1 小时前
Spring Cloud 配置热更新与 Bean 代理机制梳理
java·后端
码事漫谈1 小时前
我用 Seed Evolving 做了个 AI 小说写作工具
后端
DantyWei2 小时前
kube-controller-manager的leader选举流程和策略
后端
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十四):管理员个人资料页面、管理员日志优化
前端·后端·go
大卫陈3 小时前
PCB 拼版系统近期迭代复盘:大小拼跃迁、横直料重构与引擎打磨
后端·架构
二月龙3 小时前
JS 垃圾回收:为什么你明明释放了变量,内存还是爆了?
后端
长大19883 小时前
Promise 从入门到踩坑:为什么你的异步代码还是一团乱麻
后端
颜进强4 小时前
Calude Code - 25 CodeGraph:让 AI 真正读懂你的代码库
前端·后端·ai编程