Kubernetes Controller开发利器:controller-runtime
Controller-runtime是一个用于开发Kubernetes Controller的库,包含了各种Controller常用的模块,兼顾了灵活性和模块化。本文使用V0.8.0版本做介绍。
一开始做Kubernetes Controller开发时,是学习simple-controller使用client-go进行开发,中间会有很多与业务无关的重复工作。后来社区推出了kubebuilder,它可以方便的渲染出Controller的整个框架,让开发者只用专注Controller本身的业务逻辑,特别是在开发CRD时,极为方便,而kubebuilder渲染出的框架使用的则是controller-runtime。
主要模块
Controller-runtime中为Controller的开发提供了各种功能模块,主要包括:
Client
:用于读写Kubernetes资源Cache
:本地缓存,可供Client直接读取资源。Manager
:可以管理协调多个Controller,提供Controller共用的依赖。Controller
:“组装”多个模块(例如Source
、Queue
、Reconciler
),实现Kubernetes Controller的通用逻辑:- 1)监听k8s资源,缓存资源,并根据
EventHandler
入队事件; - 2)启动多个goroutine,每个goroutine会从队列中获取event,并调用
Reconciler
方法处理。
- 1)监听k8s资源,缓存资源,并根据
Reconciler
:状态同步的逻辑所在,是开发者需要实现的主要接口,供Controller调用。Reconciler的重点在于“状态同步”,由于Reconciler传入的参数是资源的Namespace
和Name
,而非event,Reconciler并非用于“处理事件”,而是根据指定资源的状态,来同步“预期集群状态”与“当前集群状态”。Webhook
:用于开发webhook server,实现Kubernetes Admission Webhooks机制。Source
:source of event,Controller从中获取event。EventHandler
:顾名思义,event的处理方法,决定了一个event是否需要入队列、如何入队列。Predicate
:相当于event的过滤器。
整体架构
Controller-runtime目的是提供一系列Kubernetes Controller开发的工具,可以从三方面去了解整个Controller-runtime:
- Controller是如何生成与管理?
- 开发者如何与集群交互?
- 有哪些额外工具可用?
Controller的生成与管理
总的来说,生成Controller用pkg/builder
,管理Controller用pkg/manager
。
Builder
pkg/builder
下的Builder
可以用于生成Controller,并提供了一系列配置Controller的方法,通过优雅的链式调用,可以组装出自己需要的Controller。
一般来说,在创建Controller之前,需要先创建manager,因为需要manager提供了创建Controller所需的依赖。下面是一个样例,组装了一个ReplicaSet
的Controller
,Controller
除了监听ReplicaSet
外,还会监听Pod,根据Pod的ownerReferences
入队相应的ReplicaSet
。具体的,先通过ControllerManagerBy()
中传入manager,在ControllerManagedBy()
和Complete()
之间,是一系列对Controller的配置(样例中调用For()
和Owns()
进行配置),最后在Complete()
中,会从manager中获取Controller的依赖,然后和传入的Reconciler
一起,用于创建Controller,并将创建的Controller注册到manager中。
err = builder.
ControllerManagedBy(mgr). // Create the ControllerManagedBy
For(&appsv1.ReplicaSet{}). // ReplicaSet is the Application API
Owns(&corev1.Pod{}, builder.OnlyMetadata). // ReplicaSet owns Pods created by it, and caches them as metadata only
Complete(reconcile.Func(myReconcile))
除了上面的For()
和Owns()
外,你还可以对Controller进行更多的配置,比如用WithEventFilter()
对Controller的事件进行过滤;用Named()
配置Controller的名称等;用Watches()
配置其他需要监听的资源。但整体上,你只需要“告诉”Controller “what to do“(监听什么资源?对应各种event做何种反应?),而”how to do“(如何监听事件,如何缓存对象,如何维护队列)都是由Controller以及其相关依赖完成的。
在开发复杂的Controller中,你可能要监听多个资源,并且监听的资源与”主资源“(主资源是指For()
中配置的资源类型,也是传入Reconciler
的元数据所指的资源类型)不存在附属关系,此时Watches()
就给了很大的灵活性。Watches()
有三个参数,分别为Source
、EventHandler
、WatchesOption
。
1)Source
负责watch相应的资源,将资源的event发送到队列中。其接口只包含一个Start()
方法,由Controller调用,用于初始化watch操作所需的相关结构,比如eventHandler
、queue
等。接口的几个实现在pkg/source/source.go
里,开发常使用的,是用于监听K8s的Source
实现:Kind
。
// Kind is used to provide a source of events originating inside the cluster from Watches (e.g. Pod Create)
type Kind struct {
// Type is the type of object to watch. e.g. &v1.Pod{}
Type client.Object
// cache used to watch APIs
cache cache.Cache
}
实际上Kind
不实现真正的watch操作,而是通过cache
(下面会详细介绍)来watch指定的资源,Kind
只是将eventHandler
、queue
等注册到cache中。使用Kind
时,你只需要设置完Type
就可以传递给Watches()
了,而在后续执行Controller.Watch()
时(在Complete()
中会调用),会自动调用 manager的SetFields
方法注入cache
,整个过程对开发人员是透明的。顺便说下,SetFields
是Controller用于从manager提取依赖的主要方法,每个Controller都会保存所属的manager的SetFields
函数引用。
func (cm *controllerManager) SetFields(i interface{}) error {
if _, err := inject.InjectorInto(cm.SetFields, i); err != nil {
return err
}
if _, err := inject.StopChannelInto(cm.internalProceduresStop, i); err != nil {
return err
}
if _, err := inject.LoggerInto(cm.logger, i); err != nil {
return err
}
// cluster.SetFields可以将cluster内的cache,注入到i中
if err := cm.cluster.SetFields(i); err != nil {
return err
}
return nil
}
2)EventHandler
是一个处理各种Event的接口,需要实现的是“对指定事件如何入队列”的逻辑。
type EventHandler interface {
// Create is called in response to an create event - e.g. Pod Creation.
Create(event.CreateEvent, workqueue.RateLimitingInterface)
// Update is called in response to an update event - e.g. Pod Updated.
Update(event.UpdateEvent, workqueue.RateLimitingInterface)
// Delete is called in response to a delete event - e.g. Pod Deleted.
Delete(event.DeleteEvent, workqueue.RateLimitingInterface)
// Generic is called in response to an event of an unknown type or a synthetic event triggered as a cron or
// external trigger request - e.g. reconcile Autoscaling, or a Webhook.
Generic(event.GenericEvent, workqueue.RateLimitingInterface)
}
Controller-runtime已经在pkg/handler
下,提供了四类handler:
EnqueueRequestForObject
:一个简单的实现,直接将Object的metadata入队列。上面的For()
就使用的它。Enqueue_mapped
:用的较多。Object在入队前使用用户实现的映射方法MapFunc()
做映射,将映射后的结果入队列。例如可以将Endpoint Event
映射为对应的Service,从而入队Service。
type MapFunc func(client.Object) []reconcile.Request
Enqueue_owner
:将Object的Owner资源入队列,上面的Owns()
就使用的它。Funcs
:一个空的父类,需要你实现接口的四个方法。
3)最后WatchesOption
用于修改Watch配置,目前在pkg/builder/options.go
中提供两种:
Predicates
:用于过滤事件。你可以自己实现,或者在pkg/predicate/predicate.go
中有些预设类型,比如AnnotationChangedPredicate
只过滤Annotation发生变化的event。
type Predicate interface {
// Create returns true if the Create event should be processed
Create(event.CreateEvent) bool
// Delete returns true if the Delete event should be processed
Delete(event.DeleteEvent) bool
// Update returns true if the Update event should be processed
Update(event.UpdateEvent) bool
// Generic returns true if the Generic event should be processed
Generic(event.GenericEvent) bool
}
OnlyMetadate
:用于告诉Controller,只缓存Watch对象的Metadata数据,用于提升性能。
Manager
manager主要是提供了Controller的依赖,并控制Controller的运行,通过如下函数创建。manager对Controller提供的许多依赖都包含在Cluster
中,我们后面介绍,这里先介绍manager对Controller运行的控制。
func New(config *rest.Config, options Options) (Manager, error){}
准确的来说,manager不是控制Controller,而是控制更广泛意义上的“可运行程序”,向manager中注册的都是接口Runnable
,你可以注册一个http server到manager中,用manager来启动http server,只要http server实现了对应的Start()
接口。要向一个manager中注册Runnable
可以使用接口Manager.Add()
,上面的Builder.Complete()
就调用了此接口。
type Runnable interface {
Start(context.Context) error
}
当一个Runnable通过Manager.Add()
方法注册到manager中后,manger会根据Runnable
是否受“选举机制”的影响,将其分类到leaderElectionRunnables
或nonLeaderElectionRunnables
两个数组中,依据Runnable
可能实现的func NeedLeaderElection() bool
方法的返回值进行划分,未实现此方法的会被归类到leaderElectionRunnables
中。
在调用Manager.Start()
方法以启动manager后,manager会通过goroutine运行所有注册的Runnable
,对于nonLeaderElectionRunnables
会直接运行,对于leaderElectionRunnables
会根据选举结果运行。在启动manager后,仍然可以通过Manager.Add()
方法将其他的Runnable
注册到manager中,一旦注册,Runnable
就会进入到上面的运行流程。
manager的选举机制使用k8s.io/client-go/tools/leaderelection
实现,可以通过创建manager时传入的manager.Options
参数设置,其他更详细的实现可以查看pkg/manager/internal.go
。
与集群的交互
一般开发Controller需要涉及到与集群的交互,例如从集群或缓存中的操作某个资源;生成K8s Event并记录到集群中。这些功能主要由Cluster
接口提供,而Manager
接口直接继承了Cluster
接口,因此,一般直接使用Manager
调用相应的方法。另外,各个Controller也会通过Manager
从Cluster
中获取集群相关的依赖,例如Scheme
、RESTMapper
等。
Cluster
Cluster
提供各种与集群相关的方法,开发者常用的接口包括:
- 通过
Cluster.GetEventRecorderFor()
获取用于记录K8s Event的Recorder。 - 通过
Cluster.GetClient()
获取K8s的Client,用于读写。 - 通过
Cluster.GetCache()
获取后端的Cache。
另外,Cluster
也会为Controller的创建提供共同的依赖,例如:
- 在
Controller.Watch()
中,会通过Cluster.SetFields()
注入cache
、RESTMapper
等。 - 在
Builder.Complate()
中,会通过Cluster.Scheme()
获取Scheme
,从而获取Source
对应的GroupVersionKind
。
Cluster
的详细实现在pkg/cluster
下,里面主要涉及两个关键类型:Client
和Cache
。
Cache
Cache
开发者很少直接使用,因为一般不会直接对Cache
进行操作。在Controller-runtime中,Cache
的实现是InformerCache
,当然,你也可以不使用自定义的Cache实现,通过创建manager时,设置manager.Options.NewCache
参数,传入Cache的创建函数。
从下图可以看到,InformerCache
中包含了三组specificInformersMap
,分别用于支持structured
、unstructured
、metadata
三种资源类型,实现三类资源的List-watch。而specificInformersMap
中包含了一个key为GroupVersionKind
、value为MapEntry
的Map类型,是为了支持多种资源的监听。MapEntry
类似于Client-go
中的GenericInformer
,里面包含了Client-go
中的SharedIndexInformer
和Index
,因此最终仍然是使用SharedIndexInformer
实现资源的List-watch ,使用Index
作为本地的缓存。
Client
Controller-runtime实现了多种Client,开发人员一般可以通过Manager.GetClient()
与Manager.GetAPIReader()
获取manager的Client,区别在于:
1)Manager.GetClient()
返回的Client可以用于Get、Update、Patch、Create等多种操作,但在Get、List时,优先从cache
中的读取;
2)Manager.GetAPIReader()
返回的Reader对象用于读操作,但会直接通过请求Kube-apiserver来获取结果。
与Cache一样,Manager.GetClient()
返回的Client也支持操作structured
、unstructured
、metadata
三种资源类型。
type client struct {
typedClient typedClient
unstructuredClient unstructuredClient
metadataClient metadataClient
...
}
开发人员还可以自己定义manager使用的Client,或者自定义Client是否使用Cache,通过创建manager时,设置manager.Options
相关的参数。
// manager.Options健康检查的配置
type Options struct {
...
// 自定义Client的创建方法
ClientBuilder ClientBuilder
// 设置哪些类型的资源不使用Cache
ClientDisableCacheFor []client.Object
...
}
额外工具
健康检查
在manager中集成了健康检查的功能,你可以通过Manager.AddHealthzCheck()
与Manager.AddReadyzCheck()
方法,注册自己的监控检查逻辑。在Manager.Start()
中,会启动一个HTTP Server,提供监控检查的返回结果,HTTP Server的端口、URL可以在创建Manager时,通过manager.Options
进行配置。
// manager.Options健康检查的配置
type Options struct {
...
// HealthProbeBindAddress is the TCP address that the controller should bind to
// for serving health probes
HealthProbeBindAddress string
// Readiness probe endpoint name, defaults to "readyz"
ReadinessEndpointName string
// Liveness probe endpoint name, defaults to "healthz"
LivenessEndpointName string
...
}
metric
Controller-runtime内置了许多prometheus
的监控指标,主要在pkg/metrics
下,定义了Client
、Refecltor
(Informer中的模块,负责List-Watch资源,并存储到缓存中)以及workQueue
相关的监控指标。除此之外在pkg/internal/controller/metrics
下还定义Controller状态同步的相关监控指标。
在manager启动时,会相应的启动metrics server,开发人员还可以通过manager.Options
配置metrics server的端口,或是通过pkg/metrics
下的Registry
方法自定义监控指标。
// manager.Options监控的配置
type Options struct {
...
MetricsBindAddress string
...
}
webhook
webhook模块目前我还未使用过,后续使用后再补充。