参考
- K8s源码分析(7)-序列化的codec和codec factory
- K8s源码分析(8)-codec和codec factory的创建
- K8s源码分析(9)-codec的decode和encode操作
内外部版本转换
1 | 序列化的codec和codec factory
- 之前介绍过 json 序列化,主要以 serializer.json.Serializer 组件做为例子,介绍了对于 json 协议格式资源的序列化。其中包括了该组件是如何利用 decode 操作去从请求中来提取相关的 resource, 以及如何去利用 encode 操作来把相关 resource 写入到响应中去
- 只是资源正常版本的序列化和反序列化操作,例如 apps/deployment/v1 资源的序列化和反序列化
- 因此如果涉及到内部版本和其它版本之间相互转化的序列化以及反序列化呢?
- 这就需要用到 codec 组件和 codec factory 组件了
1.1 | codec 组件
codec 组件主要通过:
- decode 方法实现了 resource 从正常的版本转化为内部版本
- 通过 encode 方法实现了 resource 从内部版本转化为正常版本
如果从形象的角度来看, codec 可以用以下图片描述。
- codec 组件实现了 runtime.Serializer 接口,会由 Encode 和 Decode 方法来实现正常版本和内部版本相互转化的序列化以及反序列化。
- codec 内部有 Encoder 和 Decoder 成员,主要完成正常版本下资源的某种格式序列化和反序列化,例如我们上一篇文章介绍的 json 格式。
- codec 内部有 ObjectConveror 关键成员,主要完成资源的正常版本和内部版本之间的相互转化。
- codec 内部有 ObjectCreater 关键成员和 ObjectDefaulter 关键成员,主要用于资源完成在 decode 操作中正常版本的创建和赋默认值。
- codec 内部有 ObjectTyper 关键成员, 以用来确定资源的类型,即 GVK。
- codec 内部有 encodeVersion 关键成员和 decodeVersion 关键成员,主要用于定义资源转化的版本,正常版本或者内部版本。
- 从源代码的角度看, codec 相关定义如下
// staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go
type Encoder interface {
Encode(obj Object, w io.Writer) error
Identifier() Identifier
}
type Decoder interface {
Decode(data []byte, defaults *schema.GroupVersionKind, into Object) (Object, *schema.GroupVersionKind, error)
}
// Serializer 也是 Encoder 和 Decoder 接口的组合
type Serializer interface {
Encoder
Decoder
}
type Codec Serializer
// k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go
type codec struct {
// 内外部版本转换时的 序列化与反序列化
encoder runtime.Encoder
decoder runtime.Decoder
// 内外版本转换器
convertor runtime.ObjectConvertor
// 类型对象创建
creater runtime.ObjectCreater
// 确定资源类型 GVK
typer runtime.ObjectTyper
// 设置默认值
defaulter runtime.ObjectDefaulter
// 定义资源转换的版本 内部版本或外部版本
encodeVersion runtime.GroupVersioner
decodeVersion runtime.GroupVersioner
identifier runtime.Identifier
originalSchemeName string
}
1.2 | codec factory 组件
codec factory 主要作用是生成 codec 组件用来完成 decode 和 encode 操作,如果从形象的角度来看, codec factory 可以用以下图片描述。
- 实现了 runtime.NegatiatedSerializer 这个核心接口,在该接口中有定义 SupportedMediaTypes 方法实现对不同数据格式资源的支持,例如常见的 json,ymal,protobuf 等协议。
- 在该接口之中有定义 EncoderForVersion 和 DecoderForVersion 方法来得到相应的 Encoder 和 Decoder 来进行序列化和反序列化,这里面得到的 Encoder 和 Decoder 一般就是我们上面介绍的 codec 对象。
- codec factory 内部有 Serializerinfo 数组成员,用以支持不同数据格式的资源。
- serializerinfo 内部有关键成员 MediaType 来定义所支持的资源数据格式。
- serializerinfo 内部有关键成员 Serializer (别名为 Codec)来支持序列化和反序列化操作,同时 Serializer 也是 Encoder 和 Decoder 接口的组合,这个由上面 codec 相关源码可以看到。
- 从源代码角度, codec factory 相关定义如下
// k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go
// 实现了下面的 NegotiatedSerializer 接口
type CodecFactory struct {
scheme *runtime.Scheme
universal runtime.Decoder
accepts []runtime.SerializerInfo
legacySerializer runtime.Serializer
}
// staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go
type NegotiatedSerializer interface {
// 实现对不同数据格式资源的支持**,例如常见的 json,ymal,protobuf 等协议
SupportedMediaTypes() []SerializerInfo
// 得到相应的 Encoder 和 Decoder 来进行序列化和反序列化
// 相当于调用上面工厂结果体,调用下面两个方法,会生成 Encoder 和 Decoder
// Encoder 和 Decoder 的常用实现方式 就是第一小节介绍的 codec 结构体
EncoderForVersion(serializer Encoder, gv GroupVersioner) Encoder
DecoderToVersion(serializer Decoder, gv GroupVersioner) Decoder
}
type SerializerInfo struct {
// MediaType is the value that represents this serializer over the wire.
// 定义所支持的资源数据格式
MediaType string
// MediaTypeType is the first part of the MediaType ("application" in "application/json").
MediaTypeType string
// MediaTypeSubType is the second part of the MediaType ("json" in "application/json").
MediaTypeSubType string
EncodesAsText bool
// 关键成员 Serializer (别名为 Codec)来支持序列化和反序列化
// 同时 Serializer 也是 Encoder 和 Decoder 接口的组合,这个由上面 codec 相关源码可以看到
Serializer Serializer
PrettySerializer Serializer
StreamSerializer *StreamSerializerInfo
}
2 | codec和codec factory的创建
上一节主要介绍了 codec 组件和 codec factory 组件,这两个组件主要实现了内部版本和其他版本之间转化的序列化以及反序列化。包括了这两个组件实现的关键接口,以及这两个组件会由哪些关键成员组成。在这里我们主要介绍 codec 和 codec factory 的创建。
2.1 | codec factory 的创建
codec factory 对象的创建被定义在方法 NewCodecFactory()
中:
- 该方法内部调用了
newSerializersForScheme()
方法来创建支持不同数据格式的 Serializer 对象 - 然后又调用
newCodecFactory()
方法来实现对象创建。
newSerializersForScheme()
方法主要是创建支持各种数据格式 (json, yaml, protobuf 等) 的 serializer 对象,例如之前我们介绍支持 jason 格式的 serializer.json.Serializer,其核心逻辑如下:
- newSerializersForScheme() 的源码如下:
// k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go
// 创建支持各种数据格式 (json, yaml, protobuf 等) 的 serializer 对象
func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, options CodecFactoryOptions) []serializerType {
// json serializer 对象
jsonSerializer := json.NewSerializerWithOptions(
mf, scheme, scheme,
json.SerializerOptions{Yaml: false, Pretty: false, Strict: options.Strict},
)
jsonSerializerType := serializerType{
AcceptContentTypes: []string{runtime.ContentTypeJSON},
ContentType: runtime.ContentTypeJSON,
FileExtensions: []string{"json"},
EncodesAsText: true,
Serializer: jsonSerializer,
Framer: json.Framer,
StreamSerializer: jsonSerializer,
}
if options.Pretty {
jsonSerializerType.PrettySerializer = json.NewSerializerWithOptions(
mf, scheme, scheme,
json.SerializerOptions{Yaml: false, Pretty: true, Strict: options.Strict},
)
}
// yaml serializer 对象
yamlSerializer := json.NewSerializerWithOptions(
mf, scheme, scheme,
json.SerializerOptions{Yaml: true, Pretty: false, Strict: options.Strict},
)
// proto serializer 对象
protoSerializer := protobuf.NewSerializer(scheme, scheme)
protoRawSerializer := protobuf.NewRawSerializer(scheme, scheme)
// 创建了一个数组
serializers := []serializerType{
jsonSerializerType, // json
{
AcceptContentTypes: []string{runtime.ContentTypeYAML},
ContentType: runtime.ContentTypeYAML,
FileExtensions: []string{"yaml"},
EncodesAsText: true,
Serializer: yamlSerializer, // yaml
},
{
AcceptContentTypes: []string{runtime.ContentTypeProtobuf},
ContentType: runtime.ContentTypeProtobuf,
FileExtensions: []string{"pb"},
Serializer: protoSerializer, // proto
Framer: protobuf.LengthDelimitedFramer,
StreamSerializer: protoRawSerializer,
},
}
// 添加其他的 serializer
for _, fn := range serializerExtensions {
if serializer, ok := fn(scheme); ok {
serializers = append(serializers, serializer)
}
}
return serializers
}
- 在 newCodecFactory() 这个方法里面,主要逻辑就是调用 newSerializersForScheme()方法,用来生成可以支持各种不同数据类型的 serializerType 数组, 然后利用该数组进行封装创建 codc factory,逻辑如下:
- 方法 newCodecFactory() 的源码如下:
// k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go
func newCodecFactory(scheme *runtime.Scheme, serializers []serializerType) CodecFactory {
// 创建 decoders 数组 用于构建CodecFactory
decoders := make([]runtime.Decoder, 0, len(serializers))
// 创建 SerializerInfo 数组 用于构建CodecFactory
var accepts []runtime.SerializerInfo
alreadyAccepted := make(map[string]struct{})
// 创建 legacySerializer(不知道啥作用) 用于构建CodecFactory
var legacySerializer runtime.Serializer
// 利用 newSerializersForScheme() 函数获取的 serializers ,构建上面的创建的数组等
for _, d := range serializers {
decoders = append(decoders, d.Serializer)
for _, mediaType := range d.AcceptContentTypes {
if _, ok := alreadyAccepted[mediaType]; ok {
continue
}
alreadyAccepted[mediaType] = struct{}{}
info := runtime.SerializerInfo{
MediaType: d.ContentType,
EncodesAsText: d.EncodesAsText,
Serializer: d.Serializer,
PrettySerializer: d.PrettySerializer,
}
mediaType, _, err := mime.ParseMediaType(info.MediaType)
if err != nil {
panic(err)
}
parts := strings.SplitN(mediaType, "/", 2)
info.MediaTypeType = parts[0]
info.MediaTypeSubType = parts[1]
if d.StreamSerializer != nil {
info.StreamSerializer = &runtime.StreamSerializerInfo{
Serializer: d.StreamSerializer,
EncodesAsText: d.EncodesAsText,
Framer: d.Framer,
}
}
accepts = append(accepts, info)
if mediaType == runtime.ContentTypeJSON {
legacySerializer = d.Serializer
}
}
}
if legacySerializer == nil {
legacySerializer = serializers[0].Serializer
}
// 创建 CodecFactory
return CodecFactory{
scheme: scheme,
universal: recognizer.NewDecoder(decoders...),
accepts: accepts,
legacySerializer: legacySerializer,
}
}
2.2 | codec 的创建
codec factory 对象的 DecoderToVersion()
方法和 EncoderForVersion()
方法会创建 codec 对象:
- 而这两个方法又都会去调用相同的
NewDefaultingCodecForScheme()
方法,最终这个方法又调用NewCodec()
实现创建。
DecoderToVersion()
和EncoderForVersion()
方法逻辑如下:
codec 相关的源代码创建如下:
// k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go
// 调用 CodecForVersions 方法
func (f CodecFactory) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
return f.CodecForVersions(nil, decoder, nil, gv)
}
// 同上 调用 CodecForVersions 方法
func (f CodecFactory) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
return f.CodecForVersions(encoder, nil, gv, nil)
}
// CodecForVersions 方法 调用 NewDefaultingCodecForScheme 方法
func (f CodecFactory) CodecForVersions(encoder runtime.Encoder, decoder runtime.Decoder, encode runtime.GroupVersioner, decode runtime.GroupVersioner) runtime.Codec {
// TODO: these are for backcompat, remove them in the future
if encode == nil {
encode = runtime.DisabledGroupVersioner
}
if decode == nil {
decode = runtime.InternalGroupVersioner
}
return versioning.NewDefaultingCodecForScheme(f.scheme, encoder, decoder, encode, decode)
}
// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go
// NewDefaultingCodecForScheme 方法 调用 NewCodec 方法
func NewDefaultingCodecForScheme(
scheme *runtime.Scheme,
encoder runtime.Encoder,
decoder runtime.Decoder,
encodeVersion runtime.GroupVersioner,
decodeVersion runtime.GroupVersioner,
) runtime.Codec {
return NewCodec(encoder, decoder, runtime.UnsafeObjectConvertor(scheme), scheme, scheme, scheme, encodeVersion, decodeVersion, scheme.Name())
}
// NewCodec 方法 创建 codec 结构体
func NewCodec(
encoder runtime.Encoder,
decoder runtime.Decoder,
convertor runtime.ObjectConvertor,
creater runtime.ObjectCreater,
typer runtime.ObjectTyper,
defaulter runtime.ObjectDefaulter,
encodeVersion runtime.GroupVersioner,
decodeVersion runtime.GroupVersioner,
originalSchemeName string,
) runtime.Codec {
internal := &codec{
encoder: encoder,
decoder: decoder,
convertor: convertor,
creater: creater,
typer: typer,
defaulter: defaulter,
encodeVersion: encodeVersion,
decodeVersion: decodeVersion,
identifier: identifier(encodeVersion, encoder),
originalSchemeName: originalSchemeName,
}
return internal
}
3 | codec的decode和encode操作
上一节主要去介绍了 codec 和 codec factory 对象的创建过程,包括利用支持各种不同协议格式(json, yaml, prtotbuf)的 serializer 对象来构建 codec factory, 以及利用 codec factory 去创建最终用来完成 decode 和 encode 操作的 codec 对象。在这里我们主要来介绍 codec 对象是如何完成 decode 和 encode 操作的。
3.1 | codec 的 decode 操作
codec 对象的 Decode() 方法完成 decode 操作,即将请求中的数据转化成相应目标版本的 kuberbenes resource, 然后会将其转化为相关 resource 的内部版本来管理, 例如对于一个创建资源的请求来说, 我们以常见的 apps/v1/deployment 资源为例。 decode 操作首先会将 v1 版本的 deployment 对象在请求数据中 decode 出来,然后在将其转化为 apps group 下内部版本的 deployment 对象。在后续处理中,一般对于资源创建请求就是将这个内部版本的资源持久化在 etcd 集群里, 其核心逻辑如下:
请求二进制数据 —> 反序列化为 某一版本资源对象(如 v1 pod)—> 版本转换 转为内部版本 internal ,存储到 etcd
- codec 对象的 Decode() 方法核心代码如下:
// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go
func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
// If the into object is unstructured and expresses an opinion about its group/version,
// create a new instance of the type so we always exercise the conversion path (skips short-circuiting on `into == obj`)
decodeInto := into
// 如果输入的 into 是 Unstructured,且还含有 GVK,那么利用 reflect 包创建一个新结构体
if into != nil {
if _, ok := into.(runtime.Unstructured); ok && !into.GetObjectKind().GroupVersionKind().GroupVersion().Empty() {
decodeInto = reflect.New(reflect.TypeOf(into).Elem()).Interface().(runtime.Object)
}
}
// 读取二进制数据,将其转换为 资源对象(就是某个版本的资源) 由 defaultGVK 指定
obj, gvk, err := c.decoder.Decode(data, defaultGVK, decodeInto)
if err != nil {
return nil, gvk, err
}
// 判断是否是 NestedObjectDecoder 类型,若是,采取一些操作(不太清楚是什么操作)
if d, ok := obj.(runtime.NestedObjectDecoder); ok {
if err := d.DecodeNestedObjects(runtime.WithoutVersionDecoder{c.decoder}); err != nil {
return nil, gvk, err
}
}
// if we specify a target, use generic conversion.
// 若我们指定了目标 into, 即 into 不为空,使用泛型转换
if into != nil {
// perform defaulting if requested
if c.defaulter != nil {
c.defaulter.Default(obj)
}
// 若 into 等于 obj (就是版本一致),那么不需要再次转换,直接返回
// Short-circuit conversion if the into object is same object
if into == obj {
return into, gvk, nil
}
// 理解为转换到指定版本 c.decodeVersion
if err := c.convertor.Convert(obj, into, c.decodeVersion); err != nil {
return nil, gvk, err
}
return into, gvk, nil
}
// 若没指定目标,则正常转换
// perform defaulting if requested
if c.defaulter != nil {
c.defaulter.Default(obj)
}
out, err := c.convertor.ConvertToVersion(obj, c.decodeVersion)
if err != nil {
return nil, gvk, err
}
return out, gvk, nil
}
从 end to end 角度看,资源 decode 流程如下所示:
- 通过 MetaFactory 创建的解码器,反序列化 得到 GVK
- 通过 CodecFactory 创建解码器 Codec
- 上面两个组合,对 Request Data 二进制数据进行反序列化,得到 目标类型资源对象 obj(举例子比如 v1 版本 pod)
- 根据 Schema 中的 ObjectDefaulter 为 obj 设置默认值
- InitializedObject 不太清楚什么作用,理解 对 obj 进行初始化配置
- 根据 Schema 中的 版本转换器 ObjectConvertor 将 obj 转换为特定版本(这里 internal 指的是内部版本)
- 之后将内部版本存储到 etcd
3.2 | codec 的 encode 操作
codec 对象的 doEncode() 方法完成 decode 核心操作,即将目标对象转化成相应版本的对象, 然后序列化到响应中去。例如对于一个获取资源的请求来说(一般为资源的查询请求), 我们以常见的 apps/v1/deployment 资源为例。 encode 操作会首先将目标对象转化成相应版本的对象,这个目标对象一般是在 etcd 集群中获取的内部版本对象(根据以前文章, kubenetes 各种资源永远会以内部版本的形式存储在 etcd 集群中)。然后在 convert 转化成请求中的 v1 版本的对象,最后序列化数据到 response 数据流中, 其核心逻辑如下:
etcd 中内部版本 二进制存储数据 —> 反序列化为 内部版本 internal —> 版本转换到指定版本 v1 pod —> 序列化为二进制响应数据返回
- codec 对象的 doEncode() 方法核心代码如下:
// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go
func (c *codec) doEncode(obj runtime.Object, w io.Writer) error {
// 判断 资源对象的 类型
switch obj := obj.(type) {
case *runtime.Unknown: // 不知道 类型
return c.encoder.Encode(obj, w)
case runtime.Unstructured: // Unstructured 类型
// UnstructuredList 可以包含多个 GVK 对象,因此不能快速转换
// 因为 顶级类型需要匹配我们的目标类型,因此需要将 list 中 obj 发送到转换器进行转换
// An unstructured list can contain objects of multiple group version kinds. don't short-circuit just
// because the top-level type matches our desired destination type. actually send the object to the converter
// to give it a chance to convert the list items if needed.
// 不是 UnstructuredList 可以进行转换
if _, ok := obj.(*unstructured.UnstructuredList); !ok {
// avoid conversion roundtrip if GVK is the right one already or is empty (yes, this is a hack, but the old behaviour we rely on in kubectl)
objGVK := obj.GetObjectKind().GroupVersionKind()
if len(objGVK.Version) == 0 {
return c.encoder.Encode(obj, w)
}
targetGVK, ok := c.encodeVersion.KindForGroupVersionKinds([]schema.GroupVersionKind{objGVK})
if !ok {
return runtime.NewNotRegisteredGVKErrForTarget(c.originalSchemeName, objGVK, c.encodeVersion)
}
if targetGVK == objGVK {
return c.encoder.Encode(obj, w)
}
}
}
// 获取资源类型 gvk
gvks, isUnversioned, err := c.typer.ObjectKinds(obj)
if err != nil {
return err
}
objectKind := obj.GetObjectKind()
old := objectKind.GroupVersionKind()
// restore the old GVK after encoding
// 编码后恢复原来的 GVK
defer objectKind.SetGroupVersionKind(old)
// 若不需要版本转换
if c.encodeVersion == nil || isUnversioned {
if e, ok := obj.(runtime.NestedObjectEncoder); ok {
if err := e.EncodeNestedObjects(runtime.WithVersionEncoder{Encoder: c.encoder, ObjectTyper: c.typer}); err != nil {
return err
}
}
objectKind.SetGroupVersionKind(gvks[0])
return c.encoder.Encode(obj, w) // 写到相应中
}
// Perform a conversion if necessary
// 进行版本转换
out, err := c.convertor.ConvertToVersion(obj, c.encodeVersion)
if err != nil {
return err
}
if e, ok := out.(runtime.NestedObjectEncoder); ok {
if err := e.EncodeNestedObjects(runtime.WithVersionEncoder{Version: c.encodeVersion, Encoder: c.encoder, ObjectTyper: c.typer}); err != nil {
return err
}
}
// Conversion is responsible for setting the proper group, version, and kind onto the outgoing object
return c.encoder.Encode(out, w)
}
- 从 end to end 角度看,资源 encode 流程如下所示: