Skip to content

3. 架构设计:controller-runtime 全景 ​

一句话理解:现代 Operator 几乎都用 controller-runtime(Kubebuilder/Operator SDK 的底层)构建,它的架构可以拆成两层——Manager(管理进程生命周期:cache、client、leader election、metrics、health) 和 Controller(管理一个 reconcile 循环:Source → EventHandler → Workqueue → Reconciler);理解这十几个组件的协作,你就理解了任何一个生产 Operator 的运行时。

3.1 全景图 ​

一个 controller-runtime Operator 进程的内部结构:

┌──────────────────────────────── Operator 进程(一个 Deployment,多副本) ────────────────────────────────┐
│                                                                                                            │
│   ┌──────────────────────── Manager(ctrl.NewManager)────────────────────────┐                            │
│   │                                                                            │                            │
│   │   ┌──────────── Cache(共享 informer 工厂)─────────────┐                  │                            │
│   │   │  RayCluster informer   Deployment informer   Service informer  ...     │     watch/list             │
│   │   │   ├── List (全量)  ──┐   ├── List ──┐          ├── List ──┐            │ ◄────────────────────►     │
│   │   │   └── Watch (增量) ──┤   └── Watch ─┤          └── Watch ─┤            │        apiserver/etcd      │
│   │   │      (本地 ThreadSafeStore 缓存)      │               │     │            │                            │
│   │   └──────────────────────────────────────┘               │     │            │                            │
│   │                                                            │     │            │     write back              │
│   │   ┌──────────── Client(读走 Cache,写直通 apiserver)──┐  │     │            │ ◄────────────────────►     │
│   │   │  r.Get / r.List   → 命中 Cache(快、不压 apiserver) │◄─┘     │            │        apiserver/etcd      │
│   │   │  r.Create/Update/Delete → 直达 apiserver             │        │            │                            │
│   │   │  r.Status().Update → /status 子资源                  │        │            │                            │
│   │   └──────────────────────────────────────────────────────┘        │            │                            │
│   │                                                                    │            │                            │
│   │   ┌── Controller A (RayCluster) ──┐    ┌── Controller B (...) ──┐ │            │                            │
│   │   │  Source: For(RayCluster)      │    │  ...                   │ │            │                            │
│   │   │   └─► EventHandler            │    │                        │ │            │                            │
│   │   │         └─► 入 Workqueue      │    │                        │ │            │                            │
│   │   │  Source: Owns(Deployment)     │    │                        │ │            │                            │
│   │   │   └─► 映射到 owner CR → 入队  │    │                        │ │            │                            │
│   │   │  ┌── Workqueue ──┐            │    │                        │ │            │                           
│   │   │  │ ns/name 去重  │ ──pop──► Reconciler(req) ──► Result       │ │            │                            │
│   │   │  │ 指数退避重试  │            │    │                        │ │            │                            │
│   │   │  └───────────────┘            │    │                        │ │            │                            │
│   │   └────────────────────────────────┘    └────────────────────────┘ │            │                            │
│   │                                                                    │            │                            │
│   │   Leader Election(多副本只一个干活)  │  Metrics(/metrics) │  Webhook Server  │                            │
│   │   Healthz / Readyz                     │  (Prometheus)        │  (:9443)         │                            │
│   └────────────────────────────────────────────────────────────────────────────────┘                            │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

下面逐层拆解。

3.2 Manager:进程的大管家 ​

ctrl.NewManager(config, ctrl.Options{...}) 创建一个 Manager,它是整个 Operator 进程的容器,负责:

  1. 创建并持有 Cache 和 Client(所有 Controller 共享,避免重复 watch)。
  2. 管理 Controller 生命周期:SetupWithManager(mgr) 把 Controller 注册进来,mgr.Start(ctx) 启动所有 Controller 的 goroutine。
  3. Leader Election:多副本部署时,用 Lease(租约)保证只有一个副本真正干活,其他待命。
  4. Metrics / Health 端点:暴露 /metrics(Prometheus)、/healthz、/readyz(liveness/readiness)。
  5. Webhook Server:若 Operator 含 admission webhook,Manager 启动一个 HTTPS server(默认 :9443)接收 apiserver 的准入回调。
