Kubernetes编程范式——Controller pattern

引子

Kubernetes的定位是非常明确和简单的,就是容器的编排与调度管理的系统。Kubernetes所关注的核心就是容器(Container)。Kubernetes提出的一个很重要的理念就是:不应该只管理单个容器,而应该管理容器组(Pod)。通过对Pod功能的扩展,可以进一步定义不同的对象。比如拥有多个Pod副本的Deployment,提供访问Pod功能的Service和只执行一次的Job等。

Kubernetes它工作的整个核心就是围绕这些存储在Etcd里的对象来工作的,这就是我们称之为Reconcile。前面所提到的Pod就是最小的API对象。

1. API对象

Kubernetes使用API对象来表示容器化的应用状态。一旦创建了一个API对象,Kubernetes会持续地工作保证该对象的当前状态和所需状态一致。通过Kubernetes API可以自由地操作对象的状态,这套机制支持在资源路径通过HTTP方法(methods)来创建,更新,删除和获取资源实例。

Kubernetes定义了下列几个术语来描述API对象。

  • Kind:对象的类型,每一个对象都有Kind字段来记录它的类型。比如常用的Job类型。
  • API Group:逻辑上相关的Kinds集合。比如Job和CronJob都在batch Group里。
  • Version:每个API Group可以存在多个版本。比如v1alpha1v1beta1等等。
  • Resource:代表Kubernetes的对象实体,比如.../namespaces/default

一个API对象在Etcd里的完整资源路径,是由上述的Group、Version和Resource三个部分组成的。

这里以Job举例:

apiVersion: batch/v1  
kind: Job  
metadata:  
  name: post-deployment-job
...

在这个YAML文件中,Job就是这个API对象的资源类型(Kind),batch就是它的组(Group),v2alpha1 就是它的版本(Version)。通过这样的结果就能够在整个Kubernetes的所有API对象中找到资源。

首先是匹配对象的组Batch,然后找到API对象的版本v1,最后匹配API的资源类型Job。更准确的说,Job的实际路径是/apis/batch/v1/namespaces/$NAMESPACE/jobs。因为Job不是cluster范围的资源,所以它需要指定具体的namespace。

回到我们的题目上来,Kubernetes面向API Object的编程模型有两个操作:

  1. 通过API在Etcd里面创建一个对象
  2. 通过控制循环(Control Loop)的来调协(Reconcile)所创建对象的状态

Kubernetes里面的所有组件,Scheduller,Kubelet等等,他们都维护了自己的一个Control Loop,相当于在不断地循环监控变化并进行调协。

什么叫做调协(Reconcile)?比如说我现在创建一个Pod,那么接下来Pod的启动,更新,删除都是通过相关的Controller进行。首先Controller获取Etcd里面的Pod资源的状态,这个状态是Pod的期望状态(desired state);并通过当前状态(current state)来决定下一步的操作。然后两个状态不一致,Controller需要更新Pod的当前状态,比如image不一致需要重建container。通过对比“期望状态”和“实际状态”的差异,完成了一次调协(Reconcile)的过程。

大家可以看到整个Kubernetes里面的所有组件,他们之间的协同都是通过来watch Etcd里面它所关心的Object的变化,然后再决定这个变化之后我要做什么。像这样的一种设计模式我们称之为Controller 范式。标准的“Kubernetes 编程范式”,即:

如何使用控制器模式,同 Kubernetes 里 API 对象的“增、删、改、查”进行协作,进而完成用户业务逻辑的编写过程。

2. Controller pattern

Controller监控一组Kubernetes内的资源并且维护资源对象在集群中的状态,维护的方法是通过定期监听资源对象状态并协调(reconcile)资源对象的当前状态和声明的期望状态。Kubernetes Master的组件中包含一组controllers来管理Kubernetes原生的资源类型,比如节点,Replication,Endpoints等。这些controllers统一由kube-controller-manager组件来管理。

Kubernetes被设计成通过上述声明式API来管理资源。当我们谈论“声明式的”的时候谈论的是什么?与指令式方法相比,声明式方法不会指定Kubernetes怎么去完成部署任务,只是描述任务目标的状态应该是什么样子。例如,当我们水平扩展一个Deployment时,我们并不会通过向Kubernetes发出“创建一个新Pod”的请求来创建新的pods。而是通过修改Deployment中的replicas属性来声明资源的变化。

那么所需要的Pod是怎么被创建的呢?这就是我上面提到的Controller来完成。对于每一次状态的变化,Kubernetes都会创建一个包含这个对象变化的事件,并广播给所有监听这个资源的监听器(listeners)。这些监听器会创建其他的事件分发给其他的Controllers。监听这些事件的Controllers再进行下一步操作。比如Deployment controller在scale up的时候会更新它所控制的ReplicaSets的属性。

