kubernetes kube-apiserver启动流程分析
kubernetes代码版本:v1.20
看这篇文章的前提:
-
有 golang 的基础
-
对于 kubernetes 有基本的了解
kube-apiserver的启动过程可以分为以下几个步骤:
- 1.资源注册
- 2.解析命令行参数
- 3.创建 apiserver 通用配置
- 4.创建 APIExtensionsServer
- 5.创建 KubeAPIServer
- 6.创建 AggregatorServer
- 7.启动服务
并不会把每一步都仔细分析。我只挑主要的步骤进行研究一下。
1.资源注册
首先要把 apiserver 支持的资源注册到资源表中,资源注册这一过程并不是通过函数调用实现的,而是使用 golang 的 import 导包机制实现资源注册(golang 的导入机制我就不赘述了)。
整个组件的入口函数是这个文件:
k8s.io/kubernetes/cmd/kube-apiserver/apiserver.go
首先就是初始化 command 对象,执行了 app.NewAPIServerCommand()
函数。函数在 k8s.io/kubernetes/cmd/kube-apiserver/app/server.go
文件中。
可以发现在 import 的时候导入了这个包
k8s.io/kubernetes/pkg/api/legacyscheme
在 k8s.io/kubernetes/vendor/k8s.io/kube-aggregator/pkg/apiserver/scheme/scheme.go
文件中,在 scheme 包里面,定义了三个全局变量
var (
// 资源注册表
Scheme = runtime.NewScheme()
// 编、解码器
Codecs = serializer.NewCodecFactory(Scheme)
// 参数编、解码器
ParameterCodec = runtime.NewParameterCodec(Scheme)
)
这三个变量可以在 apiserver 组件的代码中任何地方使用。
kube-apiserver 启动时还导入了 controlplane 包,包中的 import_known_versions.go 文件调用了 kubernetes 支持资源的 install 包,代码如下:
k8s.io/kubernetes/pkg/controlplane/import_known_versions.go
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"
)
可以看到里面注册的所有资源都是在 k8s.io/kubernetes/pkg/apis
文件下,都在本资源下面的 install 包里面 定义了自己的 init 方法。
以 admission 为例:
func Install(scheme *runtime.Scheme) {
// 首先注册 admission 的内部版本
utilruntime.Must(admission.AddToScheme(scheme))
// 其他外部版本
utilruntime.Must(v1beta1.AddToScheme(scheme))
utilruntime.Must(v1.AddToScheme(scheme))
// 版本顺序
utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion))
}
***
简单说一下什么叫做内部版本,什么叫做外部版本:
kubernetes 中每个资源都有自己的不同版本,例如 v1, v1/beat1, v1/alpha1 等等,当资源需要在不同版本之间转换时,我们不能为每两个不同的版本之间都写一个转换方法,如果后续有新的版本添加进来,就意味着需要为新的版本对应已经存在的每个版本都编写转换方法,复杂度会指数级上升。在kubernetes中则通过一个内部版本的设计来进行解决,内部版本是一个稳定的版本,所有的版本都只针对目标版本来进行转换的实现,而不关注其他版本。可以理解为,这个所谓的’内部版本‘包含其他所有版本的所有字段,我们在进行转换的时候先把对象转换成内部版本,然后再从内部版本转换为目标版本即可。
***
2.解析命令行参数
kubernetes 使用的是开源的命令行库 Cobra,所有组件均使用这个解析库,此库在 github 上面是开源的,有兴趣的可以自行了解一下。
我只简单叙述一下:
Cobra既是一个用来创建强大的现代CLI命令行的golang库,也是一个生成程序应用和命令行文件的程序。使用方法如下:
创建 Cmd 主命令对象,并在对象中定义各种 Run 方法(此处只是定义,并不是执行),执行顺序是 PersistentPreRun --> PerRun --> Run --> PostRun --> PersistentPostRun.然后添加命令行参数(Flag),比如我们使用的 kubectl get pod -n kube-system
后面的 -n 就是 Flag,最后执行 command 对象的 Execute 方法回调我们此前定义的各种函数。
现在,让我们在回到最初的 NewAPIServerCommand 函数中,看一看 kubernetes 的命令行解析代码(不太重要的代码我会省略):
k8s.io/kubernetes/cmd/kube-apiserver/app/server.go
func NewAPIServerCommand() *cobra.Command {
// 初始化各个模块的默认配置
s := options.NewServerRunOptions()
// 生成 cmd 对象
cmd := &cobra.Command{
...
// 定义方法
RunE: func(cmd *cobra.Command, args []string) error {
...
// 填充成完整的参数对象
completedOptions, err := Complete(s)
...
// 验证参数的合法性
if errs := completedOptions.Validate(); len(errs) != 0 {
return utilerrors.NewAggregate(errs)
}
// 将完全的参数对象传入 Run 函数。Run 里面完成了 apiserver 组件的启动逻辑,这是一个常驻进程。
return Run(completedOptions, genericapiserver.SetupSignalHandler())
},
...
}
...
}
顺便回到 apiserver 的入口文件中,看看 main 文件中的启动代码:
func main() {
rand.Seed(time.Now().UnixNano())
command := app.NewAPIServerCommand()
logs.InitLogs()
defer logs.FlushLogs()
if err := command.Execute(); err != nil {
os.Exit(1)
}
}
就是按照我上面说的顺序定义的,kubernetes 中所有组件的启动流程大体上都是这个样子。
进入上面说的 Run 函数里面我们接着看,接下来的所有服务项的配置和创建都是在 CreateServerChain 函数中完成的。
CreateServerChain 是完成 server 初始化的方法,里面包含 APIExtensionsServer、KubeAPIServer、AggregatorServer 初始化的所有流程,最终返回 aggregatorapiserver.APIAggregator 实例,初始化流程主要有:http filter chain 的配置、API Group 的注册、http path 与 handler 的关联以及 handler 后端存储 etcd 的配置。
3.创建 apiserver 通用配置
在 CreateServerChain 函数中,基本可以从函数名猜出来每一步在干什么,通用配置的创建就是在函数 CreateKubeAPIServerConfig() 中完成的。进入到以下函数中看一下详细实现:CreateKubeAPIServerConfig() --> buildGenericConfig()
。
创建通用配置流程主要有以下几步:
- 1.GenericConfig 实例化
代码如下:
// 为 genericConfig 设置默认值。
genericConfig = genericapiserver.NewConfig(legacyscheme.Codecs)
// 启动/禁止 GV 及 resource
genericConfig.MergedResourceConfig = controlplane.DefaultAPIResourceConfigSource()
...
// openAPI 规范
genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(generatedopenapi.GetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(legacyscheme.Scheme, extensionsapiserver.Scheme, aggregatorscheme.Scheme))
...
genericConfig.Version = &kubeVersion
- 2.StorageFactoryConfig
apiserver 组件使用 etcd 作为集群的存储,系统中所使用的所有资源、集群状态、配置等都在这上面保存。代码部分如下:
// 初始化 storageFactoryConfig 配置对象。
storageFactoryConfig := kubeapiserver.NewStorageFactoryConfig()
storageFactoryConfig.APIResourceConfig = genericConfig.MergedResourceConfig
// 初始化 etcd 相关的配置信息,补全配置对象,返回 completedStorageFactoryConfig
completedStorageFactoryConfig, err := storageFactoryConfig.Complete(s.Etcd)
if err != nil {
lastErr = err
return
}
// 根据上面补完的配置信息,创建 storageFactory 对象
storageFactory, lastErr = completedStorageFactoryConfig.New()
if lastErr != nil {
return
}
- 3.Authorizer认证、授权配置
作为整个系统的存储对象交互入口,每个系统的请求都需要经过认证、授权、准入控制器这些阶段,准入控制器下面再说,涉及到的代码如下:
genericConfig.Authorization.Authorizer, genericConfig.RuleResolver, err = BuildAuthorizer(s, genericConfig.EgressSelector, versionedInformers)
这个有必要进入到函数内部在看一看具体做了些什么,按照下面的路径点进去:
BuildAuthorizer() --> authorizationConfig.New()
New 函数主要就是在 for 循环中根据 config.AuthorizationModes 配置了 authorizers 和 ruleResolvers 两个变量,这个 config.AuthorizationModes 是在最初在初始化配置对象执行 options.NewServerRunOptions() 的时候赋值的,具体路径及代码如下:
k8s.io/kubernetes/pkg/kubeapiserver/options/authorization.go
func NewBuiltInAuthorizationOptions() *BuiltInAuthorizationOptions {
return &BuiltInAuthorizationOptions{
// 初始赋值就一个 "AlwaysAllow" 字符串,这是默认的配置。
Modes: []string{authzmodes.ModeAlwaysAllow},
WebhookVersion: "v1beta1",
WebhookCacheAuthorizedTTL: 5 * time.Minute,
WebhookCacheUnauthorizedTTL: 30 * time.Second,
WebhookRetryBackoff: genericoptions.DefaultAuthWebhookRetryBackoff(),
}
}
在回到New()中,在函数最后返回了认证和授权对象。
return union.New(authorizers...), union.NewRuleResolvers(ruleResolvers...), nil
authorizers 是已启用的认证器列表,union.New将它合并成一个认证器。
ruleResolvers 是已启用的规则解析器,union.NewRuleResolvers 也是合并了一下
可以看到默认的授权是 AlwaysAllow,具体其它类型可以在启动的时候在配置里面设置,只要配置了,就会实例化该授权对象,认证的时候会遍历每一个授权器,有一个认证成功就ok。
- 4.Admission准入控制器配置
准入控制(Admission Control)在授权后对请求做进一步的验证或添加默认参数,在对kubernetes api服务器的请求过程中,先经过认证、授权后,执行准入操作,再对目标对象进行操作
在对集群进行请求时,每个准入控制插件都按顺序运行,只有全部插件都通过的请求才会进入系统,如果序列中的任何插件拒绝请求,则整个请求将被拒绝,并返回错误信息。
准入控制器是在初始化 ServerRunOptions 的时候 New 的,得再回到最开始的 NewAPIServerCommand() 函数中,
k8s.io/kubernetes/cmd/kube-apiserver/app/server.go
s := options.NewServerRunOptions()
函数中执行的操作就是给哥哥配置赋默认值,找到 Admission
字段,他执行的赋值函数是
kubeoptions.NewAdmissionOptions()
这个函数中主要执行了两个 RegisterAllAdmissionPlugins() 函数,来看代码:
func NewAdmissionOptions() *AdmissionOptions {
// 在这个函数里面执行了一个 RegisterAllAdmissionPlugins 函数,进到函数里面能看到。
options := genericoptions.NewAdmissionOptions()
// 第二个 RegisterAllAdmissionPlugins 函数。
RegisterAllAdmissionPlugins(options.Plugins)
options.RecommendedPluginOrder = AllOrderedPlugins
options.DefaultOffPlugins = DefaultOffAdmissionPlugins()
return &AdmissionOptions{
GenericAdmission: options,
}
}
这两个 RegisterAllAdmissionPlugins
所执行的注册的插件不太一样,但是能看到都是执行了不同组件中的 Register()
函数,其实这个操作就是把控制插件存放进配置对象中,上面的代码最开始赋值的变量 options,是一个 AdmissionOptions 对象,
type AdmissionOptions struct {
RecommendedPluginOrder []string
DefaultOffPlugins sets.String
EnablePlugins []string
DisablePlugins []string
ConfigFile string
Plugins *admission.Plugins
Decorators admission.Decorators
}
对象中的 Plugins
字段就是存放插件的,是一个 admission.Plugins 对象:
type Factory func(config io.Reader) (Interface, error)
type Plugins struct {
// 并发保护
lock sync.Mutex
// 以键值对的形式存放插件,key 就是插件的名称,value 是插件的实现函数,是上面的 Factory 对象
registry map[string]Factory
}
可以随便点开一个插件的注册代码看一下,例如按照下面递进的进入到函数里面:
NewAdmissionOptions() --> RegisterAllAdmissionPlugins() --> admit.Register() --> plugins.Register(name, func)
// 安全的把 name 和 Factory 对象保存到 registry 字段里。
func (ps *Plugins) Register(name string, plugin Factory) {
ps.lock.Lock()
defer ps.lock.Unlock()
if ps.registry != nil {
_, found := ps.registry[name]
if found {
klog.Fatalf("Admission plugin %q was registered twice", name)
}
} else {
ps.registry = map[string]Factory{}
}
klog.V(1).Infof("Registered admission plugin %q", name)
ps.registry[name] = plugin
}
4.创建 APIExtensionsServer
回到 CreateServerChain 函数中,接下来就是创建 apiExtensionsServer,先创建 config 对象,然后根据 config 创建 server:
apiExtensionsConfig, err := createAPIExtensionsConfig(*kubeAPIServerConfig.GenericConfig, kubeAPIServerConfig.ExtraConfig.VersionedInformers, pluginInitializer, completedOptions.ServerRunOptions, completedOptions.MasterCount,
serviceResolver, webhook.NewDefaultAuthenticationInfoResolverWrapper(proxyTransport, kubeAPIServerConfig.GenericConfig.EgressSelector, kubeAPIServerConfig.GenericConfig.LoopbackClientConfig))
if err != nil {
return nil, err
}
apiExtensionsServer, err := createAPIExtensionsServer(apiExtensionsConfig, genericapiserver.NewEmptyDelegate())
if err != nil {
return nil, err
}
直接看 createAPIExtensionsServer 函数的实现,首先用 config 对象生成了 completeConfig,然后执行 New。
- 1.创建 genericServer
涉及到的代码部分:
genericServer, err := c.GenericConfig.New("apiextensions-apiserver", delegationTarget)
genericServer 是所有的 server 对象都会依赖的基础服务。
- 2.实例化 apiResourceConfig
s := &CustomResourceDefinitions{
GenericAPIServer: genericServer,
}
- 3.实例化 apiGroupInfo
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiextensions.GroupName, Scheme, metav1.ParameterCodec, Codecs)
// 判断对应的资源组是否开启,如果开启,则将 group、version 以及 resource 与资源对象进行映射并存储到 VersionedResourcesStorageMap 字段中
if apiResourceConfig.VersionEnabled(v1beta1.SchemeGroupVersion) {
storage := map[string]rest.Storage{}
// customresourcedefinitions
customResourceDefinitionStorage, err := customresourcedefinition.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter)
if err != nil {
return nil, err
}
storage["customresourcedefinitions"] = customResourceDefinitionStorage
storage["customresourcedefinitions/status"] = customresourcedefinition.NewStatusREST(Scheme, customResourceDefinitionStorage)
apiGroupInfo.VersionedResourcesStorageMap[v1beta1.SchemeGroupVersion.Version] = storage
}
if apiResourceConfig.VersionEnabled(v1.SchemeGroupVersion) {
...
}
- 4.注册APIGroup
这一步非常重要,涉及到一个第三方库 go-restful 的一些知识,主要就是因为该框架可定制程度最灵活,但同时也意味着使用起来更加难以理解。
接下来按照函数调用的递进关系一层一层的简单记录一下:
if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
return nil, err
}
InstallAPIGroup
函数直接调用了 s.InstallAPIGroups(apiGroupInfo)
接下来做的就是遍历 apiGroupInfos,将<资源组>/<资源对象>/<资源名称> 映射到 HTTP PATH 中去。
for _, apiGroupInfo := range apiGroupInfos {
// 主要的流程是这个函数
if err := s.installAPIResources(APIGroupPrefix, apiGroupInfo, openAPIModels); err != nil {
return fmt.Errorf("unable to install api resources: %v", err)
}
...
}
在 installAPIResources
函数中通过 InstallREST
函数将资源存储对象作为资源的 Handlers 方法添加,最后使用 go-restful 中的方法将路径和方法进行注册。
InstallREST
接受一个 Container 对象,此 Container 只是 go-restful 中的概念,和 docker 并没有任何关系,一个 Container 监听一个端口,一个 Container 就相当于一个 HTTP Server,对外提供 HTTP 服务。每个 Container 可以包含多个 WebServer,WebServer 相当于一组不同的服务集合,例如我们设计一个图书管理系统,和书本相关的都可以放在 /books 路径下,包含各种增删改查的业务,和用户相关的都可以放在 /users 下面,这就相当于两个服务。每个 WebServer 下面可以包含多个 Router。
Container 监听的端口接受到请求,分发给对应的 WebServer,然后在匹到具体的 Router,调用对应的 Handlers 去处理。
简单说明一下之后可以看看代码:
func (g *APIGroupVersion) InstallREST(container *restful.Container) ([]*storageversion.ResourceInfo, error) {
// 定义了 PATH,表现形式为 <apiPrefix>/<group>/<version>,比如 /apis/apiextensions.k8s.io/v1beta1
prefix := path.Join(g.Root, g.GroupVersion.Group, g.GroupVersion.Version)
// 实例化 APIInstaller
installer := &APIInstaller{
group: g,
prefix: prefix,
minRequestTimeout: g.MinRequestTimeout,
}
// 创建一个 WebServer,即返回值的第三个 ws,为资源注册对应的 handlers 方法,完成资源与方法的绑定并且注册
apiResources, resourceInfos, ws, registrationErrors := installer.Install()
versionDiscoveryHandler := discovery.NewAPIVersionHandler(g.Serializer, g.GroupVersion, staticLister{apiResources})
versionDiscoveryHandler.AddToWebService(ws)
// 将 WebServer 添加到 container 中
container.Add(ws)
return removeNonPersistedResources(resourceInfos), utilerrors.NewAggregate(registrationErrors)
}