前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Kubernetes 资源对象序列化实现

Kubernetes 资源对象序列化实现

作者头像
我是阳明
发布2021-06-25 17:05:53
1.8K0
发布2021-06-25 17:05:53
举报
文章被收录于专栏:k8s技术圈k8s技术圈k8s技术圈

序列化和反序列化在很多项目中都有应用,Kubernetes也不例外。Kubernetes中定义了大量的API对象,为此还单独设计了一个(https://github.com/kubernetes/api),方便多个模块引用。API对象在不同的模块之间传输(尤其是跨进程)可能会用到序列化与反序列化,不同的场景对于序列化个格式又不同,比如grpc协议用protobuf,用户交互用yaml(因为yaml可读性强),etcd存储用json。Kubernetes反序列化API对象不同于我们常用的json.Unmarshal()函数(需要传入对象指针),Kubernetes需要解析对象的类型(Group/Version/Kind),根据API对象的类型构造API对象,然后再反序列化。因此,Kubernetes定义了Serializer接口,专门用于API对象的序列化和反序列化。本文引用源码为kubernetes的release-1.21分支。

Serializer

因为Kubernetes需要支持json、yaml、protobuf三种数据格式的序列化和反序列化,有必要抽象序列化和反序列化的统一接口,源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/interfaces.go#L86

// Serializer是用于序列化和反序列化API对象的核心接口,
type Serializer interface {
    // Serializer继承了编码器和解码器,编码器就是用来序列化API对象的,序列化的过程称之为编码;反之,反序列化的过程称之为解码。
    // 关于编/解码器的定义下面有注释。
 Encoder
 Decoder
}

// 序列化的过程称之为编码,实现编码的对象称之为编码器(Encoder)
type Encoder interface {
    // Encode()将对象写入流。可以将Encode()看做为json(yaml).Marshal(),只是输出变为io.Writer。
 Encode(obj Object, w io.Writer) error

    // Identifier()返回编码器的标识符,当且仅当两个不同的编码器编码同一个对象的输出是相同的,那么这两个编码器的标识符也应该是相同的。
    // 也就是说,编码器都有一个标识符,两个编码器的标识符可能是相同的,判断标准是编码任意API对象时输出都是相同的。
    // 标识符有什么用?标识符目标是与CacheableObject.CacheEncode()方法一起使用,CacheableObject又是什么东东?后面有介绍。
 Identifier() Identifier
}

// 标识符就是字符串,可以简单的理解为标签的字符串形式,后面会看到如何生成标识符。
type Identifier string

// 反序列化的过程称之为解码,实现解码的对象称之为解码器(Decoder)
type Decoder interface {
    // Decode()尝试使用Schema中注册的类型或者提供的默认的GVK反序列化API对象。
    // 如果'into'非空将被用作目标类型,接口实现可能会选择使用它而不是重新构造一个对象。
    // 但是不能保证输出到'into'指向的对象,因为返回的对象不保证匹配'into'。
    // 如果提供了默认GVK,将应用默认GVK反序列化,如果未提供默认GVK或仅提供部分,则使用'into'的类型补全。
 Decode(data []byte, defaults *schema.GroupVersionKind, into Object) (Object, *schema.GroupVersionKind, error)
}

我们平时工作中最常用的序列化格式包括json、yaml以及protobuf,非常"巧合",Kubernetes也是用这几种序列化格式。以json为例,编码器和解码器可以等同于json.Marshal()和json.Unmarshal(),定义成interface是对序列化与反序列化的统一抽象。为什么我们平时很少抽象(当然有些读者是有抽象的,我们不能一概而论),是因为我们工作中可能只用到一种序列化格式,所以抽象显得没那么必要。而Kubernetes中,这三种都是需要的,yaml的可视化效果好,比如我们写的各种yaml文件;而API对象存储在etcd中是json格式,在用到grpc的地方则需要protobuf格式。

再者,Kubernetes对于Serializer的定义有更高的要求,即根据序列化的数据中的元数据自动识别API对象的类型(GVK),这在Decoder.Decode()接口定义中已经有所了解。而我们平时使用json.Marshal()的时候传入了指定类型的对象指针,相比于Kubernetes对于反序列化的要求,我们使用的相对更"静态"。

综上所述,抽象Serializer就有必要了,尤其是RecognizingDecoder可以解码任意格式的API对象就可以充分体现这种抽象的价值。

json

json.Serializer实现了将API对象序列化成json数据和从json数据反序列化API对象,源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/serializer/json/json.go#L100

// Serializer实现了runtime.Serializer接口。
type Serializer struct {
    // MetaFactory从json数据中提取GVK(Group/Version/Kind),下面有MetaFactory注释。
    // MetaFactory很有用,解码时如果不提供默认的GVK和API对象指针,就要靠MetaFactory提取GVK了。
    // 当然,即便提供了供默认的GVK和API对象指针,提取的GVK的也是非常有用的,详情参看Decode()接口的实现。
 meta    MetaFactory
    // SerializerOptions是Serializer选项,可以看做是配置,下面有注释。
 options SerializerOptions
    // runtime.ObjectCreater根据GVK构造API对象,在反序列化时会用到,其实它就是Schema。
    // runtime.ObjectCreater的定义读者可以自己查看源码,如果对Schema熟悉的读者这都不是事。
 creater runtime.ObjectCreater
    // runtime.ObjectTyper根据API对象返回可能的GVK,也是用在反序列化中,其实它也是Schema。
    // 这个有什么用?runtime.Serializer.Decode()接口注释说的很清楚,在json数据和默认GVK无法提供的类型元数据需要用输出类型补全。
 typer   runtime.ObjectTyper
    // 标识符,Serializer一旦被创建,标识符就不会变了。
 identifier runtime.Identifier
}

// SerializerOptions定义了Serializer的选项.
type SerializerOptions struct {
    // true: 序列化/反序列化yaml;false: 序列化/反序列化json
    // 也就是说,json.Serializer既可以序列化/反序列json,也可以序列化/反序列yaml。
 Yaml bool

    // Pretty选项仅用于Encode接口,输出易于阅读的json数据。当Yaml选项为true时,Pretty选项被忽略,因为yaml本身就易于阅读。
    // 什么是易于阅读的?举个例子就立刻明白了,定义测试类型为:
    // type Test struct {
    //     A int
    //     B string    
    // }
    // 则关闭和开启Pretty选项的对比如下:
    // {"A":1,"B":"2"}
    // {
    //   "A": 1,
    //   "B": "2"
    // }
    // 很明显,后者更易于阅读。易于阅读只有人看的时候才有需要,对于机器来说一点价值都没有,所以这个选项使用范围还是比较有限的。
 Pretty bool

    // Strict应用于Decode接口,表示严谨的。那什么是严谨的?笔者很难用语言表达,但是以下几种情况是不严谨的:
    // 1. 存在重复字段,比如{"value":1,"value":1};
    // 2. 不存在的字段,比如{"unknown": 1},而目标API对象中不存在Unknown属性;
    // 3. 未打标签字段,比如{"Other":"test"},虽然目标API对象中有Other字段,但是没有打`json:"Other"`标签
    // Strict选项可以理解为增加了很多校验,请注意,启用此选项的性能下降非常严重,因此不应在性能敏感的场景中使用。
    // 那什么场景需要用到Strict选项?比如Kubernetes各个服务的配置API,对性能要求不高,但需要严格的校验。
 Strict bool
}

MetaFactory

MetaFactory类型名定义其实挺忽悠人的,工厂类都是用来构造对象的,MetaFactory的功能虽然也是构造类型元数据的,但是它更像是一个解析器,所以笔者认为"MetaParser"更加贴切,源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/serializer/json/meta.go#L28

type MetaFactory interface {
    // 解析json数据中的元数据字段,返回GVK(Group/Version/Kind)。
    // 如果MetaFactory就这么一个接口函数,笔者认为叫解释器或者解析器更加合理。
 Interpret(data []byte) (*schema.GroupVersionKind, error)
}

// SimpleMetaFactory是MetaFactory的一种实现,用于检索在json中由"apiVersion"和"kind"字段标识的对象的类型和版本。
type SimpleMetaFactory struct {
}

// Interpret()实现了MetaFactory.Interpret()接口。
func (SimpleMetaFactory) Interpret(data []byte) (*schema.GroupVersionKind, error) {
    // 定义一种只有apiVersion和kind两个字段的匿名类型
 findKind := struct {
  // +optional
  APIVersion string `json:"apiVersion,omitempty"`
  // +optional
  Kind string `json:"kind,omitempty"`
 }{}
    // 只解析json中apiVersion和kind字段,这个玩法有点意思,但是笔者认为这个方法有点简单粗暴。
    // 读者可以尝试阅读json.Unmarshal(),该函数会遍历整个json,开销不小,其实必要性不强,因为只需要apiVersion和kind字段。
    // 试想一下,如果每次反序列化一个API对象都要有一次Interpret()和Decode(),它的开销相当于做了两次反序列化。
 if err := json.Unmarshal(data, &findKind); err != nil {
  return nil, fmt.Errorf("couldn't get version/kind; json parse error: %v", err)
 }
    // 将apiVersion解析为Group和Version
 gv, err := schema.ParseGroupVersion(findKind.APIVersion)
 if err != nil {
  return nil, err
 }
    // 返回API对象的GVK
 return &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: findKind.Kind}, nil
}

