Kubernetes 编程 / Operator 专题【左扬精讲】—— runtime.Codec 资源编解码:serializer 与 codec 差异、编解码数据结构、codec 核心调用链路

Kubernetes 编程 / Operator 专题【左扬精讲】—— runtime.Codec 资源编解码

当你通过 kubectl 提交一个 Pod YAML 时,背后经过了怎样的序列化旅程?

APIServer 收到了字节流,如何把它还原成 Go 对象?

client-go 客户端拿到了原始字节,如何把它反序列化成 typed 对象?

这一进一出之间,serializer 和 codec 是怎么分工的?

本文基于 Kubernetes 1.36.1 源码,带你把整个编解码体系从接口定义到具体实现彻底打通。

Kubernetes Go runtime v1.36.1

🔓 学习重点提示
★ 重点掌握(必须)
   • Codec vs Serializer 的本质区别:Codec 携带版本转换逻辑,Serializer 只负责格式化
   • versioning.codec 的 Encode 流程:对象先版本转换再委托序列化
   • versioning.codec 的 Decode 流程:字节流先反序列化再版本转换
   • CodecFactory 实例化链路:工厂→序列化器→编解码器→codec 的完整创建路径

☆ 次重点(了解即可)
   • json/Protobuf/CBOR Serializer 的实现差异
   • UnstructuredJSONScheme 的 doEncode / decodeToList 内部机制
   • ParameterCodec 的参数编解码
   • APIServer 请求处理链路中 transformResponseObject 的角色


一、为什么需要 Codec:Serializer vs Codec 的本质差异

在 Kubernetes 里,"把 Go 对象变成字节流"这件事分成了两个层次,理解这两个层次的区分是掌握整个编解码体系的关键。

Serializer 就像一个翻译员,只负责把中文翻译成英文(或反过来),但不懂业务逻辑。

Codec 则像一个项目经理,不仅协调翻译,还知道客户要的是哪个版本(中文 v1 还是 v2),负责版本之间的转换后再翻译。

Serializer(序列化器)只负责将 Go 对象格式化为特定格式(JSON / YAML / Protobuf / CBOR)的字节流,或从字节流还原 Go 对象。

它不携带版本转换逻辑,只知道输入和输出对象是什么格式。

Codec(编解码器)在 Serializer 基础上捆绑了版本转换器(GroupVersioner)。

Encode 时,它先把对象从内部版本转为外部版本,再委托给底层的 Serializer 序列化;Decode 时,它先委托 Serializer 反序列化,再把对象从外部版本转为内部版本。

换句话说:Codec = Serializer + 版本转换逻辑。

APIServer 内部存储用内部版本(internal),对外通信用外部版本(v1、apps/v1 等),所以在 APIServer 中,Codec 是绝对的核心引擎。

1.1 核心接口定义

在 staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go 中,这几个核心接口的定义如下:

// staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go(行 51-125)

// Encoder:将对象写入序列化字节流的接口
type Encoder interface {
    Encode(obj Object, w io.Writer) error
    Identifier() Identifier
}

// Decoder:从字节流还原对象的接口
type Decoder interface {
    Decode(data []byte, defaults *schema.GroupVersionKind, into Object) (Object, *schema.GroupVersionKind, error)
}

// Serializer:Encoder + Decoder,即同时支持序列化和反序列化
type Serializer interface {
    Encoder
    Decoder
}

// Codec:同样是 Encoder + Decoder,但它是"标记接口"
// 告诉消费者:这个序列化器知道版本信息,Encode 时需要版本转换
type Codec Serializer

Codec 和 Serializer 在 Go 语言层面都是 type Codec Serializer,即 Codec 只是 Serializer 的别名,没有任何额外方法。

真正的区别在于语义和用法:当你需要一个会做版本转换的编解码器时,你请求 Codec;当你只需要格式化/反格式化(不需要版本转换)时,你请求 Serializer。

1.2 CodecFactory 提供的核心方法

在实际使用中,你不会直接 new 一个 Serializer,而是通过 CodecFactory 来创建。

CodecFactory 是 Kubernetes 中最核心的工厂类,它不仅仅产出 Serializer,还产出经过版本转换包装的 Codec。

CodecFactory 位于 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go,提供的方法包括:

  • LegacyCodec(version):创建对特定版本编码/任意版本解码的 Codec(已deprecated)
  • UniversalDeserializer():返回可以解码任意已知格式的 decoder(不做版本转换)
  • UniversalDecoder(versions):返回对特定版本做解码时转换的 decoder
  • CodecForVersions(encoder, decoder, encodeGV, decodeGV):创建精确控制版本转换方向的 Codec
  • EncoderForVersion(encoder, gv):创建编码到特定版本的 encoder
  • DecoderToVersion(decoder, gv):创建解码到特定版本的 decoder

二、CodecFactory 实例化:从 Scheme 到编解码器的完整链路

2.1 CodecFactory 的结构

CodecFactory 本身只包含三个字段:

// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go(行 101-108)

type CodecFactory struct {
    scheme    *runtime.Scheme              // 类型注册表:GVK  Go Type 双向映射
    universal runtime.Decoder             // 通用解码器:可解码所有已知格式
    accepts   []runtime.SerializerInfo     // 支持的媒体类型列表(JSON/YAML/Protobuf)
    legacySerializer runtime.Serializer    // JSON 序列化器(向后兼容用)
}

2.2 SerializerInfo:序列化格式元数据

accepts 字段是 []SerializerInfo,这是 Kubernetes 序列化体系中最重要的数据结构之一。

每个 SerializerInfo 代表一种支持的序列化格式:

// staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go(行 144-165)

