[RTFSC]Kubernetes 资源对象与控制器

prerequisite

  • 本文基于 kubernetes 1.18

基本数据结构

在 Kubernetes 中,最重要的一个概念就是资源,Kubernetes 将 Pod、Service、Deployment 等或具体的,或不具体的概念都抽象成了“资源”,并将资源分组 (Group) 和版本 (Version) 来进行管理。

由上,Kubernetes 源码中大量使用 Group、Version、Resource 这样的数据结构来定义一个资源,简称为 GVR,在 vendor/k8s.io/apimachinery/pkg/runtime/schema 中定义:

1
2
3
4
5
6
// vendor/k8s.io/apimachinery/pkg/runtime/schema/group_version.go
type GroupVersionResource struct {
Group string
Version string
Resource string
}

以 Deployment 资源为例,描述如下:

1
2
3
4
5
schema.GroupVersionResource{
Group: "apps",
Version: "v1",
Resource: "deployments"
}

k8s-basic-struct

如上图,Kubernetes 系统支持多个 Group,每个 Group 支持多个 Version,而每个 Version 中包含多个 Resource,其中部分资源同时会拥有自己的 SubResource。

每个资源都至少有两个版本,分别是外部版本 (External Version) 和内部版本 (Internal Version) 。外部版本用于对外暴露给用户请求的接口所使用的版本,内部版本仅用于 Kube API Server 使用。

每个资源都对应一定数量的资源操作方法 (Verbs) ,Verbs 用于对 Etcd 集群存储中的资源对象进行增、删、改、查的操作。

下面从源码中分析各个数据结构。

通用结构 APIResourceList

vendor/k8s.io/apimachinery/pkg/apis/meta/v1 中的 APIResourceList 数据结构可以描述所有 Group、Version、Resource 信息。

1
2
3
4
5
type APIResourceList struct {
TypeMeta `json:",inline"`
GroupVersion string `json:"groupVersion" protobuf:"bytes,1,opt,name=groupVersion"`
APIResources []APIResource `json:"resources" protobuf:"bytes,2,rep,name=resources"`
}

APIResourceList : 主要用于向外暴露特定资源组/资源版本中可用的资源名,以及是否 namespaced

GroupVersion : 同时描述了资源组/资源版本;

APIResources : 描述资源名与是否 namespaced ,详细信息如下。

更具体的,APIResource 信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
type APIResource struct {
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
SingularName string `json:"singularName" protobuf:"bytes,6,opt,name=singularName"`
Namespaced bool `json:"namespaced" protobuf:"varint,2,opt,name=namespaced"`
Group string `json:"group,omitempty" protobuf:"bytes,8,opt,name=group"`
Version string `json:"version,omitempty" protobuf:"bytes,9,opt,name=version"`
Kind string `json:"kind" protobuf:"bytes,3,opt,name=kind"`
Verbs Verbs `json:"verbs" protobuf:"bytes,4,opt,name=verbs"`
ShortNames []string `json:"shortNames,omitempty" protobuf:"bytes,5,rep,name=shortNames"`
Categories []string `json:"categories,omitempty" protobuf:"bytes,7,rep,name=categories"`
StorageVersionHash string `json:"storageVersionHash,omitempty" protobuf:"bytes,10,opt,name=storageVersionHash"`
}

singularName : 定义为资源的单数名,允许客户端隐式地处理单数个或复数个的资源,一般情况下更适合作为单数资源的状态回报,不过对于 kubectl CLI 接口而言,单数和复数的形式都是可以的;

Group : 为资源的首选组,空值表示包含资源列表的组;

Version : 为资源的首选版本,空值表示包含资源列表的版本;

Verbs :支持的 kube 资源操作方法;

ShortNames : 资源名的缩写,通常在 kubectl 的操作中用到;

Catagories : 资源组所属的资源大组;

StorageVersionHash : 用于校验数据库中的存储。


资源组 APIGroup

