看 K8s 源码:一个 Deployment 是怎么变成 /apis/apps/v1/deployments 的?Scheme 和 RESTStorage 全解析

前言:那个让我无言以对的面试题

面试时被问过一个问题:

“你执行 kubectl create -f deploy.yaml,从 YAML 文件到 etcd 里多出一条记录,APIServer 内部到底发生了什么?”

我当时只答上来 “认证 → 鉴权 → 准入 → 存储”,面试官接着问:“存储那一步具体是怎么把 JSON 转成 Go struct、再存到 etcd 的?

我懵了。

回去啃源码,发现答案藏在两个核心抽象里:

  1. Scheme —— 一张全局的"资源注册表",记录所有 Go 类型 ↔ GVK(Group/Version/Kind)的映射,以及它们的序列化/反序列化方法
  2. RESTStorage —— 每个资源的 CRUD 接口,把 REST 请求(GET/POST/DELETE)翻译成对 etcd 的操作

这俩抽象一搞清楚,你就明白了为什么 K8s 能做到:

  • 同一个 Pod,能用 JSON / YAML / Protobuf 序列化
  • 同一个资源,能在 v1beta1v1 之间自动转换
  • 新增 CRD,APIServer 不重启就能识别

这篇就把这两个抽象逐行拆开讲,并把我踩过的坑标出来。


本节重点

  • Scheme 定义了资源序列化/反序列化方法资源类型与版本的对应关系——可以理解成一张全局记录表
  • 所有 K8s 资源必须先注册到 Scheme 才能使用
  • RESTStorage 定义了一种资源该如何 CRUD、如何和存储打交道
  • 各个资源的 restStore 塞入 restStorageMap:key 是"资源名/子资源名",value 是对应的 restStore

一、入口:InstallLegacyAPI 在干嘛?

上节讲到 kubeAPIServer 初始化时会调用 InstallLegacyAPI,把 K8s 内置的核心资源(Pod / Service / Node / ConfigMap …)注册到 HTTP 路由。

位置:pkg/controlplane/instance.go

// InstallLegacyAPI will install the legacy APIs for the restStorageProviders if they are enabled.
func (m *Instance) InstallLegacyAPI(
    c *completedConfig,
    restOptionsGetter generic.RESTOptionsGetter,
    legacyRESTStorageProvider corerest.LegacyRESTStorageProvider,
) error {
    // 1. 构造所有 core group 资源的 RESTStorage
    legacyRESTStorage, apiGroupInfo, err := legacyRESTStorageProvider.NewLegacyRESTStorage(restOptionsGetter)
    if err != nil {
        return fmt.Errorf("error building core storage: %v", err)
    }

    // 2. 注册 bootstrap-controller 的钩子
    controllerName := "bootstrap-controller"
    coreClient := corev1client.NewForConfigOrDie(c.GenericConfig.LoopbackClientConfig)
    bootstrapController := c.NewBootstrapController(
        legacyRESTStorage, coreClient, coreClient, coreClient,
        coreClient.RESTClient(),
    )
    m.GenericAPIServer.AddPostStartHookOrDie(controllerName, bootstrapController.PostStartHook)
    m.GenericAPIServer.AddPreShutdownHookOrDie(controllerName, bootstrapController.PreShutdownHook)

    // 3. 把所有 storage 挂到 /api 路由前缀下
    if err := m.GenericAPIServer.InstallLegacyAPIGroup(genericapiserver.DefaultLegacyAPIPrefix, &apiGroupInfo); err != nil {
        return fmt.Errorf("error in registering group versions: %v", err)
    }
    return nil
}