type SerializerInfo struct {
    MediaType        string    // 完整媒体类型,如 "application/json"
    MediaTypeType    string    // 第一部分,如 "application"
    MediaTypeSubType string    // 第二部分,如 "json"
    EncodesAsText    bool      // 是否可编码为纯文本(JSON/YAML 可以,Protobuf 不行)
    Serializer       Serializer  // 单对象序列化器(Encode/Decode)
    PrettySerializer Serializer  // 格式化序列化器(带缩进,调试友好)
    StrictSerializer Serializer  // 严格模式序列化器(遇到未知字段报错)
    StreamSerializer *StreamSerializerInfo  // 流式序列化器(用于 Watch 等场景)
}

2.3 实例化链路流程图

让我们用流程图来展示从 runtime.Scheme 到最终 Codec 的完整链路:

┌───────────────────────────────────────────────────────────────┐
│  runtime.Scheme(类型注册表,GVK ↔ Go Type 双向映射)           │
└─────────────────────────────┬─────────────────────────────────┘
                              │
┌─────────────────────────────▼─────────────────────────────────┐
│  CodecFactoryOptions(序列化选项:Pretty/Strict/Streaming…)    │
└─────────────────────────────┬─────────────────────────────────┘
                              │
┌─────────────────────────────▼─────────────────────────────────┐
│  newSerializersForScheme(scheme, json.DefaultMetaFactory)      │
│  ─────────────────────────────────────────────────────         │
│  创建 3 类 SerializerInfo:                                     │
│  ① JSON  SerializerInfo (MediaType="application/json")        │
│  ② YAML SerializerInfo (MediaType="application/yaml")          │
│  ③ Proto SerializerInfo                                        │
│      (MediaType="application/vnd.kubernetes.protobuf")         │
└─────────────────────────────┬─────────────────────────────────┘
                              │
┌─────────────────────────────▼─────────────────────────────────┐
│  newCodecFactory(scheme, []SerializerInfo)                      │
│  ─────────────────────────────────────────────────────         │
│  ① decoders[] = 各 SerializerInfo.Serializer                  │
│  ② universal = recognizer.NewDecoder(decoders...)             │
│      (多格式自动识别解码器)                                    │
│  ③ legacySerializer = JSON Serializer                         │
│  ④ accepts[] = SerializerInfo 列表                           │
└─────────────────────────────┬─────────────────────────────────┘
                              │
                              │  调用方请求 Codec
                              ▼
┌───────────────────────────────────────────────────────────────┐
│  CodecForVersions(encoder, decoder, encodeGV, decodeGV)       │
│  ─────────────────────────────────────────────────────         │
│  ↓ 内部调用 versioning.NewDefaultingCodecForScheme            │
│  → versioning.NewCodec(...)                                 │
│  → 返回 *versioning.codec(带版本转换逻辑的 Codec)           │
└───────────────────────────────────────────────────────────────┘

整个实例化链路的关键在于:CodecFactory.NewCodecFactory() 只负责创建 Serializer,而 CodecForVersions() 才负责把 Serializer 包装成带版本转换逻辑的 Codec。

2.4 三种序列化格式的创建

在 newSerializersForScheme 函数中,CodecFactory 初始化时创建了三种序列化器:

// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go(行 28-99)

// ① JSON 序列化器
jsonSerializer := json.NewSerializerWithOptions(
    mf, scheme, scheme,
    json.SerializerOptions{Yaml: false, Pretty: false, Strict: options.Strict},
)
jsonSerializerType := runtime.SerializerInfo{
    MediaType:     runtime.ContentTypeJSON,   // "application/json"
    EncodesAsText: true,                      // JSON 是纯文本,可直接在终端打印
    Serializer:    jsonSerializer,
    StreamSerializer: &runtime.StreamSerializerInfo{
        Serializer: jsonSerializer,
        Framer:    json.Framer,   // JSON 换行分隔帧
    },
}

// ② YAML 序列化器(底层是 JSON,输出前转 YAML)
yamlSerializer := json.NewSerializerWithOptions(
    mf, scheme, scheme,
    json.SerializerOptions{Yaml: true, Pretty: false, Strict: options.Strict},
)

// ③ Protobuf 序列化器(二进制格式,高效但不可读)
protoSerializer := protobuf.NewSerializerWithOptions(scheme, scheme, protobuf.SerializerOptions{})
protoSerializerType := runtime.SerializerInfo{
    MediaType:     runtime.ContentTypeProtobuf,
    EncodesAsText: false,   // 二进制,不可读
    Serializer:    protoSerializer,
    StreamSerializer: &runtime.StreamSerializerInfo{
        Serializer: protobuf.NewRawSerializer(scheme, scheme),
        Framer:    protobuf.LengthDelimitedFramer,   // 长度前缀帧
    },
}

注意:这三个 Serializer 都只实现了"格式转换"(Go 对象 ↔ JSON/YAML/Protobuf 字节流),它们本身不做版本转换。版本转换是在 Codec 层完成的。


三、Codec 核心实现:versioning.codec 的编解码流程

3.1 versioning.codec 的数据结构

versioning.codec 是 Kubernetes 中 Codec 接口的核心实现,位于 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go:

// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go(行 75-93)

type codec struct {
    encoder   runtime.Encoder       // 底层序列化器(JSON/YAML/Protobuf)
    decoder   runtime.Decoder       // 底层反序列化器(JSON/YAML/Protobuf)
    convertor runtime.ObjectConvertor   // 版本转换器:internal  external
    creater   runtime.ObjectCreater     // 对象创建器:根据 GVK 创建 Go 对象
    typer     runtime.ObjectTyper       // 类型推断器:从 Go Type 推断 GVK
    defaulter runtime.ObjectDefaulter   // 默认填充器:应用默认值
    encodeVersion runtime.GroupVersioner   // Encode 时,目标版本是什么
    decodeVersion runtime.GroupVersioner   // Decode 时,目标版本是什么
    identifier runtime.Identifier
}

// 便捷构造函数:scheme 同时充当 convertor/creater/typer/defaulter
func NewDefaultingCodecForScheme(scheme *runtime.Scheme, encoder, decoder runtime.Encoder,
    encodeGV, decodeGV runtime.GroupVersioner) runtime.Codec {
    return NewCodec(encoder, decoder,
        runtime.UnsafeObjectConvertor(scheme), scheme, scheme, scheme,
        encodeGV, decodeGV, scheme.Name())
}

