Прекратите возиться с финализаторами Kubernetes

Мы все там бывали — очень неприятно видеть, как удаление ресурса Kubernetes застревает, зависает или занимает очень много времени. Возможно, вы «решили» эту проблему с помощью ужасного совета удалить финализаторы или выполнить команду kubectl delete ... --force --grace-period=0 для принудительного немедленного удаления. В 99% случаев это ужасная идея, и в этой статье я покажу вам, почему.

Финализаторы

Прежде чем мы перейдем к тому, почему принудительное удаление — плохая идея, сначала нам нужно поговорить о финализаторах.

Финализаторы — это значения в метаданных ресурса, которые сигнализируют о необходимых операциях перед удалением — они сообщают контроллеру ресурса, какие операции необходимо выполнить перед удалением объекта.

Наиболее распространенными из них являются:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
  finalizers:
  - kubernetes.io/pvc-protection
...
Войти в полноэкранный режим Выход из полноэкранного режима

Их цель — остановить удаление ресурса, в то время как контроллер или Kubernetes Operator чисто и изящно очищает любые зависимые объекты, такие как базовые устройства хранения.

При удалении объекта, имеющего финализатор, в метаданные ресурса добавляется deletionTimestamp, делая объект доступным только для чтения. Единственным исключением из правила «только для чтения» является то, что финализаторы могут быть удалены. Как только все финализаторы удалены, объект ставится в очередь на удаление.

Важно понимать, что финализаторы — это просто элементы/ключи в метаданных ресурса. Финализаторы не указывают код для выполнения. Они должны быть добавлены/удалены контроллером ресурса.

Также не путайте финализаторы со ссылками владельца. Поле .metadata.OwnerReferences определяет отношения родитель/потомок между такими объектами, как Deployment -> ReplicaSet -> Pod. При удалении такого объекта, как Deployment, может быть удалено целое дерево дочерних объектов. Этот процесс (удаление) происходит автоматически, в отличие от финализаторов, где контроллер должен предпринять некоторые действия и удалить поле финализатора.

Что может пойти не так?

Как упоминалось ранее, наиболее распространенным финализатором, с которым вы можете столкнуться, является тот, который прикреплен к Persistent Volume (PV) или Persistent Volume Claim (PVC). Этот финализатор защищает хранилище от удаления, пока его использует Pod. Поэтому, если PV или PVC не хочет удаляться, это, скорее всего, означает, что он все еще смонтирован Pod. Если вы решите принудительно удалить PV, имейте в виду, что резервное хранилище в Cloud или любой другой инфраструктуре может не удалиться, поэтому вы можете оставить висящий ресурс, который все еще стоит вам денег.

Другой пример — пространство имен, которое может застрять в состоянии Terminating, поскольку в пространстве имен все еще существуют ресурсы, которые контроллер пространства имен не может удалить. Принудительное удаление пространства имен может оставить висящие ресурсы в вашем кластере, например, балансировщик нагрузки облачного провайдера, который впоследствии будет очень сложно отследить.

Хотя это не обязательно связано с финализаторами, стоит упомянуть, что ресурсы могут застревать по многим другим причинам, кроме ожидания финализаторов:

Простейшим примером может быть застревание Pod в состоянии Terminating, что обычно сигнализирует о проблемах с Node, на котором работает Pod. «Решение» этой проблемы с помощью kubectl delete pod --grace-period=0 --force ... удалит Pod с сервера API (etcd), но он все еще может быть запущен на узле, что определенно нежелательно.

Другим примером может быть StatefulSet, где принудительное удаление Pod может создать проблемы, поскольку Pod имеют фиксированные имена (pod-0,pod-1). Распределенная система может зависеть от этих имен/идентификаторов — если Pod удален принудительно, но все еще работает на узле, вы можете получить 2 pod с одинаковыми идентификаторами, когда контроллер StatefulSet заменит оригинальный «удаленный» Pod. Эти два стручка могут попытаться получить доступ к одному и тому же хранилищу, что может привести к повреждению данных. Подробнее об этом в документации.

Финализаторы в дикой природе

Теперь мы знаем, что не стоит связываться с ресурсами, на которые наложены финализаторы, но что это за ресурсы?

В «ванильном» Kubernetes чаще всего встречаются kubernetes.io/pv-protection и kubernetes.io/pvc-protection, связанные с Persistent Volumes и Persistent Volume Claims соответственно (плюс еще несколько, появившихся в версии 1.23), а также финализатор kubernetes, присутствующий на пространствах имен. Последний, однако, находится не в поле .metadata.finalizers, а скорее в .spec.finalizers — этот особый случай описан в архитектурном документе.