三件事:

  1. NewLegacyRESTStorage —— 给每个 core 资源生成 RESTStorage 实例
  2. bootstrap-controller —— 创建 kubernetes 这个默认 Service、维护 default-token、初始化集群所需的 namespace 等
  3. InstallLegacyAPIGroup —— 把 RESTStorage 注册到 HTTP 路由(/api/v1/...

💡 bootstrap-controller 是什么? 启动 K8s 集群时,default namespace、kubernetes Service、system:masters 这些"开箱即有"的资源就是它创建的。它跟 RESTStorage 强相关——必须等所有 storage 准备好才能跑——所以挂在 PostStartHook 里。


二、K8s 资源的完整身份:GVK

进入 Scheme 之前,先把 K8s 资源的"完整身份"讲清楚。

很多人写 YAML 时只关心 kind: Deployment这是不准确的。K8s 的资源完整定位是:

<Group> / <Version> / <Resource> / <SubResource>
   ↓        ↓           ↓             ↓
  apps  /  v1   /  deployments  /  status

Deployment 为例:

维度说明
Groupapps资源组(APIGroup)
Versionv1资源版本(APIVersion)
Resourcedeployments资源(复数小写)
SubResourcestatus子资源(如 status、scale)
KindDeployment资源种类(单数大写)

2.1 为什么要分 Group 和 Version?

                K8s API
       ┌──────────┼──────────┐
       │          │          │
    core/v1   apps/v1   batch/v1
       │          │          │
   Pod, Svc   Deployment   Job
   ConfigMap  StatefulSet  CronJob
   ……         DaemonSet    ……

设计意图:

  • Group:按业务域划分,让 K8s 能模块化演进。比如 apps 专门管工作负载、networking.k8s.io 专门管网络
  • Version:允许同一资源多版本共存,并支持平滑升级。比如 Ingressextensions/v1beta1networking.k8s.io/v1beta1networking.k8s.io/v1,三个版本能在过渡期同时存在

🤔 GVK vs GVR:源码里经常看到这两个缩写——GVK(Group/Version/Kind)用于"类型",GVR(Group/Version/Resource)用于"REST 路径"。Kind 是 Go 类型名(Deployment),Resource 是 URL 路径段(deployments)。一个 Kind 可能对应多个 Resource(含子资源)。

2.2 Resource 和 Kind 的微妙关系

KindResourceURL
Podpods/api/v1/pods
Podpods/status/api/v1/pods/{name}/status
Podpods/exec/api/v1/pods/{name}/exec

同一个 Kind(Pod),可以对应多个 Resource——主资源加上一堆子资源。每个子资源都是独立的 REST endpoint,有自己的权限控制(这就是为什么 RBAC 里可以单独授予 pods/exec 权限而不给 pods/delete)。


三、Scheme:全局资源注册表

3.1 Scheme 是什么

位置:staging/src/k8s.io/apimachinery/pkg/runtime/scheme.go

Scheme 定义了资源序列化/反序列化方法以及资源类型与版本的对应关系——可以理解成一张全局记录表。

K8s 系统有几百种资源类型,每一种都需要:

  • 知道 Go struct 是哪个
  • 知道怎么序列化成 JSON/YAML/Protobuf
  • 知道怎么从 v1beta1 转成 v1
  • 知道怎么 deep copy

Scheme 就是把这些信息统一起来的注册表,是一个内存型的资源注册表,特点:

  • ✅ 支持注册多种资源类型(内部版本 + 外部版本)
  • ✅ 支持多种版本转换机制
  • ✅ 支持不同资源的序列化/反序列化机制

3.2 Scheme 支持的两种资源类型

类型注册方法应用场景
KnownType(拥有版本的资源)scheme.AddKnownTypes99% 的资源都是这种
UnversionedType(无版本的资源)scheme.AddUnversionedTypes早期遗留,少数 meta 类型

UnversionedType 在现代 K8s 里已经基本被弱化,但仍有几个保留:

  • metav1.Status —— API 错误响应(401/403/404 等)
  • metav1.APIVersions —— /api 端点返回的版本列表
  • metav1.APIGroupList —— /apis 返回的 group 列表
  • metav1.APIGroup —— 单个 group 信息
  • metav1.APIResourceList —— 某个 group/version 下的资源列表

这些类型不需要做版本转换——它们只是 API metadata,不属于业务资源。

3.3 Scheme 结构体定义

s := &Scheme{
    gvkToType:                 map[schema.GroupVersionKind]reflect.Type{},
    typeToGVK:                 map[reflect.Type][]schema.GroupVersionKind{},
    unversionedTypes:          map[reflect.Type]schema.GroupVersionKind{},
    unversionedKinds:          map[string]reflect.Type{},
    fieldLabelConversionFuncs: map[schema.GroupVersionKind]FieldLabelConversionFunc{},
    defaulterFuncs:            map[reflect.Type]func(interface{}){},
    versionPriority:           map[string][]string{},
    schemeName:                naming.GetNameFromCallsite(internalPackages...),
}

字段含义:

字段作用
gvkToType存储 GVK → reflect.Type 的映射(正向查找
typeToGVK存储 reflect.Type → []GVK 的映射(反向查找,一个 Type 可能对应多个 GVK)
unversionedTypesUnversionedType → GVK 的映射
unversionedKindsKind 名称 → reflect.Type 的映射
fieldLabelConversionFuncs字段选择器(spec.nodeName=xxx)的转换函数
defaulterFuncs给资源填默认值的函数
versionPriority同一个 group 下的版本优先级(决定 v1 还是 v1beta1 优先返回)

💡 为什么 typeToGVK[]GVK 切片? 因为一个 Go 类型可能对应多个 GVK!比如 K8s 内部有"内部版本"和"外部版本"之分:apps.Deployment(内部)→ 对应 apps/v1/Deploymentapps/v1beta1/Deploymentapps/v1beta2/Deployment 等多个外部 GVK。Scheme 用切片记录这种一对多关系。

🚀 性能特性:Scheme 全部基于 Go map,正向/反向检索时间复杂度都是 O(1)。K8s APIServer 每秒要处理几千次序列化/反序列化,这个 O(1) 是性能的基石。

3.4 使用 Scheme 的标准姿势

// 1. 创建一个 Scheme 实例
var Scheme = runtime.NewScheme()

// 2. 通过 AddToScheme 注册各种资源类型
func init() {
    _ = clientgoscheme.AddToScheme(Scheme)   // 注册 K8s 内置资源
    _ = crdv1.AddToScheme(Scheme)            // 注册 CRD 类型
    _ = oamcore.AddToScheme(Scheme)          // 注册 OAM
    _ = oamstandard.AddToScheme(Scheme)
    _ = istioclientv1beta1.AddToScheme(Scheme)
    _ = certmanager.AddToScheme(Scheme)
    _ = kruise.AddToScheme(Scheme)
    // +kubebuilder:scaffold:scheme
}

// 3. 从 Scheme 派生 Codec(编码器/解码器)
var Codecs = serializer.NewCodecFactory(Scheme)
var ParameterCodec = runtime.NewParameterCodec(Scheme)

// 4. 用 Codec 解码
var decode = Codecs.UniversalDeserializer().Decode

🚨 新手最容易踩的坑:自己写 controller / operator 时,忘记调用 AddToScheme 注册 CRD 类型。后果:用 client.Get(...) 取自定义资源会报:

no kind "MyCRD" is registered for version "mygroup.io/v1" in scheme

这个错误很有迷惑性,因为它说的是 scheme 里没有,但你明明 import 了 CRD 包。原因是 import 不会触发注册,必须显式 AddToScheme

3.5 实战示例:webhook 里的 Scheme 用法

回到我们前几节写的 sidecar 注入 webhook,开头那段代码就是标准的 Scheme 初始化:

var (
    runtimeScheme = runtime.NewScheme()
    codecs        = serializer.NewCodecFactory(runtimeScheme)
    deserializer  = codecs.UniversalDeserializer()
)

func init() {
    _ = corev1.AddToScheme(runtimeScheme)   // 注册 core/v1(包含 Pod)
}

然后在 handler 里就可以用 deserializer 解码 AdmissionReview:

if _, _, err := deserializer.Decode(body, nil, &ar); err != nil {
    glog.Errorf("Can't decode body: %v", err)
    admissionResponse = &v1beta1.AdmissionResponse{
        Result: &metav1.Status{
            Message: err.Error(),
        },
    }
}

💡 为什么这里只注册 corev1 因为 webhook 只处理 Pod 资源。但严格来说还应该 _ = admissionv1.AddToScheme(runtimeScheme)——因为 AdmissionReview 本身也是个 K8s 资源。不注册的话用 UniversalDeserializer 还能跑(它有 fallback 逻辑),但严谨起见建议注册。


四、NewLegacyRESTStorage:核心资源 storage 的工厂

讲完了"类型注册表",回到 NewLegacyRESTStorage 看怎么创建 storage。

位置:pkg/registry/core/rest/storage_core.go

4.1 构造 APIGroupInfo

func (c LegacyRESTStorageProvider) NewLegacyRESTStorage(
    restOptionsGetter generic.RESTOptionsGetter,
) (LegacyRESTStorage, genericapiserver.APIGroupInfo, error) {
    apiGroupInfo := genericapiserver.APIGroupInfo{
        PrioritizedVersions:          legacyscheme.Scheme.PrioritizedVersionsForGroup(""),
        VersionedResourcesStorageMap: map[string]map[string]rest.Storage{},
        Scheme:                       legacyscheme.Scheme,
        ParameterCodec:               legacyscheme.ParameterCodec,
        NegotiatedSerializer:         legacyscheme.Codecs,
    }
    // ...
}

APIGroupInfo 是 K8s 给每个 API Group 的描述对象,里面塞了:

  • PrioritizedVersions:该 group 下所有版本的优先级排序
  • VersionedResourcesStorageMap核心字段,双层 map:版本 → 资源名 → RESTStorage
  • Scheme:刚才讲的资源注册表
  • ParameterCodec:URL 参数的编码器(解析 ?fieldSelector=spec.nodeName%3Dxxx
  • NegotiatedSerializer:根据 HTTP Accept 头自动选择 JSON / YAML / Protobuf

💡 legacyscheme.Scheme 是 K8s 的"主 Scheme 单例"——全局唯一,所有 core 资源都注册到这里。源码里到处能看到 legacyscheme.Scheme.AddKnownTypes(...) 这种用法。

4.2 创建 RESTStorage:以 ConfigMap 为例(最简单的资源)

位置:pkg/registry/core/configmap/storage/storage.go

// REST implements a RESTStorage for ConfigMap
type REST struct {
    *genericregistry.Store    // 关键:嵌入通用 Store
}

// NewREST returns a RESTStorage object that will work with ConfigMap objects.
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
    store := &genericregistry.Store{
        NewFunc:                  func() runtime.Object { return &api.ConfigMap{} },
        NewListFunc:              func() runtime.Object { return &api.ConfigMapList{} },
        PredicateFunc:            configmap.Matcher,
        DefaultQualifiedResource: api.Resource("configmaps"),

        CreateStrategy: configmap.Strategy,
        UpdateStrategy: configmap.Strategy,
        DeleteStrategy: configmap.Strategy,

        TableConvertor: printerstorage.TableConvertor{
            TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers),
        },
    }
    options := &generic.StoreOptions{
        RESTOptions: optsGetter,
        AttrFunc:    configmap.GetAttrs,
        TriggerFunc: map[string]storage.IndexerFunc{
            "metadata.name": configmap.NameTriggerFunc,
        },
    }
    if err := store.CompleteWithOptions(options); err != nil {
        return nil, err
    }
    return &REST{store}, nil
}

字段拆解:

字段作用
NewFunc创建该资源单个对象时用的工厂(GET /configmaps/foo 会用)
NewListFunc创建该资源列表对象时的工厂(GET /configmaps 会用)
PredicateFunc?labelSelector=...&fieldSelector=... 转换成 K8s 内部的匹配器
DefaultQualifiedResource资源的复数小写名(configmaps
CreateStrategy创建时的策略:校验、补默认值、生成 UID 等
UpdateStrategy更新时的策略:判断哪些字段可改、哪些只读(如 metadata.uid
DeleteStrategy删除时的策略:处理 finalizer、cascading delete
TableConvertor把资源对象转成 kubectl get 输出的表格格式
AttrFunc提取资源的索引属性(用于 fieldSelector)
TriggerFuncwatch 事件触发函数

💡 Strategy 模式是 K8s 设计的精髓:把"业务校验"和"通用 CRUD"解耦。genericregistry.Store 提供通用 CRUD,每种资源通过自己的 Strategy 注入业务逻辑。比如 Pod 的 Strategy 会校验"不能修改 spec.containers",ConfigMap 的 Strategy 会校验"data + binaryData 总大小不能超过 1MiB"。

4.3 CompleteWithOptions:必填字段校验

func (e *Store) CompleteWithOptions(options *generic.StoreOptions) error {
    if e.DefaultQualifiedResource.Empty() {
        return fmt.Errorf("store %#v must have a non-empty qualified resource", e)
    }
    if e.NewFunc == nil {
        return fmt.Errorf("store for %s must have NewFunc set", e.DefaultQualifiedResource.String())
    }
    if e.NewListFunc == nil {
        return fmt.Errorf("store for %s must have NewListFunc set", e.DefaultQualifiedResource.String())
    }
    if (e.KeyRootFunc == nil) != (e.KeyFunc == nil) {
        return fmt.Errorf("store for %s must set both KeyRootFunc and KeyFunc or neither", e.DefaultQualifiedResource.String())
    }
    if e.TableConvertor == nil {
        return fmt.Errorf("store for %s must set TableConvertor; rest.NewDefaultTableConvertor(e.DefaultQualifiedResource) can be used to output just name/creation time", e.DefaultQualifiedResource.String())
    }
    // ...
}

🚨 自己写 Aggregated APIServer 时,最容易在这里 panic:忘了设 NewFuncTableConvertor,启动就报错。TableConvertor 漏写是最常见的——如果你不需要自定义表格,用 rest.NewDefaultTableConvertor(...) 兜底即可(只输出 name + age)。

4.4 Pod 的特殊性:一堆子资源 storage 共享同一个 store

Pod 比 ConfigMap 复杂得多——它有 11 个子资源(status、log、exec、attach、portforward、proxy、binding、ephemeralcontainers、eviction…)。位置:pkg/registry/core/pod/storage/storage.go

func NewStorage(
    optsGetter generic.RESTOptionsGetter,
    k client.ConnectionInfoGetter,
    proxyTransport http.RoundTripper,
    podDisruptionBudgetClient policyclient.PodDisruptionBudgetsGetter,
) (PodStorage, error) {
    // 1. 先建一个 "主 store"
    store := &genericregistry.Store{
        NewFunc:                  func() runtime.Object { return &api.Pod{} },
        NewListFunc:              func() runtime.Object { return &api.PodList{} },
        PredicateFunc:            registrypod.MatchPod,
        DefaultQualifiedResource: api.Resource("pods"),

        CreateStrategy:      registrypod.Strategy,
        UpdateStrategy:      registrypod.Strategy,
        DeleteStrategy:      registrypod.Strategy,
        ResetFieldsStrategy: registrypod.Strategy,
        ReturnDeletedObject: true,

        TableConvertor: printerstorage.TableConvertor{
            TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers),
        },
    }
    options := &generic.StoreOptions{
        RESTOptions: optsGetter,
        AttrFunc:    registrypod.GetAttrs,
        TriggerFunc: map[string]storage.IndexerFunc{
            "spec.nodeName": registrypod.NodeNameTriggerFunc,   // 按 nodeName 索引
        },
        Indexers:    registrypod.Indexers(),
    }
    if err := store.CompleteWithOptions(options); err != nil {
        return PodStorage{}, err
    }

    // 2. 基于主 store 派生子资源 store(关键技巧:浅拷贝 + 替换 Strategy)
    statusStore := *store                                       // 浅拷贝 store
    statusStore.UpdateStrategy = registrypod.StatusStrategy     // 替换为 status 专用 strategy
    statusStore.ResetFieldsStrategy = registrypod.StatusStrategy

    ephemeralContainersStore := *store
    ephemeralContainersStore.UpdateStrategy = registrypod.EphemeralContainersStrategy

    // 3. 组装 PodStorage(包含主资源 + 所有子资源)
    bindingREST := &BindingREST{store: store}
    return PodStorage{
        Pod:                 &REST{store, proxyTransport},
        Binding:             &BindingREST{store: store},
        LegacyBinding:       &LegacyBindingREST{bindingREST},
        Eviction:            newEvictionStorage(store, podDisruptionBudgetClient),
        Status:              &StatusREST{store: &statusStore},
        EphemeralContainers: &EphemeralContainersREST{store: &ephemeralContainersStore},
        Log:                 &podrest.LogREST{Store: store, KubeletConn: k},
        Proxy:               &podrest.ProxyREST{Store: store, ProxyTransport: proxyTransport},
        Exec:                &podrest.ExecREST{Store: store, KubeletConn: k},
        Attach:              &podrest.AttachREST{Store: store, KubeletConn: k},
        PortForward:         &podrest.PortForwardREST{Store: store, KubeletConn: k},
    }, nil
}

PodStorage 这个聚合结构体:

// PodStorage includes storage for pods and all sub resources
type PodStorage struct {
    Pod                 *REST
    Binding             *BindingREST
    LegacyBinding       *LegacyBindingREST
    Eviction            *EvictionREST
    Status              *StatusREST
    EphemeralContainers *EphemeralContainersREST
    Log                 *podrest.LogREST
    Proxy               *podrest.ProxyREST
    Exec                *podrest.ExecREST
    Attach              *podrest.AttachREST
    PortForward         *podrest.PortForwardREST
}

💡 statusStore := *store 这一行是设计精髓:通过浅拷贝复用同一份底层 etcd 连接、cache、index,但替换 Strategy 实现不同的行为约束。比如 Status 子资源的 UpdateStrategy 只允许改 status 字段,禁止改 spec——这是 K8s “主资源管 spec、子资源管 status” 这套设计的代码实现。

🤔 为什么 log / exec / attach / portforward 不用 store? 因为它们不存数据,是实时连接到 kubelet的转发请求。所以这些 storage 只持有 Store 引用(用来查 Pod 信息),实际逻辑是建立到 kubelet 的 stream。

4.5 Pod 的子资源都有什么用?

子资源用途kubectl 入口
pods/status改 Pod 状态(kubelet 上报)内部使用
pods/binding把 Pod 绑定到 Node(scheduler 用)内部使用
pods/eviction优雅驱逐 Pod(遵循 PDB)kubectl drain
pods/log拉日志kubectl logs
pods/exec执行命令kubectl exec
pods/attach附加到主进程kubectl attach
pods/portforward端口转发kubectl port-forward
pods/proxyHTTP 代理到 Podkubectl proxy
pods/ephemeralcontainers添加临时容器kubectl debug

🚨 生产实战经验:很多人 RBAC 给 dev 团队"看 Pod 日志"权限时,只给了 pods/log 是不够的——还得给 pods/get!因为查询子资源前必须能 GET 主资源。这是 K8s RBAC 的常见踩坑点。


五、塞进 restStorageMap:每条 URL 都对应一个 storage

回到 NewLegacyRESTStorage,所有资源的 storage 都创建完后,要塞进一个大 map

restStorageMap := map[string]rest.Storage{
    // === Pod 及其子资源 ===
    "pods":             podStorage.Pod,
    "pods/attach":      podStorage.Attach,
    "pods/status":      podStorage.Status,
    "pods/log":         podStorage.Log,
    "pods/exec":        podStorage.Exec,
    "pods/portforward": podStorage.PortForward,
    "pods/proxy":       podStorage.Proxy,
    "pods/binding":     podStorage.Binding,
    "bindings":         podStorage.LegacyBinding,

    // === PodTemplate ===
    "podTemplates": podTemplateStorage,

    // === ReplicationController(一主一从)===
    "replicationControllers":        controllerStorage.Controller,
    "replicationControllers/status": controllerStorage.Status,

    // === Service(一主两从)===
    "services":        serviceRest,
    "services/proxy":  serviceRestProxy,
    "services/status": serviceStatusStorage,

    // === Endpoints ===
    "endpoints": endpointsStorage,

    // === Node(一主两从)===
    "nodes":        nodeStorage.Node,
    "nodes/status": nodeStorage.Status,
    "nodes/proxy":  nodeStorage.Proxy,

    // === Events ===
    "events": eventStorage,

    // === LimitRange / ResourceQuota / Namespace ===
    "limitRanges":                   limitRangeStorage,
    "resourceQuotas":                resourceQuotaStorage,
    "resourceQuotas/status":         resourceQuotaStatusStorage,
    "namespaces":                    namespaceStorage,
    "namespaces/status":             namespaceStatusStorage,
    "namespaces/finalize":           namespaceFinalizeStorage,

    // === Secret / ServiceAccount / PV / PVC / ConfigMap ===
    "secrets":                       secretStorage,
    "serviceAccounts":               serviceAccountStorage,
    "persistentVolumes":             persistentVolumeStorage,
    "persistentVolumes/status":      persistentVolumeStatusStorage,
    "persistentVolumeClaims":        persistentVolumeClaimStorage,
    "persistentVolumeClaims/status": persistentVolumeClaimStatusStorage,
    "configMaps":                    configMapStorage,

    // === ComponentStatus(已废弃但还在)===
    "componentStatuses": componentstatus.NewStorage(componentStatusStorage{c.StorageFactory}.serversToValidate),
}

这个 map 就是 K8s core API 的"URL 路由表"——key 是路径段,value 是对应的 storage。

最后塞到 apiGroupInfo 的双层 map 里:

// 双层 map:第一层 key 是版本,第二层 key 是资源名称
apiGroupInfo.VersionedResourcesStorageMap["v1"] = restStorageMap

之后 InstallLegacyAPIGroup遍历这个 map,每个 entry 自动生成对应的 HTTP handler:

"pods"              → POST/GET/DELETE  /api/v1/namespaces/{ns}/pods
"pods/status"       → PUT              /api/v1/namespaces/{ns}/pods/{name}/status
"pods/exec"         → POST             /api/v1/namespaces/{ns}/pods/{name}/exec
"configMaps"        → POST/GET/DELETE  /api/v1/namespaces/{ns}/configmaps
"nodes"             → POST/GET/DELETE  /api/v1/nodes   (集群级,无 namespace)

💡 K8s 资源的 namespace scope 怎么判断? 看资源的 Strategy 实现里的 NamespaceScoped() 方法。返回 true 的资源(如 Pod、Service)URL 里有 namespaces/{ns};返回 false 的(如 Node、PersistentVolume、Namespace 自己)则是集群级。


六、整体数据流:一次 kubectl create -f deploy.yaml 的完整链路

回到开头那个面试题。现在我们可以把整条链路画清楚了:

       kubectl create -f deploy.yaml
                  │
                  │ HTTPS POST /apis/apps/v1/namespaces/default/deployments
                  ▼
       ┌─────────────────────────────────┐
       │ APIServer HTTPS handler         │
       │  (handler chain: 认证→鉴权→准入)│
       └──────────────┬──────────────────┘
                      ▼
       ┌─────────────────────────────────┐
       │ NegotiatedSerializer 解码        │
       │  根据 Content-Type 选解码器     │
       │  application/json → JSON 解码器  │
       └──────────────┬──────────────────┘
                      ▼
       ┌─────────────────────────────────┐
       │ Scheme.Convert                  │
       │  v1.Deployment → 内部版本        │
       │  (查 typeToGVK 映射)            │
       └──────────────┬──────────────────┘
                      ▼
       ┌─────────────────────────────────┐
       │ RESTStorage.Create              │
       │  ├─ Strategy.PrepareForCreate   │
       │  │   (补默认值、生成 UID)       │
       │  ├─ Strategy.Validate           │
       │  │   (校验字段)                 │
       │  └─ etcd Put                    │
       └──────────────┬──────────────────┘
                      ▼
       ┌─────────────────────────────────┐
       │ Scheme.Convert (回程)           │
       │  内部版本 → v1.Deployment        │
       └──────────────┬──────────────────┘
                      ▼
       ┌─────────────────────────────────┐
       │ NegotiatedSerializer 编码        │
       │  根据 Accept 头选编码器          │
       └──────────────┬──────────────────┘
                      ▼
              HTTP 201 Created
                      │
                      ▼
                  kubectl

每个箭头都对应源码里的一个明确步骤。Scheme 在两个地方关键发力:进来时反序列化 + 版本转换,出去时版本转换 + 序列化。RESTStorage 是中间那一段:校验 → 写 etcd。


七、踩坑总结(实操经验)

#现象解决
1写 controller 没 AddToScheme 自定义 CRDno kind "Foo" is registered for version "...""显式 _ = foov1.AddToScheme(scheme)
2写 Aggregated APIServer 没设 NewFunc启动 panic必填,对照 CompleteWithOptions 校验
3RBAC 只给 pods/log 没给 pods/getforbidden: cannot get resource "pods"子资源访问必须先有主资源 get 权限
4CRD 不写 status 子资源就直接改 status改不动,spec 反而被覆盖CRD 定义里加 subresources.status
5Strategy 漏写 NamespaceScopedURL 错位(集群级资源出现在 namespace 路径下)显式实现 NamespaceScoped() bool
6自定义资源没注册到 legacyscheme.Scheme内部转换报错core group 用 legacy scheme,其他 group 用各自 scheme

八、本节小结

  • Scheme 是 K8s 的全局资源类型注册表,记录 Go 类型 ↔ GVK 的映射、序列化方法、版本转换函数。它是 K8s 多版本资源管理的核心基础设施。
  • 资源完整身份是 GVK(Group/Version/Kind),不是单独的 Kind。一个 Kind 可能对应多个 Resource(含子资源)。
  • RESTStorage 是每个资源的 CRUD 接口,本质是 genericregistry.Store + 资源专属 Strategy。Strategy 模式让 K8s 把通用 CRUD 和资源特定校验/默认值解耦。
  • 子资源 storage 通过浅拷贝主 store + 替换 Strategy 实现,这是 K8s 用极少代码实现 Pod 11 个子资源的设计精髓。
  • 所有 storage 最终塞进 restStorageMap,再挂到 apiGroupInfo.VersionedResourcesStorageMap["v1"],由 InstallLegacyAPIGroup 自动生成 HTTP 路由。

下一节会进入更具体的话题:HTTP 请求到 etcd 的完整数据流——RESTStorage 内部到底怎么调 etcd、watch 机制怎么实现、cache 在哪儿、序列化的多种格式(JSON/YAML/Protobuf)怎么协商。


九、你踩过这些坑吗?

  1. 自己写过 Aggregated APIServer 或 controller-runtime operator 吗?最坑你的一次是什么场景?
  2. CRD 用 status subresource 的时候,有没有遇到过"客户端改 status 改不动"的问题?最后是怎么定位到是 subresources 配置漏写的?
  3. K8s 的多版本资源转换(如 Ingress v1beta1v1)你有研究过 conversion webhook 吗?踩过哪些坑?

欢迎在评论区分享你的实战经验。

十、延伸思考

  • K8s 为什么要设计"内部版本"(internal version)?为什么不直接在 v1v1beta1 之间转换?
  • Scheme 是全局单例,那不同的 controller import 同一个 scheme 包会不会有竞争问题?为什么不会?
  • CRD 的 additionalPrinterColumns 和我们这里讲的 TableConvertor 是什么关系?

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加倍巴巴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值