Finalizer в Kubernetes

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