深入kube-apiserver配置初始化:GenericConfig如何构建API服务基石

前言

上周帮同事排查apiserver启动问题,同样的配置文件,在测试环境能正常启动,到了生产环境却报错failed to create real external clientset: unable to load in-cluster configuration。排查了半天,最后发现是LoopbackClientConfigContentType设置问题——生产环境用了protobuf格式,但内部的informer客户端不支持。

这次经历让我意识到,kube-apiserver的初始化配置远比想象中复杂。在真正启动HTTP服务之前,它需要完成大量的准备工作:创建传输层、初始化etcd存储、构建clientset、设置健康检查等等。这些工作都在GenericConfig的构建过程中完成。

今天就带大家深入源码,看看CreateKubeAPIServerConfig是如何一步步准备好所有配置的。

GenericConfig:API服务的通用配置基座

在K8s中,apiserver不是单一的服务,而是包含KubeAPIServerAPIExtensionsServerAggregatorServer三个层次。这三个服务有很多共同的配置需求:

  • 都需要监听HTTPS端口
  • 都需要连接etcd存储
  • 都需要处理认证鉴权
  • 都需要提供健康检查

GenericConfig就是抽取这些通用配置形成的基座。它定义在genericapiserver包中,被所有具体的apiserver复用。

┌─────────────────────────────────────────────────────────────────┐
│                      GenericConfig                              │
├─────────────────────────────────────────────────────────────────┤
│  安全配置                                                        │
│  ├─ SecureServing          HTTPS服务配置                         │
│  ├─ Authentication         认证配置                              │
│  ├─ Authorization          鉴权配置                              │
│  └─ Audit                  审计配置                              │
├─────────────────────────────────────────────────────────────────┤
│  存储配置                                                        │
│  ├─ RESTOptionsGetter      REST存储选项                          │
│  ├─ StorageFactory         存储工厂                              │
│  └─ EtcdHealthCheck        etcd健康检查                          │
├─────────────────────────────────────────────────────────────────┤
│  通信配置                                                        │
│  ├─ LoopbackClientConfig   回环客户端配置                        │
│  ├─ ProxyTransport         代理传输层                            │
│  └─ DiscoveryAddresses     服务发现地址                          │
├─────────────────────────────────────────────────────────────────┤
│  生命周期                                                        │
│  ├─ HealthzChecks          健康检查列表                          │
│  ├─ ReadyzChecks           就绪检查列表                          │
│  └─ PostStartHooks         启动后钩子                            │
└─────────────────────────────────────────────────────────────────┘

配置构建完整流程

CreateKubeAPIServerConfig()
    │
    ├─ CreateProxyTransport()          创建代理传输层
    │
    ├─ buildGenericConfig()            构建通用配置(核心)
    │   │
    │   ├─ NewConfig()                 创建基础Config
    │   │
    │   ├─ SecureServing.ApplyTo()     应用HTTPS配置
    │   │
    │   ├─ Authentication.ApplyTo()    应用认证配置
    │   │
    │   ├─ Authorization.ApplyTo()     应用鉴权配置
    │   │
    │   ├─ Audit.ApplyTo()             应用审计配置
    │   │
    │   ├─ 初始化etcd存储              
    │   │   ├─ NewStorageFactoryConfig()
    │   │   ├─ Complete()
    │   │   ├─ New()                   创建StorageFactory
    │   │   └─ ApplyWithStorageFactoryTo()
    │   │       └─ addEtcdHealthEndpoint()  添加etcd健康检查
    │   │
    │   ├─ 设置内部通信                
    │   │   ├─ ContentType = protobuf
    │   │   └─ DisableCompression = true
    │   │
    │   └─ 创建clientset
    │       ├─ NewForConfig()
    │       └─ NewSharedInformerFactory()
    │
    └─ 返回所有配置对象

第一步:创建代理传输层(ProxyTransport)

为什么需要ProxyTransport?

apiserver的一个重要功能是代理请求到Pod,比如kubectl execkubectl logskubectl port-forward都需要apiserver作为代理。

