前言:那个让我看了三遍源码才理顺的启动流程
线上集群有次升级 K8s 版本,APIServer 启动后 readiness 探针整整 30 秒才返回 200,期间 kubelet、controller-manager 全部连不上,节点状态飘成一片红。
我以为是配置问题,查了半天 systemd unit、网络、证书,都没问题。直到打开 APIServer 日志,看到一长串 PostStartHook 注册和执行的输出,我才意识到:APIServer 的"启动"远不止 ListenAndServe 那一行——它在 Run() 和真正提供服务之间还干了一大堆事情。
那次故障让我下决心啃一遍源码。看完发现,APIServer 启动的核心其实就 3 件事:
- 构建 三个 server 的委托链(apiExtensionsServer → kubeAPIServer → aggregatorServer)
- 调用
GenericAPIServer.New给每个 server 织好 handler / hook / 健康检查 - 调用
PrepareRun→NonBlockingRun启动 HTTPS server
听起来简单,但每一步都有坑。这篇就把这条链路逐行讲清楚。
本节重点
- 通用的
GenericApiServer.New函数 kube-apiserver核心服务的初始化- 最终的 APIServer 启动流程
一、三个 server 的"委托链"是什么?
APIServer 不是一个 server,而是三个 server 串成一条委托链。每个 server 处理自己负责的 URL 路径,处理不了的就委托给下一个。
┌──────────────────────────────────────────────┐
│ HTTPS 请求 (6443) │
└─────────────────┬────────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ aggregatorServer (聚合层 / APIService) │
│ - metrics-server, custom.metrics.k8s.io... │
│ - 处理不了?delegate ↓ │
└─────────────────┬────────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ kubeAPIServer (核心) │
│ - Pod / Deployment / Service / Node ... │
│ - 处理不了?delegate ↓ │
└─────────────────┬────────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ apiExtensionsServer (CRD) │
│ - 用户自定义资源 (Custom Resources) │
│ - 处理不了?delegate ↓ │
└─────────────────┬────────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ NotFoundHandler (404) │
└──────────────────────────────────────────────┘
1.1 委托链的代码
位置:cmd/kube-apiserver/app/server.go 的 CreateServerChain 函数。
// 1. apiExtensionsServer (CRD)
apiExtensionsServer, err := createAPIExtensionsServer(
apiExtensionsConfig,
genericapiserver.NewEmptyDelegate(), // 链尾,委托给空 handler (404)
)
// 2. kubeAPIServer (核心资源)
kubeAPIServer, err := CreateKubeAPIServer(
kubeAPIServerConfig,
apiExtensionsServer.GenericAPIServer, // 委托给 apiExtensions
)
// 3. aggregatorServer (聚合 API)
aggregatorServer, err := createAggregatorServer(
aggregatorConfig,
kubeAPIServer.GenericAPIServer, // 委托给 kubeAPIServer
apiExtensionsServer.Informers,
)
💡 为什么是这个顺序? 这是面试常考点。
aggregatorServer在最外层,因为聚合 API(如 metrics-server)是用户通过 APIService 注册的扩展,必须最先匹配;中间是kubeAPIServer处理 K8s 内置资源;最内层是apiExtensionsServer处理 CRD。这个顺序保证了用户扩展 > 内置资源 > 自定义资源 的优先级。
1.2 三个 server 共享同一个 New:GenericConfig.New
三个 server 的 New 函数里都会调用同一个底层方法 c.GenericConfig.New(...):
// kubeAPIServer 的初始化
s, err := c.GenericConfig.New("kube-apiserver", delegationTarget)
// apiExtensionsServer 的初始化
genericServer, err := c.GenericConfig.New("apiextensions-apiserver", delegationTarget)
// aggregatorServer 的初始化
genericServer, err := c.GenericConfig.New("kube-aggregator", delegationTarget)
name 参数仅用于日志区分,三个 server 共用同一份 GenericConfig、同一套 hook 框架、同一个 HTTPS 监听器——只是各自的资源路由不同。
🤔 这种设计的好处:用一个
GenericAPIServer抽象,统一管理认证、授权、审计、限流等公共逻辑。Kubernetes 自己用,CRD 扩展也用,aggregator 也用——复用最大化。这也是为什么自己写 controller / aggregated API 时,可以直接 importk8s.io/apiserver复用所有能力。
二、GenericAPIServer.New:源码逐行解析
位置:staging/src/k8s.io/apiserver/pkg/server/config.go
这个函数大约 200 行,干了 5 件大事:构建 handler 链 → 实例化 server → 注册 hook → 注册健康检查 → 安装通用路由。
2.1 构建 HTTP handler 链
handlerChainBuilder := func(handler http.Handler) http.Handler {
return c.BuildHandlerChainFunc(handler, c.Config)
}
apiServerHandler := NewAPIServerHandler(
name,
c.Serializer,
handlerChainBuilder,
delegationTarget.UnprotectedHandler(), // 委托给下一个 server
)
💡
BuildHandlerChainFunc是 APIServer 的核心:它会层层包装出认证、授权、审计、限流、超时、Panic 恢复等中间件,最终把所有 HTTP 请求都经过这些过滤器。这条链路是 APIServer 安全性和可观测性的关键,下一节会专门拆。
2.2 实例化 GenericAPIServer
s := &GenericAPIServer{
discoveryAddresses: c.DiscoveryAddresses,
LoopbackClientConfig: c.LoopbackClientConfig,
legacyAPIGroupPrefixes: c.LegacyAPIGroupPrefixes,
admissionControl: c.AdmissionControl,
Serializer: c.Serializer,
AuditBackend: c.AuditBackend,
Authorizer: c.Authorization.Authorizer,
delegationTarget: delegationTarget, // 委托链下一环
EquivalentResourceRegistry: c.EquivalentResourceRegistry,
HandlerChainWaitGroup: c.HandlerChainWaitGroup,
minRequestTimeout: time.Duration(c.MinRequestTimeout) * time.Second,
ShutdownTimeout: c.RequestTimeout,
ShutdownDelayDuration: c.ShutdownDelayDuration,
SecureServingInfo: c.SecureServing,
ExternalAddress: c.ExternalAddress,
Handler: apiServerHandler,
listedPathProvider: apiServerHandler,
openAPIConfig: c.OpenAPIConfig,
skipOpenAPIInstallation: c.SkipOpenAPIInstallation,
postStartHooks: map[string]postStartHookEntry{},
preShutdownHooks: map[string]preShutdownHookEntry{},
disabledPostStartHooks: c.DisabledPostStartHooks,
healthzChecks: c.HealthzChecks,
livezChecks: c.LivezChecks,
readyzChecks: c.ReadyzChecks,
livezGracePeriod: c.LivezGracePeriod,
DiscoveryGroupManager: discovery.NewRootAPIsHandler(c.DiscoveryAddresses, c.Serializer),
maxRequestBodyBytes: c.MaxRequestBodyBytes,
livezClock: clock.RealClock{},
lifecycleSignals: c.lifecycleSignals,
ShutdownSendRetryAfter: c.ShutdownSendRetryAfter,
APIServerID: c.APIServerID,
StorageVersionManager: c.StorageVersionManager,
Version: c.Version,
}
这段代码看着长,但其实就是把 config 里的字段一对一复制到 server 实例。重点关注几个字段:
| 字段 | 作用 |
|---|---|
delegationTarget | 委托链下一环,处理不了的请求扔给它 |
admissionControl | 准入控制器(mutating + validating webhook 在这) |
Authorizer | 授权器(RBAC / Node / Webhook 都接到这) |
lifecycleSignals | 生命周期信号(启动完成、shutdown 触发等) |
livezGracePeriod | livez 检查的容忍期,启动期内不算失败 |
2.3 注册 PostStartHook(启动后钩子)
这是整个启动流程最容易被忽略的部分——开头那个让我熬夜的 30 秒延迟,就是因为某个 hook 启动慢。
PostStartHook 注册分三个来源:
① 从委托的下游 server 继承:
for k, v := range delegationTarget.PostStartHooks() {
s.postStartHooks[k] = v
}
for k, v := range delegationTarget.PreShutdownHooks() {
s.preShutdownHooks[k] = v
}
这意味着 aggregatorServer 会继承 kubeAPIServer 和 apiExtensionsServer 的所有 hook——委托链合并 hook。
② 从 completedConfig 中预配置的 hook 注册:
// add poststarthooks that were preconfigured.
// Using the add method will give us an error if the same name has already been registered.
for name, preconfiguredPostStartHook := range c.PostStartHooks {
if err := s.AddPostStartHook(name, preconfiguredPostStartHook.hook); err != nil {
return nil, err
}
}
注意这里的重名检测——如果两个地方注册了同名 hook,会直接报错。这避免了 hook 被静默覆盖。
③ 经典示例:admission post-start hook
位置:cmd/kube-apiserver/app/server.go
if err := config.GenericConfig.AddPostStartHook(
"start-kube-apiserver-admission-initializer",
admissionPostStartHook,
); err != nil {
return nil, nil, nil, err
}
对应的 hook 实现(pkg/kubeapiserver/admission/config.go):
admissionPostStartHook := func(context genericapiserver.PostStartHookContext) error {
discoveryRESTMapper.Reset()
go utilwait.Until(discoveryRESTMapper.Reset, 30*time.Second, context.StopCh)
return nil
}
功能:APIServer 启动后立即刷新一次 RESTMapper(资源类型映射表),然后每 30 秒刷一次。这就是为什么 webhook 启动后能立即识别新注册的 CRD——不用重启 APIServer,靠这个 hook 自动同步。
2.4 关键 hook 速览
① Informer 启动 hook
genericApiServerHookName := "generic-apiserver-start-informers"
if c.SharedInformerFactory != nil {
if !s.isPostStartHookRegistered(genericApiServerHookName) {
err := s.AddPostStartHook(genericApiServerHookName, func(context PostStartHookContext) error {
c.SharedInformerFactory.Start(context.StopCh)
return nil
})
if err != nil {
return nil, err
}
}
err := s.AddReadyzChecks(healthz.NewInformerSyncHealthz(c.SharedInformerFactory))
if err != nil {
return nil, err
}
}
🚨 30 秒卡顿的真相:
SharedInformerFactory.Start后,K8s 需要等所有 informer 完成首次 list+watch 同步才会让 readyz 返回 200。集群里 CRD/资源越多,首次 list 越慢。我那次故障就是因为加了几百个 CRD,informer 同步花了 30 秒。排查方法:grep apiserver 日志里
caches synced,能看到每个 informer 同步耗时。
② 限流相关 hook (APF: API Priority and Fairness)
const priorityAndFairnessConfigConsumerHookName = "priority-and-fairness-config-consumer"
if s.isPostStartHookRegistered(priorityAndFairnessConfigConsumerHookName) {
// 已注册,跳过
} else if c.FlowControl != nil {
err := s.AddPostStartHook(priorityAndFairnessConfigConsumerHookName, func(context PostStartHookContext) error {
go c.FlowControl.MaintainObservations(context.StopCh)
go c.FlowControl.Run(context.StopCh)
return nil
})
// ...
} else {
klog.V(3).Infof("Not requested to run hook %s", priorityAndFairnessConfigConsumerHookName)
}
// 维护限流水位的 hook
if c.FlowControl != nil {
// APF 启用 → 用 APF 限流
const priorityAndFairnessFilterHookName = "priority-and-fairness-filter"
if !s.isPostStartHookRegistered(priorityAndFairnessFilterHookName) {
err := s.AddPostStartHook(priorityAndFairnessFilterHookName, func(context PostStartHookContext) error {
genericfilters.StartPriorityAndFairnessWatermarkMaintenance(context.StopCh)
return nil
})
// ...
}
} else {
// 否则 fallback 到 max-in-flight 限流
const maxInFlightFilterHookName = "max-in-flight-filter"
if !s.isPostStartHookRegistered(maxInFlightFilterHookName) {
err := s.AddPostStartHook(maxInFlightFilterHookName, func(context PostStartHookContext) error {
genericfilters.StartMaxInFlightWatermarkMaintenance(context.StopCh)
return nil
})
// ...
}
}
💡 APF 和 max-in-flight 是二选一:开了 APF(feature gate
APIPriorityAndFairness=true),就用 APF 做限流;否则 fallback 到老的 max-in-flight 限流。代码里用if-else优雅地处理了这个切换。这部分逻辑很深,下一节有专门的"APIServer 限流"章节,这里先有个印象就行。
2.5 添加健康检查
for _, delegateCheck := range delegationTarget.HealthzChecks() {
skip := false
for _, existingCheck := range c.HealthzChecks {
if existingCheck.Name() == delegateCheck.Name() {
skip = true
break
}
}
if skip {
continue
}
s.AddHealthChecks(delegateCheck)
}
逻辑:从委托的下游 server 继承健康检查项,但同名的不重复添加。
AddHealthChecks 的实现有个细节(staging/src/k8s.io/apiserver/pkg/server/healthz.go):
// AddHealthChecks adds HealthCheck(s) to health endpoints (healthz, livez, readyz) but
// configures the liveness grace period to be zero, which means we expect this health check
// to immediately indicate that the apiserver is unhealthy.
func (s *GenericAPIServer) AddHealthChecks(checks ...healthz.HealthChecker) error {
// we opt for a delay of zero here, because this entrypoint adds generic health checks
// and not health checks which are specifically related to kube-apiserver boot-sequences.
return s.addHealthChecks(0, checks...)
}
💡
gracePeriod=0是什么意思? 委托下游传过来的健康检查,不给容忍期——一旦失败立即标记不健康。这是因为这些 check 是"通用检查",不涉及 APIServer 启动序列里那些"必须等待"的逻辑(比如 informer 同步),所以可以严格判断。
2.6 安装通用路由 installAPI
最后给 server 装上几个与业务资源无关的通用路由:
// 1. / 和 /index.html
if c.EnableIndex {
routes.Index{}.Install(s.listedPathProvider, s.Handler.NonGoRestfulMux)
}
// 2. /debug/pprof 性能分析
if c.EnableProfiling {
routes.Profiling{}.Install(s.Handler.NonGoRestfulMux)
if c.EnableContentionProfiling {
goruntime.SetBlockProfileRate(1)
}
routes.DebugFlags{}.Install(s.Handler.NonGoRestfulMux, "v", routes.StringFlagPutHandler(logs.GlogSetter))
}
// 3. /metrics Prometheus 指标
if c.EnableMetrics {
if c.EnableProfiling {
routes.MetricsWithReset{}.Install(s.Handler.NonGoRestfulMux)
} else {
routes.DefaultMetrics{}.Install(s.Handler.NonGoRestfulMux)
}
}
// 4. /version 版本信息
routes.Version{Version: c.Version}.Install(s.Handler.GoRestfulContainer)
// 5. 服务发现 /apis 等
if c.EnableDiscovery {
s.Handler.GoRestfulContainer.Add(s.DiscoveryGroupManager.WebService())
}
🚨 生产环境强烈建议关掉
/debug/pprof!这个端点暴露后,任何能访问 APIServer 的人都能拉取堆栈、profile,严重影响性能甚至泄露敏感信息。如果非要开(debug 阶段),务必加 RBAC 限制。
三、kubeAPIServer 核心服务的初始化
GenericAPIServer.New 是通用框架,但 kubeAPIServer 在它的基础上还要做一件最重要的事:注册所有 K8s 内置资源的 REST API(Pod、Deployment、Service 等)。
位置:pkg/controlplane/instance.go
3.1 用通用 server 包一层 Instance
func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*Instance, error) {
// 1. 先用通用方法生成 GenericAPIServer
s, err := c.GenericConfig.New("kube-apiserver", delegationTarget)
// ...
// 2. 用 GenericAPIServer 实例化 Instance(master 实例)
m := &Instance{
GenericAPIServer: s,
ClusterAuthenticationInfo: c.ExtraConfig.ClusterAuthenticationInfo,
}
// ...
}
Instance 就是 K8s 源码里我们常说的 “master 实例”——它在 GenericAPIServer 之上包了一层,加了 K8s 内置资源相关的元数据。
3.2 注册核心资源的 API(Legacy API: /api/v1)
if c.ExtraConfig.APIResourceConfigSource.VersionEnabled(apiv1.SchemeGroupVersion) {
legacyRESTStorageProvider := corerest.LegacyRESTStorageProvider{
StorageFactory: c.ExtraConfig.StorageFactory,
ProxyTransport: c.ExtraConfig.ProxyTransport,
KubeletClientConfig: c.ExtraConfig.KubeletClientConfig,
EventTTL: c.ExtraConfig.EventTTL,
ServiceIPRange: c.ExtraConfig.ServiceIPRange,
SecondaryServiceIPRange: c.ExtraConfig.SecondaryServiceIPRange,
ServiceNodePortRange: c.ExtraConfig.ServiceNodePortRange,
LoopbackClientConfig: c.GenericConfig.LoopbackClientConfig,
ServiceAccountIssuer: c.ExtraConfig.ServiceAccountIssuer,
ExtendExpiration: c.ExtraConfig.ExtendExpiration,
ServiceAccountMaxExpiration: c.ExtraConfig.ServiceAccountMaxExpiration,
APIAudiences: c.GenericConfig.Authentication.APIAudiences,
}
if err := m.InstallLegacyAPI(&c, c.GenericConfig.RESTOptionsGetter, legacyRESTStorageProvider); err != nil {
return nil, err
}
}
💡 什么是 Legacy API?
/api/v1/...这些路径(Pod、Service、Node、Namespace 等核心资源)是 K8s 最早设计的 API,没有 group 前缀。后来加的资源都有 group(如/apis/apps/v1/deployments)。出于历史兼容性,core group 一直保留/api/v1这个特殊路径,被称为 “Legacy API”。
LegacyRESTStorageProvider 这个结构体里塞了一堆和 Pod/Service 紧密相关的配置:
KubeletClientConfig:APIServer 通过它和 kubelet 通信(exec/log/portforward)ServiceIPRange:Service 的 ClusterIP 池ServiceAccountIssuer:用来签发 ServiceAccount Token
3.3 注册其他 API Group
if err := m.InstallAPIs(
c.ExtraConfig.APIResourceConfigSource,
c.GenericConfig.RESTOptionsGetter,
restStorageProviders...,
); err != nil {
return nil, err
}
restStorageProviders 是个列表,里面有 apps、batch、networking、storage、policy、authentication 等所有非 core group 的 storage provider。每个 provider 负责自己 group 下的资源注册。
🤔 想自己看看都注册了哪些 group? 起一个 K8s 集群,跑
kubectl api-resources -o wide,输出里APIGROUP列就是所有 group。这些 group 的 storage provider 都是在InstallAPIs里被加载的。
下一节会专门讲 scheme 和 RESTStorage 的初始化——这块涉及 K8s 的资源序列化和持久化层,是另一个深坑。
四、最终的 APIServer 启动流程
绕了这么大一圈,终于回到顶层入口。
位置:cmd/kube-apiserver/app/server.go
4.1 Run 函数
// Run runs the specified APIServer. This should never exit.
func Run(completeOptions completedServerRunOptions, stopCh <-chan struct{}) error {
// To help debugging, immediately log version
klog.Infof("Version: %+v", version.Get())
// 1. 创建三个 server 的委托链
server, err := CreateServerChain(completeOptions, stopCh)
if err != nil {
return err
}
// 2. PrepareRun:执行所有准备工作(OpenAPI 注册等)
prepared, err := server.PrepareRun()
if err != nil {
return err
}
// 3. Run:阻塞运行(直到 stopCh 关闭)
return prepared.Run(stopCh)
}
三步:Create → Prepare → Run。我之前 30 秒卡顿就发生在 Prepare 和 Run 之间——所有 hook 在这里执行。
4.2 preparedGenericAPIServer.Run
位置:staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go
stoppedCh, listenerStoppedCh, err := s.NonBlockingRun(stopHttpServerCh, shutdownTimeout)
注意函数名 NonBlockingRun——HTTP server 在 goroutine 里跑,调用者(也就是 Run)会通过等待 stopCh 来阻塞。
4.3 NonBlockingRun:真正起 HTTPS
if s.SecureServingInfo != nil && s.Handler != nil {
var err error
stoppedCh, listenerStoppedCh, err = s.SecureServingInfo.ServeWithListenerStopped(
s.Handler,
shutdownTimeout,
internalStopCh,
)
if err != nil {
close(internalStopCh)
close(auditStopCh)
return nil, nil, err
}
}
最终落到 ServeWithListenerStopped,位置 staging/src/k8s.io/apiserver/pkg/server/secure_serving.go:
// ServeWithListenerStopped runs the secure http server.
// It fails only if certificates cannot be loaded or the initial listen call fails.
// The actual server loop (stoppable by closing stopCh) runs in a go routine,
// i.e. ServeWithListenerStopped does not block.
// It returns a stoppedCh that is closed when all non-hijacked active requests have been processed.
// It returns a listenerStoppedCh that is closed when the underlying http Server has stopped listening.
func (s *SecureServingInfo) ServeWithListenerStopped(
handler http.Handler,
shutdownTimeout time.Duration,
stopCh <-chan struct{},
) (<-chan struct{}, <-chan struct{}, error) {
// ... 加载 TLS 证书、起 net.Listener、起 http.Server
}
这里两个 channel 设计很精妙:
stoppedCh:所有未 hijack 的活跃请求都处理完后才关闭(优雅退出)listenerStoppedCh:底层 HTTP server 停止监听时关闭
💡 为什么要两个 channel 区分? 因为 APIServer 处理的请求有 watch 这种长连接。
listenerStoppedCh关闭后,新连接进不来;但已有的 watch 连接还在跑——必须等它们都收尾完,stoppedCh才关闭。负载均衡器先在listenerStoppedCh后摘流量,然后等stoppedCh关再杀进程,实现零丢请求滚动升级。
五、整体启动流程图
┌──────────────────────┐
│ main() / Run() │
└──────────┬───────────┘
▼
┌────────────────────────────────────┐
│ CreateServerChain() │
│ ┌──────────────────────────────┐ │
│ │ apiExtensionsServer (CRD) │ │
│ └─────────┬────────────────────┘ │
│ ▼ delegate │
│ ┌──────────────────────────────┐ │
│ │ kubeAPIServer (核心资源) │ │
│ └─────────┬────────────────────┘ │
│ ▼ delegate │
│ ┌──────────────────────────────┐ │
│ │ aggregatorServer (聚合) │ │
│ └──────────────────────────────┘ │
└──────────┬─────────────────────────┘
▼
┌────────────────────────────┐
│ 每个 server 都执行: │
│ GenericConfig.New(...) │
│ ├── 构建 handler 链 │
│ ├── 实例化 GenericAPIServer│
│ ├── 注册 PostStartHook │
│ ├── 注册健康检查 │
│ └── 安装通用路由 │
│ │
│ kubeAPIServer 额外执行: │
│ ├── InstallLegacyAPI │
│ └── InstallAPIs │
└──────────┬─────────────────┘
▼
┌────────────────────────────┐
│ server.PrepareRun() │
│ - 安装 OpenAPI 路由 │
│ - 注册 audit │
│ - 启动通用 hook │
└──────────┬─────────────────┘
▼
┌────────────────────────────┐
│ prepared.Run(stopCh) │
│ └─ NonBlockingRun() │
│ └─ ServeWithListener… │
│ 启动 6443 端口 │
└──────────┬─────────────────┘
▼
┌────────────────────────────┐
│ PostStartHooks 并发执行:│
│ - 启动 informer │
│ - admission 初始化 │
│ - APF / max-in-flight │
│ - bootstrap controller │
│ …… │
└──────────┬─────────────────┘
▼
┌────────────────────────────┐
│ readyz 返回 200 │
│ APIServer 真正可用 ✅ │
└────────────────────────────┘
六、启动性能排查清单
如果你也遇到了 APIServer 启动慢的问题,按这个顺序排查:
| # | 检查项 | 命令 / 方法 | 可能原因 |
|---|---|---|---|
| 1 | 看 PostStartHook 耗时 | grep apiserver 日志 "post-start hook .* finished" | 某个 hook 阻塞 |
| 2 | 看 Informer 同步耗时 | grep "caches synced" | CRD 太多 / etcd 慢 |
| 3 | 看 OpenAPI 注册耗时 | grep "OpenAPI .* AggregationController" | aggregated server 不可达 |
| 4 | 看 etcd 健康检查 | curl /healthz/etcd | etcd 连接慢 |
| 5 | 看准入控制器加载 | grep "admission" | webhook 不可达 |
| 6 | 看证书加载 | grep "Serving securely on" 出现时间 | 证书读取慢 |
🚨 生产实战经验:APIServer 启动慢 90% 的原因是 etcd 慢 或 某个 webhook 不可达。前者通过 etcd-defrag 和 SSD 解决;后者一定要把 webhook 的
failurePolicy设成Ignore,并且timeoutSeconds设小(5 秒以内),不然 APIServer 会被一个挂掉的 webhook 拖死。
七、源码阅读建议
如果你想自己跟一遍源码,推荐这个顺序:
- 入口:
cmd/kube-apiserver/app/server.go的Run→CreateServerChain - 通用 server:
staging/src/k8s.io/apiserver/pkg/server/config.go的Config.Complete().New() - 核心 server:
pkg/controlplane/instance.go的completedConfig.New - 资源注册:
pkg/controlplane/instance.go的InstallLegacyAPI/InstallAPIs - HTTPS 服务:
staging/src/k8s.io/apiserver/pkg/server/secure_serving.go
💡 小技巧:用 GoLand 打开 K8s 源码,从
Run函数开始 ctrl+B 跳定义。重点关注delegationTarget这个参数怎么一层层传——理解了它,整个委托链就通了。
八、本节小结
APIServer 启动的完整路径:
Run是入口,做了三件事:CreateServerChain→PrepareRun→RunCreateServerChain构建了三个 server 的委托链:apiExtensions → kubeAPIServer → aggregator- 每个 server 都通过
GenericConfig.New()完成通用初始化(handler 链、hook、健康检查、通用路由) kubeAPIServer额外执行InstallLegacyAPI和InstallAPIs注册所有 K8s 内置资源PrepareRun执行准备工作(OpenAPI 注册、generic hook 启动)NonBlockingRun→ServeWithListenerStopped启动 HTTPS 6443 端口- PostStartHook 在后台并发执行,全部完成且 informer 同步完后 readyz 才返回 200
整条链路最容易踩坑的地方:readyz 返回 200 ≠ APIServer 启动完成那一刻——NonBlockingRun 之后还有 hook 在跑。如果监控做得不细,会以为 APIServer "立即就绪"了,但实际上请求可能依然有 30+ 秒的失败率。
下一节会进入更深的话题:scheme 和 RESTStorage 是怎么把 Go struct 变成 /apis/apps/v1/deployments 这条 URL 的,那是 K8s 序列化和持久化的核心。
九、你踩过这些坑吗?
- 你们生产环境的 APIServer 启动到 readyz 返回 200 通常要多久?最长见过多少秒?
- 有没有遇到过某个 webhook 不可达导致 APIServer 启动卡住的情况?最后是怎么排查到根因的?
- 自定义 aggregated API server 时,有没有踩过委托链的坑?比如 PostStartHook 注册重名、URL 路径冲突?
欢迎在评论区分享你的排查经验。
十、延伸思考
- 如果让你设计一个 APIServer 的"启动时长 SLI",应该用哪些指标?
/healthzok 算就绪?还是/readyzok?还是/livez/poststarthook/xxx全 ok? aggregatorServer处于委托链最上层意味着什么?如果一个用户 APIService 的 webhook 卡死,会不会影响kubectl get pods这种核心请求?- K8s 的
PreShutdownHook在优雅退出里扮演什么角色?和NonBlockingRun返回的两个 channel 是怎么配合的?


223

被折叠的 条评论
为什么被折叠?