资源组在 Kubernetes API Server 中可以称其为 APIGroup,Kubernetes 系统中定义了不同的资源组,这些资源组按照不同功能将资源进行划分,资源组特点:

  • 资源分组管理,允许单独启用/禁用资源(组);
  • 便于资源组根据版本进行迭代升级;
  • 支持同名的资源种类 (Kind) 存在于不同的资源组内;
  • 允许开发者通过 HTTP 协议进行交互并通过动态客户端 (DynamicClient) 进行资源发现;
  • 支持 CRD 自定义资源扩展;
  • 用户交互简单。

vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go 中资源组表:

1
2
3
4
type APIGroupList struct {
TypeMeta `json:",inline"`
Groups []APIGroup `json:"groups" protobuf:"bytes,1,rep,name=groups"`
}

资源组结构定义:

1
2
3
4
5
6
7
type APIGroup struct {
TypeMeta `json:",inline"`
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
Versions []GroupVersionForDiscovery `json:"versions" protobuf:"bytes,2,rep,name=versions"`
PreferredVersion GroupVersionForDiscovery `json:"preferredVersion,omitempty" protobuf:"bytes,3,opt,name=preferredVersion"`
ServerAddressByClientCIDRs []ServerAddressByClientCIDR `json:"serverAddressByClientCIDRs,omitempty" protobuf:"bytes,4,rep,name=serverAddressByClientCIDRs"`
}

部分资源资源组名为空 (如 Pod、Service 等) ,这类没有组名的资源组被称为 Core Group (核心资源组) 或 Legacy Groups,也可被称为 GroupLess 。

而上述两类资源组不仅表现形式向有所不同,其形成的 HTTP PATH 路径也有不同:

  • 拥有组名的资源组的 HTTP PATH 以 /apis 为前缀,其表现形式为 /apis/<group>/<version>/<resource> ,例如:http://localhost:8080/apis/apps/v1/deployments
  • 没有组名的资源组的 HTTP PATH 以 /api 为前缀,其表现形式为 /api/<version>/<resource> ,例如 http://localhost:8080/api/v1/pods

版本控制 APIVersion

Kubernetes 中的资源版本控制类似于语义版本控制 (Semantic Versioning),在该基础上的资源版本定义允许版本号以 v 开头,例如 v1beta1 。这在保证了对旧版本功能进行兼容的情况下不断对新功能进行迭代开发,同时也让用户知道某个功能处在什么阶段。

目前 Kubernetes 内的资源版本控制分为 Alpha、Beta、Stable 三种,Alpha 阶段表示仅用于内部测试;Beta 表示已修复大部分不完善的地方,但仍存在漏洞与缺陷,由特定的用户群来进行测试;Stable 表示功能已经达到了一定的成熟度,可稳定运行。

vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go 中关于版本信息的定义:

1
2
3
4
5
type APIVersions struct {
TypeMeta `json:",inline"`
Versions []string `json:"versions" protobuf:"bytes,1,rep,name=versions"`
ServerAddressByClientCIDRs []ServerAddressByClientCIDR `json:"serverAddressByClientCIDRs" protobuf:"bytes,2,rep,name=serverAddressByClientCIDRs"`
}

ServerAddressByClientCIDRs 中建立了一个客户端 CIDR 到服务端地址的映射,使它们之间的网络访问更加高效。


APIResource

资源是 Kubernetes 中最重要的概念,虽然 Kubernetes 系统中有一系列相当复杂的功能,但是它本质上是一个资源控制系统——管理、调度资源与资源状态的维护。

一个资源被实例化之后会表达为一个资源对象 (Resource Object),Kubernetes 视其为一个实体 (Entity)。

可以通过 Kubenetes API Server 进行查询和更新每一个资源对象,Kubernetes 目前支持两种 Entity:

  • Persistent Entity : 在资源对象被创建后,Kubernetes 会持久确保该资源对象存在,大部分资源对象属于 Persistent Entity,如 Deployment 等;
  • Ephemeral Entity : 在资源对象被创建后,对于故障或调度失败不会重新创建该资源对象,如 Pod 等。

vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go 中关于 APIResource 的描述前面已经提到过,不再重复。


资源外部版本与内部版本

前面已经提到过资源是分为外部版本与内部版本的,外部版本资源对象用于对外暴露给用户请求的接口。一般我们使用中接触到的都是外部资源对象。

内部资源对象用于多资源版本的转换,不对外暴露。内部版本资源对象通过 runtime.APIVersionInternal 进行标识。

  • pkg/apis/core/types.go Pod 内部版本:

    1
    2
    3
    4
    5
    6
    7
    // pkg/apis/core/tyeps.go
    type Pod struct {
    metav1.TypeMeta
    metav1.ObjectMeta
    Spec PodSpec
    Status PodStatus
    }
  • vendor/k8s.io/api/core/v1/types.go Pod 外部版本:

    1
    2
    3
    4
    5
    6
    7
    // vendor/k8s.io/api/core/v1/types.go
    type Pod struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
    Spec PodSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
    Status PodStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
    }

可以发现由于外部版本的资源需要对外部暴露给用户请求的借口,所以资源代码需要定义 JSON Tags 和 Proto Tags,而内部版本只用于内部调用,并不需要这些。

补充分析

除了基本的数据结构之外,为了对资源整体的关系有更加深刻的理解,还需要对其他部分进行一些补充说明。

资源文件项目结构

Deployment 资源为例,内部版本定义位于 pkg/apis/apps 目录,拓扑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
./pkg/apis/apps
├── BUILD
├── doc.go
├── fuzzer
├── install
├── OWNERS
├── register.go
├── types.go
├── v1
├── v1beta1
├── v1beta2
├── validation
└── zz_generated.deepcopy.go

其中:

  • doc.go : GoDoc 文件,定义当前包的注释信息,在 Kubernetes 资源包中,同时也充当代码生成器的全局 Tags 描述文件;
  • register.go : 向 scheme 注册了资源组、资源版本信息;
  • types.go : 定义当前资源组、资源版本下所支持的资源类型;
  • v1v1beta1v1beta2 : 资源版本 (注意这里指的是外部版本),其中的 conversion.go 定义了资源的转换函数,并将默认转换函数注册到资源注册表中;
  • install : 将当前资源组下的所有资源注册到资源注册表中;
  • validation : 定义了资源的验证方法;
  • zz_generated.deepcopy.go : 定义了资源的深复制操作,由代码生成器生成。

每一个 Kubernetes 资源目录下,都通过 types.go 文件定义当前 GV 下所支持的资源类型。

资源注册与资源初始化

当然,由于 Kubernetes 中是由 kube-apiserver 的 legacyscheme 对全局资源进行统一管理,因此除了定义资源之外,还需要将所定义的资源注册到全局资源注册表 legacyscheme 中。

同样以 pkg/apis/apps 资源组为例:

1
2
3
4
5
6
7
8
9
10
11
12
func init() {
Install(legacyscheme.Scheme)
}

// Install registers the API group and adds types to a scheme
func Install(scheme *runtime.Scheme) {
utilruntime.Must(apps.AddToScheme(scheme))
utilruntime.Must(v1beta1.AddToScheme(scheme))
utilruntime.Must(v1beta2.AddToScheme(scheme))
utilruntime.Must(v1.AddToScheme(scheme))
utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta2.SchemeGroupVersion, v1beta1.SchemeGroupVersion))
}

apps.AddToScheme 函数向注册表注册 apps 资源组的内部版本资源,而如 v1.AddToScheme 则会向注册表注册 apps 资源组外部 v1 版本资源。

另外就在这顺便多提一句,scheme.SetVersionPriority 接收多个资源版本,此处定义的先后顺序决定了默认资源版本的优先级。