go
func main() {
    mgr, _ := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
        Scheme:                  scheme,
        Metrics:                 server.Options{BindAddress: ":8080"},
        HealthProbeBindAddress:  ":8081",
        LeaderElection:          true,
        LeaderElectionID:        "ray-operator.io",
    })
    // 注册 Controller
    (&RayClusterReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme()}).
        SetupWithManager(mgr)
    // 注册 webhook
    (&RayCluster{}).SetupWebhookWithManager(mgr)
    mgr.AddHealthzCheck("healthz", healthz.Ping)
    mgr.Start(ctrl.SetupSignalHandler())
}

为什么所有 Controller 共享一个 Manager/Cache:一个 Operator 进程常含多个 Controller(如 KubeRay 有 RayCluster + RayService + RayJob),它们 watch 的资源有重叠。共享 Cache 避免对 apiserver 发起重复 watch(watch 是有成本的),这是 controller-runtime 的一个重要优化。

3.3 Cache 与 Informer:本地缓存 + watch ​

Cache 包装了 client-go 的 Informer 机制(见 K8s 第 6 章),为每种 watch 的资源维护一份本地缓存:

apiserver ──List(全量)──► Informer ──► ThreadSafeStore(内存里的 map)
          ──Watch(增量)──►  (Add/Update/Delete 事件) ──► 触发 EventHandler
  • 启动时:List 拉一份全量进缓存(这是"建立基线")。
  • 运行时:Watch 接收增量事件,更新缓存,同时分发给注册的 EventHandler。
  • 断线重连:watch 断了,Informer 自动重新 List + Watch 重建缓存(这个过程可能丢一些中间事件,但 level-triggered 的 Reconcile 不在乎——它每次读全量)。

Cache 的意义:

  1. Reconciler 读资源走本地内存(r.Get 命中 Cache),极快、不压 apiserver。一个频繁 reconcile 的 Operator 若每次都直查 apiserver,会把 apiserver 打爆。
  2. watch 解耦:Reconciler 不直接 watch,而是 Informer watch 后把"哪个 CR 变了"塞进 Workqueue,Reconciler 只管处理队列。

3.4 Client:读写分离(读走 Cache,写直通) ​

controller-runtime 的 client.Client 是一个智能读写分离的接口:

操作路径原因
Get / ListCache(本地内存)快、不压 apiserver;最终一致(缓存有几秒延迟可接受)
Create / Update / Delete直通 apiserver写必须落 etcd 才生效
Status().Updateapiserver 的 /status 子资源单独权限、不触发 spec 的 reconcile

关键细节——读后写的一致性:Reconciler 从 Cache 读到的是"几秒前的快照",写入用 apiserver 的乐观并发(基于 resourceVersion)。如果在你读和写之间别人改了对象,你的 Update 会被 apiserver 拒(Conflict),controller-runtime 自动重试——这保证不会"覆盖别人的改动"。

这正是 Helm 三方合并要解决的同类问题(见 Helm 第 3 章):多个写者并发改同一对象时如何不互相覆盖。K8s 的答案是 resourceVersion 乐观锁 + 冲突重试;Helm 的答案是三方合并 patch。场景不同,目标一致。

3.5 Controller:Source → EventHandler → Workqueue → Reconciler ​

一个 Controller 由四个组件串成一个流水线:

Source(事件源) ​

Source 告诉 Controller "watch 什么"。最常见的是 source.Kind(watch 某种资源):

  • For(&RayCluster{}):主资源,每个事件映射到它自己(ns/name)。
  • Owns(&Deployment{}):子资源,事件映射到它的 owner CR(通过 ownerReference 反查)——这样子资源变 → 触发 owner CR 的 reconcile。

也可以 watch 外部资源(Watches(&ExternalCR{}, handler)),用自定义映射函数把外部事件翻译成"该调和哪个 CR"。

EventHandler(事件 → 队列项) ​

EventHandler 决定"一个资源事件,该把哪些 ns/name 入队"。默认 handler:

  • EnqueueRequestForObject:把对象自己入队(用于主资源)。
  • EnqueueRequestForOwner:把对象的 owner 入队(用于 Owns 的子资源)。
  • EnqueueRequestsFromMapFunc:用函数把事件映射成 0~N 个队列项(用于跨资源 watch,如"ConfigMap 变了 → 重新调和所有引用它的 CR")。

Workqueue(去重 + 退避重试的工作队列) ​

