1. Controller Runtime 概述
Controller Runtime 是 Kubernetes 社区官方维护的 Go 库,用于构建控制器和 Operator。它封装了 client-go 的底层细节,提供了一套声明式的、可组合的 API,让开发者专注于业务逻辑(Reconcile),而非重复编写事件监听、队列管理、领导者选举等基础设施代码。
1.1 什么是 Controller Runtime
Controller Runtime 本质上是一个 控制器框架,它定义了控制面控制器的标准构建范式。其核心设计哲学是:
- 关注点分离:将事件源、调度、业务逻辑解耦为独立组件。
- 约定优于配置:提供默认的 Manager、Controller、Reconciler 实现,开箱即用。
- 可测试性:通过 fake client 和 envtest 支持单元测试与集成测试。
关键认知:Controller Runtime 不是 client-go 的替代品,而是 client-go 的上层抽象。它使用 client-go 与 API Server 通信,但隐藏了 Informer、WorkQueue、Reflector 等复杂组件。
1.2 核心组件:Manager、Controller、Reconciler
Controller Runtime 的架构围绕三个核心抽象构建,它们形成了从启动到业务处理的完整链路:
| 组件 | 职责 | 关键方法/接口 |
|---|---|---|
| Manager | 生命周期管理、共享依赖注入(Client、Cache、Scheme)、领导者选举 | New()、Start()、Add() |
| Controller | 事件源注册、队列调度、并发控制、触发 Reconciler | New()、Watch()、Start() |
| Reconciler | 业务逻辑实现:将实际状态调整为期望状态 | Reconcile(ctx, Request) (Result, error) |
三者协作流程如下:
- Manager 启动时初始化共享 Cache 和 Client,并启动所有已注册的 Controller。
- Controller 通过
Watch()注册事件源(如对 Deployment 的变更),事件进入内部 WorkQueue。 - Reconciler 从队列中取出请求,执行
Reconcile()方法,返回结果或错误。
调试切入点:当控制器行为异常时,首先检查 Manager 的启动日志(是否成功连接 API Server),然后检查 Controller 的 Watch 是否注册了正确的事件源,最后在 Reconciler 中打日志或使用 Delve 断点。
1.3 与 client-go 的关系
client-go 是 Kubernetes 官方提供的 Go 客户端库,提供对 API Server 的底层访问能力。Controller Runtime 在 client-go 之上构建了更高层的抽象:
| 维度 | client-go | Controller Runtime |
|---|---|---|
| 抽象层级 | 底层 HTTP 客户端、REST 调用 | 高层控制器框架 |
| 事件处理 | 手动管理 Informer、WorkQueue | 自动通过 Watch 注册,内置队列 |
| 缓存 | 需自行构建或使用 Informer 缓存 | Manager 提供共享 Cache |
| 领导者选举 | 需手动实现 | Manager 内置支持 |
| 典型使用场景 | CLI 工具、一次性查询、自定义客户端 | Operator、控制器、Webhook |
Controller Runtime 内部大量使用 client-go 的组件:
- Cache 基于 client-go 的
SharedInformerFactory实现。 - Client 封装了 client-go 的
ClientSet或DynamicClient,提供类型安全的读写接口。 - WorkQueue 直接复用 client-go 的
workqueue.RateLimitingInterface。
调试陷阱:Controller Runtime 的 Client 默认使用缓存读取(直接从 Cache 返回),而非每次都请求 API Server。这在调试时可能导致“读到旧数据”的错觉。如需强制读取 API Server,应使用
client.Reader 或设置 CacheOption。
1.4 整体架构图
下图展示了 Controller Runtime 的核心组件及其与 client-go 的协作关系:
1.5 组件协作时序图
以下时序图展示了一个典型 Reconcile 请求的完整生命周期:
1.6 最小控制器代码示例
以下代码展示了 Controller Runtime 三个核心组件的典型使用方式:
package main
import (
"context"
"fmt"
"os"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// PodReconciler 实现 Reconciler 接口
type PodReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// Reconcile 是业务逻辑入口
func (r *PodReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
log := log.FromContext(ctx)
log.Info("Reconciling Pod", "namespace", req.Namespace, "name", req.Name)
var pod corev1.Pod
if err := r.Get(ctx, req.NamespacedName, &pod); err != nil {
log.Error(err, "unable to fetch Pod")
return reconcile.Result{}, client.IgnoreNotFound(err)
}
fmt.Printf("Pod %s/%s 当前 Phase: %s\n", pod.Namespace, pod.Name, pod.Status.Phase)
return reconcile.Result{RequeueAfter: 30 * time.Second}, nil
}
func main() {
// 1. 设置日志
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))
// 2. 创建 Manager
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: ":8080",
LeaderElection: true,
LeaderElectionID: "pod-controller.example.com",
})
if err != nil {
fmt.Println("unable to start manager:", err)
os.Exit(1)
}
// 3. 创建 Controller 并注册 Reconciler
err = ctrl.NewControllerManagedBy(mgr).
For(&corev1.Pod{}). // 监听 Pod 事件
Owns(&corev1.Event{}). // 同时监听关联的 Event
Complete(&PodReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
})
if err != nil {
fmt.Println("unable to create controller:", err)
os.Exit(1)
}
// 4. 启动 Manager(阻塞)
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
fmt.Println("problem running manager:", err)
os.Exit(1)
}
}
调试建议:在
Reconcile 方法中增加 log.Info() 输出请求的 NamespacedName,配合 --v=4 启动参数,可以清晰看到每个 Reconcile 请求的触发时机和结果。这是定位“控制器未按预期触发”问题的最快方法。