☰
深入理解controller-runtime Manager:启动流程、核心组件与生产实践
2026/10/19 6:22:39 网站建设 项目流程

1. Manager到底是个什么角色

1.1 一句话理解Manager

做过Kubernetes Operator开发的读者,大概率都跟controller-runtime打过照面。这个库几乎是现代Operator项目的默认底座,而Manager则是整个controller-runtime的顶层入口。我见过不少刚接触这个库的开发者,上来就直接在main函数里写ctrl.NewManager(...),然后盯着mgr.Start(ctx)发呆,完全不理解为什么一个"管理器"要承担那么多杂活。

简单说,Manager是controller-runtime所有共享依赖的容器和生命周期总管。它替你管好了全局的Scheme、RESTMapper、Cache、Client、EventRecorder、健康检查端点、指标端点,以及Leader Election这一类基础设施。Controller和Webhook是挂在这套基础设施上的业务单元,由Manager负责注册和协调运行。

如果只写一个极简Operator,不用Manager也可以做到,自己初始化client-go的ClientSet、自己建Informer、自己处理Leader Election,全部手写。但controller-runtime把这些高频、容易出错、重复劳动的部分抽象成Manager,让开发者只需关注Reconciler的业务逻辑。

1.2 Manager到底解决了什么问题

从最朴素的视角看,开发一个Controller需要以下几件事:一个能读写K8s资源的客户端,一个能监听资源变化并维护本地缓存的Informer框架,一套事件记录机制,一组健康检查和指标暴露端口。这些需求几乎是每个Controller都逃不掉的。

问题在于,如果每个项目都从零手搓,每个项目都可能踩一遍相同的坑。比如Informer的Resync周期设置不合理导致内存暴涨,或者Client直接读写API Server造成请求压力过大,再或者Leader Election的配置漏掉租约时长导致频繁抢锁。Manager把这些问题收敛成一套默认合理的机制:Cache机制缓存了读操作,写操作走API Server,Leader Election开箱即用,健康检查端点默认就绪。

所以Manager的核心价值不是提供一个"类",而是提供一套经过生产验证的"最佳实践骨架"。你不需要理解每个零件的内部原理也能跑起来,但如果想要在复杂场景下用好它,就必须把它拆开看清楚。

1.3 这篇分析适合谁看

本文适合已经用controller-runtime写过简单Controller、但还想深入理解Manager工作机制的开发者。我会把Manager的启动流程、核心组件、配置项逻辑、常见坑逐一拆开讲,附带一些实际排查经验。相信看完之后,你至少能回答这几个问题:Manager初始化时到底做了什么?mgr.Start()发生了什么事?为什么有的配置写了等于没写?Leader Election的候选者是怎么选出来的?

2. Manager的骨架——核心组件与职责拆解

2.1 对外暴露的五个核心能力

Manager的第一层价值在它向外部暴露的若干基础能力接口。这些接口通过mgr.GetClient()、mgr.GetCache()、mgr.GetScheme()这类方法对外提供,实际上它们背后各自对应一套独立机制。

先看Scheme。这是K8s API的类型注册表,Controller所有涉及GVK(GroupVersionKind)解析、对象序列化反序列化的操作都依赖它。Manager会在初始化时要求你提供一个*runtime.Scheme,后期所有通过Client或Cache触发的类型转换都受这个Scheme管辖。如果某个自定义CRD的GVK没有注册进Scheme,非常大概率会在运行时报出"No kind is registered for the type"这类问题。

再来看Cache。Manager默认构造一个Informer-based缓存,所有通过mgr.GetClient()发起的读请求,只要命中了缓存支持的范围,默认都会走Cache。这个设计大幅缓解了API Server的压力。Cache默认以Namespace为单位建立Informer,如果跨Namespace查询,要么配置Cache.DefaultNamespaces,要么显式提供List对象并设置cache.Options{...}。

然后是Client。Manager的Client是Cache和API Server的组合体:读多走缓存、写直接走API Server。这一点很多初学者会有误解,以为Manager的Client是一个和client-go的ClientSet完全等价的东西。实际上它是一个装饰器,通过client.NewDelegatingClient包装出来,背后同时持有直连API Server的Reader和走缓存的Reader。

