Kubernetes CRD (CustomResourceDefinition) 自定义资源类型及源码分析

1、CRD (CustomResourceDefinition) 介绍

我们知道,Kubernetes 中一切都可视为资源,它提供了很多默认资源类型,如 Pod、Deployment、Service、Volume等一系列资源,能够满足大多数日常系统部署和管理的需求。但是,在一些特殊的需求场景下,这些现有资源类型就满足不了,那么这些就可以抽象为 Kubernetes 的自定义资源,在 Kubernetes 1.7 之后增加了对 CRD 自定义资源二次开发能力来扩展 Kubernetes API,通过 CRD 我们可以向 Kubernetes API 中增加新资源类型,而不需要修改 Kubernetes 源码或创建自定义的 API server,该功能大大提高了 Kubernetes 的扩展能力。它是 (TPR) ThirdPartyResource 的替代者,在 1.9 以上版本 TPR 将被废弃。

下图展示了 client-go 库各组件如何工作以及同自定义 Container 的交互。

Kubernetes CRD (CustomResourceDefinition) 自定义资源类型及源码分析_第1张图片

通过图示,我们可以看到几个核心组件以及交互流程,以上蓝色部分是 client-go 组件,黄色部分是自定义 Controller 组件,各组件作用介绍如下:

1.1 client-go 组件
Reflector:该组件是用来监测指定资源类型的 Kubernetes API,当监测 API 接收到新资源类型实例变化时,它将通过 List API 来获取新创建的 Object,并将其放入到 Delta Fifo queue(一个先进先出队列)中。
Informer:该组件是用来将 Delta Fifo queue 中的 Object 循环取出,并且保存 Object 供后边索引,并调用我们自定义 Controller 传递 Object。
Indexer:该组件是为 Object 提供索引功能,典型的用例就是通过 Object 的标签创建索引,并且使用线程安全的数据存储来存储 Object 以及它的 Keys。

1.2 Custom Controller 组件
Informer reference:该组件是知道如何使用自定义资源 Object 的 Informer 实例的引用,我们需要在自定义 Controller 代码中创建适当的 Informer。
Indexer reference:该组件是知道如何使用自定义资源 Object 的 Indexer 实例的引用,我们需要在自定义 Controller 代码中创建适当的 Indexer,并且将使用该引用处理后续检索 Object。
Resource Event Handlers:该组件是当 Informer 要部署 Object 到我们自定义 Controller 时,调用的 Callback 函数。这些函数可以获取被调度 Object 的 Key,并将 Key 存入工作队列以便进行下一步处理。
Work queue:该组件是我们自定义 Controller 中创建用来解耦一个处理中的 Object,也是上边 Resource Event Handlers 存储 Key 的地方。
Process Item:该组件是我们自定义 Controller 中创建用来处理 Work queue 的一些列函数,这些方法通常使用 Indexer reference 并检索该 Object 对应的 Key。

简单的说,整个处理流程大概为:Reflector 通过检测 Kubernetes API 来跟踪该扩展资源类型的变化,一旦发现有变化,就将该 Object 存储队列中,Informer 循环取出该 Object 并将其存入 Indexer 进行检索,同时触发 Callback 回调函数,并将变更的 Object Key 信息放入到工作队列中,此时自定义 Controller 里面的 Process Item 就会获取工作队列里面的 Key,并从 Indexer 中获取 Key 对应的 Object,从而进行相关的业务处理。

2、Kubernetes CRD 示例 sample-controller 使用

我们通过官方示例 sample-controller 来演示下如何使用 Kubernetes CRD。

通过该示例 sample-controller,我们可以清楚的了解到:

  • 如何通过 CRD 创建一个新的自定义资源类型 Foo

  • 如何创建、获取、List 该新资源类型 Foo 的实例

  • 如何在资源处理创建、更新、删除事件上设置 Controller

废话少说,直接操作一下吧!首先进入到本地 $GOPATH 目录,编译并启动该项目。

$ cd $GOPATH/src/k8s.io/sample-controller/
$ go build -o sample-controller .
$./sample-controller -kubeconfig=$HOME/.kube/config

然后,直接使用该示例提供的 CRD 模板文件来创建一个新资源类型 Foo,并创建一个 Foo 类型的资源实例。

创建 kind 为 Foo 的 CRD 类型