Decode

其实Serializer的重头戏在解码,因为解码需要考虑的事情比较多,比如提取类型元数据(GVK),根据类型元数据构造API对象,当然还要考虑复用传入的API对象。而编码就没有这么复杂,所以理解了解码的实现,编码就基本可以忽略不计了。源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/serializer/json/json.go#L209

// Decode实现了Decoder.Decode(),尝试从数据中提取的API类型(GVK),应用提供的默认GVK,然后将数据加载到所需类型或提供的'into'匹配的对象中:
// 1. 如果into为*runtime.Unknown,则将提取原始数据,并且不执行解码;
// 2. 如果into的类型没有在Schema注册,则使用json.Unmarshal()直接反序列化到'into'指向的对象中;
// 3. 如果'into'不为空且原始数据GVK不全,则'into'的类型(GVK)将用于补全GVK;
// 4. 如果'into'为空或数据中的GVK与'into'的GVK不同,它将使用ObjectCreater.New(gvk)生成一个新对象;
// 成功或大部分错误都会返回GVK,GVK的计算优先级为originalData > default gvk > into.
func (s *Serializer) Decode(originalData []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
 data := originalData
    // 如果配置选项为yaml,则将yaml格式转为json格式,是不是有一种感觉:“卧了个槽”!所谓可支持json和yaml,就是先将yaml转为json!
    // 那我感觉我可以支持所有格式,都转为json就完了呗!其实不然,yaml的使用场景都是需要和人交互的地方,所以对于效率要求不高(qps低)。
    // 那么实现简单易于维护更重要,所以这种实现并没有什么毛病。
 if s.options.Yaml {
        // yaml转json
  altered, err := yaml.YAMLToJSON(data)

  if err != nil {
   return nil, nil, err
  }
  data = altered
 }
    // 此时data是json了,以后就不用再考虑yaml选项了

    // 解析类型元数据,原理已经在MetaFactory解释过了。
 actual, err := s.meta.Interpret(data)
 if err != nil {
  return nil, nil, err
 }

    // 解析类型元数据大部分情况是正确的,除非不是json或者apiVersion格式不对。
    // 但是GVK三元组可能有所缺失,比如只有Kind,Group/Version,其他字段就用默认的GVK补全。
    // 这也体现出了原始数据中的GVK的优先级最高,其次是默认的GVK。gvkWithDefaults()函数下面有注释。
 if gvk != nil {
  *actual = gvkWithDefaults(*actual, *gvk)
 }

    // 如果'into'是*runtime.Unknown类型,需要返回*runtime.Unknown类型的对象。
    // 需要注意的是,此处复用了'into'指向的对象,因为返回的类型与'into'指向的类型完全匹配。
 if unk, ok := into.(*runtime.Unknown); ok && unk != nil {
        // 输出原始数据
  unk.Raw = originalData
        // 输出数据格式为JSON,那么问题来了,不是'data'才能保证是json么?如s.options.Yaml == true,originalData是yaml格式才对。
        // 所以笔者有必要提一个issue,看看官方怎么解决,如此明显的一个问题为什么没有暴露出来?
        // 笔者猜测:runtime.Unknown只在内部使用,而内部只用json格式,所以自然不会暴露出来。
  unk.ContentType = runtime.ContentTypeJSON
        // 输出GVK。
  unk.GetObjectKind().SetGroupVersionKind(*actual)
  return unk, actual, nil
 }

    // 'into'不为空,通过into类型提取GVK,这样在原始数据中的GVK和默认GVK都没有的字段用into的GVK补全。
 if into != nil {
        // 判断'into'是否为runtime.Unstructured类型
  _, isUnstructured := into.(runtime.Unstructured)
        // 获取'into'的GVK,需要注意的是返回的是一组GVK,types[0]是推荐的GVK。
  types, _, err := s.typer.ObjectKinds(into)
  switch {
        // 'into'的类型如果没有被注册或者为runtime.Unstructured类型,则直接反序列成'into'指向的对象。
        // 没有被注册的类型自然无法构造对象,而非结构体等同于map[string]interface{},不可能是API对象(因为API对象必须是结构体)。
        // 所以这两种情况直接反序列化到'into'对象就可以了,此时与json.Unmarshal()没什么区别。
  case runtime.IsNotRegisteredError(err), isUnstructured:
   if err := caseSensitiveJSONIterator.Unmarshal(data, into); err != nil {
    return nil, actual, err
   }
   return into, actual, nil
        // 获取'into'类型出错,多半是因为不是指针
  case err != nil:
   return nil, actual, err
        // 用'into'的GVK补全未设置的GVK,所以GVK的优先级:originalData > default gvk > into
  default:
   *actual = gvkWithDefaults(*actual, types[0])
  }
 }

    // 如果没有Kind
 if len(actual.Kind) == 0 {
  return nil, actual, runtime.NewMissingKindErr(string(originalData))
 }
    // 如果没有Version
 if len(actual.Version) == 0 {
  return nil, actual, runtime.NewMissingVersionErr(string(originalData))
 }
    // 那么问题来了,为什么不判断Group?Group为""表示"core",比如我们写ymal的时候Kind为Pod,apiVersion是v1,并没有设置Group。

    // 从函数名字可以看出复用'into'或者重新构造对象,复用的原则是:如果'into'注册的一组GVK有任何一个与*actual相同,则复用'into'。
    // runtime.UseOrCreateObject()源码读者感兴趣可以自己看下
 obj, err := runtime.UseOrCreateObject(s.typer, s.creater, *actual, into)
 if err != nil {
  return nil, actual, err
 }

    // 反序列化对象,caseSensitiveJSONIterator暂且不用关心,此处可以理解为json.Unmarshal()。
    // 当然,读者非要知道个究竟,可以看看代码,笔者此处不注释。
 if err := caseSensitiveJSONIterator.Unmarshal(data, obj); err != nil {
  return nil, actual, err
 }

    // 如果是非strict模式,可以直接返回了。其实到此为止就可以了,后面是针对strict模式的代码,是否了解并不重要。
 if !s.options.Strict {
  return obj, actual, nil
 }

    // 笔者第一眼看到下面的以为看错了,但是擦了擦懵逼的双眼,发现就是YAMLToJSON。如果原始数据是json不会有问题么?
    // 笔者查看了一下yaml.YAMLToJSONStrict()函数注释:由于JSON是YAML的子集,因此通过此方法传递JSON应该是没有任何操作的。
    // 除非存在重复的字段,会解析出错。所以此处就是用来检测是否有重复字段的,当然,如果是yaml格式顺便转成了json。
    // 感兴趣的读者可以阅读源码,笔者只要知道它的功能就行了,就不“深究”了。
 altered, err := yaml.YAMLToJSONStrict(originalData)
 if err != nil {
  return nil, actual, runtime.NewStrictDecodingError(err.Error(), string(originalData))
 }
    // 接下来会因为未知的字段报错,比如对象未定义的字段,未打标签的字段等。
    // 此处使用DeepCopyObject()等同于新构造了一个对象,而这个对象其实又没什么用,仅作为一个临时的变量使用。
 strictObj := obj.DeepCopyObject()
 if err := strictCaseSensitiveJSONIterator.Unmarshal(altered, strictObj); err != nil {
  return nil, actual, runtime.NewStrictDecodingError(err.Error(), string(originalData))
 }
 // 返回反序列化的对象、GVK,所谓的strict模式无非是再做了一次转换和反序列化来校验数据的正确性,结果直接丢弃。
    // 所以说strict没有必要不用开启,除非你真正理解他的作用并且能够承受带来的后果。
 return obj, actual, nil
}

