【kubernetes源码阅读】apiserver中如何使用etcd

281 阅读5分钟

源码:kubernetes 1.24.0
kubeapiserver作为k8s的核心组件,其主要功能是提供REST接口供其他组件操作底层ETCD存储组件的能力。在看源码过程中,搞清楚以下几个问题:

  • etcd在apiserver中如何进行初始化
  • apiserver如何将etcd的操作与REST接口绑定在一起
  • 为什么etcd的操作在apiserver中这么抽象
  • 从中学到了什么

注:kubernets代码的一般工作流程为:

初始化

在创建kubeApiserver的配置时,第一次能够查看到etcd的初始化过程,主要流程如下图所示:

在这个流程中,apiserver将健康检测的执行函数加入到配置中的健康检查对象中。并且创建了一个goroutine,每秒执行一次客户端的创建,并且存入到闭包对象中,这里很有意思,后续会详细分析。
在测试过程中,如果etcd无法连接的情况下,程序在初始化过程中,会不断重试一段时间后退出程序。 那么在初始化过程中,必然存在一个需要使用etcd的流程,测试如下:
image.png继续分析:

通过这个流程,可以看到在createApiExtension的时候,会去创建etcd的客户端。为什么会创建客户端呢?因为这里是第一个需要创建APIhandler与etcd相绑定的地方。并且在会创建一个缓存对象,会LIST和WATCH的请求提供服务。所以需要一个etcd的客户端。(如果开了EnableWachCache的话)

Handler与Etcd

在kube-apiserver中,提供了许多资源的REST接口来操作ETCD中存储的对象。在先不看代码的情况下,会如何进行开发呢?一般的后台开发模式如下:

  1. 选择合适的web框架。
  2. 所有请求都需要进行鉴权,审计等操作,所以需要添加一条middlewares层来统一处理。
  3. 通过REST方式,对资源的Create,Delete, Post, Put进行handler的开发。

如果按照以上逻辑开发,由于k8s自定义的资源量比较多,那么代码量可能会非常多。所以代码层面上,apiserver做了非常多的封装。也正因为如此,apiserver的代码非常难读。

K8S使用的WEB框架是开源的go-restful。

这里用我们常用的kubectl get configmaps 进行举例,要明白整个流程,现需要搞清几个对象的流转:

一层层封装,导致底层生产handler的方法很难找到,这里给出关键函数的调用链:

这里使用createAPIExtensionsServer进行举例。apiServer的调用链也比较一致;
记录下关键的函数:

  • registerResourceHandlers: 最终所有的Storage与handler创建的地方都在这个函数中,可以打断点进行主体逻辑的查看。

调用链举例说明

在这里,通过kubectl apply 一个configmap资源对象,在断点跟踪,记录下如何创建configmap的handler,以及最终的调用函数调用关系如下:

从以上的断点调试来看,整个的创建以及调用的链路深度是有的。在这里存在容易跟丢的情况,在于内部使用了非常多的Interface,并且代码分布在两个项目中,goland无法通过接口索引功能定位到。这里直接提示下:
所有的handler最终都是调用的store对象,而store对象保存在\k8s.io\kubernetes\pkg\registry中,如configmap的对象\k8s.io\kubernetes\pkg\registry\core\configmap\storage.go中,创建时通过NewREST()函数进行创建,内部组合了genericregistry.Store类型,而这个genericregistry.Store类型存放在.\k8s.io\kubernetes\vendor\k8s.io\apiserver\pkg\registry\generic\registry\store.go中,所以导致在阅读的过程中会丢失目标。

学习记录

k8s中接口的使用

在阅读过程中,有个比较有趣的代码:

func (a *APIInstaller) registerResourceHandlers(){
    creater, isCreater := storage.(rest.Creater)
    namedCreater, isNamedCreater := storage.(rest.NamedCreater)
    lister, isLister := storage.(rest.Lister)
    getter, isGetter := storage.(rest.Getter)
    getterWithOptions, isGetterWithOptions := storage.(rest.GetterWithOptions)
    gracefulDeleter, isGracefulDeleter := storage.(rest.GracefulDeleter)
    collectionDeleter, isCollectionDeleter := storage.(rest.CollectionDeleter)
    updater, isUpdater := storage.(rest.Updater)
    patcher, isPatcher := storage.(rest.Patcher)
    watcher, isWatcher := storage.(rest.Watcher)
    connecter, isConnecter := storage.(rest.Connecter)
    storageMeta, isMetadata := storage.(rest.StorageMetadata)
    storageVersionProvider, isStorageVersionProvider := storage.(rest.StorageVersionProvider)
    gvAcceptor, _ := storage.(rest.GroupVersionAcceptor)
    if !isMetadata {
        storageMeta = defaultStorageMetadata{}
    }
}

这里的意思是,从storage中进行断言的方式,来判断当前的资源是否需要生成对应Verb(动作)的handler。在创建Storage的时候,就已经声明。这种方式,大大的减少了代码的冗余。

ETCD有趣的操作

在K8S中,创建ETCD时,会同步创建HealthyCheck的goroutine(每一秒链接一次),后续在开发的时候也可以参考这种设计:
代码路径:vendor/k8s.io/apiserver/pkg/storage/storagebackend/factory/etcd3.go:75

func newETCD3HealthCheck(c storagebackend.Config) (func() errorerror) {
 // constructing the etcd v3 client blocks and times out if etcd is not available.
 // retry in a loop in the background until we successfully create the client, storing the client or error encountered

 clientValue := &atomic.Value{}

 clientErrMsg := &atomic.Value{}
 clientErrMsg.Store("etcd client connection not yet established")
 // !!!!这里的意思是,每秒执行一边参数中的func(), 并且永不停止!!!!!!
 go wait.PollUntil(time.Second, func() (boolerror) {
        //@@@ 在这里我们可以看到每秒创建一个client
  client, err := newETCD3Client(c.Transport)
  if err != nil {
   clientErrMsg.Store(err.Error())
   return falsenil
  }
        // !!将client保存到原子类型中!!考虑到了并发场景。
  clientValue.Store(client)
  clientErrMsg.Store("")
  return truenil
 }, wait.NeverStop)

 return func() error {
  if errMsg := clientErrMsg.Load().(string); len(errMsg) > 0 {
   return fmt.Errorf(errMsg)
  }
        // 在这里取出Client
  client := clientValue.Load().(*clientv3.Client)
  healthcheckTimeout := storagebackend.DefaultHealthcheckTimeout
  if c.HealthcheckTimeout != time.Duration(0) {
   healthcheckTimeout = c.HealthcheckTimeout
  }
  ctx, cancel := context.WithTimeout(context.Background(), healthcheckTimeout)
  defer cancel()
  // 调用etcd的/health 。
  _, err := client.Get(ctx, path.Join("/", c.Prefix, "health"))
  if err == nil {
   return nil
  }
  return fmt.Errorf("error getting data from etcd: %v", err)
 }, nil
}

返回函数最终保存到了Server的healthz.HealthChecker中。这里通过原子+闭包的方式,将客户端创建与实际使用的地方进行了区分。
另外:后续 使用etcd可以参考k8s的使用。

小结

本篇只对kube-apiserver的后端存储做了详细的解释,不会过多的原始代码,只说明了关键路径以及相对应的文件地址,有兴趣可以自行阅读,确实挺有意思。
下一步开始查看kube-apiserver的中间fliter的执行步骤;

kube-apiserver中etcd与handler的创建流程,我看了快3天,因为k8s的代码正在做迁移操作,分了两个不同的包,如果只是单纯读源码,我估计还是会劝退非常多的人。所以使用断点调试难度还是降低了不少; 看下来也发现了k8s的套路,后续在不断调试的过程中,慢慢的开始上手。

感兴趣可关注公众号 【小唐云原生】