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:“组装”多个模块(例如SourceQueueReconciler),实现Kubernetes Controller的通用逻辑:
    • 1)监听k8s资源,缓存资源,并根据EventHandler入队事件;
    • 2)启动多个goroutine,每个goroutine会从队列中获取event,并调用Reconciler方法处理。
  • Reconciler:状态同步的逻辑所在,是开发者需要实现的主要接口,供Controller调用。Reconciler的重点在于“状态同步”,由于Reconciler传入的参数是资源的NamespaceName,而非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所需的依赖。下面是一个样例,组装了一个ReplicaSetControllerController除了监听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()有三个参数,分别为SourceEventHandlerWatchesOption

1)Source负责watch相应的资源,将资源的event发送到队列中。其接口只包含一个Start()方法,由Controller调用,用于初始化watch操作所需的相关结构,比如eventHandlerqueue等。接口的几个实现在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只是将eventHandlerqueue等注册到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是否受“选举机制”的影响,将其分类到leaderElectionRunnablesnonLeaderElectionRunnables两个数组中,依据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也会通过ManagerCluster中获取集群相关的依赖,例如SchemeRESTMapper等。

Cluster

Cluster提供各种与集群相关的方法,开发者常用的接口包括:

  • 通过Cluster.GetEventRecorderFor()获取用于记录K8s Event的Recorder。
  • 通过Cluster.GetClient()获取K8s的Client,用于读写。
  • 通过Cluster.GetCache()获取后端的Cache。

另外,Cluster也会为Controller的创建提供共同的依赖,例如:

  • Controller.Watch()中,会通过Cluster.SetFields()注入cacheRESTMapper等。
  • Builder.Complate()中,会通过Cluster.Scheme()获取Scheme,从而获取Source对应的GroupVersionKind

Cluster的详细实现在pkg/cluster下,里面主要涉及两个关键类型:ClientCache

Cache

Cache开发者很少直接使用,因为一般不会直接对Cache进行操作。在Controller-runtime中,Cache的实现是InformerCache,当然,你也可以不使用自定义的Cache实现,通过创建manager时,设置manager.Options.NewCache参数,传入Cache的创建函数。

从下图可以看到,InformerCache中包含了三组specificInformersMap,分别用于支持structuredunstructuredmetadata三种资源类型,实现三类资源的List-watch。而specificInformersMap中包含了一个key为GroupVersionKind、value为MapEntry的Map类型,是为了支持多种资源的监听。MapEntry类似于Client-go中的GenericInformer,里面包含了Client-go中的SharedIndexInformerIndex,因此最终仍然是使用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也支持操作structuredunstructuredmetadata三种资源类型。

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下,定义了ClientRefecltor(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模块目前我还未使用过,后续使用后再补充。