Помимо этих «ванильных» финализаторов, вы можете столкнуться со многими другими, если установите операторы Kubernetes, которые часто выполняют логику предварительного удаления на своих пользовательских ресурсах. Быстрый поиск в коде некоторых популярных проектов выявил следующее:

  • Istio — istio-finalizer.install.istio.io.
  • Cert Manager — finalizer.acme.cert-manager.io
  • Strimzi (Kafka) — service.kubernetes.io/load-balancer-cleanup
  • Quay — quay-operator/finalizer
  • Ceph/Rook — ceph.rook.io/disaster-protection
  • ArgoCD — argoproj.io/finalizer
  • Litmus Chaos — chaosengine.litmuschaos.io/finalizer

Если вы хотите найти все финализаторы, которые присутствуют в вашем кластере, то вам нужно выполнить следующую команду для каждого типа ресурсов:

kubectl get some-resource -o custom-columns=Kind:.kind,Name:.metadata.name,Finalizers:.metadata.finalizers
Войти в полноэкранный режим Выйти из полноэкранного режима

Вы можете использовать kubectl api-resources, чтобы получить список типов ресурсов, доступных в вашем кластере.

Независимо от того, какой финализатор останавливает удаление ваших ресурсов, негативные последствия принудительного удаления этих ресурсов будут в целом одинаковыми, то есть что-то останется позади, будь то хранилище, балансировщик нагрузки или простой pod.

Также правильное решение будет в целом таким же — найти финализатор, который останавливает удаление, выяснить его назначение — возможно, посмотрев исходный код контроллера/оператора — и решить проблему, которая мешает контроллеру удалить финализатор.

Если вы все равно решите принудительно удалить проблемные ресурсы, то решение будет следующим:

kubectl patch some-resource/some-name 
    --type json 
    --patch='[ { "op": "remove", "path": "/metadata/finalizers" } ]'
Войти в полноэкранный режим Выйти из полноэкранного режима

Исключением может быть пространство имен, которое имеет API метод finalize, который обычно вызывается, когда все ресурсы в данном пространстве имен очищены. Если Namespace отказывается удаляться, даже когда не осталось ресурсов для удаления, то вы можете вызвать этот метод самостоятельно:

cat <<EOF | curl -X PUT 
  localhost:12345/api/v1/namespaces/my-namespace/finalize 
  -H "Content-Type: application/json" 
  --data-binary @-
{
  "kind": "Namespace",
  "apiVersion": "v1",
  "metadata": {
    "name": "my-namespace"
  },
  "spec": {
    "finalizers": null,
  }
}
EOF
Войти в полноэкранный режим Выйти из полноэкранного режима

Создание собственных

Теперь, когда мы знаем, что это такое и как они работают, должно быть понятно, что они весьма полезны, поэтому давайте посмотрим, как мы можем применить их к нашим собственным ресурсам и рабочим нагрузкам.

Экосистема Kubernetes основана на Go, но для простоты я буду использовать здесь Python. Если вы не знакомы с клиентской библиотекой Python Kubernetes, прочтите сначала мою предыдущую статью — Automate All the Boring Kubernetes Operations with Python.

Прежде чем мы начнем использовать финализаторы, нам сначала нужно создать какой-либо ресурс в кластере — в данном случае Deployment:

# initialize the client library...

deployment_name = "my-deploy"
ns = "default"

v1 = client.AppsV1Api(api_client)

deployment_manifest = client.V1Deployment(
    api_version="apps/v1",
    kind="Deployment",
    metadata=client.V1ObjectMeta(name=deployment_name),
    spec=client.V1DeploymentSpec(
        replicas=3,
        selector=client.V1LabelSelector(match_labels={
            "app": "nginx"
        }),
        template=client.V1PodTemplateSpec(
            metadata=client.V1ObjectMeta(labels={"app": "nginx"}),
            spec=client.V1PodSpec(
                containers=[client.V1Container(name="nginx",
                                               image="nginx:1.21.6",
                                               ports=[client.V1ContainerPort(container_port=80)]
                                               )]))))

response = v1.create_namespaced_deployment(body=deployment_manifest, namespace=ns)
Войти в полноэкранный режим Выйти из полноэкранного режима

Приведенный выше код создает образец Deployment под названием my-deploy, на данный момент без каких-либо финализаторов. Чтобы добавить пару финализаторов, мы воспользуемся следующим патчем:

finalizers = ["test/finalizer1", "test/finalizer2"]

v1.patch_namespaced_deployment(deployment_name, ns, {"metadata": {"finalizers": finalizers}})

while True:
    try:
        response = v1.read_namespaced_deployment_status(name=deployment_name, namespace=ns)
        if response.status.available_replicas != 3:
            print("Waiting for Deployment to become ready...")
            time.sleep(5)
        else:
            break
    except ApiException as e:
        print(f"Exception when calling AppsV1Api -> read_namespaced_deployment_status: {e}n")
Войти в полноэкранный режим Выход из полноэкранного режима

Важной частью здесь является вызов patch_namespaced_deployment, который устанавливает .metadata.finalizers в список финализаторов, которые мы определили. Каждый из них должен быть полностью квалифицирован, то есть содержать /, поскольку они должны соответствовать спецификации DNS-1123. В идеале, чтобы сделать их более понятными, вы должны использовать формат вроде kubernetes.io/pvc-protection, где вы префиксируете его с именем хоста вашего сервиса, который связан с контроллером, отвечающим за финализатор.

