Kubernetes API Scheme 解析

概述

Kubernetes API 多版本和序列化 这篇文章中,介绍了API多版本的功能和实现原理,其中Scheme就是其实现原理的一项重要机制,在平时的开发中也经常会遇到,本篇文章就对其进行下分析。

Scheme起到了一个类型(Type)注册中心的作用,在API Server内部,全局只有一个Scheme实例,各个版本的API资源,会将他们的类型,注册到Scheme中来,同时,也会将如何进行类型转换的方法注册到Scheme中来,后续在Handler中进行版本转换以及序列化时,则会使用Scheme中注册的类型创建对应版本的对象,以及使用注册的类型转换的方法对不同版本的对象进行转换。

什么是类型

所以,理解什么是类型,即Type,很关键,我觉得可以简单的将类型理解为一个Go Struct的定义,就是各种API资源的结构体定义,可以从这个类型直接创建出来该结构体的实例,而不用直接使用该结构体去创建,这到底是怎么实现的呢?答案就是反射,即Reflect

关于反射,这里不过多解释,建议提前阅读下官方的这篇博客,The Laws of Reflection,比较清晰。这里我们就举个简单的小例子来实际感受下:

// 目录结构
.
├── go.mod
├── main.go
├── types.go

// types.go
package main

type Foo struct {
  X1 string
  X2 string
}

// main.go
package main

import (
  "fmt"
  "reflect"
)

func main() {
  f := &Foo{}
  t := reflect.TypeOf(f).Elem()
  fmt.Println(t) // main.Foo
  fmt.Println(t.Name()) // Foo

  v, _ := reflect.New(t).Interface().(Foo)
  v.X1 = "nice"
  v.X2 = "woce"
  fmt.Println(v) //{nice woce}

  fv := Foo{X1: "nice", X2: "woce"}
  fmt.Println(fv) //{nice woce}
}

可以看到在types.go中定义了一个Foo结构体,有两个属性X1和X2,然后在main方法中,先创建了一个空的Foo实例,将其指针赋值给 f,然后通过 reflect.TypeOf(f).Elem() 得到的值 t 就是Foo结构体的 类型,有了这个类型,就可以通过 reflect.New(t).Interface() 创建一个该类型的实例,但是这得到的只是一个interface类型的实例,还需要将其转换成具体的Foo类型的实例才能使用,这样就相当于创建了一个Foo结构体的实例 v,跟下面的 fv 直接使用Foo结构体创建的实例其实是等价的。

所以,反射其实还是挺好理解的,就是给一个变量,能够通过反射,知道该变量的类型以及具体的值,很多语言里都有反射的机制,像最熟悉的Python,可以通过 getattr()setattr()方法去获取、设置某个变量的属性,还有通过 __import__() 方法动态的根据一个字符串路径去导入一个模块。

某种程度上,Go反射里面的 类型 Type 其实就跟Python里的 __import__() 有异曲同工之处,知道了一个字符串,就可以导入一个模块,知道了一个类型,就可以去实例化一个它的对象,所以Scheme就是这样一个类型注册中心,把所有的API资源结构体的类型全注册进来,需要时,就找到对应资源的类型,然后实例化一个它的对象。

我们再来把上面的例子稍微扩展一下,用简单的例子模拟下Scheme中的用法:

// 目录结构
.
├── go.mod
├── main.go
├── meta
│   └── types.go
├── types.go
├── v1
│   └── types.go
└── v2
    └── types.go

// meta/types.go
package meta

type Status struct {
  X1 string
}

// types.go
package main

import (
  "testgo/meta"
)

type Foo struct {
  X1 string
  X2 string

  Status meta.Status
}

// v1/types.go
package v1

import (
  "testgo/meta"
)

type Foo struct {
  X1 string

  Status meta.Status
}

// v2/types.go
package v2

import (
  "testgo/meta"
)

type Foo struct {
  X1 string
  X2 string

  Status meta.Status
}

// main.go
package main

import (
  "fmt"
  "reflect"
  "testgo/v1"
  "testgo/v2"
  "testgo/meta"
)