用户 → kubectl exec → apiserver → kubelet → Pod
                ↑              ↑
           建立连接         转发数据

这个代理功能需要大量的HTTP连接,如果每次都新建TCP连接,性能会很差。所以apiserver创建了专门的ProxyTransport缓存长连接

源码实现

// cmd/kube-apiserver/app/server.go
func CreateKubeAPIServerConfig(s completedServerRunOptions) (*master.Config, ...) {
    // 创建代理传输层
    proxyTransport := CreateProxyTransport()
    
    // ... 后续配置
}

// 实际创建函数
func CreateProxyTransport() *http.Transport {
    return &http.Transport{
        // 使用TLS配置
        TLSClientConfig: &tls.Config{
            InsecureSkipVerify: true,  // 跳过证书验证(内部通信)
        },
        
        // 连接池配置
        DialContext: (&net.Dialer{
            Timeout:   30 * time.Second,
            KeepAlive: 30 * time.Second,
        }).DialContext,
        
        // 长连接配置
        MaxIdleConns:          100,              // 最大空闲连接数
        MaxIdleConnsPerHost:   100,              // 每个host最大空闲连接
        MaxConnsPerHost:       100,              // 每个host最大连接数
        IdleConnTimeout:       90 * time.Second, // 空闲连接超时
        TLSHandshakeTimeout:   10 * time.Second, // TLS握手超时
        ExpectContinueTimeout: 1 * time.Second,  // 100-continue超时
        
        // 启用HTTP/2
        ForceAttemptHTTP2: true,
    }
}

关键配置解析

配置项说明
MaxIdleConns100最大空闲连接数,控制总连接池大小
MaxIdleConnsPerHost100每个目标host的最大空闲连接,用于kubelet代理
IdleConnTimeout90s空闲连接回收时间
ForceAttemptHTTP2true优先使用HTTP/2,支持多路复用

设计亮点:apiserver与kubelet之间有大量短连接请求(如metrics获取),使用连接池可以避免频繁建立TCP连接,显著提升性能。

第二步:构建GenericConfig(核心)

buildGenericConfig是整个配置构建的核心函数,它负责组装GenericConfig的各个部分。

2.1 创建基础Config

// 创建基础Config,设置序列化器
genericConfig = genericapiserver.NewConfig(legacyscheme.Codecs)

// NewConfig的实现
func NewConfig(codecs serializer.CodecFactory) *Config {
    return &Config{
        Serializer:                codecs,
        ReadWritePort:             443,
        BuildHandlerChainFunc:     DefaultBuildHandlerChain,
        HandlerChainWaitGroup:     new(utilwaitgroup.SafeWaitGroup),
        // ... 其他默认值
    }
}

Codecs是什么?

Codecs是K8s的序列化/反序列化工具,支持多种格式:

// 支持的ContentType
- application/json                    // JSON格式,人类可读
- application/yaml                    // YAML格式,配置常用
- application/vnd.kubernetes.protobuf // protobuf格式,性能最好
- application/cbor                    // CBOR格式(较新)

2.2 应用HTTPS配置(SecureServing)

// 应用HTTPS服务配置
if lastErr = s.SecureServing.ApplyTo(&genericConfig.SecureServing, &genericConfig.LoopbackClientConfig); lastErr != nil {
    return
}

对应的命令行参数

