Finalizer в Kubernetes

09f311bbfd525ea5938228216a683c78.jpg

Привет, Хабр!

Cегодня рассмотрим механизм Finalizers в Kubernetes. Finalizer — это своего рода последний дозор для Kubernetes‑объектов. Когда мы удаляем ресурс, Kubernetes не просто выкидывает его из кластера мгновенно. Вместо этого применяется двухфазное удаление:

  1. Фаза отметки: на объект добавляется временная метка удаления deletionTimestamp, и если в списке metadata.finalizers присутствуют какие‑либо элементы, удаление блокируется до их удаления.

  2. Фаза завершения: когда все Finalizer’ы убраны, объект окончательно удаляется.

В общем говоря, Finalizer’ы дают возможность контроллерам или другим системам выполнить завершающие операции перед тем, как объект будет стер.

Как работает жизненный цикл Finalizer

При создании объекта можно добавить Finalizer в список метаданных. Это выглядит примерно так в YAML:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  finalizers:
    - my.custom/finalizer
spec:
  containers:
    - name: app
      image: my-image:latest

Добавили кастомный finalizer my.custom/finalizer. Контроллер, ответственный за этот ресурс, должен будет в нужный момент удалить этот finalizer, чтобы завершить процесс удаления.

Когда контроллер завершает свою работу (например, удаляет зависимые ресурсы), он должен обновить объект, убрав finalizer. Для этого можно использовать PATCH‑запрос к API. Приведу пример на Python:

from kubernetes import client, config
from kubernetes.client.rest import ApiException

def remove_finalizer(api_instance, namespace, name, finalizer):
    try:
        # Получаем текущий объект
        obj = api_instance.read_namespaced_pod(name, namespace)
        if finalizer in obj.metadata.finalizers:
            # Удаляем finalizer из списка
            obj.metadata.finalizers.remove(finalizer)
            # Формируем патч
            body = {"metadata": {"finalizers": obj.metadata.finalizers}}
            api_instance.patch_namespaced_pod(name, namespace, body)
            print(f"Finalizer {finalizer} удален для pod {name} в namespace {namespace}")
        else:
            print(f"Finalizer {finalizer} не найден для pod {name}")
    except ApiException as e:
        print("Ошибка при обновлении pod: %s\n" % e)

if __name__ == "__main__":
    config.load_kube_config()  # Загружаем конфигурацию из kubeconfig
    v1 = client.CoreV1Api()
    remove_finalizer(v1, "default", "my-pod", "my.custom/finalizer")

Читаем объект, удаляем из списка finalizers нужный элемент и отправляем PATCH‑запрос.

Если контроллер по каким‑то причинам не смог удалить finalizer (например, из‑за ошибки или если контроллер был отключён), объект остаётся в состоянии »Terminating». Это и есть тот самый зомби, который мы так боимся увидеть в etcd. Объект висит, занимает место в базе данных, а Kubernetes не считает его полностью удалённым. Такая ситуация может привести к накоплению зомби‑подов, что, в свою очередь, негативно скажется на производительности кластера.

Возможные ошибки

Оставленный Finalizer и удалённый контроллер

Одна из классических ошибок — это когда finalizer остаётся у объекта, а сам контроллер уже не работает или был удалён. Например, вы обновили приложение, а старая логика уже не запускается, но finalizer по‑прежнему висит в объектах, не давая им удалиться. Это приводит к ситуации, когда объекты вечно остаются в статусе «Terminating».

Finalizer без idempotent логики

Контроллеры, отвечающие за обработку finalizer’ов, должны быть идемпотентными. Т.е повторное выполнение операции не должно приводить к ошибкам или непредвиденному поведению. Если, например, при удалении зависимого ресурса возникает ошибка, и finalizer не удаляется, то последующие попытки не должны приводить к дубляжу операций или падению всего цикла reconcile. Пример плохой практики:

