☰
Kubernetes Controller原理与自定义控制器开发实战
2026/10/22 9:41:07 网站建设 项目流程

说起 Kubernetes Controller,很多人第一反应是“这不就是 Operator 吗?其实 Controller 是比 Operator 更底层、也更核心的概念。我最早接触 Kubernetes 的时候,被 Deployment、StatefulSet、Service 这些资源搞得晕头转向,后来才明白,真正让这些资源“活”起来的,是一群在背后默默工作的控制器。Kubernetes Controller 就是这套自动化的心脏:它负责盯着集群里的状态,发现实际状态和期望状态不一致,就出手把差距抹平。这篇文章适合想深入理解 Kubernetes 控制平面、准备开发自定义控制器或 Operator 的读者,我会从原理讲到代码,再讲到排障,把这条路上我踩过的坑和积累的经验一起倒给你。

1. 先搞清楚 Kubernetes Controller 到底是个什么东西

1.1 控制器不是“一个组件”,而是一套模式

新手最容易产生的误解,是把 Controller 当成 Kubernetes 里的某种独立组件,类似于 kubelet 或者 kube-proxy。实际上,“Controller”指的是 Kubernetes 核心设计里的一类控制循环实现,它不只有一个,而是有一群。kube-controller-manager 这个进程里就塞了十几个控制器,比如我们最熟悉的 ReplicaSet 控制器、Deployment 控制器、Namespace 控制器、ServiceAccount 控制器等等。换句话说,Controller 是 Kubernetes 控制平面的核心工作模式,而不是某个单一的二进制。

理解这一点非常重要,因为你在写自定义控制器的时候,本质上做的事情和 Kubernetes 内置控制器做的事情是一样的:通过 API 监听资源变化,然后确保集群的实际状态与用户声明的期望状态一致。你写的自定义控制器,会和内置控制器一样运行在集群里,通过 Kubernetes API 来读取资源、修改资源。它不需要什么特殊权限,就是一个普通的 Pod,只是它通常需要一套灵活的 RBAC 权限来读写它关心的资源。

为什么 Kubernetes 要这么设计?因为“让系统自愈”这件事,没法靠一两个脚本或者定时任务完成。系统里可能有成千上万个资源对象,它们之间互相依赖、互相影响,Pod 被杀掉、节点宕机、网络分区,各种意外都会让实际状态偏离期望状态。控制器这种“事件驱动 + 持续调谐”的模式,能保证系统在任意时刻都向着期望状态收敛,而不是只在某个时间点去检查一次。你可以把它想象成一个恒温器:设定温度就是期望状态,房间当前温度就是实际状态,控制器就是那个不断对比、不断调节空调开关的机制。

1.2 控制器的三个核心角色:观察、决策、执行

拆开来看,任何一个 Kubernetes Controller 都可以抽象成三个动作:观察、决策、执行。这个抽象不是我自己发明的,而是来自 Kubernetes 官方文档对控制器模式的描述,但它能帮你把后面所有细节串起来。

观察,就是控制器需要知道集群里发生了什么。典型实现是用 informer 机制监听 API 对象的事件。我们写自定义控制器时,通常会用到 client-go 提供的 informer 或者 controller-runtime 封装的 Source 组件。informer 内部做了缓存和事件分发,它会连接到 API Server,发起一个 ListAndWatch 请求,先拉取全量对象放进本地缓存,然后持续 watch 变化事件。这样做的好处是,控制器读数据不用反复打 API Server,直接从本地缓存里读就行,极大地降低了 API Server 的压力。

决策,就是拿到事件后,控制器需要判断当前状态是否满足期望状态。它会把资源对象的 spec 和 status 做对比,把需要处理的对象丢进工作队列。这里有个关键点:控制器不会每收到一个事件就立即处理一次,而是把事件转换成“对象 key”,通常是“命名空间/名字”这种格式,然后放进工作队列。工作队列有去重机制,同一个 key 如果短时间内来了很多次事件,会被合并,这样可以避免无效的重复处理。

执行,就是真正去调用 Kubernetes API 创建、更新、删除资源。执行完以后通常会更新被管理对象的 status 字段,把当前的实际状态写回去。这样,用户通过 kubectl get 看到的状态,其实是控制器“汇报”上来的情况。比如你创建一个 ReplicaSet,声明副本数是 3,ReplicaSet 控制器看到后就去创建 3 个 Pod;等 Pod 创建完成,控制器会更新 ReplicaSet 的 status,告诉你说当前有 3 个副本就绪。整个过程完全自动,不需要人工干预。