// staging/src/k8s.io/apiserver/pkg/server/options/serving.go
func (s *SecureServingOptions) AddFlags(fs *pflag.FlagSet) {
    // --bind-address:监听地址
    fs.IPVar(&s.BindAddress, "bind-address", s.BindAddress,
        "The IP address on which to listen for the --secure-port port.")
    
    // --secure-port:HTTPS端口
    fs.IntVar(&s.BindPort, "secure-port", s.BindPort,
        "The port on which to serve HTTPS with authentication and authorization.")
    
    // --cert-directory:证书目录
    fs.StringVar(&s.ServerCert.CertDirectory, "cert-directory", s.ServerCert.CertDirectory,
        "The directory where the TLS certs are located.")
    
    // --tls-cert-file/--tls-private-key-file:指定证书文件
    fs.StringVar(&s.ServerCert.CertKey.CertFile, "tls-cert-file", s.ServerCert.CertKey.CertFile,
        "File containing the default x509 Certificate for HTTPS.")
    fs.StringVar(&s.ServerCert.CertKey.KeyFile, "tls-private-key-file", s.ServerCert.CertKey.KeyFile,
        "File containing the default x509 private key matching --tls-cert-file.")
}

ApplyTo做了什么?

func (s *SecureServingOptions) ApplyTo(config **server.SecureServingInfo, loopbackClientConfig **restclient.Config) error {
    // 1. 检查是否有证书配置
    if s == nil || s.BindPort <= 0 {
        return nil
    }
    
    // 2. 生成或加载证书
    serverCert, err := s.ServerCert.CertKey.GetCertKeyPair()
    if err != nil {
        return err
    }
    
    // 3. 配置SecureServingInfo
    *config = &server.SecureServingInfo{
        Listener:                     listener,  // 监听器
        Cert:                         serverCert, // 服务器证书
        CipherSuites:                 s.CipherSuites, // 加密套件
        MinTLSVersion:                s.MinTLSVersion, // 最小TLS版本
        HTTP2MaxStreams:              s.HTTP2MaxStreams, // HTTP/2最大流数
        HTTP2MaxConcurrentStreams:    s.HTTP2MaxConcurrentStreams,
    }
    
    // 4. 配置LoopbackClientConfig(apiserver自己访问自己用的客户端配置)
    if *loopbackClientConfig == nil {
        *loopbackClientConfig = &restclient.Config{
            // 使用本地回环地址
            Host: fmt.Sprintf("https://%s:%d", s.BindAddress.String(), s.BindPort),
            // 使用相同的证书(因为是自签名的)
            TLSClientConfig: restclient.TLSClientConfig{
                CertFile: s.ServerCert.CertKey.CertFile,
                KeyFile:  s.ServerCert.CertKey.KeyFile,
            },
        }
    }
    
    return nil
}

关键点LoopbackClientConfig是apiserver自己访问自己API时用的客户端配置。因为apiserver内部也需要获取资源(如通过informer监听),所以它自己也是一个"API客户端"。

2.3 应用其他配置

除了SecureServing,还有很多其他配置也需要ApplyTo:

// 认证配置
if lastErr = s.Authentication.ApplyTo(&genericConfig.Authentication, ...); lastErr != nil {
    return
}

// 鉴权配置
if lastErr = s.Authorization.ApplyTo(&genericConfig.Authorization); lastErr != nil {
    return
}

// 审计配置
if lastErr = s.Audit.ApplyTo(genericConfig); lastErr != nil {
    return
}

// Features配置
if lastErr = s.Features.ApplyTo(genericConfig); lastErr != nil {
    return
}

// API启用配置
if lastErr = s.APIEnablement.ApplyTo(genericConfig, ...); lastErr != nil {
    return
}

每个配置都有对应的命令行参数

配置类型主要参数作用
SecureServing–bind-address, --secure-portHTTPS服务监听
Authentication–client-ca-file, --token-auth-file客户端认证
Authorization–authorization-modeRBAC/ABAC等鉴权模式
Audit–audit-log-path审计日志配置
Features–feature-gates功能开关
APIEnablement–runtime-configAPI版本启用控制

第三步:初始化etcd存储

为什么需要StorageFactory?

apiserver需要把资源存储到etcd,但不同的资源可能有不同的存储需求:

  • 不同的etcd集群(事件数据和资源数据分开)
  • 不同的序列化格式(json vs protobuf)
  • 不同的编码版本(v1 vs v2)

StorageFactory就是用来管理这些存储配置的

存储配置初始化流程