这里最核心的设计是:encodeVersion 和 decodeVersion 两个 GroupVersioner 字段,正是它们决定了 Encode 时要转换成哪个版本、Decode 时要转换成哪个版本。

3.2 Encode 流程:版本转换 + 序列化

① codec.Encode(obj, w)  →  ② CacheableObject 缓存检查  →  ③ doEncode 版本转换  →  ④ encoder.Encode()

// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go(行 217-285)

// Encode:对外部暴露的编码入口
func (c *codec) Encode(obj runtime.Object, w io.Writer) error {
    return c.encode(obj, w, nil)
}

// encode:处理 CacheableObject 缓存逻辑
func (c *codec) encode(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error {
    if co, ok := obj.(runtime.CacheableObject); ok {
        return co.CacheEncode(c.Identifier(), func(obj, w) error {
            return c.doEncode(obj, w, memAlloc)
        }, w)
    }
    return c.doEncode(obj, w, memAlloc)
}

// doEncode:真正的编码核心逻辑
func (c *codec) doEncode(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error {
    encodeFn := c.encoder.Encode
    if memAlloc != nil {
        if ea, ok := c.encoder.(runtime.EncoderWithAllocator); ok {
            encodeFn = func(obj, w io.Writer) error { return ea.EncodeWithAllocator(obj, w, memAlloc) }
        }
    }

    // 特殊路径:runtime.Unknown 直接编码;UnstructuredList 检查版本
    switch obj := obj.(type) {
    case *runtime.Unknown: return encodeFn(obj, w)
    case runtime.Unstructured:
        if !isList {
            objGVK := obj.GetObjectKind().GroupVersionKind()
            if len(objGVK.Version) == 0 { return encodeFn(obj, w) }
            targetGVK, ok := c.encodeVersion.KindForGroupVersionKinds([]schema.GroupVersionKind{objGVK})
            if !ok { return ErrNotRegistered }
            if targetGVK == objGVK { return encodeFn(obj, w) }
        }
    }

    // 查询对象的 GVK
    gvks, isUnversioned, err := c.typer.ObjectKinds(obj)
    if err != nil { return err }

    objectKind := obj.GetObjectKind()
    oldGVK := objectKind.GroupVersionKind()
    defer objectKind.SetGroupVersionKind(oldGVK)

    // 无版本转换器 或 无版本对象:直接编码
    if c.encodeVersion == nil || isUnversioned {
        objectKind.SetGroupVersionKind(gvks[0])
        return encodeFn(obj, w)
    }

    // ========== 核心:版本转换 ==========
    // 调用 Scheme.ConvertToVersion() 将对象从内部版本转为外部版本
    // 例如:internal.Pod → v1.Pod
    out, err := c.convertor.ConvertToVersion(obj, c.encodeVersion)
    if err != nil { return err }

    // 处理嵌套对象的版本编码
    if nested, ok := out.(runtime.NestedObjectEncoder); ok {
        nested.EncodeNestedObjects(runtime.WithVersionEncoder{
            Version: c.encodeVersion, Encoder: c.encoder, ObjectTyper: c.typer,
        })
    }

    // 调用底层 Serializer.Encode() 将 Go 对象序列化为字节
    return encodeFn(out, w)
}

Encode 流程的关键三步:

  • 第一步:拿到对象后,通过 typer.ObjectKinds() 查询它的所有可能 GVK
  • 第二步:如果需要版本转换(encodeVersion != nil),调用 convertor.ConvertToVersion() 将对象从内部版本转为目标外部版本
  • 第三步:最后调用 encoder.Encode() 将版本化后的 Go 对象序列化为字节流写入 io.Writer

3.3 Decode 流程:反序列化 + 版本转换

① codec.Decode(data, defaultGVK, into)  →  ② decoder.Decode() 反序列化  →  ③ defaulter.Default() 应用默认值  →  ④ convertor.Convert() 版本转换

// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go(行 125-195)

// Decode:外部调用入口
func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into runtime.Object) (
    runtime.Object, *schema.GroupVersionKind, error) {

    // 如果 into 是 Unstructured 且指定了版本,创建新实例以确保走完整转换路径
    decodeInto := into
    if into != nil {
        if _, ok := into.(runtime.Unstructured); ok &&
            !into.GetObjectKind().GroupVersionKind().GroupVersion().Empty() {
            decodeInto = reflect.New(reflect.TypeOf(into).Elem()).Interface().(runtime.Object)
        }
    }

    // ========== 第一步:反序列化 ==========
    // 调用底层的 Serializer.Decode()(JSON/YAML/Protobuf Decoder)
    // 这一步只是把字节流还原成 Go 对象(外部版本)
    obj, gvk, err := c.decoder.Decode(data, defaultGVK, decodeInto)
    if err != nil {
        if strictErr, ok := runtime.AsStrictDecodingError(err); obj != nil && ok {
            strictDecodingErrs = append(strictDecodingErrs, strictErr.Errors()...)
        } else {
            return nil, gvk, err
        }
    }

    // 处理嵌套对象的解码
    if nested, ok := obj.(runtime.NestedObjectDecoder); ok {
        nested.DecodeNestedObjects(runtime.WithoutVersionDecoder{Decoder: c.decoder})
    }

    // ========== 第二步:应用默认值 ==========
    if c.defaulter != nil { c.defaulter.Default(obj) }

    // ========== 第三步:版本转换 ==========
    // 如果传入了 into 目标对象:直接用 Convert 在 into 上做转换(原地复用)
    if into != nil {
        if into == obj { return into, gvk, strictDecodingErr }
        err := c.convertor.Convert(obj, into, c.decodeVersion)
        return into, gvk, err
    }

    // 如果没有 into:调用 ConvertToVersion 创建新对象并转换
    out, err := c.convertor.ConvertToVersion(obj, c.decodeVersion)
    return out, gvk, err
}

Decode 流程中最精妙的设计是对 into 参数的处理:有 into 时用 Convert()(原地转换,复用传入的对象),没有 into 时用 ConvertToVersion()(创建新对象)。

这避免了大量不必要的对象分配。

defaultGVK 参数的作用是"兜底":如果字节流中的 GVK 信息不完整(例如只有 kind 没有 apiVersion),就用 defaultGVK 来补充。


四、具体序列化器实现:JSON / Protobuf / Unstructured

4.1 json.Serializer:JSON/YAML 编解码器

json.Serializer 是 Kubernetes 中使用最广泛的序列化器,它既可以处理 JSON 也可以处理 YAML(通过 SerializerOptions.Yaml=true 切换)。

// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go(行 101-113)

type Serializer struct {
    meta    json.MetaFactory        // 从 JSON 数据中提取 GVK 的工厂
    options SerializerOptions       // Yaml/Pretty/Strict/StreamingCollectionsEncoding
    creater runtime.ObjectCreater  // 根据 GVK 创建对象
    typer   runtime.ObjectTyper    // 推断对象的 GVK
    identifier runtime.Identifier
}

var _ runtime.Serializer = &Serializer{}
var _ recognizer.RecognizingDecoder = &Serializer{}   // 支持格式自动识别

JSON Serializer 的 Decode 实现需要处理 YAML 输入(先转 JSON)、未知类型对象、Strict 模式等多种情况:

// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go(行 138-216)

func (s *Serializer) Decode(originalData []byte, gvk *schema.GroupVersionKind, into runtime.Object) (
    runtime.Object, *schema.GroupVersionKind, error) {
    data := originalData

    // 第一步:如果输入是 YAML,先转为 JSON
    if s.options.Yaml {
        altered, err := yaml.YAMLToJSON(data)
        if err != nil { return nil, nil, err }
        data = altered
    }

    // 第二步:从 JSON 数据中提取 GVK(从 apiVersion 和 kind 字段)
    actual, err := s.meta.Interpret(data)
    if err != nil { return nil, nil, err }

    // 第三步:用 defaultGVK 补充缺失的 GVK 信息
    if gvk != nil { *actual = gvkWithDefaults(*actual, *gvk) }

    // 第四步:处理 runtime.Unknown(无法识别的类型,直接保留原始字节)
    if unk, ok := into.(*runtime.Unknown); ok && unk != nil {
        unk.Raw = originalData
        unk.ContentType = runtime.ContentTypeJSON
        unk.GetObjectKind().SetGroupVersionKind(*actual)
        return unk, actual, nil
    }

    // 第五步:反序列化到目标对象
    if into != nil {
        _, isUnstructured := into.(runtime.Unstructured)
        types, _, err := s.typer.ObjectKinds(into)
        switch {
        case runtime.IsNotRegisteredError(err), isUnstructured:
            strictErrs, err := s.unmarshal(into, data, originalData)
            if len(strictErrs) > 0 { return into, actual, runtime.NewStrictDecodingError(strictErrs) }
            return into, actual, err
        case err != nil: return nil, actual, err
        default: *actual = gvkWithDefaults(*actual, types[0])
        }
    }

    // 第六步:检查 GVK 完整性
    if len(actual.Kind) == 0 { return nil, actual, runtime.NewMissingKindErr(...) }
    if len(actual.Version) == 0 { return nil, actual, runtime.NewMissingVersionErr(...) }

    // 第七步:创建目标对象实例
    obj, err := runtime.UseOrCreateObject(s.typer, s.creater, *actual, into)
    if err != nil { return nil, actual, err }

    // 第八步:反序列化数据到对象
    strictErrs, err := s.unmarshal(obj, data, originalData)
    if len(strictErrs) > 0 { return obj, actual, runtime.NewStrictDecodingError(strictErrs) }
    return obj, actual, err
}

JSON Serializer 的 Encode 则相对简洁:

// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go(行 218-259)

func (s *Serializer) doEncode(obj runtime.Object, w io.Writer) error {
    // YAML 模式:JSON → YAML 转换
    if s.options.Yaml {
        jsonBytes, _ := json.Marshal(obj)
        yamlBytes, err := yaml.JSONToYAML(jsonBytes)
        if err != nil { return err }
        _, err = w.Write(yamlBytes)
        return err
    }
    // Pretty 模式:带缩进格式化(调试友好)
    if s.options.Pretty {
        data, err := json.MarshalIndent(obj, "", "  ")
        if err != nil { return err }
        _, err = w.Write(data)
        return err
    }
    // 标准 JSON 编码:直接写入 io.Writer
    encoder := json.NewEncoder(w)
    return encoder.Encode(obj)
}

4.2 protobuf.Serializer:Protobuf 编解码器

Protobuf Serializer 是 Kubernetes 内部通信(APIServer 与 etcd 之间、Watch 流的二进制编码)使用的序列化格式。

相比 JSON,优势在于二进制编码体积小、编解码速度快,但代价是调试困难(不可读)。

Protobuf Serializer 在编码时会在数据前面加上 magic bytes 前缀(用于识别这是 Kubernetes Protobuf 格式)。解码时先检查前缀,不匹配则报错。

// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/protobuf/protobuf.go(行 200-279)

func (s *Serializer) doEncode(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error {
    prefixSize := uint64(len(s.prefix))   // s.prefix = protoEncodingPrefix (magic bytes)

    // 对于 runtime.Unknown:直接序列化原始字节 + 前缀
    switch t := obj.(type) {
    case *runtime.Unknown:
        estimatedSize := prefixSize + uint64(t.Size())
        data := memAlloc.Allocate(estimatedSize)
        i, err := t.MarshalTo(data[prefixSize:])
        copy(data, s.prefix)
        _, err = w.Write(data[:prefixSize+uint64(i)])
        return err
    default:
        kind := obj.GetObjectKind().GroupVersionKind()
        unk = runtime.Unknown{TypeMeta: runtime.TypeMeta{
            Kind: kind.Kind, APIVersion: kind.GroupVersion().String(),
        }}
    }

    // 根据对象实现的接口选择最优编码路径:
    switch t := obj.(type) {
    case bufferedMarshaller:
        // 高效路径:实现了 MarshalToSizedBuffer
        encodedSize := uint64(t.Size())
        estimatedSize := prefixSize + estimateUnknownSize(&unk, encodedSize)
        data := memAlloc.Allocate(estimatedSize)
        i, err := unk.NestedMarshalTo(data[prefixSize:], t, encodedSize)
        copy(data, s.prefix)
        _, err = w.Write(data[:prefixSize+uint64(i)])

    case unbufferedMarshaller:
        // 普通路径:只实现了 Marshal,需要多一次中间分配
        raw, err := t.Marshal()
        unk.Raw = raw
        estimatedSize := prefixSize + uint64(unk.Size())
        data = memAlloc.Allocate(estimatedSize)
        i, err := unk.MarshalTo(data[prefixSize:])
        copy(data, s.prefix)
        _, err = w.Write(data[:prefixSize+uint64(i)])

    default: return errNotMarshalable{reflect.TypeOf(obj)}
    }
}

Protobuf Serializer 的 Decode 与 JSON Serializer 类似,但有一个关键区别:Protobuf 解码时需要 into 参数(不接受 nil),因为 Protobuf 格式没有 JSON 那样的 GVK 推断机制。

4.3 UnstructuredJSONScheme:处理未知类型

UnstructuredJSONScheme 是一个特殊的 Codec,专门用于处理无法预知类型的对象(如 CRD 自定义资源)。

在 Kubernetes 中它被定义为全局单例:

// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers.go(行 369-512)

// 全局单例:用于处理完全无类型的 JSON 数据
var UnstructuredJSONScheme runtime.Codec = unstructuredJSONScheme{}

type unstructuredJSONScheme struct{}

// Encode:不走 Scheme,直接将 map[string]interface{} 序列化为 JSON
func (s unstructuredJSONScheme) Encode(obj runtime.Object, w io.Writer) error {
    if co, ok := obj.(runtime.CacheableObject); ok {
        return co.CacheEncode(s.Identifier(), s.doEncode, w)
    }
    return s.doEncode(obj, w)
}

func (unstructuredJSONScheme) doEncode(obj runtime.Object, w io.Writer) error {
    switch t := obj.(type) {
    case *Unstructured:
        return json.NewEncoder(w).Encode(t.Object)
    case *UnstructuredList:
        items := make([]interface{}, 0, len(t.Items))
        for _, i := range t.Items { items = append(items, i.Object) }
        listObj := make(map[string]interface{}, len(t.Object)+1)
        for k, v := range t.Object { listObj[k] = v }
        listObj["items"] = items
        return json.NewEncoder(w).Encode(listObj)
    case *runtime.Unknown:
        _, err := w.Write(t.Raw)
        return err
    default:
        return json.NewEncoder(w).Encode(t)
    }
}

// Decode:自动判断是列表还是单个对象,再分发
func (s unstructuredJSONScheme) Decode(data []byte, _ *schema.GroupVersionKind, obj runtime.Object) (
    runtime.Object, *schema.GroupVersionKind, error) {
    if obj != nil { err = s.decodeInto(data, obj) } else { obj, err = s.decode(data) }
    gvk := obj.GetObjectKind().GroupVersionKind()
    if len(gvk.Kind) == 0 { return nil, &gvk, runtime.NewMissingKindErr(...) }
    return obj, &gvk, nil
}

// decode:自动判断类型
func (s unstructuredJSONScheme) decode(data []byte) (runtime.Object, error) {
    type detector struct { Items gojson.RawMessage `json:"items"` }
    var det detector
    if err := json.Unmarshal(data, &det); err != nil { return nil, err }
    if det.Items != nil {
        list := &UnstructuredList{}
        err := s.decodeToList(data, list)
        return list, err
    }
    unstruct := &Unstructured{}
    err := s.decodeToUnstructured(data, unstruct)
    return unstruct, err
}

// decodeToUnstructured:将 JSON 反序列化为 map[string]interface{}
func (unstructuredJSONScheme) decodeToUnstructured(data []byte, unstruct *Unstructured) error {
    m := make(map[string]interface{})
    if err := json.Unmarshal(data, &m); err != nil { return err }
    unstruct.Object = m
    return nil
}

UnstructuredJSONScheme 的最大特点是:它完全不依赖 runtime.Scheme,而是直接操作 map[string]interface{}。

这使得它可以处理任意 JSON 数据,即使该类型从未在 Go 代码中注册过——这正是 CRD 能够工作的底层基础。


五、ParameterCodec:URL 查询参数的编解码

除了 JSON/Protobuf 这类主体数据格式,Kubernetes 还需要处理 URL 查询参数(如 ?labelSelector=app=nginx)的编解码。

ParameterCodec 负责将 Go 对象 ↔ url.Values 的双向转换。

// staging/src/k8s.io/apimachinery/pkg/runtime/codec.go(行 130-203)

type parameterCodec struct {
    typer     ObjectTyper
    convertor ObjectConvertor
    creator   ObjectCreater
    defaulter ObjectDefaulter
}

// EncodeParameters:将对象编码为查询参数
// 例如:ListOptions { LabelSelector: "app=nginx" } → url.Values { "labelSelector": ["app=nginx"] }
func (c *parameterCodec) EncodeParameters(obj Object, to schema.GroupVersion) (url.Values, error) {
    gvks, _, err := c.typer.ObjectKinds(obj)
    gvk := gvks[0]
    if to != gvk.GroupVersion() {
        obj, err = c.convertor.ConvertToVersion(obj, to)
        if err != nil { return nil, err }
    }
    return queryparams.Convert(obj)
}

// DecodeParameters:将查询参数解码为对象
// 例如:url.Values { "fieldSelector": ["metadata.name=test"] } → ListOptions { FieldSelector: "metadata.name=test" }
func (c *parameterCodec) DecodeParameters(parameters url.Values, from schema.GroupVersion, into Object) error {
    if len(parameters) == 0 { return nil }
    targetGVKs, _, err := c.typer.ObjectKinds(into)
    for i := range targetGVKs {
        if targetGVKs[i].GroupVersion() == from {
            c.convertor.Convert(&parameters, into, nil)
            c.defaulter.Default(into)
            return nil
        }
    }
    input, err := c.creator.New(from.WithKind(targetGVKs[0].Kind))
    c.convertor.Convert(&parameters, input, nil)
    c.defaulter.Default(input)
    return c.convertor.Convert(input, into, nil)
}

ParameterCodec 在 client-go 的 Request 对象中被广泛使用。

当调用 client.CoreV1().Pods().List(ctx, metav1.ListOptions{LabelSelector: "app=nginx"}) 时,ListOptions 对象就是通过 ParameterCodec 被编码为 URL 查询参数的。


六、Codec 核心调用链路:APIServer 请求处理全链路

6.1 APIServer 请求处理的编解码全景图

在 APIServer 中,每一条 HTTP 请求都经过编解码处理。

下面是从"请求进来"到"响应出去"的完整链路:

╔══════════════════════════════════════════════════════════════════╗
║  HTTP 请求进入 APIServer                                      ║
╚══════════════════════════════════════════════════════════════════╝
                           │
           ┌───────────────▼───────────────┐
           │  Accept Header 协商          │
           │  Negotiation.NegotiateOutput│
           └───────────────┬───────────────┘
                           │
           ┌───────────────▼───────────────┐
           │  请求体解码                  │
           │  codec.Decode(data, gvk, into)│
           │  ① decoder.Decode()          │
           │     → JSON/YAML/Proto 字节   │
           │     → 外部版本 Go 对象      │
           │  ② convertor.Convert()      │
           │     → 内部版本 Go 对象      │
           │  ③ defaulter.Default()     │
           └───────────────┬───────────────┘
                           │
           ┌───────────────▼───────────────┐
           │  Storage 存储层(etcd)        │
           └───────────────┬───────────────┘
                           │
           ┌───────────────▼───────────────┐
           │  响应体编码                  │
           │  codec.Encode(obj, w)        │
           │  ① convertor.ConvertToVersion│
           │  ② encoder.Encode()         │
           └───────────────┬───────────────┘
                           │
           ┌───────────────▼───────────────┐
           │  transformResponseObject()    │
           │  WriteObjectNegotiated()     │
           └───────────────┬───────────────┘
                           ▼
╔══════════════════════════════════════════════════════════════════╗
║  HTTP 响应(编码后的字节流)                                  ║
╚══════════════════════════════════════════════════════════════════╝

6.2 getter 的完整调用链路

让我们以 GET 请求为例,追踪从 HTTP 请求到最终响应输出的完整调用链路:

// staging/src/k8s.io/apiserver/pkg/endpoints/handlers/get.go(行 52-84)

func getResourceHandler(scope *RequestScope, getter getterFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, req *http.Request) {
        ctx := req.Context()
        namespace, name, err := scope.Namer.Name(req)
        if err != nil { scope.err(err, w, req); return }

        // 第一步:Content Negotiation —— 根据 Accept Header 选择 Serializer
        outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, scope)
        if err != nil { scope.err(err, w, req); return }

        // 第二步:getter 从存储层获取对象(内部版本)
        result, err := getter(ctx, name, req)
        if err != nil { scope.err(err, w, req); return }

        // 第三步:transformResponseObject —— 特殊响应类型处理
        // 例如:?watch=true → 返回 watch.Event 流
        //      ?param=Table → 转换为 Table 格式
        transformResponseObject(ctx, scope, req, w, http.StatusOK, outputMediaType, result)
    }
}

