kubernetes namespace删除失败分析指北

0x00. 现象说明

在kubernetes集群中执行kubectl delete ns {ns-name}命令来删除ns-name后,发现ns-name一直停留在Terminating状态,如下所示:

aliyun$ kubectl get ns
NAME                    STATUS        AGE
ack-system-e2e-test-0   Terminating   14m     <-- 停留在Terminating状态
default                 Active        5d16h
kube-public             Active        5d16h
kube-system             Active        5d16h

我们可能会有一个模糊印象,namespace下所有的资源全部删除完成后,系统才能安心删除掉namespace。然后执行kubectl get all -n {ns-name},输出如下所示:

aliyun$ kubectl get all -n ack-system-e2e-test-0
No resources found.

namespace下明明没有资源,但是namespace却一直停留在terminating状态,嗯~跟自己的认知出现偏差~

0x01. 初步分析

既然namespace处在Terminating状态,我们尝试再次执行删除命令kubectl delete ns {ns-name},输出如下:

aliyun$ kubectl delete ns ack-system-e2e-test-0
Error from server (Conflict): Operation cannot be fulfilled on namespaces "ack-system-e2e-test-0": The system is ensuring all content is removed from this namespace.  Upon completion, this namespace will automatically be purged by the system.

根据错误信息可以看出,当系统确定namespace下所有资源已移除时,namespace会被自动删除。说明namespace下还有资源需要删除或者在确认是否已经全部删除时出现问题。

接下来我们看下namespace的内容(具体如下),可以猜测应该是spec.finalizers.kubernetes让系统一直没有删除namespace,因此一直停留在Terminating状态。

aliyun$ kubectl get ns ack-system-e2e-test-0 -o yaml
apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: "2019-11-26T23:33:28Z"
  deletionTimestamp: "2019-11-26T23:33:43Z"
  name: ack-system-e2e-test-0
  resourceVersion: "187978124"
  selfLink: /api/v1/namespaces/ack-system-e2e-test-0
  uid: 260db8c3-10a5-11ea-933e-8656ce263494
spec:
  finalizers:
  - kubernetes
status:
  phase: Terminating

那系统在什么情况下才能最终删除掉上面的spec.finalizers.kubernetes,从而删除namespace呢,有必要分析一下namespace controller的源码实现。

0x02. 源码探秘

从kubernetes架构可以推测出,删除namespace时系统删除namespace关联资源的处理应该是在contorller里面实现的。因此顺其自然去分析namespace controller的源码。可以理解如下两点:

  • namespace下所有关联资源全部删除后,才会删除spec.finalizers。在Delete方法中实现。
  • 关联资源删除处理由deleteAllContent方法实现

具体代码分析如下:

// k8s.io/kubernetes/pkg/controller/namespace/deletion/namespaced_resources_deleter.go
func (d *namespacedResourcesDeleter) Delete(nsName string) error {
    ...
    // 删除namespace所有关联资源
    estimate, err := d.deleteAllContent(namespace.Name, *namespace.DeletionTimestamp)
    ...

    // 然后删除namespace中的spec.finalizers
    namespace, err = d.retryOnConflictError(namespace, d.finalizeNamespace)
    ...

    // 最后删除namespace
    if d.deleteNamespaceWhenDone && finalized(namespace) {
        return d.deleteNamespace(namespace)
    }
    return nil
}

func (d *namespacedResourcesDeleter) deleteAllContent(namespace string, namespaceDeletedAt metav1.Time) (int64, error) {
    // 获取集群中所有注册namespace scope资源
    // discoverResourcesFn = DiscoveryClient.ServerPreferredNamespacedResources
    resources, err := d.discoverResourcesFn()
      if err != nil {
            return estimate, err  // 错误退出处理1: 获取失败错误
      }
      ...
      
      // 从namespace scope资源结果中,过滤出所有支持delete的资源
      deletableResources := discovery.FilteredBy(discovery.SupportsAllVerbs{Verbs: []string{"delete"}}, resources)
      groupVersionResources, err := discovery.GroupVersionResources(deletableResources)
      if err != nil {
            return estimate, err  // 错误退出2: gvr解析错误
      }
      ...
      
      // 在循环中删除namespace下的各个资源
      // 支持deletecollection的资源直接调用deletecollection删除,否则list所有该gvr资源然后逐个删除
      for gvr := range groupVersionResources {
            gvrEstimate, err := d.deleteAllContentForGroupVersionResource(gvr, namespace, namespaceDeletedAt)
                ...
        }
        if len(errs) > 0 {
            return estimate, utilerrors.NewAggregate(errs) // 错误退出3: namespace下gvr资源删除失败
      }
        ...
}

从源码分析可以看出,namespace一直处在Terminating状态,肯定是因为下面某种Error给hang住了。

  • 错误1: 获取所有注册namesapce scope资源失败
  • 错误2: 获取资源的gvr信息解析失败
  • 错误3: namespace下某些gvr资源删除失败

0x03. 困惑解除

针对上面的错误,如果有条件分析到kube-controller-manager组件的日志,直接去看日志应该很快可以定位到错误信息。我这边因为网络原因无法查看日志,采用逐步确认错误的方案。

  • 利用如下命令,获取所有注册namespace scope资源(顺便提取能delete的资源),输出如下:
aliyun$ kubectl api-resources --namespaced=true --verbs=delete
helinbodeMacBook-Pro:.kube helinbo$ kubectl api-resources --namespaced=true --verbs=delete
NAME                       SHORTNAMES   APIGROUP                    NAMESPACED   KIND
...
rolebindings                            rbac.authorization.k8s.io   true         RoleBinding
roles                                   rbac.authorization.k8s.io   true         Role
error: unable to retrieve the complete list of server APIs: metrics.k8s.io/v1beta1: the server is currently unable to handle the request

根据输出信息,发现获取metrics.k8s.io/v1beta1资源的注册信息时出错了,接着可以执行kubectl get apiservice查询系统中注册服务,输出如下(省略了时间信息):

aliyun$ kubectl get apiservice
helinbodeMacBook-Pro:.kube helinbo$ kubectl get apiservice
NAME                                    SERVICE                      AVAILABLE
...
v1beta1.events.k8s.io                   Local                        True
v1beta1.extensions                      Local                        True
v1beta1.metrics.k8s.io                  kube-system/metrics-server   False (MissingEndpoints)
...

根据输出信息,发现原来是metrics-server出问题了。当metrics-server恢复正常后(metrics-server组件恢复属于另一个话题,后续再分享),apiservice等待片刻也恢复正常,同时Terminating状态的namespace也自动被删除了。至此namespace删除失败问题算真正解决了。

0x04. 最后总结

  1. 当看到因为spec.finalizers卡住namespace无法删除,有可能会直接手动删除namespace中的finalizers信息,从而让系统删除namespace。但是要相信spec.finalizers的设计是有原因的,如果偷懒跳过就错过了问题背后的真正原因。
  2. 当碰到kubernetes系统没有按预期工作时,当根据系统日志,事件等信息仍无法分析到原因并解决时,直接阅读kubernetes源码不失为一种解决问题的方法。同时在理解kubernetes系统架构基础上阅读源码会有更好的效果。
上一篇:图文详解:漏洞扫描快速入门的流程


下一篇:Linux 文件系统