func main() {
  f := &Foo{}
  t := reflect.TypeOf(f).Elem()
  fmt.Println(t) // main.Foo
  fmt.Println(t.Name()) // Foo

  v, _ := reflect.New(t).Interface().(Foo)
  v.X1 = "nice"
  v.X2 = "woce"
  v.Status = meta.Status{X1: "tace"}
  fmt.Println(v) //{nice woce {tace}}

  fv := Foo{X1: "nice", X2: "woce", Status: meta.Status{X1: "tace"}}
  fmt.Println(fv) //{nice woce {tace}}

  f1 := &v1.Foo{}
  t1 := reflect.TypeOf(f1).Elem()
  fmt.Println(t1) // v1.Foo
  fmt.Println(t1.Name()) // Foo

  f2 := &v2.Foo{}
  t2 := reflect.TypeOf(f2).Elem()
  fmt.Println(t2) // v2.Foo
  fmt.Println(t2.Name()) // Foo

  fmt.Println(t1 == t2) // false

  s1 := &meta.Status{}
  s2 := &meta.Status{}
  t3 := reflect.TypeOf(s1).Elem()
  t4 := reflect.TypeOf(s2).Elem()

  fmt.Println(t3 == t4) // true
}

上例中,在原来的基础上,又添加了一个meta.Status结构体,并且添加了v1, v2版本的Foo,而且给每个版本的Foo都加了一个meta.Status属性,然后分别获得了他们的类型:t, t1, t2, t3, t4,从上面的 t1 == t2 为 false,可以判断t1和t2是两个不同的类型,虽然他们都叫Foo,而 t3 == t4 为true,说明他们是同一个类型,虽然是从两个对象上获取的类型,所以本质上,每一个结构体的定义,就对应着一个类型,不论这个结构体定义在哪里,只要我们知道了它的类型,就能够实例化它。

而在Kubernetes中,类型一般是比较复杂的,一个API资源类型会定义很多个字段,而且类型是分版本的,而版本又分内部版本和外部版本,所以这个类型就是多版本API的基础。回过头来看看上一个小节提到的 FlowScheme 示例,k8s.io/api/flowcontrol/v1beta2/types.gok8s.io/api/flowcontrol/v1beta3/types.go 中定义的Struct就是内部版本的类型,并且从上面的分析可以知道,v1beta2 中的 FlowSchemav1beta3 中的 FlowSchema 其实是两个类型,属于不同的版本,虽然他们的名字一样,但是他们里面的属性可能会有差别,而且他们是定义在单独的第三方库 k8s.io/api 中的,可以独立发布,方便客户端进行引用,而 kubernetes/pkg/apis/flowcontrol/types.go 中定义的Struct则是内部版本的类型,因为只在Kubernetes内部使用到,所以放到了Kubernetes代码目录树内,是Kubernetes本身的一部分。在Kubernetes中,所有的内部版本的类型,都放到了 kubernetes/pkg/apis/ 目录下,而所有的外部版本的类型,都放到了 k8s.io/api 项目中,然后都以组的方式进行分类管理。

理解了类型,我们就比较好理解Scheme了,是时候祭出Scheme的类图,来近距离看看它了:

它的核心代码位于 k8s.io/apimachinery/pkg/runtime/scheme.go 中,k8s.io/apimachinery 也是一个第三方库,跟 k8s.io/api 类似,都是为了方便客户端开发引用,所以才从Kubernetes主代码树里剥离出来的,可以看到Scheme还是一个比较复杂的结构体,属性虽然不多,但是方法很多,而且实现了很多接口,我们来介绍几个比较主要的内容,先忽略一些不重要的信息,否则内容太多。

类型注册

首先最最重要的就是 gvkToTypetypeToGVK 这两个map了,他们就是存放注册进来的类型的,通过下面的 AddKnownTypes()AddKnownTypeWithName()方法注册进来,在该方法中,就调用了上面示例中提到的 reflect.TypeOf(f).Elem() 方法去获取一个对象的类型,我们先来看看这个方法:

func (s *Scheme) AddKnownTypes(gv schema.GroupVersion, types ...Object) {
  s.addObservedVersion(gv)
  for _, obj := range types {
    t := reflect.TypeOf(obj)
    if t.Kind() != reflect.Pointer {
      panic("All types must be pointers to structs.")
    }
    t = t.Elem()
    s.AddKnownTypeWithName(gv.WithKind(t.Name()), obj)
  }
}

