目录
我们知道,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 的交互。
通过图示,我们可以看到几个核心组件以及交互流程,以上蓝色部分是 client-go 组件,黄色部分是自定义 Controller 组件,各组件作用介绍如下:
简单的说,整个处理流程大概为:Reflector 通过检测 Kubernetes API 来跟踪该扩展资源类型的变化,一旦发现有变化,就将该 Object 存储队列中,Informer 循环取出该 Object 并将其存入 Indexer 进行检索,同时触发 Callback 回调函数,并将变更的 Object Key 信息放入到工作队列中,此时自定义 Controller 里面的 Process Item 就会获取工作队列里面的 Key,并从 Indexer 中获取 Key 对应的 Object,从而进行相关的业务处理。
本次演示环境,我是在本机 MAC OS 上操作,以下是安装的软件及版本:
注意:这里 Kubernetes 集群搭建使用 Minikube 来完成,Minikube 启动的单节点 k8s Node 实例是需要运行在本机的 VM 虚拟机里面,所以需要提前安装好 VM,这里我选择 Oracle VirtualBox。k8s 运行底层使用 Docker 容器,所以本机需要安装好 Docker 环境,这里忽略 Docker、VirtualBox、Minikube、Kubectl 的安装过程,可以参考之前文章 Minikube & kubectl 升级并配置, 这里着重介绍下 Kubernetes CRD 示例 sample-controller 的使用以及源码分析。
上一篇文章 部署 Prometheus Operator 监控 Kubernetes 集群 中,我们讲到 Prometheus Operator 部署了几种自定义资源类型,如 Alertmanager、Prometheus、ServiceMonitor,通过这些 CRD 资源,很轻松就能部署完整个监控系统,当时,就勾引了我的兴趣,通过几天的摸索和实践,也慢慢了解了 CRD 的工作机制和原理,接下来,我们通过官方示例 sample-controller 来演示下如何使用 Kubernetes CRD。
通过该示例 sample-controller,我们可以清楚的了解到:
Foo
Foo
的实例废话少说,直接操作一下吧!首先进入到本地 $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 类型
$ 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 是如何实现的。
我们通过源码简要分析一下,自定义 CRD Controller 是如何实现的。首先,在该实例项目根目录下存在两个主要实现文件:main.go 和 controller.go。
main.go
文件主要是作为整个程序的入口主启动程序,使用异步处理,调用 controller.go
中 Run
方法来启动 Foo
Controller。
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 函数等等操作。
首先需要定义一个 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
}
最新版本里,把 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,
}
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 过滤出来,并将对应的事件加入到工作队列中。
// 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
方法。
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"
)
参考资料