// 1. 创建存储工厂配置
storageFactoryConfig := kubeapiserver.NewStorageFactoryConfig()
storageFactoryConfig.APIResourceConfig = genericConfig.MergedResourceConfig

// 2. 补全配置
completedStorageFactoryConfig, err := storageFactoryConfig.Complete(s.Etcd)

// 3. 创建存储工厂
storageFactory, lastErr = completedStorageFactoryConfig.New()

// 4. 应用到genericConfig
if lastErr = s.Etcd.ApplyWithStorageFactoryTo(storageFactory, genericConfig); lastErr != nil {
    return
}

3.1 StorageFactoryConfig结构

// pkg/kubeapiserver/options/storage_config.go
type StorageFactoryConfig struct {
    StorageConfig                    storagebackend.Config  // etcd连接配置
    APIResourceConfig                *serverstorage.ResourceConfig  // API资源启用配置
    DefaultResourceEncoding          *serverstorage.DefaultResourceEncoding  // 默认编码
    DefaultStorageMediaType          string  // 默认存储格式(application/vnd.kubernetes.protobuf)
    Serializer                       runtime.StorageSerializer  // 序列化器
    ResourceEncodingOverrides        []schema.GroupVersionResource  // 资源编码覆盖
    EtcdServersOverrides             []string  // etcd地址覆盖(用于特定资源)
}

3.2 应用存储配置到GenericConfig

// ApplyWithStorageFactoryTo将存储工厂应用到server配置
func (s *EtcdOptions) ApplyWithStorageFactoryTo(
    factory serverstorage.StorageFactory, 
    c *server.Config,
) error {
    // 1. 添加etcd健康检查端点
    if err := s.addEtcdHealthEndpoint(c); err != nil {
        return err
    }
    
    // 2. 设置对象计数追踪器(用于监控)
    s.StorageConfig.StorageObjectCountTracker = c.StorageObjectCountTracker
    
    // 3. 设置RESTOptionsGetter
    // 这是关键!后续所有RESTStorage都通过它获取存储选项
    c.RESTOptionsGetter = &StorageFactoryRestOptionsFactory{
        Options:        *s,
        StorageFactory: factory,
    }
    
    return nil
}

3.3 添加etcd健康检查

func (s *EtcdOptions) addEtcdHealthEndpoint(c *server.Config) error {
    // 创建etcd健康检查函数
    healthCheck, err := storagefactory.CreateHealthCheck(s.StorageConfig)
    if err != nil {
        return err
    }
    
    // 添加到健康检查列表
    c.AddHealthChecks(healthz.NamedCheck("etcd", func(r *http.Request) error {
        return healthCheck()
    }))
    
    // 如果配置了KMS加密,还添加KMS健康检查
    if s.EncryptionProviderConfigFilepath != "" {
        kmsPluginHealthzChecks, err := encryptionconfig.GetKMSPluginHealthzCheckers(
            s.EncryptionProviderConfigFilepath,
        )
        if err != nil {
            return err
        }
        c.AddHealthChecks(kmsPluginHealthzChecks...)
    }
    
    return nil
}

etcd健康检查实现

func CreateHealthCheck(c storagebackend.Config) (func() error, error) {
    switch c.Type {
    case storagebackend.StorageTypeETCD2:
        // etcd v2已不再支持
        return nil, fmt.Errorf("%s is no longer a supported storage backend", c.Type)
        
    case storagebackend.StorageTypeUnset, storagebackend.StorageTypeETCD3:
        // 创建etcd v3健康检查
        return newETCD3HealthCheck(c)
        
    default:
        return nil, fmt.Errorf("unknown storage type: %s", c.Type)
    }
}

func newETCD3HealthCheck(c storagebackend.Config) (func() error, error) {
    // 创建etcd客户端
    client, err := etcd3.New(c.Transport)
    if err != nil {
        return nil, err
    }
    
    // 返回健康检查函数
    return func() error {
        // 执行etcd集群健康检查
        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
        defer cancel()
        
        // 尝试获取etcd版本信息,验证连接
        _, err := client.Maintenance.Status(ctx)
        return err
    }, nil
}