transformResponseObject 是响应处理的核心函数:

// staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go(行 311-343)

func transformResponseObject(ctx context.Context, scope *RequestScope, req *http.Request,
    w http.ResponseWriter, statusCode int, mediaType negotiation.MediaTypeOptions, result runtime.Object) {

    // 第一步:处理 options(如 TableOptions)
    options, err := optionsForTransform(mediaType, req)
    if err != nil { scope.err(err, w, req); return }

    // 处理空列表(确保返回 [] 而不是 null)
    if meta.IsListType(result) && meta.LenList(result) == 0 {
        meta.SetList(result, []runtime.Object{})
    }

    // 第二步:doTransformObject —— 特殊格式转换
    obj, err := doTransformObject(ctx, result, options, mediaType.Convert, scope)
    if err != nil { scope.err(err, w, req); return }

    // 第三步:获取目标 GVK 和 Serializer
    kind, serializer, _ := targetEncodingForTransform(scope, mediaType, req)

    // 第四步:WriteObjectNegotiated —— 最终写入
    // 这里传入的 serializer 是经过 EncoderForVersion 包装的,
    // 会自动完成版本转换 + 序列化
    responsewriters.WriteObjectNegotiated(
        serializer, scope, kind.GroupVersion(), w, req, statusCode, obj, false)
}