资源操作方法

这里同样是对前面提到的 verbs 的一个补充。

vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go 中,我们可以发现对 Verbs 的定义:

1
2
3
4
5
type Verbs []string

func (vs Verbs) String() string {
return fmt.Sprintf("%v", []string(vs))
}

由于资源对象最终都是要存储到 etcd 数据库中的,因此自然需要具有 “增删改查” 这样的基本方法和其他的一些扩展方法。

Verbs Interfaces Description
create rest.Creater() 资源对象创建
delete rest.GracefulDeleter() 资源对象删除
deletecollection rest.CollectionDeleter 资源对象删除 (多个)
update rest.Updater 资源对象更新 (fully)
patch rest.Patcher 资源对象更新 (partly)
get rest.Getter 资源对象获取
list rest.Lister 资源对象获取 (多个)
watch rest.Watcher 资源对象监控

以上,在 vendor/k8s.io/apiserver/pkg/registry/rest/rest.go 中,我们可以找到对各个接口的定义。

那么具体的实现在哪呢?

pkg/registry/core 下的核心资源为例,可以在各个核心资源项目文件中找到 storage 文件夹,storage 中就包含了对以上接口的实现。

Pod 为例,在 pkg/registry/core/pod/storage.go 中定义了 PodStorage ,其内封装了它的一些子资源对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
type PodStorage struct {
Pod *REST
Binding *BindingREST
LegacyBinding *LegacyBindingREST
Eviction *EvictionREST
Status *StatusREST
EphemeralContainers *EphemeralContainersREST
Log *podrest.LogREST
Proxy *podrest.ProxyREST
Exec *podrest.ExecREST
Attach *podrest.AttachREST
PortForward *podrest.PortForwardREST
}

关注第一个,也就是 Pod 资源对象本身,下面就是它的定义部分:

1
2
3
4
type REST struct {
*genericregistry.Store
proxyTransport http.RoundTripper
}

genericregistry.Store 中就封装好了 Pod 资源的操作代码,在 staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go 中可以找到对 Create、Update、Get 、Delete 等方法的实现。

1
2
3
4
5
6
7
8
9
10
11
func (e *Store) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {...}

func (e *Store) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {...}

func (e *Store) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {...}

func (e *Store) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {...}

func (e *Store) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {...}

...

再看 Pod 资源的子资源对象 Log ,其定义部分:

1
2
3
4
5
// pkg/registry/core/pod/rest/log.go
type LogREST struct {
KubeletConn client.ConnectionInfoGetter
Store *genericregistry.Store
}

Log 的资源操作方法就只有 Get :

1
func (r *LogREST) Get(ctx context.Context, name string, opts runtime.Object) (runtime.Object, error) {...}

runtime.Object

经过前面对资源对象数据结构的学习,对 Kubernetes 中“资源”的理解也算是更加深刻了。

前面提到的方法中,我们发现 runtime 出现的频率特别高,我们将 runtime 称作 “运行时” ,表示程序或语言核心库的实现。

而我们前面提到的所有资源对象的数据结构,其实都有一个共同的结构 runtime.Object

1
2
3
4
5
// staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}

runtime.Object 事实上是一个通用的资源对象,我们之前提到的 Pod、Deployment 其实都源于 runtime.Object

1
2
3
4
5
6
7
8
9
10
11
// staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}

// staging/src/k8s.io/apimachinery/pkg/runtime/schema/interfaces.go
type ObjectKind interface {
SetGroupVersionKind(kind GroupVersionKind)
GroupVersionKind() GroupVersionKind
}
  • GetObjectKind : 设置并返回 GVK;
  • DeepCopyObject : 用于深复制当前资源对象并返回,将数据结构克隆一份,因此它不语原始对象共享任何内容,使代码可以在不修改原始对象的情况下改变克隆对象的属性;
  • SetGroupVersionKind : 修改资源对象 GVK;