我们可以根据kube-apiserver启动服务的主流程来划分多个模块:
go// cmd/kube-apiserver/app/server.go // 初始化config config, err := NewConfig(opts) if err != nil { return err } // 补充完整配置 completed, err := config.Complete() if err != nil { return err } // 创建完整server链 server, err := CreateServerChain(completed) if err != nil { return err } // 启动health等服务 prepared, err := server.PrepareRun() if err != nil { return err } // 启动服务 return prepared.Run(ctx)
NewConfig
首先我们可以先从总配置:Config结构体可以看出,整个kubde-apiserver分为3部分:
go
type Config struct {
Options options.CompletedOptions
// 聚合 API
Aggregator *aggregatorapiserver.Config
// 核心 API
KubeAPIs *controlplane.Config
// CRD API
ApiExtensions *apiextensionsapiserver.Config
ExtraConfig
}
而初始化的函数如下:
go
func NewConfig(opts options.CompletedOptions) (*Config, error) {
// 是在 Kubernetes 中构造一个 genericapiserver.Config(通用 API Server 配置) 的函数,主要用于为各种基于 generic-apiserver 构建的 API Server(如 kube-apiserver、aggregator-apiserver、apiextensions-apiserver)准备初始化配置。
genericConfig, versionedInformers, storageFactory, err := controlplaneapiserver.BuildGenericConfig(
opts.CompletedOptions,
[]*runtime.Scheme{legacyscheme.Scheme, apiextensionsapiserver.Scheme, aggregatorscheme.Scheme},
controlplane.DefaultAPIResourceConfigSource(),
generatedopenapi.GetOpenAPIDefinitions,
)
...
// 配置核心 API Server的配置
kubeAPIs, serviceResolver, pluginInitializer, err := CreateKubeAPIServerConfig(opts, genericConfig, versionedInformers, storageFactory)
...
c.KubeAPIs = kubeAPIs
// 配置CRD API server的配置
apiExtensions, err := controlplaneapiserver.CreateAPIExtensionsConfig(*kubeAPIs.ControlPlane.Generic, kubeAPIs.ControlPlane.VersionedInformers, pluginInitializer, opts.CompletedOptions, opts.MasterCount,
serviceResolver, webhook.NewDefaultAuthenticationInfoResolverWrapper(kubeAPIs.ControlPlane.ProxyTransport, kubeAPIs.ControlPlane.Generic.EgressSelector, kubeAPIs.ControlPlane.Generic.LoopbackClientConfig, kubeAPIs.ControlPlane.Generic.TracerProvider))
...
c.ApiExtensions = apiExtensions
// 最后聚合的配置
aggregator, err := controlplaneapiserver.CreateAggregatorConfig(*kubeAPIs.ControlPlane.Generic, opts.CompletedOptions, kubeAPIs.ControlPlane.VersionedInformers, serviceResolver, kubeAPIs.ControlPlane.ProxyTransport, kubeAPIs.ControlPlane.Extra.PeerProxy, pluginInitializer)
c.Aggregator = aggregator
return c, nil
}
值得关注的是在构建通用配置的时候,会把序列化反序列化器等通用配置一起初始化:
go
func BuildGenericConfig(
s options.CompletedOptions,
schemes []*runtime.Scheme,
resourceConfig *serverstorage.ResourceConfig,
getOpenAPIDefinitions func(ref openapicommon.ReferenceCallback) map[string]openapicommon.OpenAPIDefinition,
) (
genericConfig *genericapiserver.Config,
versionedInformers clientgoinformers.SharedInformerFactory,
storageFactory *serverstorage.DefaultStorageFactory,
lastErr error,
) { // 这个是关键配置,全局变量Codecs(CodecFactory类型)传入,作为genericConfig的Serializer属性。而全局变量Codecs是基于Scheme = // runtime.NewScheme()构建的,所有k8s的对象在程序初始化的时候都会在Scheme注册对应类型
genericConfig = genericapiserver.NewConfig(legacyscheme.Codecs)
// 下面是补充etcd相关的配置
storageFactoryConfig := kubeapiserver.NewStorageFactoryConfigEffectiveVersion(genericConfig.EffectiveVersion)
storageFactoryConfig.APIResourceConfig = genericConfig.MergedResourceConfig
storageFactoryConfig.DefaultResourceEncoding.SetEffectiveVersion(genericConfig.EffectiveVersion)
storageFactory, lastErr = storageFactoryConfig.Complete(s.Etcd).New()
...
}
CreateServerChain
这个函数主要是创建多个服务,利用delegation来把这些给串联起来。那么这个串联,到底是怎么个串联法,其实在于一个关键的接口
go
//DelegationTarget 是一个 "API Server 委托目标接口",用于把多个 API Server 组装起来,让它们共享某些处理链(handler chain)、健康检查、钩子函数
//等。
type DelegationTarget interface {
// 返回一个 不带认证/授权/审计链条的裸 handler,用于 handler 链组装时的底层拼接,不加中间件直接暴露,供上层包一层自己的处理逻辑
UnprotectedHandler() http.Handler
// 返回需要执行的 PostStart hook 函数,在 API Server 启动后执行
PostStartHooks() map[string]postStartHookEntry
// 返回需要执行的 PreShutdown hook 函数,在 API Server 停止前执行
PreShutdownHooks() map[string]preShutdownHookEntry
// 返回健康检查器列表,合并后用于 /healthz 路由
HealthzChecks() []healthz.HealthChecker
// 返回当前 apiserver 支持的 HTTP 路径列表(用于构建首页 / 路径)
ListedPaths() []string
// 返回链上的下一级委托目标:这是核心,形成链式结构,如 Aggregator → apiextension → kube-apiserver
NextDelegate() DelegationTarget
// API 安装后最后的准备步骤:会递归调用下一层 API Server 的 PrepareRun,用于启动周期最后阶段,如 OpenAPI 聚合等
PrepareRun() preparedGenericAPIServer
// 返回注册的 mux 路径与资源 discovery 注册完成信号,信号机制:通知相关组件 /apis 注册完毕,可以继续启动其他模块
MuxAndDiscoveryCompleteSignals() map[string]<-chan struct{}
// 资源清理方法,线程安全,可多次调用
Destroy()
}
Aggregator, apiextension,kube-apiserver这三个server都实现了这个接口,而实际上,他们都是采用下面通用结构体GenericAPIServer,GenericAPIServer实现了这个接口:
go
type GenericAPIServer struct {
...}
我们现在来看CreateServerChain这个函数的主流程:
go
func CreateServerChain(config CompletedConfig) (*aggregatorapiserver.APIAggregator, error) {
...
// 先创建crd server
apiExtensionsServer, err := config.ApiExtensions.New(genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))
...
// 然后创建core server
kubeAPIServer, err := config.KubeAPIs.New(apiExtensionsServer.GenericAPIServer)
...
/// 然后返回聚合
aggregatorServer, err := controlplaneapiserver.CreateAggregatorServer(config.Aggregator, kubeAPIServer.ControlPlane.GenericAPIServer, apiExtensionsServer.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdAPIEnabled, apiVersionPriorities)
...
}
//
func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*CustomResourceDefinitions, error) {
// 返回通用的server
genericServer, err := c.GenericConfig.New("apiextensions-apiserver", delegationTarget)
if err != nil {
return nil, err
}
...
}
我们首先只讲一下genericServer(GenericAPIServer):
go
func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*GenericAPIServer, error) {
...
// 关键路由函数
apiServerHandler := NewAPIServerHandler(name, c.Serializer, handlerChainBuilder, delegationTarget.UnprotectedHandler())
s := &GenericAPIServer{
...
Handler: apiServerHandler,
...}
}
type APIServerHandler struct {
FullHandlerChain http.Handler // 完整的 handler 链,外部调用的入口,包含所有认证/授权过滤器
GoRestfulContainer *restful.Container // GoRestful 路由容器(用于大多数 API,如 /api、/apis)
NonGoRestfulMux *mux.PathRecorderMux // 原生 net/http 多路复用器,用于处理未注册到 gorestful 的路径
Director http.Handler// 路由选择器,根据请求路径决定转发到 gorestful 或 NonGoRestfulMux
}
关键地方在于Director,而Director的结构和方法如下:
go
type director struct {
name string
goRestfulContainer *restful.Container
nonGoRestfulMux *mux.PathRecorderMux
}
func (d director) ServeHTTP(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path
// 判断goRestfulContainer
for _, ws := range d.goRestfulContainer.RegisteredWebServices() {
switch {
case ws.RootPath() == "/apis":
// if we are exactly /apis or /apis/, then we need special handling in loop.
// normally these are passed to the nonGoRestfulMux, but if discovery is enabled, it will go directly.
// We can't rely on a prefix match since /apis matches everything (see the big comment on Director above)
// 简单来说就是:因为 gorestful 无法容忍未注册路径,只能精确匹配 /apis 或 /apis/,否则一旦路径不匹配就直接 404,因此必须对 /apis 做特
// 别处理,避免误伤 fallback 的请求。
if path == "/apis" || path == "/apis/" {
klog.V(5).Infof("%v: %v %q satisfied by gorestful with webservice %v", d.name, req.Method, path, ws.RootPath())
// don't use servemux here because gorestful servemuxes get messed up when removing webservices
// TODO fix gorestful, remove TPRs, or stop using gorestful
d.goRestfulContainer.Dispatch(w, req)
return
}
case strings.HasPrefix(path, ws.RootPath()):
// ensure an exact match or a path boundary match
if len(path) == len(ws.RootPath()) || path[len(ws.RootPath())] == '/' {
klog.V(5).Infof("%v: %v %q satisfied by gorestful with webservice %v", d.name, req.Method, path, ws.RootPath())
// don't use servemux here because gorestful servemuxes get messed up when removing webservices
// TODO fix gorestful, remove TPRs, or stop using gorestful
d.goRestfulContainer.Dispatch(w, req)
return
}
}
}
// 找不到匹配的,那就走nonGoRestfulMux
klog.V(5).Infof("%v: %v %q satisfied by nonGoRestful", d.name, req.Method, path)
d.nonGoRestfulMux.ServeHTTP(w, req)
}
简单来说,大概流程是:
go
请求进来
↓
如果匹配某个 WebService RootPath
↓
特殊处理 "/apis" 精确匹配
或
匹配路径前缀 + 后面是/ 或路径结束
→ go-restful 处理 (Dispatch)
↓
否则
→ nonGoRestfulMux 处理(交给普通 ServeMux)
然后我们可以来到**config.ApiExtensions.New(genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))**这个函数里面,可以看到,函数里面是这样执行的:
go
// New returns a new instance of CustomResourceDefinitions from the given config.
func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*CustomResourceDefinitions, error) {
genericServer, err := c.GenericConfig.New("apiextensions-apiserver", delegationTarget)
if err != nil {
return nil, err
}
...
// 返回的genericServer被嵌入到里面去了
s := &CustomResourceDefinitions{
GenericAPIServer: genericServer,
}
// 生成准备安装路由用的apiGroupInfo
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiextensions.GroupName, Scheme, metav1.ParameterCodec, Codecs)
...
// 安装路由,这里安装路由是安装到crd的crud路由到goRestfulContainer里面的,后面详细说InstallAPIGroup的实现
if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
return nil, err
}
...
// 这里的Informers会在后续多个驱动用到,当crd保存到etcd时候,Informers会监听然后通知多个controller处理
s.Informers = externalinformers.NewSharedInformerFactory(crdClient, 5*time.Minute)
...
// crd存储到etcd后,对应cr的handler就是下面,后面会详细解析
crdHandler, err := NewCustomResourceDefinitionHandler(
versionDiscoveryHandler,
groupDiscoveryHandler,
s.Informers.Apiextensions().V1().CustomResourceDefinitions(),
delegateHandler,
c.ExtraConfig.CRDRESTOptionsGetter,
c.GenericConfig.AdmissionControl,
establishingController,
c.ExtraConfig.ServiceResolver,
c.ExtraConfig.AuthResolverWrapper,
c.ExtraConfig.MasterCount,
s.GenericAPIServer.Authorizer,
c.GenericConfig.RequestTimeout,
time.Duration(c.GenericConfig.MinRequestTimeout)*time.Second,
apiGroupInfo.StaticOpenAPISpec,
c.GenericConfig.MaxRequestBodyBytes,
)
...
// 把cr的handler注册到NonGoRestfulMux上。
s.GenericAPIServer.Handler.NonGoRestfulMux.Handle("/apis", crdHandler)
s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/apis/", crdHandler)
s.GenericAPIServer.RegisterDestroyFunc(crdHandler.destroy)
...
}
上面是apiExtensionsServer(crd)的初始化的主流程,那么我们可以提出两个问题:
- 当kubectl apply -f crd.yaml创建一个crd对象时候,摸的是哪个api,这个api是怎么注册进去的,api的处理流程是啥
- 当kubectl apply -f cr.yaml创建一个cr对象的时候,摸的是哪个api,这个api是怎么注册进去的,api的处理流程是啥
首先回答第一个问题,当我们执行kubectl get crd 的时候,访问api是/apis/apiextensions.k8s.io/v1/customresourcedefinitions,而该api关键在于**GenericAPIServer.InstallAPIGroup(&apiGroupInfo)**这个方法中的InstallAPIGroup函数:
go
// InstallAPIGroup->InstallAPIGroups->installAPIResources
func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo, typeConverter managedfields.TypeConverter) error {
var resourceInfos []*storageversion.ResourceInfo
for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
...
//提取出对应的版本api和group信息,生成apiGroupVersion结构体
apiGroupVersion, err := s.getAPIGroupVersion(apiGroupInfo, groupVersion, apiPrefix)
if err != nil {
return err
}
...
// 注册路由关键函数
discoveryAPIResources, r, err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer)
...}
// 我们首先来看看APIGroupVersion这个结构体
// 这是为了将多个 rest.Storage 暴露成 HTTP 路由(Handler)的工具。
// 每个 APIGroupVersion 实例对应一个具体的组版本,比如 "apps/v1",里面包含了该版本下所有资源的路由信息和逻辑控制。
type APIGroupVersion struct {
// Storage是核心字段:资源名 → 资源的 handler 实现
// key 是资源名,比如 "deployments"、"replicasets"
// value 是 rest.Storage 接口,定义了如 Create、Get、List、Update、Delete 的实现
// 例如当你访问 /apis/apps/v1/deployments,就从这里找到 "deployments" 对应的 Storage,并调用其方法处理请求
Storage map[string]rest.Storage
...
}
func (g *APIGroupVersion) InstallREST(container *restful.Container) ([]apidiscoveryv2.APIResourceDiscovery, []*storageversion.ResourceInfo, error) {
...
// 生成结构体为了后续Install
installer := &APIInstaller{
group: g,
prefix: prefix,
minRequestTimeout: g.MinRequestTimeout,
}
apiResources, resourceInfos, ws, registrationErrors := installer.Install()
...
// 最终注册对应的ws到GoRestfulContainer中
container.Add(ws)
...
}
func (a *APIInstaller) Install() ([]metav1.APIResource, []*storageversion.ResourceInfo, *restful.WebService, []error) {
...
//生成go-restful的ws
ws := a.newWebService()
paths := make([]string, len(a.group.Storage))
var i int = 0
// 生成对应gvr的path列表
for path := range a.group.Storage {
paths[i] = path
i++
}
sort.Strings(paths)
for _, path := range paths {
// ws绑定对应gvr,如pod等
apiResource, resourceInfo, err := a.registerResourceHandlers(path, a.group.Storage[path], ws)
...
}
而关键函数registerResourceHandlers就是最终把对应gvr的urlpath和web action绑定到对应的etcd storage的逻辑,这里面代码特别长,直接贴一下别人总结的伪代码:
go
// 从 path 中解析从资源名和子资源名
// 例如: path 为 deployments/status,则:resource=deployments,subresource=status
resource, subresource, err := splitSubresource(path)
if err != nil {
return nil, nil, err
}
// 判断是否支持 Create 方法
creater, isCreater := storage.(rest.Creater)
...
getter, isGetter := storage.(rest.Getter) // getter 保存了 RESTStorage的Get方法
// 判断是否是命名空间级别
namespaceScoped = scoper.NamespaceScoped()
// 如果是 NamespaceScoped
...
namespaceParamName := "namespaces"
// Handler for standard REST verbs (GET, PUT, POST and DELETE).
namespaceParam := ws.PathParameter("namespace", "object name and auth scope, such as for teams and projects").DataType("string")
namespacedPath := namespaceParamName + "/{namespace}/" + resource
namespaceParams := []*restful.Parameter{namespaceParam}
resourcePath := namespacedPath
resourceParams := namespaceParams
itemPath := namespacedPath + "/{name}"
nameParams := append(namespaceParams, nameParam)
proxyParams := append(nameParams, pathParam)
itemPathSuffix := ""
if isSubresource {
itemPathSuffix = "/" + subresource
itemPath = itemPath + itemPathSuffix
resourcePath = itemPath // namespaces/{namespace}/deployments/{name}
resourceParams = nameParams
}
...
// 构建 actions 数组。这里会根据 RESTStorage 实现的方法,判断支持哪个HTTP方法。
actions = appendIf(actions, action{"LIST", resourcePath, resourceParams, namer, false}, isLister)
actions = appendIf(actions, action{"POST", resourcePath, resourceParams, namer, false}, isCreater)
...
for _, action := range actions {
...
switch action.Verb {
...
// 根据 action 的 Verb值,调用 WebService的相应方法添加具体的路由
case "GET": // Get a resource.
var handler restful.RouteFunction
if isGetterWithOptions {
handler = restfulGetResourceWithOptions(getterWithOptions, reqScope, isSubresource)
} else {
// 会在 handler 中调用 getter 方法
handler = restfulGetResource(getter, reqScope)
}
route := ws.GET(action.Path).To(handler).
Doc(doc).
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).")).
Operation("read"+namespaced+kind+strings.Title(subresource)+operationSuffix).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
Returns(http.StatusOK, "OK", producedObject).
Writes(producedObject)
}
}
然后我们回到第一个问题:当kubectl apply -f crd.yaml创建一个crd对象时候,摸的是哪个api,这个api是怎么注册进去的,api的处理流程是啥
当我们执行kubectl get crd 的时候,访问api是/apis/apiextensions.k8s.io/v1/customresourcedefinitions,而这个函数在可以看到是前面提到的这段:
go
// 安装路由,这里安装路由是安装到crd的crud路由到goRestfulContainer里面的,后面详细说InstallAPIGroup的实现
if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
return nil, err
}
,把对应的handler安装进去,然后调用对应的customresourcedefinitions的getter方法(从etcd)读取数据,其他crud的web action也是类似操作对应的etcd。
然后我们来看第二个问题:当 kubectl apply -f cr.yaml创建一个cr对象的时候,摸的是哪个api,这个api是怎么注册进去的,api的处理流程是啥 。比如当我们执行kubectl get foos.example3.com的时候,调用的是接口是/apis/example3.com/v1/namespaces/default/foos,那么对应的handler在前面提到的crdHandler, err := NewCustomResourceDefinitionHandler生成,我们可以直接看看crdHandler对应的结构体的核心属性:
go
type crdHandler struct {
...
customStorageLock sync.Mutex
// 核心属性,存储所有 CRD 的 rest.Storage 映射(即实现 CRUD 的对象)
// 同时值得注意的是作者说明了,采用atomic.Value的性能比一般的RWMutex要好,适用于读多写少的情况。
customStorage atomic.Value
// 也是关键属性,负责在接到对应cr的接口调用后,查找对应的crd对象
crdLister listers.CustomResourceDefinitionLister
...
}
// 对应路由方法
func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
...
crdName := requestInfo.Resource + "." + requestInfo.APIGroup
// 如果crd已经被新建过,那么crdLister这个informer就可以查到对应的crd对象
crd, err := r.crdLister.Get(crdName)
if apierrors.IsNotFound(err) {
// 如果查找不到,就转交下一级delegate处理(有可能404)
r.delegate.ServeHTTP(w, req)
return
}
...
// getOrCreateServingInfoFor是关键函数,会在customStorage判断是否有存有对应的crd映射,没有则创建
crdInfo, err := r.getOrCreateServingInfoFor(crd.UID, crd.Name)
...
switch {
case subresource == "status" && subresources != nil && subresources.Status != nil:
handlerFunc = r.serveStatus(w, req, requestInfo, crdInfo, terminating, supportedTypes)
case subresource == "scale" && subresources != nil && subresources.Scale != nil:
handlerFunc = r.serveScale(w, req, requestInfo, crdInfo, terminating, supportedTypes)
case len(subresource) == 0:
// 对应资源的处理函数
handlerFunc = r.serveResource(w, req, requestInfo, crdInfo, crd, terminating, supportedTypes)
default:
responsewriters.ErrorNegotiated(
apierrors.NewNotFound(schema.GroupResource{Group: requestInfo.APIGroup, Resource: requestInfo.Resource}, requestInfo.Name),
Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
)
}
if handlerFunc != nil {
// handlerFunc->handler处理对应的请求
handler.ServeHTTP(w, req)
return
}
}
首先看到关键函数getOrCreateServingInfoFor,这边函数太长了,直接贴出对应关键逻辑代码解析:
go
//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go
// 这里可以看到对于crdStorageMap这种读多写少基于加锁和atomic.Value的优化实现:
// 尝试直接读取已有的 storage 缓存,如果已经构建过,就直接返回缓存的 crdInfo
storageMap := r.customStorage.Load().(crdStorageMap)
if ret, ok := storageMap[uid]; ok {
return ret, nil
}
// 如果不存在,但既然走过了crdLister.Get,说明该crd肯定是已经新建过的,所以直接在customStorage创建。这边需要创建的话,就需要加锁操作,使得多方并发写
// 完后store入atomic.Value不会导致版本冲突
r.customStorageLock.Lock()
defer r.customStorageLock.Unlock()
// 通过 crdLister 获取当前最新的 CRD 对象(由 informer 提供)
crd, err := r.crdLister.Get(name)
// 避免 race condition:别的 goroutine 可能在我们获取 CRD 的同时创建了 storage
storageMap = r.customStorage.Load().(crdStorageMap)
if ret, ok := storageMap[crd.UID]; ok {
return ret, nil
}
// 转为 internal 版本后用来构建:验证器等等
for _, v := range crd.Spec.Versions {
schema := ... → 转为内部格式 → 创建 structuralSchema → Prune defaults
structuralSchemas[v.Name] = s
}
// 对于每个 version,进行如下处理:
// 这是最核心的一步,最终构造出 CRUD handler
storages[v.Name] = customresource.NewStorage(...)
// 构造最终的 crdInfo 对象并存储
ret := &crdInfo{...}
// 复制一份出来,加入新的后store回去
storageMap2 := storageMap.clone()
storageMap2[crd.UID] = ret
r.customStorage.Store(storageMap2)
// 最终产物:crdInfo 包含以下内容:
type crdInfo struct {
spec *apiextensionsv1.CustomResourceDefinitionSpec
acceptedNames *apiextensionsv1.CustomResourceDefinitionNames
// 连接etcd
storages map[string]customresource.CustomResourceStorage
requestScopes map[string]*RequestScope
statusRequestScopes map[string]*RequestScope
scaleRequestScopes map[string]*RequestScope
...
}
我们接下来看第二个关键函数serveResource:
go
func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, crd *apiextensionsv1.CustomResourceDefinition, terminating bool, supportedTypes []string) http.HandlerFunc {
// 下面两个值都是getOrCreateServingInfoFor新建或者获取的
requestScope := crdInfo.requestScopes[requestInfo.APIVersion]
storage := crdInfo.storages[requestInfo.APIVersion].CustomResource
switch requestInfo.Verb {
case "get":
return handlers.GetResource(storage, requestScope)
...
}
注意了,这里的handlers.GetResource方法,其实在前面出现过。就是前面出现的registerResourceHandlers方法,最后注册的registerResourceHandlers中把对应gvr的urlpath和web action绑定到对应的etcd storage的逻辑也是注册了该方法的,这里贴一下方法的签名就大概知道了:
go
// GetResource返回一个可以从storage检索单个资源的函数。
func GetResource(r rest.Getter, scope *RequestScope) http.HandlerFunc {
return getResourceHandler(scope,
func(ctx context.Context, name string, req *http.Request) (runtime.Object, error) {
...
基本来说,crd和cr的apply我们已经很清楚了,这边可以再总结一下:
bash
kubectl apply -f crd.yaml
↓
etcd 存储了一个 apiextensions.k8s.io/v1 的 CRD 资源对象(路由注册函数为 s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); )
↓
CRD Handler 中的 customStorage 被更新(crdHandler的server方法,路由注册为s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/apis/", crdHandler),同时crdLister监听对应的crd新建事件)
↓
新的 CR 路由 `/apis/<group>/<version>/<resource>`( crdHandler的server动态处理)
那么其实k8s自身的资源api(pod,deployment等等)的注册也是基本如此,我们回头看CreateServerChain的主流程:
go
func CreateServerChain(config CompletedConfig) (*aggregatorapiserver.APIAggregator, error) {
...
kubeAPIServer, err := config.KubeAPIs.New(apiExtensionsServer.GenericAPIServer)
...
}
func (c CompletedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*Instance, error) {
...
// 根据已完成的 apiserver 配置 CompletedConfig,构建所有内置资源组的 RESTStorageProvider 列表。这些 provider 会在 API Server 启动时安装(install)到 HTTP 路由中。
restStorageProviders, err := c.StorageProviders(client)
if err != nil {
return nil, err
}
// 我们熟悉的注册路由函数,但值得注意的是,这里InstallAPIs函数调用的类型是我们之前没见过的RESTStorageProvider类型
if err := s.ControlPlane.InstallAPIs(restStorageProviders...); err != nil {
return nil, err
}
...
}
那么我们需要探究一下,RESTStorageProvider到达是啥类型:
go
// 可以看到, RESTStorageProvider是REST storage的工厂函数
type RESTStorageProvider interface {
GroupName() string
// 返回的就是熟悉的APIGroupInfo,就是我们之前注册路由InstallAPIGroup所需要的
NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, error)
}
// APIResourceConfigSource是判断哪个group和哪个vrsion是被启动的
type APIResourceConfigSource interface {
ResourceEnabled(resource schema.GroupVersionResource) bool
AnyResourceForGroupEnabled(group string) bool
ResourceExplicitlyEnabled(resource schema.GroupVersionResource) bool
VersionExplicitlyEnabled(version schema.GroupVersion) bool
}
type RESTOptionsGetter interface {
// resource:包含资源的 group 和 resource 名,如 apps/deployments
// example:一个该资源的示例对象,用于推断版本信息(可为 nil)
// If the example object is nil, the storage version will be determined by the resource's default storage version.
GetRESTOptions(resource schema.GroupResource, example runtime.Object) (RESTOptions, error)
}
这么说可能有点模糊,我们找个具体实现的结构体来看看:legacyProvider怎么实现这2个接口的,首先是NewRESTStorage方法,但方法有点长,我们只关注pod和pod的子资源方面
go
func (p *legacyProvider) NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, error) {
// 调用通用配置中的 NewRESTStorage 方法,得到已经初始化的 apiGroupInfo,里面可能已经包含了一些资源(例如 secrets、configmaps)的 storage,后续可以在此基础上扩展或覆盖。
apiGroupInfo, err := p.GenericConfig.NewRESTStorage(apiResourceConfigSource, restOptionsGetter)
// Pod 的所有子资源 handler 都在这里构造了
podStorage, err := podstore.NewStorage(
restOptionsGetter,
nodeStorage.KubeletConnectionInfo,
p.Proxy.Transport,
podDisruptionClient,
)
...
// 每种资源都被逐个注册进 storage(是一个 map[string]rest.Storage),其中包含:
// 主资源,例如 "pods"
// 子资源,例如 "pods/status", "pods/log" 等
// 子资源是在 HTTP 路由上加了子路径的资源,例如:
// POST /api/v1/namespaces/default/pods/mypod/exec
// 会交给 "pods/exec" 的 storage 处理。
if resource := "pods"; apiResourceConfigSource.ResourceEnabled(...) {
storage[resource] = podStorage.Pod
storage[resource+"/attach"] = podStorage.Attach
storage[resource+"/log"] = podStorage.Log
...
if len(storage) > 0 {
apiGroupInfo.VersionedResourcesStorageMap["v1"] = storage
}
return apiGroupInfo, nil
}
func (p *legacyProvider) GroupName() string {
// 默认核心组GroupName为空字符串
return api.GroupName
}
可以看到,通过Provider生成APIGroupInfo大概流程就是这样,现在我们可以看看 StorageProviders这个函数了:
go
func (c CompletedConfig) StorageProviders(client *kubernetes.Clientset) ([]controlplaneapiserver.RESTStorageProvider, error) {
// 在这里初始化legacyRESTStorageProvider
legacyRESTStorageProvider, err := corerest.New(corerest.Config{
...
})
...
return []controlplaneapiserver.RESTStorageProvider{
legacyRESTStorageProvider,
...
}, nil
}
// 如果对应ReststorageProviders启用了,InstallApis将安装API。
func (s *Server) InstallAPIs(restStorageProviders ...RESTStorageProvider) error {
nonLegacy := []*genericapiserver.APIGroupInfo{}
...
for _, restStorageBuilder := range restStorageProviders {
groupName := restStorageBuilder.GroupName()
//就是这里!
apiGroupInfo, err := restStorageBuilder.NewRESTStorage(s.APIResourceConfigSource, s.RESTOptionsGetter)
// 加到对应切片
nonLegacy = append(nonLegacy, &apiGroupInfo)
}
// 我们熟悉的注册路由函数!
if err := s.GenericAPIServer.InstallAPIGroups(nonLegacy...); err != nil {
return fmt.Errorf("error in registering group versions: %w", err)
}
...
}
最后的CreateAggregatorServer不是重点,这边我们总结一下流程:
- 创建 aggregator server 实例
- 初始化 auto-registration controller
- 收集待注册的 APIService 列表
- 如果启用了 CRD 支持,创建 CRD 注册控制器
- 设置 API 优先级(用于 discovery API 排序)
- 添加 PostStart Hook(服务启动后的钩子函数)
- 添加健康检查项
PrepareRun
前面注册路由等操作执行完后,这边就可以准备run起来了,这边还有个k8s的PrepareRun过程,这边也不是重点了,主要是openAPIConfig配置相关和installHealthz和installLivez相关的,返回的preparedAPIAggregator执行下面的Run函数,实际执行的结构体,是preparedGenericAPIServer来执行RunWithContext函数,这函数注释挺多的,对应文件位置是staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go
该函数:
- 启动 HTTP(S) 服务;
- 注册各种生命周期信号(LifecycleSignals);
- 执行优雅终止机制:确保停止接收新请求、等待当前请求处理完成、关闭服务监听器、关闭审计系统等;
- 直到 ctx 被取消或监听失败才返回。
这边附上注释的流程图:
go
// This is the diagram of what contexts/channels/signals are dependent on each other:
//
// | ctx
// | |
// | ---------------------------------------------------------
// | | |
// | ShutdownInitiated (shutdownInitiatedCh) |
// | | |
// | (ShutdownDelayDuration) (PreShutdownHooks)
// | | |
// | AfterShutdownDelayDuration (delayedStopCh) PreShutdownHooksStopped (preShutdownHooksHasStoppedCh)
// | | |
// | |-------------------------------------------------------|
// | |
// | |
// | NotAcceptingNewRequest (notAcceptingNewRequestCh)
// | |
// | |
// | |----------------------------------------------------------------------------------|
// | | | | |
// | [without [with | |
// | ShutdownSendRetryAfter] ShutdownSendRetryAfter] | |
// | | | | |
// | | ---------------| |
// | | | |
// | | |----------------|-----------------------| |
// | | | | |
// | | (NonLongRunningRequestWaitGroup::Wait) (WatchRequestWaitGroup::Wait) |
// | | | | |
// | | |------------------|---------------------| |
// | | | |
// | | InFlightRequestsDrained (drainedCh) |
// | | | |
// | |-------------------|---------------------|----------------------------------------|
// | | |
// | stopHttpServerCtx (AuditBackend::Shutdown())
// | |
// | listenerStoppedCh
// | |
// | HTTPServerStoppedListening (httpServerStoppedListeningCh)
也附上一个自己总结的流程图:
scss
ctx.Done() => stopCh 触发
└─> shutdownInitiatedCh.Signal() (关闭 readyz)
└─> Sleep(ShutdownDelayDuration)
└─> delayedStopCh.Signal()
└─> 等待 PreShutdownHooks
└─> notAcceptingNewRequestCh.Signal()
├─> NonLongRunningRequestWaitGroup.Wait()
├─> WatchRequestWaitGroup.Wait()
└─> drainedCh.Signal()
└─> shutdown http server
└─> listenerStoppedCh
└─> stoppedCh