6.3 client-go 请求端的编解码链路

在 client-go 客户端,编解码链路是 APIServer 端的镜像对称:

客户端:构造请求
┌──────────────────────────────────────────────┐
│ ① VersionedParams → ParameterCodec.Encode    │
│   → url.Values { labelSelector: "app=nginx" }│
│ ② client-go Request.BuildURL()             │
│   → GET /api/v1/pods?labelSelector=...       │
└──────────────────────────────────────────────┘
                    │
             HTTP 响应
                    │
┌──────────────────────────────────────────────┐
│ ③ Request.Do(ctx) → transformResponse()        │
│ ④ Result.Get() → decoder.Decode(body, nil)     │
│   → recognizer.NewDecoder(decoders...)         │
│     自动识别格式(JSON/Protobuf)               │
│   → jsonSerializer.Decode()                   │
│   → codec.ConvertToVersion()                 │
│   → 返回内部版本对象                          │
└──────────────────────────────────────────────┘

6.4 RecognizerDecoder:多格式自动识别

在 CodecFactory.newCodecFactory() 中,universal decoder 是通过 recognizer.NewDecoder() 创建的。

这个解码器内部管理多个底层的 Decoder(JSON、YAML、Protobuf),并按照优先级尝试解码:

// staging/src/k8s.io/apimachinery/pkg/runtime/serializer/recognizer/recognizer.go(行 45-128)