再往后是EventRecorder。事件机制在Controller调试和可观测性中经常被忽视,但它恰恰是运维排查时最容易看到的信息来源。Manager提供了mgr.GetEventRecorderFor(name),一个Controller只需要在初始化时调用一次,就能稳定产出针对对象的Event。

最后是RESTMapper。它负责GVK到GVR(GroupVersionResource)的映射,是CRD动态操作的基础。Manager初始化时会通过apiutil.NewDynamicRESTMapper或apiutil.NewDiscoveryRESTMapper构造一个RESTMapper,后续所有需要把Kind转成Resource的操作都经由它。手写Controller时最容易漏掉的就是这个映射关系,导致自定义资源的增删改查出现"the server could not find the requested resource"。

2.2 从New到Start的启动全流程

Manager的生命周期大体分两个阶段:初始化阶段(ctrl.NewManager)和运行阶段(mgr.Start)。

初始化阶段做的事,核心可以归纳为三块。第一块是配置校验与默认值补齐,比如MetricsBindAddress没填的时候填默认值:8080,LeaderElection的相关参数做默认化,Scheme为空时自动新建一个。第二块是构造底层依赖,比如cache.New创建Cache,client.New创建DelegatingClient,apiutil.NewDynamicRESTMapper创建RESTMapper。第三块是把这些依赖组装进controllerManager结构体,等待后续被Controller注册和Start调用。

有一个初始化阶段容易忽略的点是manager.New会调用cluster.New,而这个cluster.New内部会初始化Cache和Client。这就意味着Controller如果通过ctrl.NewControllerManagedBy(mgr).For(&某资源{}).Complete(r)完成注册,它拿到的mgr.GetCache()和mgr.GetClient()已经是完全初始化好的实例,不需要、也不应该再单独初始化一份。

2.3 Runnable机制——Manager编排一切的基础

Manager能够统一调度Controller、Webhook、自定义后台任务,关键就在一个Runnable接口。这个接口定义非常简单,只有一个方法Start(ctx context.Context) error。

所有的组件都通过mgr.Add(runnable)注册进Manager的调度列表,Manager在运行时会启动一个event loop来逐个执行这些Runnable。比如一个Controller被注册之后,实际上会包装成runnable.Group,和Manager内部的其他Runnable一起被并发执行。

实现Runnable接口并不难,难点在于理解它的生命周期语义。Manager的Start方法会异步启动这些Runnable,在Start期间新Add的Runnable也有机会被启动。当Manager的context被取消时,所有Runnable会收到取消信号,触发各自的优雅退出逻辑。所以设计一个自定义Runnable时,一定要监听ctx.Done(),否则Manager退出时你这个后台任务就成了"僵尸协程"。

3. Manager启动流程深潜

3.1 初始化阶段到底做了什么

ctrl.NewManager这个入口是整个Manager的工厂方法,签名大概长这样:

func NewManager(config *rest.Config, options Options) (Manager, error)

其中rest.Config是从kubeconfig或in-cluster配置构建出来的REST客户端配置。Options包含了大量的可配置项。在实际项目中,我通常建议不要直接改Options里的零散字段,而是通过ctrl.Options{}构造后在字段层面做定制,这样可读性更高。

NewManager内部首先会做一轮配置默认化和校验。比如如果你没有设置Scheme,它会新建一个runtime.NewScheme(),但这个空Scheme意味着你后续所有用到类型注册的操作都会失败。又比如如果你没有设置LeaderElectionNamespace,它会根据LeaderElectionID是否存在来决定是否启用Leader Election。

接下来,NewManager会创建一个cluster.Cluster对象。这个Cluster是Manager的更底层抽象,承载Cache和Client的核心构建逻辑。Manager持有Cluster实例,并在后续通过mgr.GetCache()、mgr.GetClient()时直接委托给Cluster。

再之后,Manager会创建Metrics相关的端点。controller-runtime默认暴露一个/metrics端点,由prometheus注册表收集指标。如果你的Operator部署环境有独立的监控体系,通常会把MetricsBindAddress设为"0"来禁用内置端点,或者改成其他端口。这里有个小技巧:"0"这个字符串在源码里被当作关闭信号,不是常见的":8080",我第一次从0.x版本迁移过来时就踩过这个坑。