Остальная часть кода в приведенном выше фрагменте просто убеждается, что реплики развертывания доступны, после чего мы можем приступить к управлению финализаторами:

from kubernetes import client, watch

def finalize(deployment, namespace, finalizer):
    print(f"Do some pre-deletion task related to the {finalizer} present in {namespace}/{deployment}")
    ...

v1 = client.AppsV1Api(api_client)
w = watch.Watch()
for deploy in w.stream(partial(v1.list_namespaced_deployment, namespace=ns)):
    print(f"Deploy - Message: Event type: {deploy['type']}, Deployment {deploy['object']['metadata']['name']} was changed.")
    if deploy['type'] == "MODIFIED" and "deletionTimestamp" in deploy['object']['metadata']:

        fins = deploy['object']['metadata']['finalizers']
        f = fins[0]
        finalize(deploy['object']['metadata']['name'], ns, f)
        new_fins = list(set(fins) - {f})
        body = [{
            "kind": "Deployment",
            "apiVersion": "apps/v1",
            "metadata": {
                "name": deploy['object']['metadata']['name'],
            },
            "op": "replace",
            "path": f"/metadata/finalizers",
            "value": new_fins
        }]
        resp = v1.patch_namespaced_deployment(name=deploy['object']['metadata']['name'],
                                              namespace=ns,
                                              body=body,
                                              field_manager="json")
    elif deploy['type'] == "DELETED":
        print(f"{deploy['object']['metadata']['name']} successfully deleted.")
print("Finished namespace stream.")
Войти в полноэкранный режим Выход из полноэкранного режима

Общая последовательность действий здесь следующая:

Мы начинаем с наблюдения за нужным ресурсом — в данном случае развертыванием — на предмет каких-либо изменений/событий. Затем мы ищем события, относящиеся к изменениям ресурса, и проверяем, присутствует ли deletionTimestamp. Если она есть, мы берем список финализаторов из метаданных ресурса и начинаем обрабатывать первый из них. Сначала мы выполняем все необходимые задачи перед удалением с помощью функции finalize, после чего применяем патч к ресурсу с исходным списком финализаторов за вычетом тех, которые мы обработали.

Если патч на Python кажется вам сложным, то знайте, что это эквивалент следующей команды kubectl:

kubectl patch deployment/my-deploy 
  --type json 
  --patch='[ { "op": "replace", "path": "/metadata/finalizers", "value": [test/finalizer1] } ]'
Войти в полноэкранный режим Выйти из полноэкранного режима

Если патч будет принят, мы получим еще одно событие модификации, и тогда мы обработаем еще один финализатор. Мы повторяем это до тех пор, пока все финализаторы не закончатся. В этот момент ресурс автоматически удаляется.

Имейте в виду, что вы можете получать события более одного раза, поэтому важно сделать логику предварительного удаления идемпотентной.

Если вы выполните приведенные выше фрагменты кода, а затем выполните kubectl delete deployment my-deploy, то вы должны увидеть журналы типа:

# Finalizers added to Deployment
Deploy - Message: Event type: ADDED, Deployment my-deploy was changed.
# "kubectl delete" gets executed, "deletionTimestamp" is added
Deploy - Message: Event type: MODIFIED, Deployment my-deploy was changed.
# First finalizer is removed...
Do some pre-deletion task related to the test/finalizer1 present in default/my-deploy
# Another "MODIFIED" event comes in, Second finalizer is removed...
Deploy - Message: Event type: MODIFIED, Deployment my-deploy was changed.
Do some pre-deletion task related to the test/finalizer2 present in default/my-deploy
# Finalizers are gone "DELETED" event comes - Deployment is gone.
Deploy - Message: Event type: DELETED, Deployment my-deploy was changed.
my-deploy successfully deleted.
Вход в полноэкранный режим Выход из полноэкранного режима

Приведенная выше демонстрация с использованием Python работает, но не совсем надежна. В реальном сценарии вы, скорее всего, захотите использовать фреймворк оператора либо через kopf в случае Python, либо, как правило, с помощью Kubebuilder для Go. Документация Kubebuilder также содержит целую страницу о том, как использовать финализаторы, включая пример кода.

Если вы не хотите реализовывать весь Kubernetes Operator, вы также можете построить Mutating Webhook, который описан в документации Dynamic Admission Control. Процесс будет таким же — получение события, обработка бизнес-логики и удаление финализатора.

Заключение

Из этой статьи вы должны сделать вывод, что вам стоит дважды подумать, прежде чем использовать --force --grace-period=0 или удалять финализаторы из ресурсов. Могут быть ситуации, когда игнорировать финализаторы нормально, но ради собственного блага, проведите исследование, прежде чем использовать ядерное решение, и будьте осведомлены о возможных последствиях, поскольку это может скрывать системную проблему в вашем кластере.

Оцените статью
Procodings.ru
Добавить комментарий