前言
上周帮同事排查apiserver启动问题,同样的配置文件,在测试环境能正常启动,到了生产环境却报错failed to create real external clientset: unable to load in-cluster configuration。排查了半天,最后发现是LoopbackClientConfig的ContentType设置问题——生产环境用了protobuf格式,但内部的informer客户端不支持。
这次经历让我意识到,kube-apiserver的初始化配置远比想象中复杂。在真正启动HTTP服务之前,它需要完成大量的准备工作:创建传输层、初始化etcd存储、构建clientset、设置健康检查等等。这些工作都在GenericConfig的构建过程中完成。
今天就带大家深入源码,看看CreateKubeAPIServerConfig是如何一步步准备好所有配置的。
GenericConfig:API服务的通用配置基座
在K8s中,apiserver不是单一的服务,而是包含KubeAPIServer、APIExtensionsServer、AggregatorServer三个层次。这三个服务有很多共同的配置需求:
- 都需要监听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 exec、kubectl logs、kubectl 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,
}
}
关键配置解析:
| 配置项 | 值 | 说明 |
|---|---|---|
MaxIdleConns | 100 | 最大空闲连接数,控制总连接池大小 |
MaxIdleConnsPerHost | 100 | 每个目标host的最大空闲连接,用于kubelet代理 |
IdleConnTimeout | 90s | 空闲连接回收时间 |
ForceAttemptHTTP2 | true | 优先使用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-port | HTTPS服务监听 |
| Authentication | –client-ca-file, --token-auth-file | 客户端认证 |
| Authorization | –authorization-mode | RBAC/ABAC等鉴权模式 |
| Audit | –audit-log-path | 审计日志配置 |
| Features | –feature-gates | 功能开关 |
| APIEnablement | –runtime-config | API版本启用控制 |
第三步:初始化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?
| 特性 | JSON | Protobuf |
|---|---|---|
| 序列化速度 | 慢 | 快(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-port | HTTPS服务监听 | 0.0.0.0:6443 |
| LoopbackClient | N/A | 内部客户端配置 | 自动配置 |
| ProxyTransport | N/A | 代理传输层 | HTTP/2连接池 |
| RESTOptionsGetter | –etcd-servers | 存储选项获取 | 必须指定 |
| StorageFactory | –storage-backend | 存储工厂 | etcd3 |
| HealthChecks | N/A | 健康检查列表 | etcd等 |
| InformerFactory | N/A | Informer工厂 | 10分钟resync |
总结
通过今天的分析,我们理解了CreateKubeAPIServerConfig如何一步步构建apiserver的配置基座:
- ProxyTransport:创建连接池,优化与kubelet的代理通信
- GenericConfig:构建通用配置,包括HTTPS、认证、鉴权等
- etcd初始化:配置存储后端,设置健康检查
- 内部通信优化:使用protobuf,禁用压缩
- ClientSet创建:创建内部使用的客户端和Informer工厂
这些准备工作为后续的API服务启动奠定了坚实基础。

1405

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