func (s *Scheme) AddKnownTypeWithName(gvk schema.GroupVersionKind, obj Object) {
    ......
    t := reflect.TypeOf(obj)
    ......
    if t.Kind() != reflect.Pointer {
        panic("All types must be pointers to structs.")
    }
    t = t.Elem()
    if t.Kind() != reflect.Struct {
        panic("All types must be pointers to structs.")
    }
    ......
    s.gvkToType[gvk] = t

    for _, existingGvk := range s.typeToGVK[t] {
        if existingGvk == gvk {
            return
        }
    }
    s.typeToGVK[t] = append(s.typeToGVK[t], gvk)
    ......
}

可以看到从obj中解析出该对象的Type(类型)之后,会将Type与GVK的对应关系分别存到 gvkToTypetypeToGVK 两个map中,gvkToTypeGroupVersionKindreflect.Type 的映射,即给出一个GVK,那就能找到它对应的类型,而且是有且仅有一个类型与GVK相对,比如GVK为 GroupVersionKind{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2", Kind: "FlowSchema"},那它对应到的类型(Type)就是定义在 k8s.io/api/flowcontrol/v1beta2/types.go 中的 FlowSchema 结构体,而 typeToGVK 则正好反过来,是类型到GVK的映射,但是这个不一样的是GVK是一个列表,即一个类型(Type)可能对应多个GVK,这个该怎么理解呢?其实这个的意思是,一个类型可能被多个GVK引用,比如一些公用的类型,像WatchEvent, ListOptions等,所以,GVK和Type是这样一个对应关系:

根据某个GVK能找到唯一的一个Type,但是根据Type找GVK,可能会有多个GVK的情况,这种一般都是公共的元数据的资源类型,其他的API资源类型基本上都是一对一的关系。

与之相关的,是下面两个方法:

func (s *Scheme) ObjectKinds(obj Object) ([]schema.GroupVersionKind, bool, error) {
  ......
  v, err := conversion.EnforcePtr(obj)
  ......
  t := v.Type()
  ......
  gvks, ok := s.typeToGVK[t]
  ......
  return gvks, unversionedType, nil
}

ObjectKinds()方法是根据一个对象的类型去 typeToGVK 中找它对应的GVK,返回的是一个GVK列表。

func (s *Scheme) New(kind schema.GroupVersionKind) (Object, error) {
  if t, exists := s.gvkToType[kind]; exists {
    return reflect.New(t).Interface().(Object), nil
  }
  ......
}

New()方法则是根据一个GVK去 gvkToType 中找到它对应的Type,然后通过 reflect.New() 方法去实例化一个它的对象。

所以各个版本的API资源,都会将自己的GVK和Type通过 AddKnownTypes() 注册到Scheme中,后续可以通过 ObjectKinds()New() 等方法去使用它们。我们还是以 FlowSchema 为例,来看看各个API资源是怎么注册其类型的:

// k8s.io/api/flowcontrol/v1beta2/register.go

// GroupName is the name of api group
const GroupName = "flowcontrol.apiserver.k8s.io"

// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta2"}

var (
  // SchemeBuilder installs the api group to a scheme
  SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
  // AddToScheme adds api to a scheme
  AddToScheme = SchemeBuilder.AddToScheme
)

// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
  scheme.AddKnownTypes(SchemeGroupVersion,
    &FlowSchema{},
    &FlowSchemaList{},
    &PriorityLevelConfiguration{},
    &PriorityLevelConfigurationList{},
  )
  metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
  return nil
}

主要关注下 addKnownTypes()方法即可,注意它的参数,是一个指针类型的 scheme,前面我们讲过,APIServer全局只有一个Scheme,这里即引用的全局的Scheme实例的指针,将本版本的API资源类型注册到Scheme中。这里展示的v1beta2版本的,v1beta3还有内部版本,都是类似的,他们的对应目录下都有一个 register.go 用来向Scheme中注册本版本的API资源类型。

类型转换方法注册

如前所述,Scheme还有一个重要功能,就是可以将不同版本的API对象进行互相转换,这个转换是在 内部版本外部版本 之间进行的,所以各个API资源都将外部版本的API资源类型如何跟内部版本类型进行转换的方法注册到Scheme中,即上面类图中的converer *convertion.Converter属性, 在Converter内部维护了一个map,key是以[source, dest]为组合的一对儿relect.Type,value则是类型转换方法,即给定了一对儿类型,就可以找到一个怎么从源类型转换到目的类型的方法。

Scheme提供了以下两个方法进行类型转换方法的注册:

func (s *Scheme) AddConversionFunc(a, b interface{}, fn conversion.ConversionFunc) error {
  return s.converter.RegisterUntypedConversionFunc(a, b, fn)
}

func (s *Scheme) AddGeneratedConversionFunc(a, b interface{}, fn conversion.ConversionFunc) error {
  return s.converter.RegisterGeneratedUntypedConversionFunc(a, b, fn)
}

然后提供了 Convert()ConvertToVersion()UnsafeConvertToVersion()等方法调用注册进来的类型转换方法对某一对儿特定的类型进行转换。那问题来了,这类型到底是怎么转换的呢?我们还是来看个示例,还是以 FlowSchema 为例,来看看它的类型转换方法:

// kubernetes/pkg/apis/flowcontrol/v1beta2/zz_generated.conversion.go

func RegisterConversions(s *runtime.Scheme) error {
  ......
  if err := s.AddGeneratedConversionFunc((*v1beta2.FlowSchema)(nil), (*flowcontrol.FlowSchema)(nil), func(a, b interface{}, scope conversion.Scope) error {
    return Convert_v1beta2_FlowSchema_To_flowcontrol_FlowSchema(a.(*v1beta2.FlowSchema), b.(*flowcontrol.FlowSchema), scope)
  })
  ......
  if err := s.AddGeneratedConversionFunc((*flowcontrol.FlowSchema)(nil), (*v1beta2.FlowSchema)(nil), func(a, b interface{}, scope conversion.Scope) error {
    return Convert_flowcontrol_FlowSchema_To_v1beta2_FlowSchema(a.(*flowcontrol.FlowSchema), b.(*v1beta2.FlowSchema), scope)
  })
  ......
}

可以看到这里也是引用的Scheme的指针,通过调用scheme的 AddGeneratedConversionFunc() 方法,注册了两个类型转换方法,即 v1beta2.FlowSchema 这个外部版本的类型与 flowcontrol.FlowSchema 这个内部版本的类型之间的互相转换,而跟踪到最后,发现其实这个类型转换方法就是很简单的两个对象之间属性的赋值,就是把源类型对象的属性值取出来,赋值给目的类型对象的对应属性:

// kubernetes/pkg/apis/flowcontrol/v1beta2/zz_generated.conversion.go

func autoConvert_v1beta2_FlowSchema_To_flowcontrol_FlowSchema(in *v1beta2.FlowSchema, out *flowcontrol.FlowSchema, s conversion.Scope) error {
  out.ObjectMeta = in.ObjectMeta
  if err := Convert_v1beta2_FlowSchemaSpec_To_flowcontrol_FlowSchemaSpec(&in.Spec, &out.Spec, s); err != nil {
    return err
  }
  if err := Convert_v1beta2_FlowSchemaStatus_To_flowcontrol_FlowSchemaStatus(&in.Status, &out.Status, s); err != nil {
    return err
  }
  return nil
}

......

func autoConvert_v1beta2_FlowSchemaSpec_To_flowcontrol_FlowSchemaSpec(in *v1beta2.FlowSchemaSpec, out *flowcontrol.FlowSchemaSpec, s conversion.Scope) error {
  if err := Convert_v1beta2_PriorityLevelConfigurationReference_To_flowcontrol_PriorityLevelConfigurationReference(&in.PriorityLevelConfiguration, &out.PriorityLevelConfiguration, s); err != nil {
    return err
  }
  out.MatchingPrecedence = in.MatchingPrecedence
  out.DistinguisherMethod = (*flowcontrol.FlowDistinguisherMethod)(unsafe.Pointer(in.DistinguisherMethod))
  out.Rules = *(*[]flowcontrol.PolicyRulesWithSubjects)(unsafe.Pointer(&in.Rules))
  return nil
}

......

func autoConvert_v1beta2_FlowSchemaStatus_To_flowcontrol_FlowSchemaStatus(in *v1beta2.FlowSchemaStatus, out *flowcontrol.FlowSchemaStatus, s conversion.Scope) error {
  out.Conditions = *(*[]flowcontrol.FlowSchemaCondition)(unsafe.Pointer(&in.Conditions))
  return nil
}

这些转换方法都位于 zz_generated.conversion.go 这个文件中,这个文件及其内容都是根据types.go中的类型定义自动生成的,因为这种类型转换的逻辑很简单,但是代码量又大,完全可以让它自动生成。但是如前文所说,现在Kubernetes的API都趋于稳定了,beta版和稳定版之间几乎没有差异,所以外部版本跟内部版本之间的转换就是很简单的属性赋值,但是如果内外版本的属性有不一致的情况,在转换时还是要特殊处理下的,可能会忽略掉某些属性,或者是把某些属性放到别的字段去,这种特殊的情况,就需要开发者来特别指定,而不能自动生成了,比如跟 FlowSchema 在同一个组中的 LimitedPriorityLevelConfiguration 资源就有这种情况:

// k8s.io/api/flowcontrol/v1beta2/types.go

type LimitedPriorityLevelConfiguration struct {
  AssuredConcurrencyShares int32 `json:"assuredConcurrencyShares" protobuf:"varint,1,opt,name=assuredConcurrencyShares"`
  LimitResponse LimitResponse `json:"limitResponse,omitempty" protobuf:"bytes,2,opt,name=limitResponse"`
  ......
}


// k8s.io/api/flowcontrol/v1beta3/types.go

type LimitedPriorityLevelConfiguration struct {
  NominalConcurrencyShares int32 `json:"nominalConcurrencyShares" protobuf:"varint,1,opt,name=nominalConcurrencyShares"`
  LimitResponse LimitResponse `json:"limitResponse,omitempty" protobuf:"bytes,2,opt,name=limitResponse"`
  ......
}

v1beta2和v1beta3的字段名发生了改变,从v1beta2中的 AssuredConcurrencyShares 改成了 v1beta3中的 NominalConcurrencyShares,那这种情况,内部版本是什么样的呢?

// kubernets/pkg/apis/flowcontrol/types.go

type LimitedPriorityLevelConfiguration struct {
  NominalConcurrencyShares int32
  LimitResponse LimitResponse
}

可以看到内部版本,其实是跟v1beta3版本的字段保持一致的,即是跟最新版本的类型保持一致的。那这种情况的类型该怎么转换呢?


// kubernetes/pkg/apis/flowcontrol/v1beta2/conversion.go

func Convert_v1beta2_LimitedPriorityLevelConfiguration_To_flowcontrol_LimitedPriorityLevelConfiguration(in *v1beta2.LimitedPriorityLevelConfiguration, out *flowcontrol.LimitedPriorityLevelConfiguration, s conversion.Scope) error {
  if err := autoConvert_v1beta2_LimitedPriorityLevelConfiguration_To_flowcontrol_LimitedPriorityLevelConfiguration(in, out, nil); err != nil {
    return err
  }

  out.NominalConcurrencyShares = in.AssuredConcurrencyShares
  return nil
}

func Convert_flowcontrol_LimitedPriorityLevelConfiguration_To_v1beta2_LimitedPriorityLevelConfiguration(in *flowcontrol.LimitedPriorityLevelConfiguration, out *v1beta2.LimitedPriorityLevelConfiguration, s conversion.Scope) error {
  if err := autoConvert_flowcontrol_LimitedPriorityLevelConfiguration_To_v1beta2_LimitedPriorityLevelConfiguration(in, out, nil); err != nil {
    return err
  }

  out.AssuredConcurrencyShares = in.NominalConcurrencyShares
  return nil
}

可以看到在v1beta2目录中,单独定义了一个conversion.go,它定义了两个方法指定了内部版本和外部版本进行转换时,这两个属性该怎么去处理,就是简单的把两个值互相赋值下,而这两个方法又会被 zz_generated.conversion.go 中的转换方法所引用。而v1beta3的外部版本跟内部版本字段是一样的,所以是不需要额外做转换的工作的,所以可以看到v1beta3目录中,并没有convertion.go文件。

版本优先级注册

Scheme中还有一个比较重要的点,就是版本优先级,一个组中可能会有很多个版本,开发者期望用户使用什么版本,以及期望某个API对象存储到数据库时,使用哪个版本的数据结构,都是通过这个版本优先级来确定的。在Scheme中,versionPriority 这个map就是用来存储某个组的版本优先级的,可以看到value是一个[]string,即某个组有几个版本都以字符串的形式存放到这个value中,而且优先级越高的,越在前面,即排在第一位的,就是版本优先级最高的。

比如 flowcontrol API组就通过scheme的 SetVersionPriority() 方法注册进去 v1beta3, v1beta2, v1beta1, v1alpha1 四个版本,而排在第一位的v1beta3是优先级最高的:

// kubernetes/pkg/apis/flowcontrol/install/install.go

scheme.SetVersionPriority(flowcontrolv1beta3.SchemeGroupVersion, flowcontrolv1beta2.SchemeGroupVersion,
    flowcontrolv1beta1.SchemeGroupVersion, flowcontrolv1alpha1.SchemeGroupVersion)

然后可以通过 PrioritizedVersionsForGroup() 方法去获取某个组的所有版本优先级,比如在API自动发现时,当用户请求某个组的根路径时,会返回该组支持的所有版本,并且有个 preferredVersion 字段,告诉用户建议使用哪个版本,如下例:

# curl http://127.0.0.1:8001/apis/flowcontrol.apiserver.k8s.io/
{
  "kind": "APIGroup",
  "apiVersion": "v1",
  "name": "flowcontrol.apiserver.k8s.io",
  "versions": [
    {
      "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta3",
      "version": "v1beta3"
    },
    {
      "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta2",
      "version": "v1beta2"
    }
  ],
  "preferredVersion": {
    "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta3",
    "version": "v1beta3"
  }
}

这里的 perferredVersion 显示为 v1beta3,就是由上面设置的版本优先级来决定的。此外,还有当存储某个对象时,需要获取到该类资源所在组的最高优先级的版本,去存储该版本的数据结构,也是通过 PrioritizedVersionsForGroup() 这个方法来获取的:

// k8s.io/apiserver/pkg/server/storage/resource_encoding_config.go

func (o *DefaultResourceEncodingConfig) StorageEncodingFor(resource schema.GroupResource) (schema.GroupVersion, error) {
  if !o.scheme.IsGroupRegistered(resource.Group) {
    return schema.GroupVersion{}, fmt.Errorf("group %q is not registered in scheme", resource.Group)
  }

  resourceOverride, resourceExists := o.resources[resource]
  if resourceExists {
    return resourceOverride.ExternalResourceEncoding, nil
  }

  // return the most preferred external version for the group
  return o.scheme.PrioritizedVersionsForGroup(resource.Group)[0], nil
}

OK,以上就是Scheme的核心内容了,基本上Scheme实现的几个接口:ObjectTyper, ObjectCreater, ObjectConvertor,我们都有介绍过了,还有一个 ObjectDefaulter 是用来设置默认值的,此处不太重要,略去不提。

最后,我们还是以 FlowControl 为例,结合它的代码目录树结构,来整体回顾下:

// kubernetes/pkg/apis/flowcontrol

// k8s.io/api/flowcontrol

曾经有很长一段时间lost在这个代码目录中,看着这些版本还有代码,不知道他们是干什么的,为什么有的在这,有的在那?为什么会有一些zz_开头的文件?为什么 types.go 在好几个地方都定义了?现在终于搞清楚了,我们就结合这个目录树的结构,来对上面介绍的Scheme内容进行一次简单的回顾总结:

  • 首先就是API资源类型有多版本的,而且分内部版本和外部版本的,外部版本定义在 k8s.io/api 这个第三方库中,而内部版本定义在 kubernetes/pkg/apis 本身的代码目录树中;
  • 每个版本中都有一个 types.go 文件,它定义了各个版的API资源类型,需要注意内部版本的类型是直接位于flowcontrol/目录下的,并没有一个 internal/ 这样一个目录结构;
  • types.go 在一起的,还有个 register.go,就是用来向Scheme中注册本版本的资源类型的;
  • zz_generated.deepcopy.go中定义了内部版本的API资源类型的深拷贝方法,即安全的拷贝一个对象,在进行类型转换等地方会用到;
  • kubernetes/pkg/apis/flowcontrol/ 目录下除了有内部版本的类型定义之外,还分了很多版本目录,里面定义了各个版本跟内部版本如何进行转换的方法以及本版本的默认值方法,以 kubernetes/pkg/apis/flowcontrol/v1beta2 目录下文件为例,介绍下各个文件的作用:
    • zz_generated.conversion.go 是根据types.go自动生成的内部版本与本版本的类型的转换方法,这里面还包含了向 scheme 中注册类型转换方法的入口;
    • conversion.go 是针对特殊的字段由开发者编写的类型转换方法;
    • zz_generated.defaults.go 是自动生成的默认值方法;
    • defaults.go 是针对特殊字段单独设置的默认值方法;
    • register.go 是用来向scheme中注册默认值方法的;
  • 再来以 k8s.io/api/flowcontrol/v1beta2/ 目录下的文件为例,介绍下各个文件的作用:
    • types.go 定义了外部版本的API资源类型;
    • register.go 是向scheme中注册本版本的API资源类型;
    • generated.proto 是根据类型自动生成的 protobuf 的定义文件;
    • generated.pb.go 则是根据 generated.proto 定义文件自动生成的对应的go代码,当客户端跟kubernetes api走gRPC通信时,就使用protobuf格式的数据,就会用到这里的代码;
    • zz_generated.deepcopy.go 则是定义的本版本的API资源类型的深拷贝方法;