// gvkWithDefaults()利用defaultGVK补全actual中未设置的字段。
// 需要注意的是,参数'defaultGVK'只是一次调用相对于actual的默认GVK,不是Serializer.Decode()的默认GVK。
func gvkWithDefaults(actual, defaultGVK schema.GroupVersionKind) schema.GroupVersionKind {
    // actual如果没有设置Kind则用默认的Kind补全
 if len(actual.Kind) == 0 {
  actual.Kind = defaultGVK.Kind
 }
    // 如果Group和Version都没有设置,则用默认的Group和Version补全。
    // 为什么必须是都没有设置?缺少Version或者Group有什么问题么?下面的代码给出了答案。
 if len(actual.Version) == 0 && len(actual.Group) == 0 {
  actual.Group = defaultGVK.Group
  actual.Version = defaultGVK.Version
 }
    // 如果Version未设置,则用默认的Version补全,但是前提是Group与默认Group相同。
    // 因为Group不同的API即便Kind/Version相同可能是两个完全不同的类型,比如自定义资源(CRD)。
 if len(actual.Version) == 0 && actual.Group == defaultGVK.Group {
  actual.Version = defaultGVK.Version
 }
    // 如果Group未设置而Version与默认的Version相同,为什么不用默认的Group补全?
    // 前面已经解释过了,应该不用再重复了。
 return actual
}