// NewDecoder:创建一个多格式自动识别解码器
// 优先级:1) RecognizingDecoder 识别成功 → 2) 其他 decoder
func NewDecoder(decoders ...runtime.Decoder) runtime.Decoder {
    return &decoder{decoders: decoders}
}

func (d *decoder) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime.Object) (
    runtime.Object, *schema.GroupVersionKind, error) {
    var skipped []runtime.Decoder

    // 第一轮:尝试 RecognizingDecoder(能判断数据是否属于自己的格式)
    for _, r := range d.decoders {
        if rd, ok := r.(RecognizingDecoder); ok {
            ok, unknown, err := rd.RecognizesData(data)
            if unknown { skipped = append(skipped, rd); continue }
            if !ok { continue }
            return r.Decode(data, gvk, into)
        }
        skipped = append(skipped, r)
    }

    // 第二轮:尝试未能识别的 decoder
    for _, r := range skipped {
        out, actual, err := r.Decode(data, gvk, into)
        if err == nil || (out != nil && !runtime.IsStrictDecodingError(err)) {
            return out, actual, err
        }
        lastErr = err
    }
    return nil, nil, lastErr
}

// RecognizingDecoder:判断数据是否属于当前 decoder 格式
type RecognizingDecoder interface {
    runtime.Decoder
    RecognizesData(peek []byte) (ok, unknown bool, err error)
}