这是 controller-runtime 包装 client-go workqueue 的产物,三个关键特性:

  1. 去重:队列里存的是 ns/name 字符串。同一个 key 短时间内入队多次,只会处理一次(避免事件风暴把 Reconciler 淹没)。
  2. 有序 + 有延迟:可以指定 AddAfter(key, 30s)(对应 Result{RequeueAfter: 30s}),30 秒后才处理。
  3. 指数退避:Reconcile 返回 error 时,该 key 会被按指数退避重新入队(5s, 10s, 20s, 40s ... 上限 1000s),避免对 apiserver 的错误风暴。

去重的精妙:K8s 资源变化可能极快(一个 Deployment 短时间内 spec 改 10 次),但 level-triggered 的 Reconciler 只需要"调和到最新状态"一次。Workqueue 的去重保证:不管中间多少事件,最终只 reconcile 一次到最新状态——这是 level-triggered 模型能扛住事件风暴的工程基础。

Reconciler(业务逻辑) ​

Reconcile(ctx, req) (Result, error)——开发者唯一需要写的部分(见第 2.3 节四条铁律)。返回值:

  • Result{Requeue: true}:立即重新入队(少用,易风暴)。
  • Result{RequeueAfter: 30s}:30 秒后再调和(用于轮询未收敛状态,如等 Pod ready)。
  • Result{} + nil error:完成,等下一次外部事件。
  • 任意 error:指数退避重试。

3.6 Leader Election:多副本只有一个干活 ​

Operator 通常部署多副本(高可用)。但若所有副本都 reconcile,会重复操作(重复创建 Pod、status 互相覆盖)。Leader Election 用一个 K8s Lease 资源做分布式锁:

  • 启动时各副本争抢 ray-operator.io 这个 Lease。
  • 抢到的成为 leader,启动所有 Controller;其他副本待命(保持进程存活但不 reconcile)。
  • Leader 心跳续约(默认每 2s),断网/崩溃导致续约失败 → 其他副本接管。
yaml
# Operator Deployment 通常 2 副本 + Manager 开 LeaderElection
spec:
  replicas: 2

注意:Leader Election 保证"只有一个副本 reconcile",但所有副本的进程都活着(webhook server 在所有副本都响应)。这和 K8s 内置 Controller Manager 的 leader election 机制完全一样。

3.7 Metrics、Health、Webhook Server ​

  • Metrics(:8080/metrics):controller-runtime 自动暴露一组 Prometheus 指标——controller_runtime_reconcile_total(reconcile 次数)、controller_runtime_reconcile_errors_total(失败次数)、workqueue_depth(队列深度)、reconcile_time_seconds(耗时)。这些是 Operator 可观测性的核心(见第 8 章)。
  • Health(:8081):/healthz(liveness,进程活着)、/readyz(readiness,Cache 同步完成可服务)。K8s 据此重启/摘流。
  • Webhook Server(:9443):若 Operator 含 admission/conversion webhook,Manager 启一个 HTTPS server,apiserver 带证书回调它。证书管理是生产痛点(见第 8 章)。

metrics 鉴权方式已换代(2025):旧的 kubebuilder 脚手架依赖 gcr.io/kubebuilder/kube-rbac-proxy sidecar 保护 metrics 端点,该镜像仓库 2025 年 3 月起停止提供。新做法(Kubebuilder v4.1+ 脚手架默认)是 controller-runtime 内置的 WithAuthenticationAndAuthorization FilterProvider:用 K8s Bearer Token + SubjectAccessReview 直接在 Manager 的 metrics server 里做认证授权,不再需要 sidecar。升级老 Operator 时这是必查项。

3.8 2025–2026:controller-runtime 的关键演进 ​

机制十年未变,但工程细节持续进化。截至 2026-10,controller-runtime 已到 v0.25.x(对齐 K8s 1.37),其中几个变化直接影响你写/读 Operator 的方式:

版本时间关键变化对你的影响
v0.212025 中跟随 K8s 1.33;客户端限流器默认关闭升级时注意 QPS/Burst 行为变化,按需显式配置
v0.222025 下controller.Options.ReconciliationTimeout;优先级队列(实验)可给单次 reconcile 加上下文超时,堵住"reconcile 卡死"类故障
v0.232026-01优先级队列默认开启(Result.Priority);subresource Apply;类型安全 webhook builder;Event 迁到 events.k8s.io/v1大规模 Operator 可以给紧急 CR 让路;写 Event 的 RBAC 要改 events.k8s.io
v0.242026-04跟随 K8s 1.36—
v0.252026-09实验性 ReadYourWritesConsistency client;TypedInformer缓解"写后读 Cache 滞后读到旧值"的老大难问题(见 3.4 节)

