kube-apiserver 初始命名空间实现方式

k8s技术圈

共 12078字,需浏览 25分钟

 · 2023-08-19

在我们第 1 回仅启动一个 kube-apiserver 组件的情况下,Kubernetes 就创建了四个初始命名空间:

  • default:默认命名空间
  • kube-node-lease:用于保存与各个节点关联的 Lease(租约)对象
  • kube-public:所有(包括未经身份验证)的客户端都可以读取,主要用于集群内部共享资源
  • kube-system:用于 Kubernetes 系统创建资源,包括 Kubernetes 运行所需的核心组件
$ kubectl get ns
NAME                 STATUS   AGE
default              Active   2d22h
kube-node-lease      Active   2d22h
kube-public          Active   2d22h
kube-system          Active   2d22h

如果删除这四个初始命名空间:

$ kubectl delete ns default kube-node-lease kube-public kube-system
namespace "kube-node-lease" deleted
Error from server (Forbidden): namespaces "default" is forbidden: this namespace may not be deleted
Error from server (Forbidden): namespaces "kube-public" is forbidden: this namespace may not be deleted
Error from server (Forbidden): namespaces "kube-system" is forbidden: this namespace may not be deleted

会发现只有 kube-node-lease 允许删除,而且经过观察,删除后 1 分钟内,该命名空间也会自动重新创建:

$ kubectl get ns
NAME                 STATUS   AGE
default              Active   2d22h
kube-node-lease      Active   10s
kube-public          Active   2d22h
kube-system          Active   2d22h

这四个初始命名空间的维护工作,实际是由 GenericAPIServer 的 Hook 机制完成的。

在第 5 回启动 HTTP Server 的 NonBlockingRun 方法中:

// k8s.io/apiserver/pkg/server/genericapiserver.go
func (s preparedGenericAPIServer) NonBlockingRun(stopCh <-chan struct{}, shutdownTimeout time.Duration) (<-chan struct{}, <-chan struct{}, error) {
 // ...
 if s.SecureServingInfo != nil && s.Handler != nil {
  // 启动 HTTP Server
  stoppedCh, listenerStoppedCh, err = s.SecureServingInfo.Serve(s.Handler, shutdownTimeout, internalStopCh)
  // ...
 }

  // HTTP Server 退出通知
 go func() {
  <-stopCh
  close(internalStopCh)
 }()

 // 运行 Hook
 s.RunPostStartHooks(stopCh)
 // ...
}

当 HTTP Server 启动成功后,就会开始运行所有的 Hook :

// k8s.io/apiserver/pkg/server/hooks.go

type PostStartHookFunc func(context PostStartHookContext) error

// Hook 实体
type postStartHookEntry struct {
 // hook 方法
 hook PostStartHookFunc
  // hook 方法的调用栈信息
 originatingStack string
 done chan struct{}
}

func (s *GenericAPIServer) RunPostStartHooks(stopCh <-chan struct{}) {
 s.postStartHookLock.Lock()
 defer s.postStartHookLock.Unlock()
 s.postStartHooksCalled = true

 context := PostStartHookContext{
  LoopbackClientConfig: s.LoopbackClientConfig,
  StopCh:               stopCh,
 }

 // postStartHooks 是一个保存所有 Hook 的 map[string]postStartHookEntry
  // 遍历所有 Hook 并运行其 hook 方法
 for hookName, hookEntry := range s.postStartHooks {
  go runPostStartHook(hookName, hookEntry, context)
 }
}

func runPostStartHook(name string, entry postStartHookEntry, context PostStartHookContext) {
 var err error
 func() {
  // don't let the hook *accidentally* panic and kill the server
  defer utilruntime.HandleCrash()
  // 运行其 hook 方法
  err = entry.hook(context)
 }()
 // if the hook intentionally wants to kill server, let it.
 if err != nil {
  klog.Fatalf("PostStartHook %q failed: %v", name, err)
 }
 close(entry.done)
}

s.postStartHooks 位置断点调试,可以看到当前版本一共有 24 种不同功能的 Hook :

其中 start-system-namespaces-controller 便是本文的主角。

如果从 debug 的角度去找,可以看其对应的 hook 的调用栈信息定位到该 Hook 的创建位置:

或者继续跟随我的思路,来到第 3 回注册 /api/* 核心路由的 InstallLegacyAPI 方法:

// pkg/controlplane/instance.go
func (m *Instance) InstallLegacyAPI(c *completedConfig, restOptionsGetter generic.RESTOptionsGetter) error {
 // 创建 RESTStorage
 legacyRESTStorage, apiGroupInfo, err := legacyRESTStorageProvider.NewLegacyRESTStorage(c.ExtraConfig.APIResourceConfigSource, restOptionsGetter)


 controllerName := "bootstrap-controller"
 client := kubernetes.NewForConfigOrDie(c.GenericConfig.LoopbackClientConfig)
 // Kubernetes clusters contains the following system namespaces:
 // kube-system, kube-node-lease, kube-public, default
 // 注册 start-system-namespaces-controller Hook
 m.GenericAPIServer.AddPostStartHookOrDie("start-system-namespaces-controller"func(hookContext genericapiserver.PostStartHookContext) error {
  go systemnamespaces.NewController(client, c.ExtraConfig.VersionedInformers.Core().V1().Namespaces()).Run(hookContext.StopCh)
  return nil
 })

 bootstrapController, err := c.NewBootstrapController(legacyRESTStorage, client)
 if err != nil {
  return fmt.Errorf("error creating bootstrap controller: %v", err)
 }
 m.GenericAPIServer.AddPostStartHookOrDie(controllerName, bootstrapController.PostStartHook)
 m.GenericAPIServer.AddPreShutdownHookOrDie(controllerName, bootstrapController.PreShutdownHook)

 // 核心路由注册
 if err := m.GenericAPIServer.InstallLegacyAPIGroup(genericapiserver.DefaultLegacyAPIPrefix, &apiGroupInfo); err != nil {
  return fmt.Errorf("error in registering group versions: %v", err)
 }
 return nil
}

在 RESTStorage 创建之后,核心路由注册之前,注册了两个 Hook :bootstrap-controllerstart-system-namespaces-controller

Hook 注册的 AddPostStartHookOrDie 方法实际就是将 Hook 添加到 postStartHooks 中:

// k8s.io/apiserver/pkg/server/hooks.go
func (s *GenericAPIServer) AddPostStartHookOrDie(name string, hook PostStartHookFunc) {
 if err := s.AddPostStartHook(name, hook); err != nil {
  klog.Fatalf("Error registering PostStartHook %q: %v", name, err)
 }
}

func (s *GenericAPIServer) AddPostStartHook(name string, hook PostStartHookFunc) error {
 // ...
  // 添加到 postStartHooks map 中
 s.postStartHooks[name] = postStartHookEntry{hook: hook, originatingStack: string(debug.Stack()), done: done}

 return nil
}

因为 Hook 运行时实际是直接调用 hook 方法,所以重点还是各个 Hook 注册时自身所传入的 hook PostStartHookFunc 参数。

对于 start-system-namespaces-controller Hook 注册的 PostStartHookFunc 方法:

func(hookContext genericapiserver.PostStartHookContext) error {
  // 创建 start-system-namespaces-controller 并调用 Run 运行
  go systemnamespaces.NewController(client, c.ExtraConfig.VersionedInformers.Core().V1().Namespaces()).Run(hookContext.StopCh)
  return nil
}

该 controller 的实现很简单,我直接全部贴出来:

// pkg/controlplane/controller/systemnamespaces/system_namespaces_controller.go

// 创建 start-system-namespaces-controller
func NewController(clientset kubernetes.Interface, namespaceInformer coreinformers.NamespaceInformer) *Controller {
 // 需要维护的四个初始命名空间
 systemNamespaces := []string{metav1.NamespaceSystem, metav1.NamespacePublic, v1.NamespaceNodeLease, metav1.NamespaceDefault}
 // 定时周期为 1 分钟
 interval := 1 * time.Minute

 return &Controller{
  client:           clientset,
  namespaceLister:  namespaceInformer.Lister(),
  namespaceSynced:  namespaceInformer.Informer().HasSynced,
  systemNamespaces: systemNamespaces,
  interval:         interval,
 }
}

// 运行 start-system-namespaces-controller
func (c *Controller) Run(stopCh <-chan struct{}) {
 defer utilruntime.HandleCrash()
 defer klog.Infof("Shutting down system namespaces controller")

 klog.Infof("Starting system namespaces controller")

 if !cache.WaitForCacheSync(stopCh, c.namespaceSynced) {
  utilruntime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
  return
 }

 // 定时执行 c.sync 方法,定时周期为 1 分钟
 go wait.Until(c.sync, c.interval, stopCh)

 <-stopCh
}

func (c *Controller) sync() {
 // Loop the system namespace list, and create them if they do not exist
  // 遍历四个初始命名空间
 for _, ns := range c.systemNamespaces {
  // 如果需要则创建
  if err := c.createNamespaceIfNeeded(ns); err != nil {
   utilruntime.HandleError(fmt.Errorf("unable to create required kubernetes system Namespace %s: %v", ns, err))
  }
 }
}

func (c *Controller) createNamespaceIfNeeded(ns string) error {
 if _, err := c.namespaceLister.Get(ns); err == nil {
  // the namespace already exists
    // 命名空间已存在
  return nil
 }
 // 命名空间不存在,就去创建命名空间
 newNs := &v1.Namespace{
  ObjectMeta: metav1.ObjectMeta{
   Name:      ns,
   Namespace: "",
  },
 }
 _, err := c.client.CoreV1().Namespaces().Create(context.TODO(), newNs, metav1.CreateOptions{})
 if err != nil && errors.IsAlreadyExists(err) {
  err = nil
 }
 return err
}

这就是为什么启动一个 kube-apiserver 组件就创建了四个初始命名空间,并且 kube-node-lease 命名空间删除后 1 分钟内会自动重新创建的原因。

对于其它 23 种 Hook ,感兴趣也可以直接全局搜索 Hook name 快速定位到其注册位置去看。


浏览 1324
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报