最后,NewManager会配置Leader Election所需的资源锁,比如resourcelock.New创建租约对象。它支持ConfigMap、Lease、Endpoints等锁类型,默认现在已经切到Lease。如果你不是特别想折腾历史兼容问题,建议直接用Lease。

3.2 内部循环与Controller注册

Manager完成初始化后,通常你会去注册Controller。这一步的入口是ctrl.NewControllerManagedBy(mgr),它返回一个Builder对象。Builder的核心能力是帮你把某个资源(For)、相关的Owner资源(Owns)、以及自定义事件源(Watches)组装成一个Controller,并自动生成对应的Controller对象注册到Manager中。

Builder在底层做的事情,其实是解析你传入的对象的GVK,然后用该GVK去构造source.Kind,再创建handler.EnqueueRequestForObject,最后调用ctrl.NewController创建一个具体的Controller,并把它作为Runnable添加到Manager里面。

有一个非常值得注意的点:For和Owns处理的是同一类Informer监听机制,但队列里事件的来源不同。For是把对象自身变化转化为该对象的 reconcile 请求;Owns则把子对象变化转化为父对象的reconcile请求。我在实际项目里看到过不少新手把所有资源都用For注册,结果导致事件数量膨胀、Reconcile暴增。正确的做法是:被协调的资源用For,被依赖的资源用Owns,除非你有明确的独立处理需求。

3.3 Start之后发生了什么

当一切就绪,调用mgr.Start(ctx)后,Manager会启动内部的多路协程。它先启动Cache的Informer同步,等待所有已注册资源的Informer完成synced。Cache没有Ready之前,Controller的事件源无法正常工作,所以Controller的启动实际上会被Cache的同步阻塞。

Cache同步完成后,Manager会启动所有已注册的Runnable,包括Leader Election的竞选逻辑。在多副本场景下,所有副本都会尝试获取Leader Election锁,但只有一个副本能成功。获得锁的副本才会真正启动Controller的Reconcile循环,其他副本则处于等待状态。

Leader Election的默认实现,其实是每个副本都在本地启动了一个LeaderElector,这个LeaderElector会定时刷新租约,持有锁的那个副本持续续约,其他副本不断尝试抢占或等待。当持有锁的副本异常退出后,租约到期,另一个副本会接棒成为新的Leader。这也是为什么你需要给Leader Election配置合理的LeaseDuration、RenewDeadline、RetryPeriod。

这里有一个很容易忽略的点:Manager的Start并不会等你所有的Runnable都优雅退出后才返回。它使用errgroup来管理多个goroutine,一旦其中一个Runnable返回错误,整个Manager都会触发取消。所以如果你的自定义Runnable内部有阻塞操作,不监听ctx.Done,Manager是无法快速退出的。

4. 关键配置项与选型逻辑

4.1 Scheme注册——最基础也最容易翻车的地方

Scheme是K8s API的类型注册表。在Manager里,Scheme决定了你对哪些GVK有"认知能力"。很多初学者会在NewManager之前把Scheme准备好,像这样:

scheme := runtime.NewScheme() clientgoscheme.AddToScheme(scheme) mygroupv1.AddToScheme(scheme)

这三行大概意思分别是:创建空Scheme,注册K8s内置类型(Pod、Deployment、ConfigMap等),注册你自己的CRD类型。缺了第一行第二行还不太明显,但缺了最后一行,几乎必然会在运行时报错。

还有一种情况是依赖间接注册,比如某些依赖库会在init()函数里自动AddToScheme,这在做单元测试时会造成全局状态污染。我在实际项目中就遇到过:某个依赖包会在init阶段往全局Scheme注册类型,结果本地测试和集群运行的Scheme内容不一致,定位了很久才发现问题根源。所以我的建议是,在项目入口显式注册所有依赖的类型,不要依赖隐式初始化。

4.2 Cache配置——内存与延迟的权衡

Manager的Cache默认基于Namespace维度建立Informer。如果直接mgr.GetCache().Get(ctx, key, obj),默认只能命中当前Namespace的内存缓存。对于多Namespace场景,我通常会在ctrl.Options{}里指定Cache.DefaultNamespaces,把需要监控的Namespace列表塞进去。