# 创建 kind 为 Foo 的 CRD 类型 
$ kubectl create -f artifacts/examples/crd.yaml
customresourcedefinition.apiextensions.k8s.io/foos.samplecontroller.k8s.io created
$ kubectl get crd
NAME                                    CREATED AT
foos.samplecontroller.k8s.io            2018-08-17T06:53:25Z

# 创建一个 Foo 类型的资源实例
$ kubectl create -f artifacts/examples/example-foo.yaml
foo.samplecontroller.k8s.io/example-foo created
$ kubectl get deployments
NAME          DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
example-foo   1         1         1            1           1m
$ kubectl get pods
NAME                          READY     STATUS    RESTARTS   AGE
example-foo-d74cd7fbc-2dnwn   1/1       Running   0          1m
$ kubectl get foo
NAME          CREATED AT
example-foo   1m

启动完毕,不过,大家肯定该是不懂,为什么就这么简单操作,就可以完成 Foo 资源类型的创建,并且创建了 example-foo deployments 实例呢?那么,我们看下这两个配置文件,到底配置了什么?

$ cat artifacts/examples/crd.yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: foos.samplecontroller.k8s.io
spec:
  group: samplecontroller.k8s.io
  version: v1alpha1
  names:
    kind: Foo
    plural: foos
  scope: Namespaced

