Привет, Хабр!
Cегодня рассмотрим механизм Finalizers в Kubernetes. Finalizer — это своего рода последний дозор для Kubernetes‑объектов. Когда мы удаляем ресурс, Kubernetes не просто выкидывает его из кластера мгновенно. Вместо этого применяется двухфазное удаление:
-
Фаза отметки: на объект добавляется временная метка удаления
deletionTimestamp
, и если в спискеmetadata.finalizers
присутствуют какие‑либо элементы, удаление блокируется до их удаления. -
Фаза завершения: когда все 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/
Добавить комментарий