2. 调谐循环(Reconcile Loop)是如何工作的

2.1 期望状态与当前状态

控制器模式的灵魂,就是“声明式 API + 控制循环”。在 Kubernetes 里,几乎所有资源对象都有两个核心字段:spec 和 status。spec 是你想要的期望状态,status 是当前实际状态。Deployment 的 spec.replicas 等于 3,这就是期望状态;status.readyReplicas 等于 2,这就是当前状态。控制器要做的事情,就是让 status 不断逼近 spec,直到完全一致。

这里有一个很容易忽略的点:控制器从来不会直接修改别人的 spec,它只会修改自己管理的对象的 status,或者创建/删除子资源来让系统达到期望状态。比如 Deployment 控制器不会改 Deployment 的 spec,它只负责创建 ReplicaSet,并更新 Deployment 的 status。ReplicaSet 控制器又负责创建 Pod,并更新 ReplicaSet 的 status。这种层层递进的关系,避免了控制器之间互相抢资源、互相覆盖配置的问题。写自定义控制器的时候也要遵守这个原则:你要管理的 CR,它的 spec 是用户的声明,你不能去篡改,只能通过创建子资源、更新状态来响应它。

可能有人会问:如果控制器不直接改 spec,那用户改了 spec 怎么办?比如把副本数从 3 改成 5,这时候控制器收到 Deployment 更新的通知,发现期望状态变了,就再去调整子资源。所以控制循环不光是处理“创建”事件,还要处理“更新”事件,甚至是“删除”事件。删除事件里,控制器通常需要做一些清理工作,比如删除相关的子资源或者外部资源。这些逻辑不全在 Reconcile 里,但都会被事件触发。

2.2 Reconcile 流程详解

调谐循环说起来抽象,拆成步骤其实很清晰。我以自定义控制器为例,把一个对象从产生事件到最终被处理的完整流程捋一遍。

第一步,API Server 收到资源变更请求,比如用户创建了一个自定义资源。第二步,控制器里的 informer watch 到这个事件,把对象更新到本地缓存,同时把事件分发给注册的 handler。第三步,handler 把对象的 key 放进工作队列,key 就是“命名空间/名字”。第四步,控制器的工作队列拿到 key,调用对应的 Reconciliation 逻辑。在 controller-runtime 里,这个逻辑就是 Reconciler 的 Reconcile 方法。第五步,Reconcile 方法从缓存里读取最新的对象,判断期望状态和实际状态差多少,决定需要执行哪些操作。第六步,操作完成后,控制器会更新对象的 status 或者返回一个 requeue 请求,告诉队列过一段时间再处理一次。

每一步都可能出错,所以 controller-runtime 做了很多封装来简化这个过程。比如它默认的 Builder 会自动根据你声明的资源类型生成 informer、handler 和 workqueue,你只需要实现 Reconcile 方法。但如果你用底层 client-go 自己写,这些都得手动组装。我见过不少直接用 client-go 写控制器的项目,逻辑一复杂就很容易在事件处理、队列管理上出错。这也是我后来推荐大家优先用 controller-runtime 的原因。

还有一个非常关键的概念:Reconcile 的职责是“让状态收敛”,而不是“处理某个事件”。比如你处理一个 CR 时,发现它对应的子资源已经存在,并且状态正常,那么你应该直接返回 nil,什么都不做。控制器每调谐一次,都要检查当前完整状态,而不是假设这次调用就是因为某个具体事件发生了。很多初学者在 Reconcile 里加一些只在创建事件时该执行的逻辑,结果更新事件触发时逻辑不生效,或者重复执行,这种设计基本都会出问题。

2.3 为什么控制器要幂等

在写控制器代码之前,必须先记住一个词:幂等。所谓幂等,就是同一个操作执行一次和执行多次,最终结果是一样的。Reconcile 方法不是只调用一次,而是会被反复调用:事件触发会调用、requeue 会调用、控制器重启后重新 list 一遍所有对象也会调用。所以你在 Reconcile 里写的每一条逻辑,都必须能接受“这个对象之前已经被处理过”的事实。

举个例子,你想在 CR 创建时自动创建一个 ConfigMap。如果直接写“创建 ConfigMap”,第一次 Reconcile 是成功的,但第二次 Reconcile 时,这个 ConfigMap 已经存在了,再调创建就会报 AlreadyExists 错误。正确的写法是:先从缓存里查一下 ConfigMap 是否存在,如果不存在就创建;如果存在,就检查内容是否和期望的一致,不一致就更新,一致就什么都不做。这样无论 Reconcile 被调用多少次,结果都是稳定的。