访问健康检查端点

# 检查etcd健康状态
curl -k https://localhost:6443/healthz/etcd

# 输出:ok 或错误信息

注意:从K8s 1.20+开始,apiserver只支持etcd v3,不再支持etcd v2。

第四步:设置内部通信优化

为什么要特殊设置?

apiserver内部有很多组件需要访问API(如informer、controller、admission webhook等),这些内部通信有一些特点:

  • 网络可靠:都是在本地或者内部网络,不需要考虑网络不稳定
  • 性能要求高:需要低延迟、高吞吐
  • 带宽充足:内网带宽通常不是瓶颈

基于这些特点,apiserver对内部通信做了一些优化:

// 设置内部通信使用protobuf格式(性能更好)
genericConfig.LoopbackClientConfig.ContentConfig.ContentType = "application/vnd.kubernetes.protobuf"

// 禁用压缩(内部网络快,不需要省带宽)
genericConfig.LoopbackClientConfig.DisableCompression = true

为什么用protobuf而不是JSON?

特性JSONProtobuf
序列化速度快(3-10倍)
数据大小小(1/3到1/2)
可读性差(二进制)
CPU消耗
适用场景人类查看机器间通信

内部通信用protobuf可以显著提升性能

第五步:创建ClientSet

为什么apiserver自己还需要ClientSet?

你可能奇怪:apiserver本身就是API服务,为什么它自己还要创建客户端?

实际上,apiserver内部有很多组件需要访问API:

  • Informers:监听资源变化(如Namespace、Node等)
  • Admission Controllers:准入控制器需要查询其他资源
  • Built-in Controllers:内置控制器(如ServiceAccount控制器)

这些组件都通过LoopbackClient访问API。

ClientSet创建流程

// 1. 使用LoopbackClientConfig创建客户端配置
kubeClientConfig := genericConfig.LoopbackClientConfig

// 2. 创建clientset
clientgoExternalClient, err := clientgoclientset.NewForConfig(kubeClientConfig)
if err != nil {
    lastErr = fmt.Errorf("failed to create real external clientset: %v", err)
    return
}

// 3. 创建SharedInformerFactory
versionedInformers = clientgoinformers.NewSharedInformerFactory(clientgoExternalClient, 10*time.Minute)

SharedInformerFactory的作用

Informer是K8s客户端库的核心组件,用于监听资源变化

// 创建共享的Informer工厂
// 参数2(10*time.Minute)是resync周期:定期重新List全量数据
versionedInformers = clientgoinformers.NewSharedInformerFactory(clientgoExternalClient, 10*time.Minute)

为什么需要"Shared"?

没有SharedInformerFactory的情况:
┌────────────────────────────────────────┐
│ Controller A → Informer A → Watch API  │  ← 3个独立的Watch连接
│ Controller B → Informer B → Watch API  │
│ Controller C → Informer C → Watch API  │
└────────────────────────────────────────┘

使用SharedInformerFactory后:
┌────────────────────────────────────────┐
│ Controller A ─┐                        │
│ Controller B ─┼→ Shared Informer ──→ API  ← 1个Watch连接
│ Controller C ─┘                        │
└────────────────────────────────────────┘

多个控制器共享同一个Informer,减少apiserver压力

使用Informer的示例

// 获取Pod informer
podInformer := versionedInformers.Core().V1().Pods()

// 添加事件处理器
podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
    AddFunc: func(obj interface{}) {
        pod := obj.(*corev1.Pod)
        fmt.Printf("Pod added: %s/%s\n", pod.Namespace, pod.Name)
    },
    UpdateFunc: func(oldObj, newObj interface{}) {
        // 处理更新
    },
    DeleteFunc: func(obj interface{}) {
        // 处理删除
    },
})

// 启动informer(必须在Run之前启动)
versionedInformers.Start(stopCh)

踩坑实录:配置初始化常见问题