def faulty_remove_finalizer(api_instance, namespace, name, finalizer):
    # Пытаемся удалить зависимый ресурс без проверки его состояния
    delete_dependent_resource(name)
    # Если зависимый ресурс уже удалён, то здесь может возникнуть ошибка,
    # и finalizer так и останется висеть в объекте.
    patch_finalizer_removal(api_instance, namespace, name, finalizer)

правильный подход:

def safe_remove_finalizer(api_instance, namespace, name, finalizer):
    try:
        if delete_dependent_resource(name):
            # Если ресурс успешно удалён или он уже отсутствует, убираем finalizer
            patch_finalizer_removal(api_instance, namespace, name, finalizer)
        else:
            print(f"Зависимый ресурс для {name} не найден или уже удалён")
    except Exception as e:
        print(f"Ошибка при удалении зависимого ресурса для {name}: {e}")
        # В данном случае finalizer не удаляется, но контроллер может повторить попытку позже.

Важно, чтобы функция delete_dependent_resource обрабатывала ситуацию, когда зависимый ресурс уже отсутствует. Это гарантирует, что повторные вызовы не приведут к ошибкам.

Finalizer, блокирующий GC

Ещё одна проблема возникает, когда finalizer блокирует автоматический сборщик мусора. Если finalizer ожидает завершения какого‑то долгого процесса или зависает в бесконечном цикле, то объект не будет удалён. Такое поведение может привести к тому, что в etcd окажется огромное количество мертвы» объектов, замедляя работу кластера и усложняя диагностику.

Как правильно использовать Finalizer

Реализация удаления зависимых ресурсов

При написании контроллера для кастомных ресурсов убедитесь, что вы:

  • Адекватно определяете зависимости ресурса.

  • Проверяете существование зависимых объектов перед попыткой их удаления.

  • Добавляете таймауты и логирование на случай неудачи.

Например, если ресурс зависит от ConfigMap, можно реализовать логику так:

def delete_configmap(api_instance, namespace, configmap_name):
    try:
        api_instance.delete_namespaced_config_map(configmap_name, namespace)
        print(f"ConfigMap {configmap_name} успешно удален")
        return True
    except ApiException as e:
        if e.status == 404:
            print(f"ConfigMap {configmap_name} уже отсутствует")
            return True
        else:
            print(f"Ошибка при удалении ConfigMap {configmap_name}: {e}")
            return False

Idempotent reconcile-циклы

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

def reconcile_finalizer(api_instance, namespace, name, finalizer):
    # Читаем объект заново для получения актуального состояния
    obj = api_instance.read_namespaced_custom_object(
        group="mygroup.example.com",
        version="v1",
        namespace=namespace,
        plural="myresources",
        name=name
    )
    if obj.get("metadata", {}).get("deletionTimestamp"):
        # Объект уже помечен на удаление, можно безопасно убрать finalizer
        if finalizer in obj["metadata"].get("finalizers", []):
            try:
                # Используем PATCH с корректным списком finalizer'ов
                new_finalizers = [f for f in obj["metadata"]["finalizers"] if f != finalizer]
                body = {"metadata": {"finalizers": new_finalizers}}
                api_instance.patch_namespaced_custom_object(
                    group="mygroup.example.com",
                    version="v1",
                    namespace=namespace,
                    plural="myresources",
                    name=name,
                    body=body
                )
                print(f"Finalizer {finalizer} успешно удален для ресурса {name}")
            except ApiException as e:
                print(f"Ошибка при удалении finalizer {finalizer} для ресурса {name}: {e}")

Баг в контроллере, приводящий к захламлению etcd

Допустим, в нашем кластере внезапно начало скапливаться десятки, сотни, а то и тысячи объектов в статусе »Terminating».

Как диагностировать: начинаем с поиска зависших объектов командой kubectl get pods -A --field-selector metadata.deletionTimestamp!=null — если вывод не радует, значит finalizer’ы не удаляются. Далее заглядываем в логи контроллера, который должен обрабатывать удаление — там часто всплывают ошибки или таймауты. Ну и если дело совсем плохо — посмотрите на метрики etcd: при массовом захламлении начнёт тормозить, а это уже тревожный звоночек.

