本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

使用 Finalizer 控制删除

在 Kubernetes 中删除对象可能具有挑战性。您可能认为已经删除了某个东西,却发现它仍然存在。虽然发出 kubectl delete 命令并寄希望于最好的结果可能适用于日常操作,但了解 Kubernetes delete 命令的运作方式将有助于您理解为什么某些对象在删除后仍然存在。

在这篇文章中,我将探讨:

  • 资源的哪些属性控制删除
  • Finalizer 和 Owner Reference 如何影响对象删除
  • 如何使用传播策略更改删除顺序
  • 删除的运作方式,并附带示例

为简单起见,所有示例都将使用 ConfigMap 和基本的 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

删除的状态图

尽管此操作很简单,但其他因素可能会干扰删除,包括 finalizer 和 owner reference。

理解 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,因为它通常用于命名空间。以下是尝试删除 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。如果我们要删除一个对象,我们可以在命令行上简单地修补它以删除 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

终结的状态图

因此,如果您尝试删除一个带有终结器 (finalizer) 的对象,它将一直处于终结状态,直到控制器删除了终结器键,或者使用 Kubectl 删除了终结器。一旦该终结器列表为空,Kubernetes 就可以真正回收该对象,并将其放入队列中以从注册表中删除。

Owner Reference

所有者引用描述了对象组之间的关系。它们是资源上的属性,指定了彼此之间的关系,因此可以删除整个资源树。

当存在所有者引用时,将处理 Finalizer 规则。所有者引用由名称和 UID 组成。所有者引用链接同一命名空间中的资源,并且它还需要一个 UID 才能使该引用生效。Pod 通常拥有对拥有副本集的 Owner Reference。因此,当删除 Deployment 或 StatefulSet 时,子副本集和 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。现在,当从父级(而不是子级)删除时,如果子级对父级有 Owner Reference,当我们 get 这些 ConfigMap 时,命名空间中没有任何 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 中的传播策略,该策略允许您更改树中对象被删除的顺序。以下示例使用 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(孤立):所有者引用被忽略

请记住,当您删除一个对象并且已指定了 Owner Reference 时,Finalizer 将在此过程中得到遵守。这可能导致对象树持久存在,并且最终导致部分删除。此时,您必须查看对象上任何现有的 Owner Reference 以及任何 Finalizer,才能了解正在发生什么。

强制删除命名空间

有一种情况可能需要强制终结命名空间。如果您已经删除了一个命名空间并清理了其中的所有对象,但该命名空间仍然存在,则可以通过更新命名空间子资源 finalize 来强制删除。这会通知命名空间控制器需要从命名空间中移除终结器并执行任何清理操作:

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

应谨慎执行此操作,因为它可能只删除命名空间,而留下孤立对象在现在不存在的命名空间中——这对于 Kubernetes 来说是一种混乱的状态。如果发生这种情况,可以手动重新创建命名空间,有时孤立的对象会重新出现在刚创建的命名空间下,这将允许手动清理和恢复。

主要要点

正如这些示例所示,finalizer 会阻碍 Kubernetes 中资源的删除,尤其是在对象之间存在父子关系时。通常,在代码中添加 finalizer 是有原因的,因此在手动删除之前应始终进行调查。Owner Reference 允许您指定和删除资源树,尽管在此过程中 finalizer 将被遵守。最后,传播策略可用于通过自定义 API 调用指定删除顺序,从而让您控制对象的删除方式。现在您对 Kubernetes 中删除的工作原理有了更多了解,我们建议您在测试集群上亲自尝试一下。