Kubernetes 中 apiserver 加载 schema 流程

在 apiserver 初始化、启动时,会加载所有能识别的 schema。加载的过程通过 import pkg 后,触发对应包下 init() 方法来加载。入口cmd/kube-apiserver/apiserver.go: 初始化 apiserver 命令行,并运行。func main() {co

在 apiserver 初始化、启动时,会加载所有能识别的 schema。加载的过程通过 import pkg 后,触发对应包下 init() 方法来加载。

入口

cmd/kube-apiserver/apiserver.go: 初始化 apiserver 命令行,并运行。

func main() {
	command := app.NewAPIServerCommand()
	code := cli.Run(command)
	os.Exit(code)
}

app.NewAPIServerCommand() 中,有一长串 import,其中这两条 import 语句完成了 schema 的加载。

import (
  ...
	"k8s.io/kubernetes/pkg/api/legacyscheme"
	"k8s.io/kubernetes/pkg/controlplane"
  ...
)

初始化

导入 k8s.io/kubernetes/pkg/api/legacyscheme 后,此包下只有一个文件,名为 scheme.go ,该文件中会初始化 3 个变量。其中 Scheme 中会存储 k8s 默认能识别的 scheme。

package legacyscheme

import (
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/serializer"
)

var (
	// Scheme is the default instance of runtime.Scheme to which types in the Kubernetes API are already registered.
	// NOTE: If you are copying this file to start a new api group, STOP! Copy the
	// extensions group instead. This Scheme is special and should appear ONLY in
	// the api group, unless you really know what you're doing.
	// TODO(lavalamp): make the above error impossible.
	Scheme = runtime.NewScheme()

	// Codecs provides access to encoding and decoding for the scheme
	Codecs = serializer.NewCodecFactory(Scheme)

	// ParameterCodec handles versioning of objects that are converted to query parameters.
	ParameterCodec = runtime.NewParameterCodec(Scheme)
)

导入 k8s.io/kubernetes/pkg/controlplane 后,在该包下的 import_known_versions.go 文件中,导入了所有 apiserver 支持的 API groups。

package controlplane

import (
	// These imports are the API groups the API server will support.
	_ "k8s.io/kubernetes/pkg/apis/admission/install"
	_ "k8s.io/kubernetes/pkg/apis/admissionregistration/install"
	_ "k8s.io/kubernetes/pkg/apis/apiserverinternal/install"
	_ "k8s.io/kubernetes/pkg/apis/apps/install"
	_ "k8s.io/kubernetes/pkg/apis/authentication/install"
	_ "k8s.io/kubernetes/pkg/apis/authorization/install"
	_ "k8s.io/kubernetes/pkg/apis/autoscaling/install"
	_ "k8s.io/kubernetes/pkg/apis/batch/install"
	_ "k8s.io/kubernetes/pkg/apis/certificates/install"
	_ "k8s.io/kubernetes/pkg/apis/coordination/install"
	_ "k8s.io/kubernetes/pkg/apis/core/install"
	_ "k8s.io/kubernetes/pkg/apis/discovery/install"
	_ "k8s.io/kubernetes/pkg/apis/events/install"
	_ "k8s.io/kubernetes/pkg/apis/extensions/install"
	_ "k8s.io/kubernetes/pkg/apis/flowcontrol/install"
	_ "k8s.io/kubernetes/pkg/apis/imagepolicy/install"
	_ "k8s.io/kubernetes/pkg/apis/networking/install"
	_ "k8s.io/kubernetes/pkg/apis/node/install"
	_ "k8s.io/kubernetes/pkg/apis/policy/install"
	_ "k8s.io/kubernetes/pkg/apis/rbac/install"
	_ "k8s.io/kubernetes/pkg/apis/scheduling/install"
	_ "k8s.io/kubernetes/pkg/apis/storage/install"
)

它们 import 后的行为是基本一致的,以 k8s.io/kubernetes/pkg/apis/apps/install 为例。该包下只有一个文件,名叫 install.go,其中有一个 init() 方法,会在包导入时执行。

// Package install installs the apps API group, making it available as
// an option to all of the API encoding/decoding machinery.
package install

import (
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/kubernetes/pkg/api/legacyscheme"
	"k8s.io/kubernetes/pkg/apis/apps"
	"k8s.io/kubernetes/pkg/apis/apps/v1"
	"k8s.io/kubernetes/pkg/apis/apps/v1beta1"
	"k8s.io/kubernetes/pkg/apis/apps/v1beta2"
)

func init() {
	Install(legacyscheme.Scheme)
}

// Install registers the API group and adds types to a scheme
func Install(scheme *runtime.Scheme) {
	utilruntime.Must(apps.AddToScheme(scheme))
	utilruntime.Must(v1beta1.AddToScheme(scheme))
	utilruntime.Must(v1beta2.AddToScheme(scheme))
	utilruntime.Must(v1.AddToScheme(scheme))
	utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta2.SchemeGroupVersion, v1beta1.SchemeGroupVersion))
}

这里可以这样简单理解(以 utilruntime.Must(apps.AddToScheme(scheme)) 为例):

apps 这个 gv 下所有的资源,通过 apps 包中的 AddToScheme 方法,都注册到了 legacyscheme.Scheme 中,即前面初始化的 Scheme 变量中。这样 k8s 就能识别该 gv 下资源。

对于仅仅了解大致过程,上述的流程已经足够。但是如果点开 apps.AddToScheme(scheme) 方法,会有一种非常熟悉的感觉——在开发operator,通过k8s提供的脚本,来为 CRD 生成对应 yaml 时,会生成相应的代码。