https://github.com/kubernetes/kubernetes/blob/v1.13.6/pkg/controller/deployment/sync.go#L402-L421

func (dc *DeploymentController) scaleReplicaSet(rs *apps.ReplicaSet, newScale int32, deployment *apps.Deployment, scalingOperation string) (bool, *apps.ReplicaSet, error) {

    sizeNeedsUpdate := *(rs.Spec.Replicas) != newScale

    annotationsNeedUpdate := deploymentutil.ReplicasAnnotationsNeedUpdate(rs, *(deployment.Spec.Replicas), *(deployment.Spec.Replicas)+deploymentutil.MaxSurge(*deployment))

    scaled := false
    var err error
    if sizeNeedsUpdate || annotationsNeedUpdate {
        rsCopy := rs.DeepCopy()
        *(rsCopy.Spec.Replicas) = newScale
        deploymentutil.SetReplicasAnnotations(rsCopy, *(deployment.Spec.Replicas), *(deployment.Spec.Replicas)+deploymentutil.MaxSurge(*deployment))
        rs, err = dc.client.AppsV1().ReplicaSets(rsCopy.Namespace).Update(rsCopy)
        if err == nil && sizeNeedsUpdate {
            scaled = true
            dc.eventRecorder.Eventf(deployment, v1.EventTypeNormal, "ScalingReplicaSet", "Scaled %s replica set %s to %d", scalingOperation, rs.Name, newScale)
        }
    }
    return scaled, rs, err
}

值得注意的是,Controller监听的不是变化事件,而是资源对象的状态。监听者可以一次又一次地检查资源状态直到当前状态满足期望的条件。

这一组监听-更新的流程被称为状态协调(state reconciliation):当期望状态和当前状态不同时,Controller的任务就是协调对象使其状态成为期望状态。从这个角度来讲,Kubernetes本质上就是分布式状态管理器。你提供给Kubernetes 应用实例的期望状态,Kubernetes会通过一系列调用来努力维护实例的状态。

当然,Kubernetes这种通用型的应用编排平台无法满足所有的应用编排需求。Kubernetes支持在原生的通过扩展的方法来实现满足开发者特殊需求的Controller。

在进行定制化前,我们需要经一步的理解状态协调的过程。我们已经知道上述Kubernetes内置的Controllers在Master节点上管理标准的Kubernetes资源。这些Controllers之间是相互不可知的,各自在一个无尽的协调循环中监控他们所感兴趣的资源状态,并更新资源实例使其状态成为目标状态。

定制化的Controller可以通过和这些原生的Controllers一样的方式,来添加复杂应用的编排逻辑。定制化的Controller遵循同样的逻辑来监听系统中的事件并完成指定的行为。状态协调包含如下3个步骤:

  • 观察:通过监控Kubernetes资源对象变化的事件来获取当前对象状态。
  • 分析:确定当前状态和期望状态的不同
  • 执行:执行能够驱动对象当前状态变化的操作

observe-analyze-act cycle

例如,ReplicaSet controller会监控ReplicaSet对象的变化,并分析目标状态需要多少Pods运行,然后在向API Server请求创建缺少的Pods。Kubernetes的后端(准确的说是是Master上的kube-scheduler)负责在节点上启动新的Pod。

Controllers属于Kuberrnetes控制平面(control plane)的组件,成为了扩展平台管理复杂应用的标准机制。并且,一类名为Operator的框架基于Controller pattern实现了更加成熟的管理应用机制。从发展关系和复杂度来说,我们可以把这两类方法总结如下:

  • Controllers:一种简单的状态协调流程,监控和管理Kubernetes标准的资源对象。经一步说,Controllers是扩展平台行为并赋予平台新的功能。

  • Operators:一种复杂的状态协调流程,通常会和CRD(我上一篇文章有介绍)结合使用。CRD是Operatorfang范式的核心。通常来说,Operators会封装复杂应用的编排逻辑,使得应用的编排管理自动化。

最后,我找到一些值得学习的Controllers例子:

  • jenkins-x/exposecontroller:这个Controller监控Kube Service的定义。当他发现对象的metadata.annotation有exposed的存在就会自动地为这个Service创建一个Ingress对象。

  • fabric8/configmapcontroller: 这个Controller监控ConfigMap对象。当ConfigMap有变化时,他会对依赖这个ConfigMap的Deployment进行滚动更新。

  • Container Linux Update Operator:这个Controller会检查节点是否有相应的annotation,并重启节点。