kube-apiserver 初始命名空间实现方式
在我们第 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-controller
和 start-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 快速定位到其注册位置去看。