В экстренных ситуациях можно вручную убрать finalizer’ы. Но будьте осторожны. Это крайняя мера, и её применение должно быть оправдано.

Пример скрипта для удаления finalizer’а вручную:

import yaml
from kubernetes import client, config
from kubernetes.client.rest import ApiException

def force_remove_finalizer(api_instance, namespace, resource_type, name, finalizer):
    # Подготовка патч-запроса
    body = {"metadata": {"finalizers": []}}
    try:
        if resource_type == "pod":
            api_instance.patch_namespaced_pod(name, namespace, body)
        elif resource_type == "customresource":
            # Пример для CRD, параметры group, version, plural надо указать согласно вашему ресурсу
            api_instance.patch_namespaced_custom_object(
                group="mygroup.example.com",
                version="v1",
                namespace=namespace,
                plural="myresources",
                name=name,
                body=body
            )
        print(f"Finalizer {finalizer} принудительно удален для {resource_type} {name} в namespace {namespace}")
    except ApiException as e:
        print(f"Ошибка при принудительном удалении finalizer для {name}: {e}")

if __name__ == "__main__":
    config.load_kube_config()
    v1 = client.CoreV1Api()
    # Например, для pod:
    force_remove_finalizer(v1, "default", "my-pod", "my.custom/finalizer")

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

Что писать в собственных CRD-контроллерах

Если вы пилите свой контроллер под CRD, то обязательно добавляйте finalizer при создании ресурса, если его ещё нет. В reconcile первым делом проверяйте deletionTimestamp — если он есть, запускайте удаление зависимых ресурсов. После успешной очистки — удаляйте finalizer. И да, не забудьте про идемпотентность: код должен спокойно выдерживать повторные вызовы и не паниковать, если объект уже наполовину удалён.

Пример контроллера на Python:

def reconcile_custom_resource(api_instance, resource):
    metadata = resource.get("metadata", {})
    name = metadata.get("name")
    namespace = metadata.get("namespace")
    finalizer = "mygroup.example.com/finalizer"

    # Если объект помечен на удаление
    if metadata.get("deletionTimestamp"):
        # Убедимся, что все зависимые ресурсы удалены
        if clean_dependent_resources(name, namespace):
            # Убираем finalizer
            try:
                new_finalizers = [f for f in metadata.get("finalizers", []) if f != finalizer]
                body = {"metadata": {"finalizers": new_finalizers}}
                api_instance.patch_namespaced_custom_object(
                    group="mygroup.example.com",
                    version="v1",
                    namespace=namespace,
                    plural="myresources",
                    name=name,
                    body=body
                )
                print(f"Finalizer успешно удален для {name}")
            except ApiException as e:
                print(f"Ошибка при удалении finalizer для {name}: {e}")
        else:
            print(f"Зависимые ресурсы для {name} ещё не очищены")
    else:
        # Если объект ещё активен, добавляем finalizer при необходимости
        if finalizer not in metadata.get("finalizers", []):
            try:
                new_finalizers = metadata.get("finalizers", []) + [finalizer]
                body = {"metadata": {"finalizers": new_finalizers}}
                api_instance.patch_namespaced_custom_object(
                    group="mygroup.example.com",
                    version="v1",
                    namespace=namespace,
                    plural="myresources",
                    name=name,
                    body=body
                )
                print(f"Finalizer {finalizer} добавлен для {name}")
            except ApiException as e:
                print(f"Ошибка при добавлении finalizer для {name}: {e}")

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

А какой опыт работы с finalizer у вас? Делитесь в комментариях.

В завершение напоминаю про открытые уроки по K8s, которые пройдут в Otus:

  • 26 марта: Деплой ASP.NET приложений в Kubernetes.
    После вебинара вы сможете запустить собственное полное ASP.NET приложение в среде Kubernetes. Записаться

  • 3 апреля: Управления приложениями в Kubernetes.
    Цель урока — научиться управлять приложениями в Kubernetes с помощью командной строки и YAML-манифестов. Записаться

Больше открытых уроков по IT-инфраструктуре, разработке и не только смотрите в календаре.

© Habrahabr.ru