幂等设计带来的另一个好处,是控制器可以随时崩溃、随时重启。Kubernetes 里的控制器本身也只是一个运行在集群里的进程,它重启之后,会重新 List 所有对象,把每个对象都排队调谐一遍。这其实是自愈机制的一部分:如果控制器之前漏掉了某些对象,重启后的全量调谐会把状态重新拉回来。所以千万不要觉得“事件丢了没关系,反正有缓存”,缓存只是加速手段,全量调谐兜底才是控制器可靠性的保障。

3. 从零写一个自定义 Controller:选型与准备

3.1 常见框架选型

如果你准备写一个自定义控制器,第一个问题就是:用底层库还是用框架?目前主流选择有三类:直接使用 client-go、使用 controller-runtime、使用 operator-sdk。我个人的建议是,除非你是在研究原理或者有非常特殊的需求,否则优先选择 controller-runtime。

client-go 是 Kubernetes 官方维护的 Go 客户端库,它提供了 informer、workqueue 等基础组件,但没有帮你搭好控制器骨架。用 client-go 写控制器,你需要自己处理事件 handler、队列消费、优雅退出等一系列问题。好处是控制粒度非常细,适合对性能有极端要求、或者需要深度定制的事件处理逻辑的场景。但大多数业务类的控制器,并不需要这么底层。

controller-runtime 是 Kubernetes 社区推荐的控制平面库,也是 kubebuilder 和 operator-sdk 的底层依赖。它把 informer、workqueue、leader election、缓存同步这些事情都封装好了,让你只需要写一个实现 Reconciler 接口的结构体。它还提供 Manager 的概念,Manager 负责管理多个 Controller 的启动和生命周期。这套抽象非常贴近“业务逻辑”而不是“集群机制”,所以写起来效率很高。

operator-sdk 则是更上层的框架,通常和 kubebuilder 一起用,帮助你生成 CRD、脚手架、权限配置等。如果你的目标是开发一个完整的 Operator,而不是单纯写控制器逻辑,用 operator-sdk 可以省很多事。三种选型的对比如下:

方案封装程度适合场景缺点
client-go低研究原理、定制事件处理需要自己搭大量骨架代码
controller-runtime中大多数自定义控制器场景底层细节被屏蔽,排障需要额外理解
operator-sdk高快速开发完整 Operator、CRD 管理脚手架较重,抽象层级多

3.2 环境准备与最小工程结构

开发自定义控制器,环境上其实不需要特别复杂的东西:一台能联网的机器,装上 Go 语言环境和 Kubernetes 集群的访问凭证。我习惯先在本地用 kind 或者 minikube 起一个测试集群,开发调试完再往测试环境部署。用 controller-runtime 写代码之前,先初始化一个 Go module:

mkdir my-controller cd my-controller go mod init my-controller go get sigs.k8s.io/controller-runtime@latest

这里有个经验:如果你不是要用 kubebuilder 生成整套工程,而是手动写,建议直接参考 controller-runtime 仓库里的示例工程,别自己琢磨工程结构。项目的主要目录如下:main.go 负责启动 Manager、注册 Controller;controllers 目录放你的 Reconciler;api 目录放自定义资源的类型定义(如果你有 CRD)。不过如果只是做一个内部工具,可以先用已有的 Kubernetes 资源做管理目标,不需要定义 CRD,后面再逐步扩展。

最小可运行的 main.go 大概长这样:

package main import ( "flag" "os" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log/zap" // 引入你的 controller 包 ) func main() { var metricsAddr string flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "metrics 监听地址") flag.Parse() ctrl.SetLogger(zap.New(zap.UseDevMode(true))) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, }) if err != nil { os.Exit(1) } if err := setupControllers(mgr); err != nil { os.Exit(1) } if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { os.Exit(1) } }

这段代码的核心是 ctrl.NewManager,它会把 kubeconfig、缓存、客户端都初始化好。ctrl.GetConfigOrDie 会自动读取集群配置,本地运行时可以依赖 ~/.kube/config,集群内运行时依赖 service account 的挂载文件。SetupSignalHandler 负责监听 SIGINT 和 SIGTERM,让控制器退出时做一些清理工作。这个骨架很稳,我每次都是从这里开始的。

3.3 核心代码:Reconciler 实现

Reconciler 是控制器逻辑的核心。在 controller-runtime 里,它必须实现一个接口,接口定义就一个方法:Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)。req 里包含被调谐对象的 key,通常是“命名空间/名字”。Reconcile 方法的返回值里有 Requeue 字段,如果为 true,控制器会把这个 key 重新放回队列,稍后再处理一次。