如果集群内资源量非常大,比如几十万量级的Pod,Cache会占用很大的内存。这时可以考虑把Cache.SyncPeriod调大,减少Resync带来的额外压力;或者考虑不用Cache,直接通过client.New构造一个直连API Server的Client。不过直连模式会失去Informers的性能收益,一般不建议全局这么做。

关于Cache的ReaderFailOnMissingInformer参数,最近几个版本里默认是false,意味着当请求的GVK没有对应的Informer时,Cache会fallback到API Server。这个行为对兼容性有好处,但也可能掩盖"该注册Informer但没注册"的问题。如果需要严格模式,可以把Cache.ReaderFailOnMissingInformer设为true,让这种失误在开发期直接暴露。

4.3 Leader Election——多副本的必修课

任何生产级Operator,只要你想多副本部署,Leader Election就是必须开启的能力。相关的配置项主要有四个:

  • LeaderElection:总开关。
  • LeaderElectionID:租约对象的名称,需要全局唯一。
  • LeaderElectionNamespace:租约对象所在的Namespace,默认是Controller所在的Pod的Namespace。
  • LeaseDuration、RenewDeadline、RetryPeriod:控制租约的持有时间、续约截止时间、重试周期。

我见过不少项目直接把LeaderElection设为true但忘了设置LeaderElectionID,然后Manager启动直接报错。其实源码里对这种情况有校验:LeaderElectionID没有设置时,会默认用options.LeaderElectionID为空字符串,然后创建租约资源时失败,错误信息并不直观。

多副本环境下的另一个坑是:Leader Election锁的Namespace如果和Pod所在Namespace不一致,RBAC可能没授权。比如你给ServiceAccount只授权了本Namespace的Lease资源,但把LeaderElectionNamespace配成了kube-system,那就会一直拿不到锁。所以配Leader Election时,要一起检查RBAC的Lease相关规则。

4.4 Metrics与健康检查端点

Manager默认暴露三个端口:MetricsBindAddress对应的/metrics、HealthProbeBindAddress对应的/healthz和/readyz。这三个配置分别对应Prometheus指标采集、存活探针和就绪探针。

容器化部署时,我会把HealthProbeBindAddress写成:8081,然后在Deployment的探针配置里指向/healthz和/readyz。这样K8s能感知Operator进程是否存活、是否Ready,便于滚动更新和自愈。

/readyz默认检查Manager内部所有Informer是否已经同步。如果你的Controller依赖某个CRD的Informer,而这个CRD没有安装到集群里,readyz会一直不通过。这种情况下,排查思路通常是检查CRD是否存在、RBAC是否有权限list/watch该资源。

Metrics端点也是我经常用来诊断Controller积压程度的入口。controller-runtime默认暴露controller_runtime_reconcile_total、controller_runtime_reconcile_errors_total、workqueue_depth等指标。如果你发现Reconcile成功次数停滞但workqueue_depth持续升高,说明Reconciler可能卡住了。这些指标对生产定位问题极有帮助。

5. 实操中的坑与排查技巧

5.1 Cache同步失败与"failed to list"错误

最常见的一个错误形态是:启动日志显示failed to list *v1.Pod,随后Manager不断重试。这个问题的根源通常是RBAC权限不足。Informer初始化时需要list和watch对应资源的权限,如果RBAC漏了,Cache就一直无法同步。

排查时第一步看Manager日志,找到具体是哪类资源list失败。第二步检查ServiceAccount的Role和RoleBinding是否覆盖了该资源。第三步确认LeaderElectionNamespace是否引发了权限范围外的访问。

还有一种隐蔽情况是集群API Server的资源版本与Controller编译时的版本不兼容。比如Controller内部用的某CRD的apiVersion是v1alpha1,但集群里只安装了v1beta1版本,Cache的RESTMapper映射不到对应GVR,list请求也会失败。这种问题通过kubectl get crd查看集群实际版本能很快确认。

5.2 Leader Election的"多Leader"危机

按理说Leader Election机制应该保证同时只有一个Leader执行Reconcile,但如果你配置失当,确实可能看到多个副本同时在Reconcile同一个资源。这种问题通常不是controller-runtime的bug,而是配置或网络问题。