两个值得展开的能力:

① 优先级队列(v0.23 起默认):默认 workqueue 是 FIFO——一个无关紧要的 CR 也可能挡在紧急 CR(如故障恢复中的推理服务)前面。现在 Reconciler 可以返回带优先级的 Result,队列按优先级出队:

go
// 高优先级:正在故障恢复的推理服务,优先于日常扩缩容
if is.Status.Phase == PhaseRecovering {
    return ctrl.Result{Priority: ptr.To(highPriority)}, nil   // 插队调和
}
return ctrl.Result{Priority: ptr.To(lowPriority)}, nil

对 AI 平台这类"一个 Operator 管几千个训练/推理 CR、且故障恢复有 SLA"的场景,优先级队列是实打实的能力升级。

② ReconciliationTimeout(v0.22+):给整个 Controller 的单次调和设硬超时——超时后 reconcile 上下文被取消、记入 controller_runtime_reconcile_timeouts_total 指标。它把第 4 章"Reconcile 不能阻塞"从约定变成框架强制的兜底。

③ ReadYourWritesConsistency(v0.25,实验):传统 Client 读走 Cache,写完立刻读可能读到旧值(3.4 节的"读后写一致性"问题)。v0.25 提供实验性 client,跟踪"自己刚写入的对象",对这些对象的读短暂绕过 Cache 直查 apiserver,写完即读不再靠运气。

版本对齐经验:controller-runtime 的 minor 版本与 K8s minor 版本一一对应(v0.21↔1.33 … v0.25↔1.37),Kubebuilder 每个 minor 跟随(v4.15 起支持 K8s 1.36)。给 Operator 选基线时,用"目标集群 K8s 版本对应的 controller-runtime"即可。

3.9 与 K8s 内置 Controller 的同构性 ​

把上面的架构和 K8s 第 3 章 的 kube-controller-manager 对照:

组件K8s 内置 Controllercontroller-runtime Operator
进程管理cloud-provider / controller-managerManager
watch 缓存informer factoryCache(共享 informer)
读写 APIclient-go clientClient(读写分离)
事件 → 队列informer + workqueueSource + EventHandler + Workqueue
业务逻辑各 Controller 的 syncHandlerReconciler
选举leader election(如 kube-scheduler)Leader Election(Lease)
指标/metrics/metrics(同一套)

几乎一一对应。这再次说明:Operator 不是新发明,而是 K8s 控制平面能力对用户的开放。K8s 把自己的 Controller 构造工具(client-go informer/workqueue)封装成 controller-runtime,让用户用同样的模式写"自己的 Deployment Controller"。

本章小结 ​

  • Manager 是进程大管家:持有共享 Cache/Client、管理 Controller 生命周期、Leader Election、Metrics/Health/Webhook Server。
  • Cache(Informer) 为每种 watch 的资源维护本地缓存,Reconciler 读资源走内存(快、不压 apiserver),靠 List+Watch+自动重连保持一致。
  • Client 读写分离:读走 Cache,写直通 apiserver,写靠 resourceVersion 乐观锁 + 冲突重试保证并发安全。
  • Controller 流水线:Source(watch 什么)→ EventHandler(事件映射成 ns/name)→ Workqueue(去重 + 退避)→ Reconciler(业务逻辑)。去重是 level-triggered 抗事件风暴的基础。
  • Leader Election:多副本只有一个 reconcile,靠 Lease 租约,和 K8s 内置机制一致。
  • Metrics/Health/Webhook 是生产可观测与准入控制的基础设施。
  • 同构性:controller-runtime 几乎与 K8s 内置 Controller 一一对应——Operator 是 K8s 控制平面能力对用户的开放。
  • 2025–2026 演进:kube-rbac-proxy 退场、metrics 鉴权内置化(WithAuthenticationAndAuthorization);v0.23 起优先级队列默认开启(Result.Priority);v0.22 起 ReconciliationTimeout 兜底卡死;v0.25 实验性 ReadYourWritesConsistency 缓解写后读滞后。controller-runtime minor 版本与 K8s minor 一一对应(v0.25↔1.37)。

参考来源

Released under CC-BY-SA-4.0 License.