我写一个最简单的例子:管理一个自定义的 HTTP 服务资源,自动创建对应的 Deployment 和 Service。这个例子看起来简单,但包含了大部分控制器常见操作。

func (r *HttpServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) var obj v1.HttpService if err := r.Get(ctx, req.NamespacedName, &obj); err != nil { // 对象不存在,说明被删除了,这里可以做清理逻辑 if apierrors.IsNotFound(err) { return ctrl.Result{}, nil } return ctrl.Result{}, err } // 1. 确保 Deployment 存在 dep := &appsv1.Deployment{} dep.Name = obj.Name dep.Namespace = obj.Namespace if _, err := ctrl.CreateOrUpdate(ctx, r.Client, dep, func() error { dep.Spec.Replicas = ptr.To[int32](obj.Spec.Replicas) dep.Spec.Selector = &metav1.LabelSelector{ MatchLabels: map[string]string{"app": obj.Name}, } dep.Spec.Template.Labels = map[string]string{"app": obj.Name} dep.Spec.Template.Spec.Containers = []v1.Container{ { Name: obj.Name, Image: obj.Spec.Image, Ports: []v1.ContainerPort{{ContainerPort: obj.Spec.Port}}, }, } return nil }); err != nil { return ctrl.Result{}, err } // 2. 确保 Service 存在 svc := &v1.Service{} svc.Name = obj.Name svc.Namespace = obj.Namespace if _, err := ctrl.CreateOrUpdate(ctx, r.Client, svc, func() error { svc.Spec.Selector = map[string]string{"app": obj.Name} svc.Spec.Ports = []v1.ServicePort{{Port: obj.Spec.Port, TargetPort: intstr.FromInt32(obj.Spec.Port)}} return nil }); err != nil { return ctrl.Result{}, err } // 3. 更新状态 obj.Status.Ready = true if err := r.Status().Update(ctx, &obj); err != nil { return ctrl.Result{}, err } return ctrl.Result{}, nil }

这里面的核心套路是 CreateOrUpdate。它会先尝试从缓存里读取目标对象,如果不存在就执行创建;如果存在,就执行你传入的 mutate 函数,然后做一次更新。这个工具函数是幂等设计的典型实现,我强烈建议在控制器里优先使用。如果你用原始 client 的 Create 方法,就一定要自己先查一遍,否则很容易在重复调谐时报 AlreadyExists。

还有几个细节值得注意。第一,在 mutate 函数里修改 Deployment 的 spec 时,不要随便改 selector,因为 selector 一旦被创建之后是不能改的,改了会报错。所以这里我把 selector 固定为基于名称的标签,避免了后续调谐时产生冲突。第二,更新 status 要调用 Status().Update 而不是 Update,因为 status 子资源有单独的权限控制。第三,Reconcile 里返回错误时,controller-runtime 会自动 requeue,但这意味着错误会持续重试,所以要尽量区分“暂时失败”和“永久失败”。暂时失败可以返回错误或者 RequeueAfter,永久失败应该记录 event 然后返回 nil 避免无效重试。

4. 调试、部署与排障实录

4.1 本地调试技巧

写控制器不能指望一次成功,本地调试能力非常重要。我第一次写自定义控制器的时候,直接在集群里部署,结果每次都是日志刷屏、状态不对,改起来特别慢。后来学聪明了,先在本地把控制器跑起来,连到测试集群调试。

把控制器跑在本地很简单:保证本地 kubeconfig 能访问目标集群,然后在命令行直接 go run main.go。这里的坑在于,本地跑的控制器用的是本地 kubeconfig 里的权限,它和部署到集群里的 service account 权限很可能不一样。如果你在本地调试时发现各种 forbidden,别急着怀疑代码,先看看 kubeconfig 对应的用户有没有操作目标资源的权限。我一般会在本地开发环境用 kubeconfig 指向测试集群,并且给这个用户绑定足够大的权限,比如 cluster-admin,这样至少不会因为 RBAC 问题干扰逻辑调试。等逻辑没问题了,再把它部署到集群里用最小权限的 RBAC 跑一遍,验证权限是否配齐。

日志也是调试的大杀器。controller-runtime 默认支持 zap 日志,开发模式可以用 DevMode 打开,会输出更详细的内容。你还可以在启动时加 --zap-devel --zap-log-level=debug,让 informer 和 workqueue 的状态都打出来。很多问题,比如“为什么我的 Reconcile 没触发”,从 Debug 日志里能看到事件到底有没有被 handler 接收。另外,不要往 Reconcile 里到处加 fmt.Println,那个在集群里基本看不到,要用 logger。我从 src 开发环境拿到的一条比较有用的经验是:所有关键分支,包括“对象不存在、子资源已存在、更新了状态”,至少打一行 Info 日志。这样上线之后出问题,你才能快速知道调谐停在哪一步。

4.2 部署到集群的注意事项

控制器写完之后,通常打包成镜像部署到集群里。部署本身和普通应用差不多:一个 Deployment、一个 ServiceAccount、一套 Role 和 RoleBinding(或者 ClusterRole 和 ClusterRoleBinding)。要注意的是,控制器的运行身份不能是默认的 default ServiceAccount,那样基本没有权限。你需要创建专用的 ServiceAccount,并绑定你对目标资源的操作权限。

这里我给一个最小 RBAC 示例。假设你的控制器要读写 HttpService、Deployment 和 Service,那么 Role 的大概内容如下:

rules: - apiGroups: ["example.io"] resources: ["httpservices"] verbs: ["get", "list", "watch", "update", "patch"] - apiGroups: ["example.io"] resources: ["httpservices/status"] verbs: ["get", "update", "patch"] - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: [""] resources: ["services"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

很多新手会漏掉 resources 里带 /status 的资源。你更新 status 时调用 Status().Update,本质上是对 httpservices/status 这个子资源的 update 操作,如果不单独授权,就会出现“明明 has permission to update httpservices,却报 forbidden”的诡异问题。这个坑我印象太深了。

部署时还有几个点需要注意。首先,如果你的控制器需要访问集群外部的资源,比如云厂商的 API,需要考虑网络访问策略。其次,控制器实例建议启用 leader election,尤其是多副本部署时。controller-runtime 的 Manager 自带 leader election 配置,只要在 ctrl.Options 里设置 LeaderElection: true 和 LeaderElectionID,就能保证同一时刻只有一个控制实例在调谐,避免两个副本同时操作同一个对象造成冲突。最后,一定要在 Deployment 里配置 resources 的 requests 和 limits,控制器也是一个程序,内存泄漏或者队列堆积都可能把节点拖垮。我见过一个没设 limits 的控制器,意外死循环后把节点内存吃满,导致同一节点上的业务 Pod 全部被驱逐,代价非常惨痛。

4.3 常见问题速查表

调试和运行控制器的过程中,有一批问题出现的频率极高。我整理了一张速查表,里面的每一条都是我或者周围同事实际踩过、并且从根因上排查过的。以后你遇到类似问题,可以先对照一下,别自己在那瞎猜。

问题现象可能原因解决办法
控制器启动后,Reconcile 从不触发CRD 没安装,或者 informer watch 不到目标资源检查 CRD 是否存在,使用 kubectl api-resources 确认资源被注册
日志报 forbidden,无法读取资源RBAC 权限缺失给控制器绑定的 ServiceAccount 增加对应资源的 get/list/watch 权限
更新 status 时报 forbidden缺少 status 子资源的 update/patch 权限在 RBAC 规则中加上 resources: ["xxx/status"] verbs: ["get","update","patch"]
对象被反复调谐,日志里同一个 key 疯狂刷屏每次 Reconcile 都返回 error,导致一直 requeue检查返回错误的位置,区分暂时失败与永久失败
CreateOrUpdate 一直报 AlreadyExists没有判断对象是否已存在,或者 mutate 函数里改变了不可变字段优先使用 ctrl.CreateOrUpdate,并检查 selector 等不可变字段是否被修改
修改 CR spec 后,子资源没有变化Reconcile 只处理了“创建”逻辑,没有处理“更新”逻辑在 Reconcile 里重新读取最新对象,并做状态对比,而不是假设只处理一次
控制器多副本部署后,两个实例同时操作未开启 leader election在 Manager 配置中设置 LeaderElection: true
控制器崩溃退出,但看不到报错没有捕获 panic 或者日志输出被吞使用 zap 日志并打开 develop 模式,确保输出到 stdout
本地调试正常,部署到集群后权限报错本地 kubeconfig 和集群内 ServiceAccount 权限不一致为集群内部署单独配置最小权限,不要以本地权限为准

除了表格里的问题,我再补充一个比较隐蔽的坑:缓存未同步。controller-runtime 的 Manager 启动时会先 List 所有关联资源放进缓存,Reconcile 触发之前,缓存可能还没完全同步。如果你依赖缓存去读对象,有时会读到“对象不存在”的假阴性。解决方式是不要自己在 Reconcile 一开始就查所有依赖资源,而是先确认主对象存在,再读子资源;在主对象 Get 的时候,如果对象确实存在,说明 informer 缓存已经同步到了主对象的创建状态。还有一个办法是利用 Manager 的 Runnable 机制,在 cache sync 完成后再启动 controller,但 controller-runtime 默认已经做了这个逻辑,一般不用手动处理。

5. 写 Controller 的几个实战经验

5.1 设计状态字段时,想清楚“可观测性”

很多控制器新手在定义 CRD 的 status 时,只简单放一个 Ready 布尔值,结果上线之后发现没法排查问题。实际上,status 字段不只服务于用户,更服务于控制器自己。我的习惯是,status 里至少要包含这几个部分:当前状态、资源的实际元数据、上次调谐时间、以及最近一次失败的描述。

比如你的控制器管理一个 Deployment,status 里可以放 observedGeneration、readyReplicas、conditions 列表。observedGeneration 是很关键的一个字段,它记录的是 controller 最后一次处理的 generation。当用户修改了 spec,资源的 generation 会增加,控制器发现 observedGeneration 落后于 metav1.Generation,就知道需要重新调谐。如果你不记录这个字段,就很难判断控制器是“没看到这次变更”还是“看到了但失败了”。conditions 列表可以用标准的 condition 类型,比如 Available、Progressing,这样 kubectl 和外部工具能和内置资源保持一致的用户体验。

多花一点时间设计 status,能省掉后面数不清的排查时间。我曾经为了赶进度,在一个内部控制器的 status 里只放了一个字符串字段,结果上线后用户频繁反馈“控制器没生效”,我只能靠日志去猜哪个对象处理到哪一步,简直是灾难。后来改成标准 conditions 之后,所有状态变化都清清楚楚。

5.2 模拟故障,验证自愈能力

最后分享一个我个人非常推崇的实践:控制器上线之前,一定要做故障演练。所谓故障演练,不是说把集群搞坏,而是针对你控制器的核心能力,故意制造一些异常场景,验证它能不能自愈。比如 Deployment 被意外删除,你的控制器能不能把它重新创建出来?用户修改了 CR 的 spec,把镜像 URL 改成一个不存在的地址,控制器会不会卡死还是一直重试?CR 被手动删除,控制器能不能把关联的 Service 清理干净?

做这些演练有几个小技巧。第一,把控制器的日志级别调到 debug,观察每次调谐的输入和输出。第二,使用 kubectl delete 故意删除子资源,然后观察它是否被自动重建。第三,模拟网络故障或者 API Server 短暂不可用,看看控制器的 workqueue 会不会堆积,恢复之后是继续处理还是卡死。这些演练看起来花时间,但比上线后被生产环境的问题打个措手不及要划算得多。

还有一点,控制器本身也是程序,你还要考虑它在极端情况下的行为。比如,目标资源的数量非常大,达到几万个,你的控制器每次 Reconcile 都做全量对比,性能可能撑不住。这时候可以考虑使用索引功能,只查询和当前对象相关的子资源,而不是 List 所有资源。在 controller-runtime 里通过缓存索引来实现,在 Manager 启动时注册一个索引字段,然后在 Reconcile 里用 IndexedField 查询,能省下大量 API Server 压力和内存占用。

我距离第一次写完一个能跑的自定义控制器,已经过去很长时间了,但每次用 controller-runtime 写新的调谐逻辑,还是会碰上各种意料之外的细节。控制器开发最大的门槛其实不是写代码,而是思维方式:你要从“处理一个个请求”切换到“保障一个状态始终收敛”。只要你习惯了用调谐循环的视角去看待所有逻辑,很多复杂问题都会变得异常清晰。如果以后你有机会负责一个稳定性要求高的平台,大概率会发现,把关键业务写成 Kubernetes 控制器,是所有自动化方案里最不容易失控的那一种。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询