对于 JSON Serializer,它会检查数据的开头是否有 JSON 对象语法(如 { 开头)来判断是否属于 JSON 格式。

Protobuf Serializer 则检查 magic bytes 前缀是否匹配。


七、数据结构关系图:编解码体系全景

┌──────────────────────────────────────────────────────────────────┐
│                    runtime/interfaces.go                            │
│  ┌──────────────────┐         ┌──────────────────┐            │
│  │ runtime.Encoder  │         │ runtime.Decoder  │            │
│  │  Encode(obj, w)  │         │  Decode(data,    │            │
│  │  Identifier()     │         │    gvk, into)   │            │
│  └────────┬─────────┘         └────────┬─────────┘            │
│           │                             │                       │
│  ┌────────▼─────────┐                  │                       │
│  │ runtime.Serializer │                  │                       │
│  │  Encoder + Decoder│                  │                       │
│  └────────┬─────────┘                  │                       │
│           │                             │                       │
│  ┌────────▼─────────┐                  │                       │
│  │ runtime.Codec    │ ← type Codec Serializer(标记接口)     │
│  └────────┬─────────┘                  │                       │
│           │                             │                       │
│  ┌────────▼──────────────────────────┐│                      │
│  │ runtime.NegotiatedSerializer       ││                      │
│  │  SupportedMediaTypes()              ││                      │
│  │  EncoderForVersion(e, gv)         ││                      │
│  │  DecoderToVersion(d, gv)          ││                      │
│  └────────┬──────────────────────────┘│                      │
└────────────┼──────────────────────────────────────────────────┘
             │
┌────────────▼──────────────────────────────────────────────────┐
│         runtime/serializer/codec_factory.go                      │
│  CodecFactory                                               │
│  ├─ scheme    *runtime.Scheme   ← 类型注册表                 │
│  ├─ universal runtime.Decoder   ← 多格式自动识别解码器         │
│  ├─ accepts   []SerializerInfo  ← JSON/YAML/Protobuf 元数据    │
│  └─ legacySerializer           ← JSON 序列化器               │
│  newSerializersForScheme → [JSON, YAML, Protobuf] SerializerInfo│
└────────────┬──────────────────────────────────────────────────┘
             │
┌────────────▼──────────────────────────────────────────────────┐
│         runtime/serializer/versioning/versioning.go             │
│  versioning.codec(Codec 核心实现)                            │
│  ├─ encoder   runtime.Encoder      ← JSON/YAML/Proto Encoder│
│  ├─ decoder   runtime.Decoder      ← JSON/YAML/Proto Decoder│
│  ├─ convertor ObjectConvertor      ← Scheme.ConvertToVersion │
│  ├─ encodeVersion GroupVersioner   ← Encode 目标版本          │
│  └─ decodeVersion GroupVersioner   ← Decode 目标版本          │
└────────────┬──────────────────────────────────────────────────┘
             │
       ┌────┴────┐
       ▼         ▼
┌────────────┐   ┌──────────────────┐
│json.Serializer│ │protobuf.Serializer│
│  json.Marshal│ │  unk.MarshalTo   │
│  yaml.JSON→YAML│ │  (带 magic 前缀)   │
│  json.Unmarshal│ │  unk.Unmarshal   │
└────────────┘   └──────────────────┘
       │                   │
       └─────────┬─────────┘
                 ▼
┌──────────────────────────────────────────────────────────┐
│  UnstructuredJSONScheme                                   │
│  → Encode:   json.NewEncoder(map[string]interface{})  │
│  → Decode:   自动判断 Unstructured vs UnstructuredList│
│  → decodeToUnstructured: json.Unmarshal → map        │
└──────────────────────────────────────────────────────────┘

八、总结:编解码体系的核心设计思想

通过这次源码级别的追踪,我们可以总结出 Kubernetes 编解码体系的三条核心设计思想:

一、关注点分离:Serializer 管格式,Codec 管版本

Serializer 只需要知道"如何把 Go 对象变成某种格式的字节"和"如何从某种格式的字节还原 Go 对象",而版本转换的逻辑完全不侵入 Serializer。

这种设计使得同一个 Serializer(JSON/YAML/Protobuf)可以被多个 Codec 复用。同一个 json.Serializer,在 APIServer 端被包装成指向 v1 的 Codec,在 client-go 端被包装成指向 internal 的 Codec。

二、零拷贝原则:Convert vs ConvertToVersion

在 Decode 流程中,如果有传入 into 目标对象,Codec 会使用 Convert() 在原地做版本转换,避免创建新的对象副本。

只有当 into == nil 时,才会用 ConvertToVersion() 创建新对象。这是 Kubernetes 在高频请求路径上做的重要性能优化。

三、工厂方法 + 版本选择器:灵活的版本管理

CodecFactory 通过 CodecForVersions() 方法接受 encodeVersion 和 decodeVersion 两个 GroupVersioner 来决定版本转换的方向。

这使得 APIServer 可以创建多个不同配置的 Codec:有的专门编码到 v1、有的专门编码到 v1beta1、有的编码到内部版本。这种设计完美支持了 Kubernetes 的多版本共存特性。

本节学到了:

  • Codec = Serializer + 版本转换逻辑,两者的本质区别在于是否携带 GroupVersioner
  • versioning.codec 是编解码的核心,Encode 时先版本转换再序列化,Decode 时先反序列化再版本转换
  • CodecFactory 实例化链路:Scheme → SerializerInfo[] → CodecFactory → CodecForVersions → *codec
  • json/Protobuf/UnstructuredJSONScheme 三种序列化器各有适用场景:typed 对象用 JSON/Protobuf,CRD 用 UnstructuredJSONScheme
  • APIServer 和 client-go 都依赖这套编解码体系,方向相反但原理相同

九、常见问题(FAQ)

▼ Q: Codec 和 Serializer 在代码层面是同一个类型,那运行时怎么区分?

A: 区分不在于 Go 类型本身,而在于语义和使用方式。当你需要做版本转换(internal ↔ external)时,你请求的是 Codec;当你只需要格式转换(Go ↔ JSON)时,你请求的是 Serializer。versioning.codec 虽然实现了 runtime.Codec 接口,但它的名字和创建方式(CodecForVersions)都在语义层面告诉你"它会做版本转换"。

▼ Q: Watch 场景下的编解码和普通 GET/LIST 有什么不同?

A: 普通 GET/LIST 使用 responsewriters.WriteObjectNegotiated 直接将对象写入 HTTP 响应。Watch 使用的是 streaming.Encoder/streaming.Decoder,编码时先将对象包装成 watch.Event(包含 Type 字段和 Object 字段),然后再编码;解码时则先解码出 watch.Event,再从 Object.Raw 中提取内部对象用 embeddedDecoder 解码。streaming 包的作用就是在 HTTP 长连接上区分多个 watch.Event 的边界。

▼ Q: Strict Decoding 是什么?什么时候会遇到?

A: Strict Decoding 是 Kubernetes 1.19+ 引入的特性。当解码 JSON 数据时,如果 StrictSerializer 检测到数据中存在该资源类型的 OpenAPI Schema 中未定义的字段(即未知字段),就会返回 runtime.NewStrictDecodingError。例如用户将 replicas 拼成 replicass 时就能得到明确报错。通过 CRD 的 x-kubernetes-preserve-unknown-fields 字段可以控制是否允许未知字段。

▼ Q: 为什么有些场景用 JSON 而不用 Protobuf?Protobuf 不是更快吗?

A: Protobuf 确实更快更省空间,但有三个限制:① Protobuf 输出是二进制不可读,kubectl diff、kubectl edit 等人类可读工具无法工作;② Protobuf 需要预先知道对象的完整 Schema,无法处理 CRD 等运行时才知道类型的资源;③ Protobuf 需要对象的 Go Type 实现 protobuf 自定义接口。因此 Kubernetes 的外部 API(kubectl 交互)用 JSON,内部存储和通信(etcd、Watch)用 Protobuf,CRD 用 UnstructuredJSONScheme。

▼ Q: CBOR Serializer 是什么?Kubernetes 1.36 支持 CBOR 吗?

A: CBOR(Concise Binary Object Representation,RFC 8949)是一种类似 JSON 的二进制序列化格式,比 Protobuf 更紧凑且支持自描述。Kubernetes 1.36.1 在 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/ 目录下确实包含了 CBOR Serializer 的实现(实现了 runtime.Serializer + runtime.NondeterministicEncoder + recognizer.RecognizingDecoder),但目前还处于实验性阶段。在 Kubernetes 1.34+ 中,CBOR 主要用于减少 Watch 流等高频场景的带宽占用。


Kubernetes 编程 / Operator 专题【左扬精讲】—— runtime.Codec 资源编解码 · Kubernetes 1.36.1 源码级精讲

来源:staging/src/k8s.io/apimachinery/pkg/runtime/serializer/

相关阅读:
   • runtime/interfaces.go — runtime 核心接口定义
   • runtime/serializer/versioning/versioning.go — Codec 核心实现
   • runtime/serializer/codec_factory.go — CodecFactory 工厂类
   • runtime/serializer/json/json.go — JSON Serializer 实现
   • runtime/serializer/protobuf/protobuf.go — Protobuf Serializer 实现
   • unstructured/helpers.go — UnstructuredJSONScheme 实现
   • apiserver/endpoints/handlers/get.go — GET 请求处理器(getter)
   • apiserver/endpoints/handlers/response.go — transformResponseObject 响应处理

posted @ 2026-06-12 14:20  左扬  阅读(3)  评论(0)    收藏  举报