那么,这一套模板到底是怎么将 Scheme 加载进 legacyscheme.Scheme 去的呢?

One more step forward

apps.AddToScheme() 方法在 pkg/apis/apps/register.go 文件中,它有 3 个变量加上一个方法,即表示这些资源(Kind)同属于这个 GV

var (
	// SchemeBuilder stores functions to add things to a scheme.
	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
	// AddToScheme applies all stored functions t oa scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)

// GroupName is the group name use in this package
const GroupName = "apps"

// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}

// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
	// TODO this will get cleaned up with the scheme types are fixed
	scheme.AddKnownTypes(SchemeGroupVersion,
		&DaemonSet{},
		&DaemonSetList{},
		&Deployment{},
		&DeploymentList{},
		&DeploymentRollback{},
		&autoscaling.Scale{},
		&StatefulSet{},
		&StatefulSetList{},
		&ControllerRevision{},
		&ControllerRevisionList{},
		&ReplicaSet{},
		&ReplicaSetList{},
	)
	return nil
}

其中执行添加操作的方式是 scheme.AddKnownTypes(),对每种资源,将会在 legacyscheme.SchemegvkToTypetypeToGVK 添加对应的值,如下:

s.gvkToType[gvk] = t
s.typeToGVK[t] = append(s.typeToGVK[t], gvk)

方法 addKnownTypes(scheme *runtime.Scheme) 是如何绕了一大圈才执行的?

变量 SchemeBuilder 的类型是 []func(*Scheme) error ,也就是说它本身是一个数组,元素类型为一个函数。即:

// SchemeBuilder collects functions that add things to a scheme. It's to allow
// code to compile without explicitly referencing generated types. You should
// declare one in each package that will have generated deep copy or conversion
// functions.
type SchemeBuilder []func(*Scheme) error

变量 SchemeBuilder 通过 runtime.NewSchemeBuilder(addKnownTypes) 初始化之后,它(数组)含有一个元素,即 addKnownTypes() 方法。

// Register adds a scheme setup function to the list.
func (sb *SchemeBuilder) Register(funcs ...func(*Scheme) error) {
    for _, f := range funcs {
        *sb = append(*sb, f)
    }
}

// NewSchemeBuilder calls Register for you.
func NewSchemeBuilder(funcs ...func(*Scheme) error) SchemeBuilder {
    var sb SchemeBuilder
    sb.Register(funcs...)
    return sb
}

当执行 utilruntime.Must(apps.AddToScheme(scheme)) 时,会执行 SchemeBuilder.AddToScheme 方法,这个方法中会依次取 SchemeBuilder 中的每个元素,然后执行该元素(方法),如下:

// AddToScheme applies all the stored functions to the scheme. A non-nil error
// indicates that one function failed and the attempt was abandoned.
func (sb *SchemeBuilder) AddToScheme(s *Scheme) error {
    for _, f := range *sb {
        if err := f(s); err != nil {
            return err
        }
    }
    return nil
}

为什么要这样设计 SchemeBuilder

  • 核心思想是让执行加载 Scheme 的逻辑不用知道具体的类型是什么
  • SchemeBuilder 设计成数组的形式,可以提高加载时的灵活性?虽然绝大多数场景下,SchemeBuilder 只有一个元素,但是有一些场景下会有两个以上的元素。
  • SchemeBuilder 的 3 个方法 AddToScheme()Register()NewSchemeBuilder() 都是基于 SchemeBuilder 是数组形式时的应有做法,分别对应执行元素(方法)、添加(数组 append())、初始化(数组)。

Read more

容器镜像(4):镜像的常用工具箱

容器镜像(4):镜像的常用工具箱

前几篇在讲多架构镜像时已经用过 skopeo 和 crane 做镜像复制,这篇系统整理这两个工具的完整能力,同时介绍几个日常操作镜像时同样好用的工具。 一、skopeo:不依赖 Daemon 的镜像瑞士军刀 skopeo 的核心价值是绕过 Docker daemon,直接与 Registry API 交互。上一篇用它做镜像复制和离线传输,但它的能力远不止于此。 1.1 安装 # Ubuntu / Debian sudo apt install -y skopeo skopeo --version # skopeo version 1.15.1 1.2 inspect:免拉取检查镜像元数据 docker inspect 需要先把镜像拉到本地,skopeo inspect 直接向 Registry

容器镜像(3):多架构镜像构建

容器镜像(3):多架构镜像构建

一、什么是多架构镜像 1.1 OCI Image Index 上一篇介绍了单平台镜像的结构:一个 Manifest 指向 Config 和若干 Layer blob。多架构镜像在此之上多了一层——OCI Image Index(也叫 Manifest List),是一个轻量的索引文件,把多个单平台 Manifest 组织在一起: $ docker manifest inspect golang:1.22-alpine { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests&

容器镜像(2):containerd 视角下的镜像

容器镜像(2):containerd 视角下的镜像

一、为什么需要了解 containerd 如果你只用 docker run 跑容器,从来不关心底层,那可以不了解 containerd。但如果你在用 Kubernetes,或者想真正理解"容器运行时"是什么,containerd 是绕不开的。 事实上,当你执行 docker run 的时候,containerd 早就在后台悄悄工作了——Docker 从 1.11 版本开始,就把核心运行时剥离出来交给 containerd 负责。 1.1 Docker 的架构演变 早期的 Docker(1.10 及之前)是一个"大一统"的单体程序:一个 dockerd