kubernetes -- 删除namespace的过程以及遇到的bug解决

通过阅读本篇文章你可以收获如下知识:

  • 解决一个bug。
  • 理解k8s的controller中,删除namespace的源码,理解其中的删除过程。

问题

执行kubectl delete ns {ns-name}命令来删除ns-name的时候,发现状态一直停留在Terminating

[root@k8smaster k8slearn]# kubectl get ns
NAME              STATUS        AGE
default           Active        99m
hello             Terminating   36m
kube-node-lease   Active        99m
kube-public       Active        99m
kube-system       Active        99m

我想到的是可能是namespace底下有资源,等资源被删除之后系统才能安心删除掉namespace,然后我们来看一下资源:

[root@k8smaster k8slearn]# kubectl get all -n hello
No resources found in hello namespace.

发现是没有资源的,那么到底是什么原因让删除失败了呢?

我们看一下namespace的具体内容:

[root@k8smaster k8slearn]# kubectl get ns hello -o yaml
apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: "2023-02-01T06:42:00Z"
  managedFields:
  - apiVersion: v1
    fieldsType: FieldsV1
    fieldsV1:
      f:status:
        f:phase: {}
    manager: kubectl-create
    operation: Update
    time: "2023-02-01T06:42:00Z"
  name: hello
  resourceVersion: "5676"
  uid: bc48ddf5-7456-44f0-8f7f-597c6a141a0f
spec:
  finalizers:
  - kubernetes
status:
  phase: Active

我猜测问题出在这里:

spec:
  finalizers:
  - kubernetes

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

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

这个源码的位置我很快就发现了,因为删除一个命名空间肯定是controller做的事情,所以我们打开controller的这样一个文件,然后很快就发现了namespace,里面有一个deletion,打开之后很容易就发现了。
kubernetes -- 删除namespace的过程以及遇到的bug解决_第1张图片

 我们来看一下删除命名空间的这个源码:

// Delete deletes all resources in the given namespace.
// Before deleting resources:
//   - It ensures that deletion timestamp is set on the
//     namespace (does nothing if deletion timestamp is missing).
//   - Verifies that the namespace is in the "terminating" phase
//     (updates the namespace phase if it is not yet marked terminating)
//
// After deleting the resources:
// * It removes finalizer token from the given namespace.
//
// Returns an error if any of those steps fail.
// Returns ResourcesRemainingError if it deleted some resources but needs
// to wait for them to go away.
// Caller is expected to keep calling this until it succeeds.
func (d *namespacedResourcesDeleter) Delete(nsName string) error {
	// Multiple controllers may edit a namespace during termination
	// first get the latest state of the namespace before proceeding
	// if the namespace was deleted already, don't do anything
	namespace, err := d.nsClient.Get(context.TODO(), nsName, metav1.GetOptions{})
	if err != nil {
		if errors.IsNotFound(err) {
			return nil
		}
		return err
	}
	if namespace.DeletionTimestamp == nil {
		return nil
	}

	klog.V(5).Infof("namespace controller - syncNamespace - namespace: %s, finalizerToken: %s", namespace.Name, d.finalizerToken)

	// ensure that the status is up to date on the namespace
	// if we get a not found error, we assume the namespace is truly gone
	namespace, err = d.retryOnConflictError(namespace, d.updateNamespaceStatusFunc)
	if err != nil {
		if errors.IsNotFound(err) {
			return nil
		}
		return err
	}

	// the latest view of the namespace asserts that namespace is no longer deleting..
	if namespace.DeletionTimestamp.IsZero() {
		return nil
	}

	// return if it is already finalized.
	if finalized(namespace) {
		return nil
	}

	// there may still be content for us to remove
	estimate, err := d.deleteAllContent(namespace)
	if err != nil {
		return err
	}
	if estimate > 0 {
		return &ResourcesRemainingError{estimate}
	}

	// we have removed content, so mark it finalized by us
	_, err = d.retryOnConflictError(namespace, d.finalizeNamespace)
	if err != nil {
		// in normal practice, this should not be possible, but if a deployment is running
		// two controllers to do namespace deletion that share a common finalizer token it's
		// possible that a not found could occur since the other controller would have finished the delete.
		if errors.IsNotFound(err) {
			return nil
		}
		return err
	}
	return nil
}