坑1:LoopbackClientConfig冲突

现象:启动时报错failed to create real external clientset: unable to load in-cluster configuration

根因LoopbackClientConfig没有正确配置Host,或者使用了in-cluster配置模式

解决方案

// 确保LoopbackClientConfig正确配置
loopbackClientConfig := &restclient.Config{
    Host: fmt.Sprintf("https://%s:%d", bindAddress, bindPort),
    TLSClientConfig: restclient.TLSClientConfig{
        CertFile: certFile,
        KeyFile:  keyFile,
        CAFile:   caFile,
    },
    // 重要:不要使用这些配置
    // InCluster: true,  // 错误!不是in-cluster模式
}

坑2:etcd证书问题

现象:健康检查端点返回etcd: connection refused或证书错误

根因:apiserver的etcd证书配置与实际etcd集群不匹配

排查步骤

# 1. 检查etcd集群状态
etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/etcd/ca.crt \
  --cert=/etc/etcd/server.crt \
  --key=/etc/etcd/server.key \
  endpoint health

# 2. 确认apiserver启动参数
ps aux | grep kube-apiserver | grep etcd

# 3. 检查证书内容
openssl x509 -in /etc/kubernetes/pki/apiserver-etcd-client.crt -text -noout

坑3:StorageFactory资源覆盖不生效

现象:配置了特定资源用不同的etcd集群,但没有生效

根因EtcdServersOverrides格式错误

正确格式

# 格式:group/resource#servers
kube-apiserver \
  --etcd-servers-overrides="/events#https://etcd-events:2379" \
  --etcd-servers-overrides="/pods#https://etcd-pods:2379"

坑4:protobuf格式导致兼容性问题

现象:apiserver启动后,某些组件无法获取资源,报错proto: wrong wireType

根因:apiserver内部使用protobuf格式,但某些客户端(如自定义operator)可能不支持

解决方案

// 内部通信仍用protobuf(性能考虑)
genericConfig.LoopbackClientConfig.ContentConfig.ContentType = "application/vnd.kubernetes.protobuf"

// 但对外暴露的客户端配置可以改为JSON(兼容性考虑)
// 或者在operator中启用protobuf支持

坑5:Informer resync导致的性能问题

现象:apiserver启动一段时间后,内存和CPU使用逐渐升高

根因:SharedInformerFactory的resync周期太短,导致频繁全量List

解决方案

// 适当调大resync周期(默认10分钟,可以改为30分钟或更长)
versionedInformers = clientgoinformers.NewSharedInformerFactory(
    clientgoExternalClient, 
    30*time.Minute,  // 调大resync周期
)

// 或者完全禁用resync(如果不需要定期同步)
versionedInformers = clientgoinformers.NewSharedInformerFactory(
    clientgoExternalClient, 
    0,  // 0表示不自动resync
)

GenericConfig配置速查表

配置项命令行参数作用默认值
SecureServing–bind-address, --secure-portHTTPS服务监听0.0.0.0:6443
LoopbackClientN/A内部客户端配置自动配置
ProxyTransportN/A代理传输层HTTP/2连接池
RESTOptionsGetter–etcd-servers存储选项获取必须指定
StorageFactory–storage-backend存储工厂etcd3
HealthChecksN/A健康检查列表etcd等
InformerFactoryN/AInformer工厂10分钟resync

总结

通过今天的分析,我们理解了CreateKubeAPIServerConfig如何一步步构建apiserver的配置基座:

  1. ProxyTransport:创建连接池,优化与kubelet的代理通信
  2. GenericConfig:构建通用配置,包括HTTPS、认证、鉴权等
  3. etcd初始化:配置存储后端,设置健康检查
  4. 内部通信优化:使用protobuf,禁用压缩
  5. ClientSet创建:创建内部使用的客户端和Informer工厂

这些准备工作为后续的API服务启动奠定了坚实基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

加倍巴巴

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

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

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

打赏作者

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

抵扣说明:

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

余额充值