Encode

相比于解码,编码就简单很多,直接按照选项(yaml、pretty)编码就行了,源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/serializer/json/json.go#L297

// Encode()实现了Encoder.Encode()接口。
func (s *Serializer) Encode(obj runtime.Object, w io.Writer) error {
    // CacheableObject允许对象缓存其不同的序列化数据,以避免多次执行相同的序列化,这是一种出于效率考虑设计的类型。
    // 因为同一个对象可能会多次序列化json、yaml和protobuf,此时就需要根据编码器的标识符找到对应的序列化数据。
 if co, ok := obj.(runtime.CacheableObject); ok {
        // CacheableObject笔者不再注释了,感兴趣的读者可以自行阅读源码。
        // 其实根据传入的参数也能猜出来具体实现:利用标识符查一次map,如果有就输出,没有就调用一次s.doEncode()。
  return co.CacheEncode(s.Identifier(), s.doEncode, w)
 }
    // 非CacheableObject对象,就执行一次json的序列化。
 return s.doEncode(obj, w)
}

// doEncode()类似于于json.Marshal(),只是写入是io.Writer而不是[]byte。
func (s *Serializer) doEncode(obj runtime.Object, w io.Writer) error {
    // 序列化成yaml?
 if s.options.Yaml {
        // 序列化对象为json
  json, err := caseSensitiveJSONIterator.Marshal(obj)
  if err != nil {
   return err
  }
        // json->yaml
  data, err := yaml.JSONToYAML(json)
  if err != nil {
   return err
  }
        // 写入io.Writer。
  _, err = w.Write(data)
  return err
 }

    // 输出易于理解的格式?那么问题来了,为什么输出yaml不需要这个判断?很简单,yaml就是易于理解的。
 if s.options.Pretty {
        // 序列化对象为json
  data, err := caseSensitiveJSONIterator.MarshalIndent(obj, "", "  ")
  if err != nil {
   return err
  }
        // 写入io.Writer。
  _, err = w.Write(data)
  return err
 }

    // 非pretty模式,用我们最常用的json序列化方法,无非我们最常用方法是json.Marshal()。
    // 如果需要写入io.Writer,下面的代码是标准写法。
 encoder := json.NewEncoder(w)
 return encoder.Encode(obj)
}