我们把几行关键的源码分析一下:

	// return if it is already finalized.
	if finalized(namespace) {
		return nil
	}

我们来看一下这个里面发生了什么?

// finalized returns true if the namespace.Spec.Finalizers is an empty list
func finalized(namespace *v1.Namespace) bool {
	return len(namespace.Spec.Finalizers) == 0
}

这个代表如果我的finalized数组为空的话,那么就算作清理完成了,直接退出函数。

所以说到底,这个Finalizers到底是什么呢?我们区kubernetes的官方文档里面查看一下。

以下内容来自于kubernetes的官方文档:

https://kubernetes.io/zh-cn/docs/concepts/overview/working-with-objects/finalizers/

Finalizer 是带有命名空间的键,告诉 Kubernetes 等到特定的条件被满足后, 再完全删除被标记为删除的资源。 Finalizer 提醒控制器清理被删除的对象拥有的资源。

当你使用清单文件创建资源时,你可以在 metadata.finalizers 字段指定 Finalizers。 当你试图删除该资源时,处理删除请求的 API 服务器会注意到 finalizers 字段中的值, 并进行以下操作:

  • 修改对象,将你开始执行删除的时间添加到metadata.deletionTimestamp字段。
  • 禁止对象被删除,直到其 metadata.finalizers 字段为空。
  • 返回 202 状态码(HTTP “Accepted”)。

管理 finalizer 的控制器注意到对象上发生的更新操作,对象的 metadata.deletionTimestamp 被设置,意味着已经请求删除该对象。然后,控制器会试图满足资源的 Finalizers 的条件。 每当一个 Finalizer 的条件被满足时,控制器就会从资源的 finalizers 字段中删除该键。 当 finalizers 字段为空时,deletionTimestamp 字段被设置的对象会被自动删除。 你也可以使用 Finalizers 来阻止删除未被管理的资源。

一个常见的 Finalizer 的例子是 kubernetes.io/pv-protection, 它用来防止意外删除 PersistentVolume 对象。 当一个 PersistentVolume 对象被 Pod 使用时, Kubernetes 会添加 pv-protection Finalizer。 如果你试图删除 PersistentVolume,它将进入 Terminating 状态, 但是控制器因为该 Finalizer 存在而无法删除该资源。 当 Pod 停止使用 PersistentVolume 时, Kubernetes 清除 pv-protection Finalizer,控制器就会删除该卷。

那么接下来我们就在源码中看一看这个过程:

	estimate, err := d.deleteAllContent(namespace)

首先是删除namespace所有关联资源。

	// we have removed content, so mark it finalized by us
	_, err = d.retryOnConflictError(namespace, d.finalizeNamespace)

然后删除namespace中的spec.finalizers。

而由官方文档得知删除finalizers之后,ns才能被删除。

因此问题就出现在了deleteAllContent这个函数里面,这个函数里面某些东西出现了问题,导致finalizers无法被删除,从而出现上面的情况。

所以我们来读一下deleteAllContent的源码。

