Finalizer в Kubernetes

от автора

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

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-инфраструктуре, разработке и не только смотрите в календаре.


ссылка на оригинал статьи https://habr.com/ru/articles/893416/