identifier

前面笔者提到了,编码器标识符可以看做是标签的字符串形式,大家应该很熟悉Kubernetes中的标签选择器,符合标签选择器匹配规则的所有API对象都可以看做"同质"。同样的道理,编码器也有自己的标签,标签相同的所有编码器是同质的,即编码同一个API对象的结果都是一样的。编码器标识符的定义没有那么复杂,就是简单的字符串,匹配也非常简单,标识符相等即为匹配,所以标识符可以理解为标签的字符串形式。源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/serializer/json/json.go#L66

// identifier()根据给定的选项计算编码器的标识符。
func identifier(options SerializerOptions) runtime.Identifier {
    // 编码器的唯一标识符是一个map[string]string,不同属性的组合形成更了唯一性。
 result := map[string]string{
        // 名字是json,表明是json编码器。
  "name":   "json",
        // 输出格式为yaml或json
  "yaml":   strconv.FormatBool(options.Yaml),
        // 是否为pretty模式
  "pretty": strconv.FormatBool(options.Pretty),
 }
    // 序列化成json,生成最终的标识符,json序列化是标签的一种字符串形式。
 identifier, err := json.Marshal(result)
 if err != nil {
  klog.Fatalf("Failed marshaling identifier for json Serializer: %v", err)
 }
    // 也就是说,只要yaml和pretty选项相同的任意两个json.Serializer,任何时候编码同一个API对象输出一定是相同的。
    // 所以当API对象被多个编码器多次编码时,以编码器标识符为键利用缓冲避免重复编码。
 return runtime.Identifier(identifier)
}