kubernetes/pkg/apis/flowcontrol/install 目录下还有个 install.go 文件,它里面就是类型注册,以及版本优先级注册的入口:

func init() {
  Install(legacyscheme.Scheme)
}

// Install registers the API group and adds types to a scheme
func Install(scheme *runtime.Scheme) {
  utilruntime.Must(flowcontrol.AddToScheme(scheme))
  utilruntime.Must(flowcontrolv1alpha1.AddToScheme(scheme))
  utilruntime.Must(flowcontrolv1beta1.AddToScheme(scheme))
  utilruntime.Must(flowcontrolv1beta2.AddToScheme(scheme))
  utilruntime.Must(flowcontrolv1beta3.AddToScheme(scheme))
  utilruntime.Must(scheme.SetVersionPriority(flowcontrolv1beta3.SchemeGroupVersion, flowcontrolv1beta2.SchemeGroupVersion,
    flowcontrolv1beta1.SchemeGroupVersion, flowcontrolv1alpha1.SchemeGroupVersion))
}

通过 init() 方法,即在启动时,就会向scheme中去注册各种版本的API资源类型,以及设置版本优先级。

OK,说了这么多,那Scheme到底在哪呢?前面说的都是引用它的指针,最后有请我们的主角隆重登场:

// kubernetes/pkg/api/legacyscheme/scheme.go

var (
  // Scheme is the default instance of runtime.Scheme to which types in the Kubernetes API are already registered.
  // NOTE: If you are copying this file to start a new api group, STOP! Copy the
  // extensions group instead. This Scheme is special and should appear ONLY in
  // the api group, unless you really know what you're doing.
  // TODO(lavalamp): make the above error impossible.
  Scheme = runtime.NewScheme()

  // Codecs provides access to encoding and decoding for the scheme
  Codecs = serializer.NewCodecFactory(Scheme)

  // ParameterCodec handles versioning of objects that are converted to query parameters.
  ParameterCodec = runtime.NewParameterCodec(Scheme)
)

总结

本篇文章从源码角度介绍了下Scheme的功能作用以及实现机制,由于Scheme比较抽象,想解释比较抽象的东西,最好的办法就是通过举例去解释它,所以本篇文章通过举例的方式,介绍了什么是类型,类型是如何注册的,类型转换方法是如何注册的,以及版本优先级的注册,基本上把Scheme最核心的功能分析了下,然后结合分析,介绍了下在开发中经常遇到的各个文件的作用。

通过这些系列分析文章,我觉得Kubernetes的代码写的还是相当不错的,尤其是真的做到了 Do not repeat your self,基本上把所有共性的逻辑都抽象出来作为公共逻辑,每个API资源,只需要实现自己相关的代码就可以了,因此,开发一个新的API变得比较简单,不需要你去实现数据库的增删查改逻辑,也不需要去关心如何进行序列化,也不用关心如何向APIServer中注册Handler,只需要定义好各个版本的数据结构,即Kubernetes中所说的类型(Type),以及各个版本跟内部版本之间如何进行转换的逻辑,然后创建好该API相关的REST Store,再用工具自动生成一些必要的代码,最终注册到相应的地方即可,相比很多应用开发一个新的API,需要从前到后,添加很多耦合代码来说,Kubernetes做的真的不错。

当然了,这种抽象,带来的一个问题,就是复杂性增加了好几个维度,尤其是Golang的,这种看似无面向对象实际又有面向对象的机制,你定义了一个接口,没法直观的判断谁实现了这些接口,也没法直观的看出来一个结构体实现了哪些接口,不像Java,C++,Python这种面向对象的语言那样清晰,Golang比较隐晦。

posted @ 2023-11-12 22:53  hackerain  阅读(121)  评论(0编辑  收藏  举报