



























当你通过 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 的角色
在 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 是绝对的核心引擎。
在 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。
在实际使用中,你不会直接 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):返回对特定版本做解码时转换的 decoderCodecForVersions(encoder, decoder, encodeGV, decodeGV):创建精确控制版本转换方向的 CodecEncoderForVersion(encoder, gv):创建编码到特定版本的 encoderDecoderToVersion(decoder, gv):创建解码到特定版本的 decoderCodecFactory 本身只包含三个字段:
// 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 序列化器(向后兼容用)
}
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 等场景)
}
让我们用流程图来展示从 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。
在 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 层完成的。
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 时要转换成哪个版本。
① 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 流程的关键三步:
① 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.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)
}
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 推断机制。
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 能够工作的底层基础。
除了 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(¶meters, into, nil)
c.defaulter.Default(into)
return nil
}
}
input, err := c.creator.New(from.WithKind(targetGVKs[0].Kind))
c.convertor.Convert(¶meters, 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 查询参数的。
在 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 响应(编码后的字节流) ║
╚══════════════════════════════════════════════════════════════════╝
让我们以 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)
}
在 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() │
│ → 返回内部版本对象 │
└──────────────────────────────────────────────┘
在 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 的多版本共存特性。
本节学到了:
▼ 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 源码级精讲
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。