为什么需要Scheme
- 因为在web开发中随着版本的更新迭代,通常要在系统中维护多个版本的api,多个版本的api在数据结构上往往也各不相同
- 为了解决上述问题 —— 出现了 Scheme —— 实现 GVK 与 api数据结构的对应
web 请求的处理流程
- 收到请求后,通常首先是webServer先进行Http协议的处理
- 解析成基础的webServer内部的一个Http请求对象
- 该 Http 请求对象持有对应请求的请求头和底层对应的字节序列(从socket流中读取)
- 接着根据对应的编码格式来进行反序列化
- 完成从字节序列到当前接口的业务模型的映射, 然后在交给业务逻辑处理
- 从而最终进行持久化存储, 本文的重点也就在反序列化部分
scheme代码分析
scheme的数据结构
Scheme 定义了资源序列化和反序列化的方法以及资源类型和版本的对应关系;这里我们可以理解成一张纪录表。定义在 k8s.io/apimachinery/pkg/runtime/scheme.go 中。需要关注的 gvkToTypeype 和 typeToGVK 字段
代码路径:staging/src/k8s.io/apimachinery/pkg/runtime/scheme.go
// Scheme defines methods for serializing and deserializing API objects, a type
// registry for converting group, version, and kind information to and from Go
// schemas, and mappings between Go schemas of different versions. A scheme is the
// foundation for a versioned API and versioned configuration over time.
//
// In a Scheme, a Type is a particular Go struct, a Version is a point-in-time
// identifier for a particular representation of that Type (typically backwards
// compatible), a Kind is the unique name for that Type within the Version, and a
// Group identifies a set of Versions, Kinds, and Types that evolve over time. An
// Unversioned Type is one that is not yet formally bound to a type and is promised
// to be backwards compatible (effectively a "v1" of a Type that does not expect
// to break in the future).
//
// Schemes are not expected to change at runtime and are only threadsafe after
// registration is complete.
type Scheme struct {
// versionMap allows one to figure out the go type of an object with
// the given version and name.
gvkToType map[schema.GroupVersionKind]reflect.Type
// typeToGroupVersion allows one to find metadata for a given go object.
// The reflect.Type we index by should *not* be a pointer.
typeToGVK map[reflect.Type][]schema.GroupVersionKind
// unversionedTypes are transformed without conversion in ConvertToVersion.
unversionedTypes map[reflect.Type]schema.GroupVersionKind
// unversionedKinds are the names of kinds that can be created in the context of any group
// or version
// TODO: resolve the status of unversioned types.
unversionedKinds map[string]reflect.Type
// Map from version and resource to the corresponding func to convert
// resource field labels in that version to internal version.
fieldLabelConversionFuncs map[schema.GroupVersionKind]FieldLabelConversionFunc
// defaulterFuncs is an array of interfaces to be called with an object to provide defaulting
// the provided object must be a pointer.
defaulterFuncs map[reflect.Type]func(interface{})
// converter stores all registered conversion functions. It also has
// default converting behavior.
converter *conversion.Converter
// versionPriority is a map of groups to ordered lists of versions for those groups indicating the
// default priorities of these versions as registered in the scheme
versionPriority map[string][]string
// observedVersions keeps track of the order we've seen versions during type registration
observedVersions []schema.GroupVersion
// schemeName is the name of this scheme. If you don't specify a name, the stack of the NewScheme caller will be used.
// This is useful for error reporting to indicate the origin of the scheme.
schemeName string
}
GVK :group、version、kind,GVK 在编码中也是结构体,但只存储 string 信息,对于 Pod 就是 core 、 v1、 pod 这些 string 信息
model 对象类型:就是数据结构 struct,比如 Pod 的数据结构等
版本:比如某种资源存在多个版本,比如 Foo 资源存在 v1、v2、v3 等三个版本,但其实在编码中,存在个内部版本 _internal ,该三个版本都是与内部版本互相转换,减少转换书写的复杂度(比如没有内部版本,则需要写 v1 和 v2,v1 和 v3,v2 和 v3,版本多的话,需要书写更多,有内部版本存在,只需要些 v1、v2、v3 分别和 内部版本的转换即可,v1转v3,就是v1–内部版本–v3)
当和 API Server 通信的时候能够处理新的 types 类型就需要知道有新的types类型,AddToScheme 会利用到反射,新定义的 types 类型的结构体的命名必须和自定义的 Kind 的命名一致,否则找不到对应的kind
var (
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: "apps.test.com", Version: "v1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
// SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme is a global function that registers this API group & version to a scheme
AddToScheme = SchemeBuilder.AddToScheme
)
从上面的图解以及源码来看 schema 结构主要有以下关键点:
- 包含 map 类型的 gvkToType 属性来维护 GVK 和 model 对象类型的关系。
- 包含 map 类型的 typeToGVK 属性来维护 model 对象类型和 GVK 的关系。
- 包含 map 类型的 defaulterFuncs 属性维护 model 对象类型和默认值函数的关系。
- 包含 conversion.Converter 指针类型的 converter 属性实现资源不同版本的转化。
- map 类型的 fieldLabelConversionFuncs 属性维护 GVK label 标签转换函数的关系。
- 包含 string 类型的 schemaName 属性用来定义 schema 的名称。
scheme实现的接口
由上面的 schema 的数据结构看,它是一个 struct 的类型,另外它还实现了一些接口,使得 schema 可以创建资源对象,给资源对象赋默认值,识别资源对象类型,完成资源对象本版之间的转换,完成资源的 label 标签转化等。其接口实现如下图:
模型映射实现
描述资源版本信息
GET /api/{version}/{resource}/{action}
上面是一个基础的web url通常我们都会为每个版本注册一个对应的URL
其中会包含很关键的两个信息即version与resource
通过这两个信息,通常我们就可以知道这可能是某个资源的那个版本
如果我们把后面的action也包裹进来,我们通常就可以知道对应的资源的那个具体操作
模型映射实现
- 我们通过url里面获取到资源的GroupVersionKind信息,如何将其映射为一个具体的类型呢?
- 这貌似就很简单了结合反射和map来进行就可以了,我们通过url获取到对应想的GVK信息,然后在通过我们的映射表,就知道对应的模型是哪个,接下来就只需要进行转换就行了
gvkToType map[schema.GroupVersionKind]reflect.Type
序列化和反序列化的定义:
(1)序列化就是指把对象转换为字节序列的过程
反序列化就是指把字节序列恢复为对象的过程。
(2)序列化最重要的作用:在传递和保存对象时.保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。
反序列化的最重要的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。
scheme在http请求中的作用
- 在 webserver 上注册 url 时,即完成了 url 与 资源的版本信息GroupVersionKind(GVK) 映射关系的构建
- 之后收到请求进行处理时
-
- 通过 url 获取到 GVK
- 根据 GVK 调用相应构造函数,创建目标资源对象
- 根据请求的 Header 得知编码格式,获取对应的解码器
- 将Body里面的字节序列进行解码到目标对象
以上流程就可以实现多版本资源的映射和反序列化操作了
Scheme 的其他作用
schema 是 kubernetes 资源管理的核心数据结构。由以前文章我们了解到 kubernetes 会将其管理的资源划分为 group/version/kind 的概念,scheme 可以利用进行 GVK 进行一下操作:
可以将资源在内部版本和其他版本中相互转化
可以序列化和反序列化的过程中识别资源类型,创建资源对象,设置默认值等等。
这些 group/version/kind 和资源 model 的对应关系,资源 model 的默认值函数,不同版本之间相互转化的函数等等全部由 schema 维护。可以说 schema 是组织 kubernetes 资源的核心
资源注册到scheme
上两节主要进行介绍了 kubernetes scheme 这个重要概念:
- 从数据结构角度上看,其中包括了它是如何来维护 group/version/kind 和资源 model 的对应关系,资源 model 和默认值函数的对应关系,不同资源版本之间相互转化函数的对应关系等等。
- 从实现接口角度看, schema 实现了一系列接口,从而具备了创建资源对象,给资源对象赋默认值,识别资源对象类型,完成资源对象本版之间的转换,完成资源的 label 标签转化等功能。
在本篇文章里, 我们主要介绍不同版本的资源到 scheme 对象中的注册。
SchemeBuilder对象
代码路径:/staging/src/k8s.io/apimachinery/pkg/runtime/scheme_builder.go
// schemabuilder 对象本质是一个函数数组集合,其中的函数入参为 schema 类型 type SchemeBuilder []func(*Scheme) error // AddToScheme 函数就是将自己包含的处理函数依次应用到 Scheme 上, // 给人的感觉好像是过滤器似的,让 Scheme 分别被定义好的函数们给处理一遍, func (sb *SchemeBuilder) AddToScheme(s *Scheme) error { for _, f := range *sb { if err := f(s); err != nil { return err } } return nil } func (sb *SchemeBuilder) Register(funcs ...func(*Scheme) error) { for _, f := range funcs { *sb = append(*sb, f) } } func NewSchemeBuilder(funcs ...func(*Scheme) error) SchemeBuilder { var sb SchemeBuilder sb.Register(funcs...) return sb }
由上面代码可以看出:
- schemabuilder 对象本质是一个函数数组集合,其中的函数入参为 scheme 类型。
- schemabuilder 对象含有 Register() 这个方法,其本质是向函数数组集合中再添加一个入参为 schema 类型的函数。
- schemabuilder 对象有 AddToSchema() 这个方法,它的入参为 schema 对象,其本质是把该 schema 对象传入函数数组集合中的每一个函数中,然后分别运行。
资源的外部版本注册
这里我们以 apps/v1beta1 为例子,介绍该组下的 v1beta1 版本的资源是如何注册到 schema 中的,其图解如下:
由图解我们发现对于外部资源版本的注册包括资源 model 类型的注册,资源的初始化函数(即默认值函数)的注册,资源的 label 转换函数的注册,和内部版本相互转换函数的注册(下面详细介绍)。
资源 model 类型的注册源码如下:
- 从源代码之中,我们会发现,在源码文件 k8s.io/api/apps/v1beta1/register.go 中去创建了 schemebuilder 这个对象,
- 并且设置了组为 apps, 设置了版本为 v1beta1
- 然后把属于apps/v1beta1 中的所有类型的资源 model 进行注册,例如我们非常熟悉的 deployment, statsfulset 等资源。
代码路径:// staging/src/k8s.io/api/apps/v1beta1/register.go
const GroupName = "apps" var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta1"} // 设置组 版本 var ( SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) localSchemeBuilder = &SchemeBuilder AddToScheme = localSchemeBuilder.AddToScheme ) func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &Deployment{}, &DeploymentList{}, &DeploymentRollback{}, &Scale{}, &StatefulSet{}, &StatefulSetList{}, &ControllerRevision{}, &ControllerRevisionList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) // 进行设置 return nil } func NewSchemeBuilder(funcs ...func(*Scheme) error) SchemeBuilder { var sb SchemeBuilder sb.Register(funcs...) return sb }
资源内部版本注册
这里我们同样以 apps 组做为例子,介绍该组下内部版本资源是如何注册到 schema 中的,其图解如下:
由图解发现对于内部资源版本注册只包括资源 model 类型的注册,其源码如下:
- 在源码文件 pkg/apis/apps/register.go 中进行了 schemebuilder 对象的创建,设置了组为 apps, 设置版本为内部版本,并把属于内部版本中的所有类型的资源 model 进行注册,例如我们非常熟悉的 deployment 资源, statsfulset 资源等等。
代码路径:// pkg/apis/apps/register.go
const GroupName = "apps" // 设置版本为内部版本 APIVersionInternal var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal} var ( SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes). AddToScheme = SchemeBuilder.AddToScheme ) func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &DaemonSet{}, &DaemonSetList{}, &Deployment{}, &DeploymentList{}, &DeploymentRollback{}, &autoscaling.Scale{}, &StatefulSet{}, &StatefulSetList{}, &ControllerRevision{}, &ControllerRevisionList{}, &ReplicaSet{}, &ReplicaSetList{}, ) return nil } // k8s.io/apimachinery/pkg/runtime/interfaces.go const ( APIVersionInternal = "__internal" )
资源的内外版本注册的驱动
这里我们同样以 apps 组作为例子从源码角度看,驱动整个内部版本资源和外部版本资源的注册如下:
- 在源代码文件 pkg/apis/apps/install/install.go 中,由 legacyscheme.Scheme 操作来得到一个 scheme 对象,然后把 app 组下的所有版本的资源都进行注册,包括内部版本,以及 v1/v1beta1/v1beta2 等所有外部版本,如下图解展示了各个组下不同版本资源到 scheme中的注册。
代码路径://pkg/apis/apps/install/install.go
func init() { Install(legacyscheme.Scheme) // 由 legacyscheme.Scheme 操作来得到一个 schema 对象 } // 把 app 组下的所有版本的资源都进行注册,包括内部版本,以及 v1/v1beta1/v1beta2 等所有外部版本 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)) }
参考
【k8s基础篇】k8s scheme3 之序列化_oceanweave的博客-优快云博客
【kubernetes/k8s概念】kubernetes scheme原理分析_张忠琳的博客-优快云博客_k8s schema