该 CRD 模板定义了一个新的资源管理对象 Foo,在没有修改任何 kubernetes 内核代码条件下,仅仅通过定义 CRD 类型就完成了,非常方便又木有。这样一个新的命名空间 RESTful API 端点就创建了,例如该示例: /apis/samplecontroller.k8s.io/v1alpha1/namespaces/*/foos/…

$ cat example-foo.yaml
apiVersion: samplecontroller.k8s.io/v1alpha1
kind: Foo
metadata:
  name: example-foo
spec:
  deploymentName: example-foo
  replicas: 1

通过该 yaml 文件,可以创建新资源类型 Foo 的 Pod 实例 example-foo,注意这里 apiVersion: samplecontroller.k8s.io/v1alpha1 要跟 crd.yaml 中配置要匹配 /,Kind 指定为新资源类型 Foo。不过这里有人会有疑问,该 yaml 文件没有指定 Deployment 类型,只是指定了 deploymentName 就创建了名称为 example-foo 的 Deployment,而且通过详情可以看到实际上该 Deployment 指定了 nginx:latest 的镜像容器。

$ kubectl describe pod/example-foo-d74cd7fbc-2dnwn
Events:
  Type    Reason                 Age   From               Message

------

  Normal  Scheduled              1m    default-scheduler  Successfully assigned example-foo-d74cd7fbc-2dnwn to minikube
  Normal  SuccessfulMountVolume  1m    kubelet, minikube  MountVolume.SetUp succeeded for volume "default-token-rnj54"
  Normal  Pulling                1m    kubelet, minikube  pulling image "nginx:latest"
  Normal  Pulled                 4s    kubelet, minikube  Successfully pulled image "nginx:latest"
  Normal  Created                4s    kubelet, minikube  Created container
  Normal  Started                4s    kubelet, minikube  Started container

那么这个是怎么实现的呢?其实这就是自定义 CRD Contorller 中定义实现的。接下来通过源码,我们简单分析一下该自定义 Contorller 是如何实现的。

3、源码分析 CRD Contorller 的实现

我们通过源码简要分析一下,自定义 CRD Controller 是如何实现的。首先,在该实例项目根目录下存在两个主要实现文件:main.go 和 controller.go。

main.go 文件主要是作为整个程序的入口主启动程序,使用异步处理,调用 controller.go 中 Run 方法来启动 Foo Controller。

3.1 main 主启动程序

import (
    # 引入依赖包
    kubeinformers "k8s.io/client-go/informers"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
    clientset "k8s.io/sample-controller/pkg/client/clientset/versioned"
    informers "k8s.io/sample-controller/pkg/client/informers/externalversions"
    ......
)   
    # 设置信号标示以便后边异步接收该信号结束进程
    stopCh := signals.SetupSignalHandler()
    ......

    # 初始化 kubeInformer、exampleInformer Factory
    kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)
    exampleInformerFactory := informers.NewSharedInformerFactory(exampleClient, time.Second*30)

    # 初始化 Foo controller
    controller := NewController(kubeClient, exampleClient,
        kubeInformerFactory.Apps().V1().Deployments(),
        exampleInformerFactory.Samplecontroller().V1alpha1().Foos())

    # 异步启动 Factory
    go kubeInformerFactory.Start(stopCh)
    go exampleInformerFactory.Start(stopCh)

    # 调用 Run 启动函数并指定 replica 数量以及异步启动
    if err = controller.Run(2, stopCh); err != nil {
        glog.Fatalf("Error running controller: %s", err.Error())
    }
    .....

controller.go 是主处理文件,包括 Controller 的定义、初始化、启动、Callback 函数等等操作。

3.2 定义 Controller 结构体
首先需要定义一个 Controller 结构体,包含 deploymentsLister、foosLister、workqueue 等等

type Controller struct {
    kubeclientset kubernetes.Interface
    sampleclientset clientset.Interface

    deploymentsLister appslisters.DeploymentLister
    deploymentsSynced cache.InformerSynced
    foosLister        listers.FooLister
    foosSynced        cache.InformerSynced

    # 工作队列
    workqueue workqueue.RateLimitingInterface
    recorder record.EventRecorder
}

3.3 初始化 Controller
最新版本里,把 kubeInformer 和 fooInformer 初始化放在了 main.go 中,这里进行了 workqueue 初始化。

func NewController(
    kubeclientset kubernetes.Interface,
    sampleclientset clientset.Interface,
    deploymentInformer appsinformers.DeploymentInformer,
    fooInformer informers.FooInformer) *Controller {

    utilruntime.Must(samplescheme.AddToScheme(scheme.Scheme))
    glog.V(4).Info("Creating event broadcaster")
    eventBroadcaster := record.NewBroadcaster()
    eventBroadcaster.StartLogging(glog.Infof)
    eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclientset.CoreV1().Events("")})
    recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerAgentName})

    controller := &Controller{
        kubeclientset:     kubeclientset,
        sampleclientset:   sampleclientset,
        deploymentsLister: deploymentInformer.Lister(),
        deploymentsSynced: deploymentInformer.Informer().HasSynced,
        foosLister:        fooInformer.Lister(),
        foosSynced:        fooInformer.Informer().HasSynced,
        workqueue:         workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Foos"),
        recorder:          recorder,
    }

3.4 Deployment & Foo informer 监控资源 CRUD 操作的回调函数

    fooInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: controller.enqueueFoo,
        UpdateFunc: func(old, new interface{}) {
            controller.enqueueFoo(new)
        },
    })

    deploymentInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: controller.handleObject,
        UpdateFunc: func(old, new interface{}) {
            newDepl := new.(*appsv1.Deployment)
            oldDepl := old.(*appsv1.Deployment)
            if newDepl.ResourceVersion == oldDepl.ResourceVersion {
                return
            }
            controller.handleObject(new)
        },
        DeleteFunc: controller.handleObject,
    })

这里要说下,fooInformer 的处理函数调用 enqueueFoo 方法将对象状态变化事件存入到工作队列中,deploymentInformer 的处理函数调用 handleObject 方法处理对象的 Add、Update、Del 事件,并最终将对象存入到工作队列中。在这里,可以看到 Controller 主要针对以下几种资源事件进行了处理,一个是 Foo 资源的 Add、Update 事件处理,一个是 deployment 资源的 Add、Update、Delete 事件处理。这里 deployment 事件调用 handleObject 方法对所有 deployment 进行过滤,将 Foo 资源实例对应的 deployment 过滤出来,并将对应的事件加入到工作队列中。

3.5 启动 Controller

// workers to finish processing their current work items.
func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {
    defer runtime.HandleCrash()
    defer c.workqueue.ShutDown()

    // Start the informer factories to begin populating the informer caches
    glog.Info("Starting Foo controller")

    // Wait for the caches to be synced before starting workers
    glog.Info("Waiting for informer caches to sync")
    if ok := cache.WaitForCacheSync(stopCh, c.deploymentsSynced, c.foosSynced); !ok {
        return fmt.Errorf("failed to wait for caches to sync")
    }

    glog.Info("Starting workers")
    // Launch two workers to process Foo resources
    for i := 0; i < threadiness; i++ {
        go wait.Until(c.runWorker, time.Second, stopCh)
    }

    glog.Info("Started workers")
    <-stopCh
    glog.Info("Shutting down workers")

    return nil
}

在该 Run 函数调用 runWorker 函数运行 Workers 前,需要等待状态的同步完成,然后启动多个 worker 并发的从工作队列中获取待处理的 Item,真正进行业务处理的函数为 runWorker 方法。

3.6 runWorker 队列处理函数
runWorker函数是一个长期运行的函数,它调用 processNextWorkItem 函数来执行读取并处理工作队列上的消息。

func (c *Controller) runWorker() {
    for c.processNextWorkItem() {
    }
}

func (c *Controller) processNextWorkItem() bool {
    obj, shutdown := c.workqueue.Get()

    if shutdown {
        return false
    }

    err := func(obj interface{}) error {
        defer c.workqueue.Done(obj)
        var key string
        var ok bool
        if key, ok = obj.(string); !ok {
            c.workqueue.Forget(obj)
            runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
            return nil
        }
        if err := c.syncHandler(key); err != nil {
            return fmt.Errorf("error syncing '%s': %s", key, err.Error())
        }
        c.workqueue.Forget(obj)
        glog.Infof("Successfully synced '%s'", key)
        return nil
    }(obj)
......
}

工作队列中的每一个 item 都要调用 syncHandler 函数进行处理,其中就包括 Foo Deployment 的创建和更新。

func (c *Controller) syncHandler(key string) error {
    ......
    deployment, err := c.deploymentsLister.Deployments(foo.Namespace).Get(deploymentName)
    if errors.IsNotFound(err) {
        deployment, err = c.kubeclientset.AppsV1().Deployments(foo.Namespace).Create(newDeployment(foo))
    }
    ......
    if foo.Spec.Replicas != nil && *foo.Spec.Replicas != *deployment.Spec.Replicas {
        glog.V(4).Infof("Foo %s replicas: %d, deployment replicas: %d", name, *foo.Spec.Replicas, *deployment.Spec.Replicas)
        deployment, err = c.kubeclientset.AppsV1().Deployments(foo.Namespace).Update(newDeployment(foo))
    }
    ......
    err = c.updateFooStatus(foo, deployment)
    if err != nil {
        return err
    }

    c.recorder.Event(foo, corev1.EventTypeNormal, SuccessSynced, MessageResourceSynced)
}

每个 Deployment 事件都会调用 newDeployment 函数产生一个新的 Deployment 实例配置如下,这里就是上边提到疑问 没有指定 Deployment 类型,只是指定了 deploymentName 就创建了名称为 example-foo 容器镜像为 nginx:latest 的 Deployment,就是在这里定义的。

func newDeployment(foo *samplev1alpha1.Foo) *appsv1.Deployment {
    labels := map[string]string{
        "app":        "nginx",
        "controller": foo.Name,
    }
    return &appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name:      foo.Spec.DeploymentName,
            Namespace: foo.Namespace,
            OwnerReferences: []metav1.OwnerReference{
                *metav1.NewControllerRef(foo, schema.GroupVersionKind{
                    Group:   samplev1alpha1.SchemeGroupVersion.Group,
                    Version: samplev1alpha1.SchemeGroupVersion.Version,
                    Kind:    "Foo",
                }),
            },
        },
        Spec: appsv1.DeploymentSpec{
            Replicas: foo.Spec.Replicas,
            Selector: &metav1.LabelSelector{
                MatchLabels: labels,
            },
            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{
                    Labels: labels,
                },
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{
                        {
                            Name:  "nginx",
                            Image: "nginx:latest",
                        },
                    },
                },
            },
        },
    }

以上简单分析了两个主要的文件,而其中它调用的一些核心自定义函数,位于项目 pkg 目录下,该目录下的函数是基于 client-go 进行的调用以及扩展,建议大家细细研究下。

import (
    ......
    samplev1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1"
    clientset "k8s.io/sample-controller/pkg/client/clientset/versioned"
    samplescheme "k8s.io/sample-controller/pkg/client/clientset/versioned/scheme"
    informers "k8s.io/sample-controller/pkg/client/informers/externalversions/samplecontroller/v1alpha1"
    listers "k8s.io/sample-controller/pkg/client/listers/samplecontroller/v1alpha1"
)

原文地址

你可能感兴趣的:(k8s)