yaml

其实json.Serializer支持json和ymal,那么还有必要定义yaml.Serializer么?毕竟构造两个json.Serializer对象,一个开启yaml选型,一个关闭yaml选项就可以了。来看看yaml.Serializer的定义,源码链接:https://github.com/kubernetes/apimachinery/blob/release-1.21/pkg/runtime/serializer/yaml/yaml.go#L26

// yamlSerializer实现了runtime.Serializer()。
// 没有定义Serializer类型,而是一个包内的私有类型yamlSerializer,如果需要使用这个类型必须通过包内的公有接口创建。
type yamlSerializer struct {
    // 直接继承了runtime.Serializer,不用想肯定是json.Serializer。
 runtime.Serializer
}

// NewDecodingSerializer()向支持json的Serializer添加yaml解码支持。
// 也就是说yamlSerializer编码json,解码yaml,当然从接口名字看,调用这个接口估计只需要用解码能力吧。
// 好在笔者检索了一下源码,yamlSerializer以及NewDecodingSerializer()没有引用的地方,应该是历史遗留的代码。
func NewDecodingSerializer(jsonSerializer runtime.Serializer) runtime.Serializer {
 return &yamlSerializer{jsonSerializer}
}

// Decode()实现了Decoder.Decode()接口。
func (c yamlSerializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
    // yaml->json
 out, err := yaml.ToJSON(data)
 if err != nil {
  return nil, nil, err
 }
    // 反序列化json
 data = out
 return c.Serializer.Decode(data, gvk, into)
}

总结

  1. json.Serializer可以实现json和yaml两种数据格式的序列化/反序列化,而yaml.Serializer基本不用了;
  2. MetaFactory的功能就是提取apiVersion和kind字段,然后返回GVK;
  3. json.Serializer也可以像json/yaml.Unmarshal()一样使用,只要传入的'into'的类型没有在Schema中注册就可以了;
  4. json.Serializer在反序列化之前需要计算API对象的GVK,计算原则是优先使用json中的GVK,如果GVK有残缺,则采用默认GVK补全,最后用'into'指向的对象类型补全;
  5. 其实runtime.Serializer只是比json/yaml.Unmarshal()多了类型提取并构造对象的过程,但是依然存在无法通用的问题,即解码json和yaml需要不同的对象,这就要RecognizingDecoder来解决了;

原文链接:https://github.com/jindezgm/k8s-src-analysis/blob/master/apimachinery/runtime/Serializer.md

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-06-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 k8s技术圈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Serializer
  • json
    • MetaFactory
      • Decode
        • Encode
          • identifier
          • yaml
          • 总结
          相关产品与服务
          文件存储
          文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档