这篇文章发表已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已不再准确。

使用 Finalizer 控制删除

在 Kubernetes 中删除对象可能具有挑战性。您可能认为已删除了某个对象,却发现它仍然存在。虽然执行 kubectl delete 命令并寄希望于其能正常工作适用于日常操作,但了解 Kubernetes delete 命令的工作原理将有助于您理解为何某些对象在删除后仍然残留。

在本文中,我将探讨

  • 资源的哪些属性控制删除
  • Finalizer 和所有者引用如何影响对象删除
  • 如何使用传播策略更改删除顺序
  • 删除的工作原理及示例

为简单起见,所有示例都将使用 ConfigMaps 和基本 Shell 命令来演示此过程。我们将探讨这些命令的工作原理,并讨论在实际使用中可能产生的后果和结果。

基本的 delete

Kubernetes 有几个不同的命令可用于创建、读取、更新和删除对象。为了本文的目的,我们将重点关注四个 kubectl 命令:creategetpatchdelete

以下是基本 kubectl delete 命令的示例

kubectl create configmap mymap
configmap/mymap created
kubectl get configmap/mymap
NAME    DATA   AGE
mymap   0      12s
kubectl delete configmap/mymap
configmap "mymap" deleted
kubectl get configmap/mymap
Error from server (NotFound): configmaps "mymap" not found

$ 开头的 Shell 命令后面是它们的输出。可以看到,我们首先执行 kubectl create configmap mymap,这会创建一个空的 ConfigMap mymap。接下来,我们需要 get 该 ConfigMap 以证明它存在。然后我们可以删除该 ConfigMap。再次尝试 get 它会产生一个 HTTP 404 错误,这意味着找不到该 ConfigMap。

基本 delete 命令的状态图非常简单

State diagram for delete

Delete 状态图

尽管此操作很简单,但其他因素可能会干扰删除过程,包括 finalizer 和所有者引用。

理解 Finalizer

在理解 Kubernetes 中的资源删除时,了解 finalizer 的工作原理很有帮助,并且可以帮助您理解为什么有些对象无法删除。

Finalizer 是资源上的键,用于指示预删除操作。它们控制资源的垃圾回收,旨在提醒控制器在移除资源之前需要执行哪些清理操作。但是,它们不一定指定要执行的代码;资源上的 finalizer 本质上只是一系列键,很像注解。像注解一样,它们可以被操作。

一些您可能遇到过的常见 finalizer 有

  • kubernetes.io/pv-protection
  • kubernetes.io/pvc-protection

上方的 finalizer 用于卷上,以防止意外删除。类似地,一些 finalizer 可以用于防止任何资源被删除,但它们不受任何控制器管理。

下方是一个自定义的 ConfigMap,它没有其他属性,但包含一个 finalizer

cat <<EOF | kubectl create -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: mymap
  finalizers:
  - kubernetes
EOF

ConfigMap 资源控制器不理解如何处理 kubernetes finalizer 键。我将这些用于 ConfigMap 的 finalizer 称为“死” finalizer,因为它通常用于 Namespace 上。尝试删除此 ConfigMap 时会发生以下情况

kubectl delete configmap/mymap &
configmap "mymap" deleted
jobs
[1]+  Running kubectl delete configmap/mymap

Kubernetes 会报告对象已删除,但是,它并没有以传统意义上的方式被删除。相反,它正在删除过程中。当我们再次尝试 get 该对象时,发现对象已被修改以包含删除时间戳。

kubectl get configmap/mymap -o yaml
apiVersion: v1
kind: ConfigMap
metadata:
  creationTimestamp: "2020-10-22T21:30:18Z"
  deletionGracePeriodSeconds: 0
  deletionTimestamp: "2020-10-22T21:30:34Z"
  finalizers:
  - kubernetes
  name: mymap
  namespace: default
  resourceVersion: "311456"
  selfLink: /api/v1/namespaces/default/configmaps/mymap
  uid: 93a37fed-23e3-45e8-b6ee-b2521db81638

简而言之,发生的情况是对象被更新了,而不是被删除了。这是因为 Kubernetes 发现对象包含 finalizer,并阻止从 etcd 中移除对象。删除时间戳表示已请求删除,但删除操作直到我们编辑对象并移除 finalizer 后才能完成。

以下是如何使用 patch 命令移除 finalizer 的演示。如果要删除对象,只需在命令行上对其进行 patch 操作以移除 finalizer。这样,在后台运行的删除操作将完成,对象将被删除。当我们尝试 get 该 ConfigMap 时,它将消失。

kubectl patch configmap/mymap \
    --type json \
    --patch='[ { "op": "remove", "path": "/metadata/finalizers" } ]'
configmap/mymap patched
[1]+  Done  kubectl delete configmap/mymap

kubectl get configmap/mymap -o yaml
Error from server (NotFound): configmaps "mymap" not found

这是 finalization 的状态图

State diagram for finalize

Finalize 状态图

因此,如果尝试删除带有 finalizer 的对象,它将一直处于 finalization 状态,直到控制器移除 finalizer 键或使用 Kubectl 移除 finalizer。一旦 finalizer 列表为空,对象才能被 Kubernetes 真正回收并放入队列中,从注册表中删除。

所有者引用

所有者引用描述了一组对象如何相互关联。它们是资源上的属性,用于指定彼此之间的关系,从而可以删除整个资源树。

存在所有者引用时,会处理 finalizer 规则。所有者引用由名称和 UID 组成。所有者引用链接同一 Namespace 内的资源,并且该引用也需要 UID 才能工作。Pod 通常包含指向所属 ReplicaSet 的所有者引用。因此,当 Deployment 或 StatefulSet 被删除时,其子 ReplicaSet 和 Pod 也会在此过程中被删除。

以下是一些所有者引用的示例及其工作原理。在第一个示例中,我们首先创建父对象,然后创建子对象。结果是一个非常简单的 ConfigMap,它包含对其父对象的所有者引用

cat <<EOF | kubectl create -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: mymap-parent
EOF
CM_UID=$(kubectl get configmap mymap-parent -o jsonpath="{.metadata.uid}")

cat <<EOF | kubectl create -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: mymap-child
  ownerReferences:
  - apiVersion: v1
    kind: ConfigMap
    name: mymap-parent
    uid: $CM_UID
EOF

当存在所有者引用时,删除子对象不会删除父对象

kubectl get configmap
NAME           DATA   AGE
mymap-child    0      12m4s
mymap-parent   0      12m4s

kubectl delete configmap/mymap-child
configmap "mymap-child" deleted

kubectl get configmap
NAME           DATA   AGE
mymap-parent   0      12m10s

在此示例中,我们重新创建了上述的父子 ConfigMap。现在,当我们从父对象(而不是子对象)删除时,子对象指向父对象的所有者引用存在,当我们 get 这些 ConfigMap 时,Namespace 中没有任何 ConfigMap。

kubectl get configmap
NAME           DATA   AGE
mymap-child    0      10m2s
mymap-parent   0      10m2s

kubectl delete configmap/mymap-parent
configmap "mymap-parent" deleted

kubectl get configmap
No resources found in default namespace.

总之,当存在子对象指向父对象的覆盖所有者引用时,删除父对象会自动删除子对象。这称为 cascade。cascade 的默认值为 true,但是,您可以使用 kubectl delete 的 --cascade=orphan 选项来删除对象并使其子对象成为孤儿。更新:从 kubectl v1.20 开始,cascade 的默认值为 background

在以下示例中,有一个父对象和一个子对象。请注意,所有者引用仍然包含在内。如果我使用 --cascade=orphan 删除父对象,父对象被删除,但子对象仍然存在

kubectl get configmap
NAME           DATA   AGE
mymap-child    0      13m8s
mymap-parent   0      13m8s

kubectl delete --cascade=orphan configmap/mymap-parent
configmap "mymap-parent" deleted

kubectl get configmap
NAME          DATA   AGE
mymap-child   0      13m21s

--cascade 选项链接到 API 中的传播策略 (propagation policy),该策略允许您更改树中对象的删除顺序。以下示例使用 API 访问来构建一个带有背景传播策略的自定义删除 API 调用

kubectl proxy --port=8080 &
Starting to serve on 127.0.0.1:8080

curl -X DELETE \
  localhost:8080/api/v1/namespaces/default/configmaps/mymap-parent \
  -d '{ "kind":"DeleteOptions", "apiVersion":"v1", "propagationPolicy":"Background" }' \
  -H "Content-Type: application/json"
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Success",
  "details": { ... }
}

请注意,传播策略不能通过 kubectl 命令行指定。您必须使用自定义 API 调用来指定它。只需创建一个代理,以便您从客户端访问 API 服务器,然后执行一个包含 URL 的 curl 命令来执行该 delete 命令。

传播策略有三种不同的选项

  • Foreground:先删除子对象,再删除父对象(后序)
  • Background:先删除父对象,再删除子对象(前序)
  • Orphan:忽略所有者引用(对象成为孤儿)

请记住,删除对象并指定所有者引用后,在此过程中将遵循 finalizer。这可能导致对象树仍然存在,最终出现部分删除的情况。此时,您必须查看对象上存在的任何所有者引用以及任何 finalizer,以了解发生了什么。

强制删除 Namespace

有一种情况可能需要强制对 Namespace 进行 finalization。如果您已删除 Namespace 并清空了其下的所有对象,但 Namespace 仍然存在,可以通过更新 Namespace 子资源 finalize 来强制删除。这将通知 Namespace 控制器需要从 Namespace 中移除 finalizer 并执行任何清理操作。

cat <<EOF | curl -X PUT \
  localhost:8080/api/v1/namespaces/test/finalize \
  -H "Content-Type: application/json" \
  --data-binary @-
{
  "kind": "Namespace",
  "apiVersion": "v1",
  "metadata": {
    "name": "test"
  },
  "spec": {
    "finalizers": null
  }
}
EOF

此操作应谨慎进行,因为它可能只删除 Namespace 而留下孤儿对象在该(现已不存在的)Namespace 中——这对于 Kubernetes 来说是一个令人困惑的状态。如果发生这种情况,可以手动重新创建 Namespace,有时孤儿对象会重新出现在刚创建的 Namespace 下,从而允许手动清理和恢复。

主要收获

正如这些示例所示,finalizer 可能会妨碍在 Kubernetes 中删除资源,尤其是在对象之间存在父子关系时。通常,在代码中添加 finalizer 是有原因的,因此在手动删除之前应始终进行调查。所有者引用允许您指定和移除资源树,尽管在此过程中会遵循 finalizer。最后,传播策略可以通过自定义 API 调用用于指定删除顺序,让您能够控制如何删除对象。现在您对 Kubernetes 中的删除操作有了更多了解,我们建议您使用测试集群自己尝试一下。