一种可能是RenewDeadline和LeaseDuration设置得太接近,导致网络抖动时续约失败,旧Leader还没来得及释放锁,新Leader就抢到了。此时两个副本以为自己是Leader,并发执行Reconcile。解决办法是把LeaseDuration至少设为RenewDeadline的2倍,把RetryPeriod设为RenewDeadline的1/4左右。

另一种可能是你手动使用了mgr.GetCache().GetInformer去启动独立Informer,而该Informer不走Leader Election机制。也就是说,Controller的Reconcile虽然在Leader下运行,但你的自定义代码可能脱离Leadership约束,直接读取/处理资源。这种情况只能靠代码审查发现,所以在设计自定义后台任务时,要明确它是否必须在Leader下执行,必要时用mgr.Add配合Leader Election的Runnable接口做约束。

5.3 DelegatingClient为什么读不到刚写入的对象

Manager的Client读写分离设计,会带来一个常见困扰:通过mgr.GetClient().Create创建了一个对象,立即用mgr.GetClient().Get去查,可能查不到。原因是Create走了API Server直写,而Get走的是Cache,Cache里的Informer还没那么快同步到最新状态,也就是存在"写后读不一致"问题。

解决方式有几种。最简单的是在Reconcile逻辑中,对刚创建的对象不立即依赖其状态做下一步判断,而是等待下一个Reconcile事件触发。其次是使用client.New直接构造一个非缓存的Client,专门用于这类强一致场景。再或者,对你需要强一致读取的资源类型,在Cache配置里关闭缓存,让它直连API Server。

我在实际项目里推荐的是第一种:调整Reconcile逻辑,让创建操作返回的ObjectKey作为下一轮队列元素。事件驱动的模型本身就天然能弥合这个时间差,硬要用同步模型去套,反而和Informer机制拧着来。

5.4 自定义Runnable的优雅退出

实现Manager的自定义Runnable时,Start(ctx)方法如果只是简单地for { time.Sleep(...) },而不监听ctx.Done(),那Manager的优雅停机流程会卡住。你的Runnable还是一个死循环的话,mgr.Start(ctx)返回了,但你的goroutine还在跑。

正确姿势是:

func (r *myRunnable) Start(ctx context.Context) error { for { select { case <-ctx.Done(): return nil default: // do something } } }

或者用更简洁的写法,直接启动一个goroutine然后监听ctx.Done():

func (r *myRunnable) Start(ctx context.Context) error { go func() { // 业务逻辑 }() <-ctx.Done() return nil }

第二种写法的好处是,启动即返回,Manager能快速感知Runnable"启动成功",然后业务协程独立运转。坏处是业务逻辑的goroutine可能在Manager退出后仍然残留,所以内部还是要配合ctx.Done()做退出。

5.5 KubeConfig超时与限流配置

凡是生产环境的Operator,都建议在rest.Config上做QPS和Burst的配置,否则默认值在大量资源场景下会触发API Server限流。Manager接收的*rest.Config实际上是从ctrl.GetConfigOrDie()或ctrl.GetConfig()拿到的,这些方法从kubeconfig加载配置,但默认的QPS/Burst并不高。

我通常在main函数里这样处理:

cfg := ctrl.GetConfigOrDie() cfg.QPS = 100 cfg.Burst = 150

然后在NewManager时传入这个cfg。如果不设置,遇到大规模集群很可能出现client rate limiter相关日志,Reconcile请求被限流丢弃,Controller越积越慢。

另外Timeout字段也值得设一下,比如cfg.Timeout = 30 * time.Second,这会影响所有通过该Config发起的API请求的默认超时,能有效避免API Server无响应时请求挂死。

6. 一个具备生产参考价值的Manager初始化案例

这部分给出一段我在实际项目中经常使用的Manager初始化骨架,重点标注哪些地方需要按业务调整。

package main import ( "fmt" "time" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/manager" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" ) var ( scheme = runtime.NewScheme() ) func init() { _ = clientgoscheme.AddToScheme(scheme) // _ = mygroupv1.AddToScheme(scheme) } func main() { ctx := ctrl.SetupSignalHandler() cfg := ctrl.GetConfigOrDie() cfg.QPS = 100 cfg.Burst = 150 mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{BindAddress: ":8080"}, HealthProbeBindAddress: ":8081", LeaderElection: true, LeaderElectionID: "operator-leader-election-demo", LeaseDuration: func() *time.Duration { d := 30 * time.Second; return &d }(), RenewDeadline: func() *time.Duration { d := 15 * time.Second; return &d }(), RetryPeriod: func() *time.Duration { d := 4 * time.Second; return &d }(), Cache: cache.Options{ DefaultNamespaces: map[string]cache.Config{ "default": {}, }, }, }) if err != nil { panic(err) } if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { panic(err) } if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { panic(err) } // 注册自定义Webhook if err := (webhook.NewServer(webhook.Options{ Port: 9443, })).Register("/validate", &customValidator{}); err != nil { panic(err) } if err := mgr.Start(ctx); err != nil { fmt.Println("manager start failed", err) } }

这个骨架中有几个值得展开说的地方。第一,ctrl.SetupSignalHandler()返回的ctx会监听SIGINT和SIGTERM,保证K8s滚动更新时Operator能够优雅退出。第二,Cache.DefaultNamespaces限制了Cache只监控default命名空间,这对那些只关心单Namespace的Operator能显著降低内存和CPU消耗。第三,AddReadyzCheck只做了基础Ping,如果你的Operator强依赖某个外部系统,建议在这里扩展一个自定义检查函数。

Webhook的注册有一个小坑:webhook.NewServer创建出来的Server本身也是一个Runnable,Manager启动后会同时运行它。如果Webhook的证书没有准备好,Server启动时报错,Manager也会跟着退出。所以生产环境要确保证书挂载和生成时机正确。

7. 实战排查实录——一个DNS和缓存交织的案例

最后分享一个我实际排查过的案例。当时某公司在测试环境部署一个Operator,发现它偶尔对自定义CRD的更新不敏感,明明改了CRD状态,Controller却没有触发Reconcile。

我们先查了Event和日志,发现Reconcile队列里根本没有新条目。再查Cache日志,看到Informer确实在运行,但LIST响应中查找的字段和预期不一致。

进一步排查发现,问题出在RESTMapper和Cache之间的配合上。自定义CRD的Versions列表里按字母顺序排序时,Controller选中的版本不是我们期望的版本,导致Cache监听的是另一个version的资源,而CRD的Status更新发生在另一个version上。这个情况在CRD有多个版本并存、且转换Webhook配置不当时尤其常见。

解决办法是显式指定Controller监听的GVK,比如:

For(&mygroupv1.MyCRD{}, builder.WithPredicates(predicate.GenerationChangedPredicate{}))

同时在Builder链中确保用到的资源版本是明确的。另外一个辅助手段是打开--v=5级别的日志,能看到Informer同步的具体版本信息,帮助快速定位。

这个案例让我更加确认:Manager虽然替我们省了很多事,但底层涉及Cache、RESTMapper、Scheme、Leader Election这些机制时,仍然需要开发者有足够的背景知识去驾驭。K8s的Informer机制和API多版本策略,是理解controller-runtime一切行为的基石。

8. 写在最后的实操体会

如果你已经读到这儿,说明你对Manager的兴趣不止于能用,而是想用明白。以我自己的经验,使用controller-runtime的Manager最有效的学习路径是:先在main.go里彻底读一遍Manager的默认行为,再试着关掉每一项默认能力,看看你的Operator会缺什么。比如把Leader Election关掉,然后多副本部署一次,观察Reconcile重复执行。你会发现,Manager每个默认配置背后都有它的道理。

还有一个小技巧:在开发阶段,把Manager对应的日志级别调到5,你会看到它启动时做了大量工作,包括从API Server拉取GVR映射、建立Informer、同步Cache。这些日志初看是噪音,但当你需要排查"为什么Controller没生效"时,这些信息比任何Debugger都更接近真相。

Manager这个组件值得花一整个系列去研究,本文着重讲的是它是谁、启动时做什么、有哪些关键配置、以及最常见的坑。后续我还会继续写controller-runtime的Controller工作队列、Predicate与事件过滤、Webhook集成等主题。如果你也在用这个库,欢迎在评论区聊聊你踩过的Manager相关的坑,或者你自定义Runnable的做法。

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

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

立即咨询