// deleteAllContent will use the dynamic client to delete each resource identified in groupVersionResources.
// It returns an estimate of the time remaining before the remaining resources are deleted.
// If estimate > 0, not all resources are guaranteed to be gone.
func (d *namespacedResourcesDeleter) deleteAllContent(ns *v1.Namespace) (int64, error) {
	namespace := ns.Name
	namespaceDeletedAt := *ns.DeletionTimestamp
	var errs []error
	conditionUpdater := namespaceConditionUpdater{}
	estimate := int64(0)
	klog.V(4).Infof("namespace controller - deleteAllContent - namespace: %s", namespace)

	resources, err := d.discoverResourcesFn()
	if err != nil {
		// discovery errors are not fatal.  We often have some set of resources we can operate against even if we don't have a complete list
		errs = append(errs, err)
		conditionUpdater.ProcessDiscoverResourcesErr(err)
	}
	// TODO(sttts): get rid of opCache and pass the verbs (especially "deletecollection") down into the deleter
	deletableResources := discovery.FilteredBy(discovery.SupportsAllVerbs{Verbs: []string{"delete"}}, resources)
	groupVersionResources, err := discovery.GroupVersionResources(deletableResources)
	if err != nil {
		// discovery errors are not fatal.  We often have some set of resources we can operate against even if we don't have a complete list
		errs = append(errs, err)
		conditionUpdater.ProcessGroupVersionErr(err)
	}

	numRemainingTotals := allGVRDeletionMetadata{
		gvrToNumRemaining:        map[schema.GroupVersionResource]int{},
		finalizersToNumRemaining: map[string]int{},
	}
	for gvr := range groupVersionResources {
		gvrDeletionMetadata, err := d.deleteAllContentForGroupVersionResource(gvr, namespace, namespaceDeletedAt)
		if err != nil {
			// If there is an error, hold on to it but proceed with all the remaining
			// groupVersionResources.
			errs = append(errs, err)
			conditionUpdater.ProcessDeleteContentErr(err)
		}
		if gvrDeletionMetadata.finalizerEstimateSeconds > estimate {
			estimate = gvrDeletionMetadata.finalizerEstimateSeconds
		}
		if gvrDeletionMetadata.numRemaining > 0 {
			numRemainingTotals.gvrToNumRemaining[gvr] = gvrDeletionMetadata.numRemaining
			for finalizer, numRemaining := range gvrDeletionMetadata.finalizersToNumRemaining {
				if numRemaining == 0 {
					continue
				}
				numRemainingTotals.finalizersToNumRemaining[finalizer] = numRemainingTotals.finalizersToNumRemaining[finalizer] + numRemaining
			}
		}
	}
	conditionUpdater.ProcessContentTotals(numRemainingTotals)

	// we always want to update the conditions because if we have set a condition to "it worked" after it was previously, "it didn't work",
	// we need to reflect that information.  Recall that additional finalizers can be set on namespaces, so this finalizer may clear itself and
	// NOT remove the resource instance.
	if hasChanged := conditionUpdater.Update(ns); hasChanged {
		if _, err = d.nsClient.UpdateStatus(context.TODO(), ns, metav1.UpdateOptions{}); err != nil {
			utilruntime.HandleError(fmt.Errorf("couldn't update status condition for namespace %q: %v", namespace, err))
		}
	}

	// if len(errs)==0, NewAggregate returns nil.
	klog.V(4).Infof("namespace controller - deleteAllContent - namespace: %s, estimate: %v, errors: %v", namespace, estimate, utilerrors.NewAggregate(errs))
	return estimate, utilerrors.NewAggregate(errs)
}

我们看有哪些if err != nil。就可以直到哪里会发生错误。

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

那么具体为什么,我们也不知道该怎么解决了,因此有一个治标不治本的方式,就是强行删除finalizers。

解决方法

强制性删除spec.finalizer[]。

  • 查看hello的namespace描述
kubectl get ns hello -o json > hello.json
  • 编辑json文件,删除spec字段的内存,因为k8s集群需要认证
 vim hello.json
"spec": {       
	"finalizers": [           
    		"kubernetes"
        ]
    },
更改为:
"spec": {
    
  },
  • 新开一个窗口运行kubectl proxy跑一个API代理在本地的8081端口
kubectl proxy --port=8081
  • 运行curl命令,直接调用kube api进行删除
curl -k -H "Content-Type:application/json" -X PUT --data-binary @hello.json http://127.0.0.1:8081/api/v1/namespaces/hello/finalize

总结

我们来梳理一下删除namespace的过程。kubernetes -- 删除namespace的过程以及遇到的bug解决_第2张图片

你可能感兴趣的:(kubernetes)