通过阅读本篇文章你可以收获如下知识:
- 解决一个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,打开之后很容易就发现了。
我们来看一下删除命名空间的这个源码:
// 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的过程。