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 的构造函数返回的是 *controllerLoop(controller_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 返回 并发退出
相关推荐
小溪学编程2 小时前
Spring @Bean 与 @Component 注解的区别详解
java·后端·spring
IT_陈寒3 小时前
Python的列表推导式用不好真的会爆内存你知道吗
前端·人工智能·后端
摸爬滚打的小李4 小时前
TomCat
后端
itmigrate4 小时前
Rust 里的所有权误区:不是不写 clone 就叫高性能
开发语言·后端·rust
码事漫谈4 小时前
Deepseek v5 来了
后端
爱勇宝5 小时前
你可能成不了技术大佬,然后呢?
前端·后端·程序员
JuiceFS5 小时前
HeyGen:如何让全球 10+ GPU 集群训练不再等待数据?
人工智能·后端
暧暧内含光5 小时前
Electron 桌面应用打包原理与实战避坑
后端
郭小李5 小时前
spring @PathVariable 注解用法
java·后端·spring
乔旭伟5 小时前
从一次「报错但成功」查起:SQLAlchemy async 的对象过期与 commit 时机
后端