kai.kzLab

Evicted: почему kubelet выселяет поды и что значит «The node was low on resource»

Команда KAI Lab · · 8 мин чтения

Статус Evicted значит, что kubelet сам остановил под, чтобы спасти ноду: на ней заканчивалась память, место на диске, inode или идентификаторы процессов. Под переходит в фазу Failed и на этой ноде больше не запускается, а Deployment или StatefulSet создаёт замену; первыми выселяются поды, которые потребляют больше своих requests.

Выселение — защитный механизм, а не сбой Kubernetes. Но частые выселения значат, что на нодах кончаются ресурсы, и следующим шагом может стать нода, которая перестала отвечать. Разберём, как прочитать причину, найти виновника и настроить кластер так, чтобы выселения были редкостью.

Что такое node-pressure eviction

kubelet следит за ресурсами ноды: памятью, местом и inode на файловых системах, числом процессов. Когда ресурс опускается ниже порога, kubelet сначала пытается освободить его сам — удаляет завершённые контейнеры и неиспользуемые образы. Если не помогло, он выбирает поды, переводит их в фазу Failed и останавливает.

Важно отличать это от выселения через API, которое делает kubectl drain:

  • при node-pressure eviction kubelet не смотрит на PodDisruptionBudget;
  • при жёстком пороге под останавливается сразу, с периодом завершения 0 секунд, без учёта terminationGracePeriodSeconds;
  • при мягком пороге kubelet ждёт заданное время и даёт поду не больше eviction-max-pod-grace-period.

Пороги по умолчанию для жёсткого выселения из документации Kubernetes:

СигналПорог по умолчаниюЧто значит
memory.availableменьше 100Mi (Linux)свободная память ноды
nodefs.availableменьше 10%место на основной файловой системе, где /var/lib/kubelet и логи
imagefs.availableменьше 15%место на файловой системе образов, если она отдельная
nodefs.inodesFreeменьше 5% (Linux)свободные inode на основной файловой системе
imagefs.inodesFreeменьше 5% (Linux)свободные inode на файловой системе образов

Память kubelet считает по cgroup, а не по free -m, поэтому цифры могут не совпадать с тем, что вы видите в top. Проверка порогов идёт с интервалом housekeeping-interval, по умолчанию раз в 10 секунд.

Как выглядит Evicted

$ kubectl get pods -n shop
NAME                            READY   STATUS    RESTARTS   AGE
report-worker-6d8f7c9b5-4xk2p   0/1     Evicted   0          3h
report-worker-6d8f7c9b5-9m2wq   1/1     Running   0          2m

Выселенный под остаётся в списке как объект в фазе Failed, рядом появляется замена. В новых версиях Kubernetes в колонке STATUS такой под может показываться как Error или ContainerStatusUnknown: kubectl выводит причину завершения контейнера поверх причины пода. Надёжнее смотреть поле .status.reason и сообщение:

$ kubectl describe pod report-worker-6d8f7c9b5-4xk2p -n shop
...
Status:           Failed
Reason:           Evicted
Message:          The node was low on resource: memory. Threshold quantity: 100Mi, available: 87604Ki. Container worker was using 1823952Ki, request is 256Mi, has larger consumption of memory.

Все выселенные поды кластера с нодами:

$ kubectl get pods -A --field-selector=status.phase=Failed \
    -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,REASON:.status.reason,NODE:.spec.nodeName
NAMESPACE   NAME                            REASON    NODE
shop        report-worker-6d8f7c9b5-4xk2p   Evicted   worker-3

Как читать сообщение

Сообщение собирает kubelet, и в нём три части:

  • The node was low on resource: memory — какой ресурс кончился: memory, ephemeral-storage (место на диске), inodes или pids;
  • Threshold quantity: 100Mi, available: 87604Ki — порог и сколько было свободно в момент решения;
  • Container worker was using 1823952Ki, request is 256Mi — какой контейнер потреблял больше своего request. Такие контейнеры kubelet выселяет в первую очередь.

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

Бывают выселения и без давления на ноду: kubelet выселяет под, который превысил собственный лимит диска. Тогда в сообщении Container worker exceeded its local ephemeral storage limit "2Gi". или Usage of EmptyDir volume "cache" exceeds the limit "500Mi". Нода в порядке, виноват конкретный под.

Пошаговая диагностика

Шаг 1. Состояние ноды

$ kubectl describe node worker-3
...
Conditions:
  Type             Status  Reason                       Message
  ----             ------  ------                       -------
  MemoryPressure   False   KubeletHasSufficientMemory   kubelet has sufficient memory available
  DiskPressure     True    KubeletHasDiskPressure       kubelet has disk pressure
  PIDPressure      False   KubeletHasSufficientPID      kubelet has sufficient PID available
  Ready            True    KubeletReady                 kubelet is posting ready status
Taints:             node.kubernetes.io/disk-pressure:NoSchedule
...
Events:
  Type     Reason                Age   From     Message
  ----     ------                ----  ----     -------
  Warning  EvictionThresholdMet  3m    kubelet  Attempting to reclaim ephemeral-storage

Колонки с временем мы опустили. DiskPressure True и taint node.kubernetes.io/disk-pressure:NoSchedule значат, что новые поды на эту ноду не планируются: вот откуда берутся соседние поды в Pending. Состояние меняется обратно не сразу: kubelet выжидает eviction-pressure-transition-period, по умолчанию 5 минут, чтобы нода не мигала.

Шаг 2. Кто съел память

$ kubectl top pod -A --sort-by=memory

Сравните потребление с requests. Под без requests относится к классу BestEffort и первым идёт на выселение, как и Burstable поды, которые потребляют больше запрошенного. QoS-класс пода показывает kubectl get pod ИМЯ -o jsonpath='{.status.qosClass}'.

Если память растёт быстрее, чем kubelet успевает её заметить, раньше выселения сработает OOM killer ядра, и вместо Evicted вы увидите OOMKilled. Документация Kubernetes описывает это как известную проблему. Чем они отличаются, мы разобрали в статье OOMKilled и exit code 137.

Шаг 3. Кто съел диск

На самой ноде:

$ df -h /var/lib/kubelet /var/lib/containerd
$ df -i /var/lib/kubelet
$ sudo du -sh /var/log/pods/* | sort -h | tail -5
$ sudo du -sh /var/lib/kubelet/pods/*/volumes/kubernetes.io~empty-dir/* | sort -h | tail -5
$ sudo crictl images

Типичные источники: временные файлы, которые приложение пишет в свой слой контейнера или в emptyDir; накопившиеся образы; логи, которые приложение пишет в файлы внутри контейнера. Логи stdout и stderr kubelet ротирует сам: по умолчанию до 10Mi на файл и 5 файлов на контейнер. Образы kubelet начинает чистить, когда диск занят на 85%, и чистит до 80%. Неиспользуемые образы можно убрать и вручную: sudo crictl rmi --prune.

Если нода работает на Docker Engine или вы видите ту же проблему на обычном сервере с Docker, разбор команд — в статье no space left on device в Docker.

Шаг 4. Почему выбрали именно этот под

kubelet сортирует кандидатов по трём признакам: превышает ли потребление requests, приоритет пода и насколько потребление больше requests. Первыми уходят BestEffort и Burstable поды, вышедшие за свои requests. Последними — Guaranteed и Burstable поды, которые укладываются в requests. При нехватке диска kubelet выбирает поды по занятому месту: локальные тома, логи и слой контейнера.

Причины Evicted: таблица

ПричинаКак узнатьКак исправить
На ноде кончается память, под ест больше requestslow on resource: memory, Container … was using …, request is …requests по реальному потреблению, limits, больше памяти на нодах
Диск ноды забит образами, логами, данными подовlow on resource: ephemeral-storage, DiskPressure Trueочистка образов, ротация логов приложения, requests и limits на ephemeral-storage
Кончились inodelow on resource: inodesнайти каталог с миллионами мелких файлов и чистить его
Под превысил свой лимит дискаexceeded its local ephemeral storage limitподнять лимит или писать данные в PersistentVolume
emptyDir вырос больше sizeLimitUsage of EmptyDir volume … exceeds the limit …чистить временные файлы, пересмотреть sizeLimit
Кончились идентификаторы процессовlow on resource: pids, PIDPressure Trueнайти утечку процессов или потоков, задать лимит PID на под

Как не допускать выселений

  • Задавайте requests по реальному потреблению. Под без requests — первый кандидат на выселение.
  • Критичным сервисам ставьте requests равными limits по памяти и CPU: класс Guaranteed выселяется последним. Для ещё большей защиты — PriorityClass.
  • Задавайте requests и limits для ephemeral-storage и sizeLimit для emptyDir, если под пишет временные файлы.
  • Не отдавайте подам всю ноду: резервы system-reserved и kube-reserved оставляют память системе и kubelet.
  • Мониторьте состояния нод MemoryPressure, DiskPressure, PIDPressure и заполненность дисков, а не только выселения.
  • Меняйте пороги осторожно. По документации, если задать в evictionHard только один порог, остальные станут нулевыми, а не останутся по умолчанию. Чтобы они унаследовали значения по умолчанию, в конфигурации kubelet включают MergeDefaultEvictionSettings.

Выселенные поды остаются объектами, чтобы их можно было изучить. Удалить их в пространстве имён можно одной командой, но она удалит и упавшие поды Job, которые вы, возможно, ещё не разобрали:

$ kubectl delete pods -n shop --field-selector=status.phase=Failed

Сборщик завершённых подов в kube-controller-manager удаляет их сам, когда их больше порога --terminated-pod-gc-threshold, по умолчанию 12 500.

Частые вопросы

Evicted под перезапустится? Сам этот под — нет, он остаётся в фазе Failed. Если под создан Deployment, StatefulSet или другим контроллером, тот создаст замену, и планировщик разместит её там, где хватает ресурсов. Под, созданный вручную без контроллера, просто исчезнет из работы.

Почему выселили мой под, а не тот, кто съел всю память? kubelet сначала выбирает поды, которые превышают свои requests. Если у «жадного» пода requests большие, а у вашего requests нет вовсе, первым уйдёт ваш. Защита — честные requests и нужный приоритет.

Помогает ли PodDisruptionBudget? Нет. PodDisruptionBudget учитывается при выселении через API, например при kubectl drain. При выселении из-за нехватки ресурсов kubelet его не смотрит, как и terminationGracePeriodSeconds при жёстком пороге.

Чем Evicted отличается от OOMKilled? Evicted — решение kubelet по всему поду из-за нехватки ресурсов на ноде; под не перезапускается на этой ноде. OOMKilled — ядро убило контейнер за превышение его собственного лимита памяти, и kubelet перезапускает контейнер в том же поде.

Как понять, что под выселили, если в STATUS написано Error? Посмотрите kubectl describe pod или поле .status.reason: у выселенного пода там Evicted, а в Message — ресурс и порог. У такого пода также есть условие DisruptionTarget с причиной TerminationByKubelet. Вопросы про QoS-классы и порядок выселения регулярно задают на собеседованиях по Kubernetes.

Потренироваться на практике

В симуляторе DevOps KAI Lab месяц про Kubernetes включает работу с ресурсами подов и обслуживание нод, а консоль в браузере отдаёт реальные выводы kubectl describe. Проверить себя можно в бесплатном тесте навыков DevOps.

Источники

  • Kubernetes. Node-pressure Eviction — сигналы, пороги по умолчанию, мягкие и жёсткие пороги, порядок выбора подов, состояния нод, MergeDefaultEvictionSettings, известные проблемы.
  • Kubernetes. Pod Quality of Service Classes — Guaranteed, Burstable, BestEffort.
  • Kubernetes. Taints and Tolerations — taints по состоянию ноды.
  • Kubernetes. Disruptions — условие DisruptionTarget и причина TerminationByKubelet.
  • Kubernetes. Kubelet Configuration (v1beta1) — containerLogMaxSize, containerLogMaxFiles, пороги очистки образов, evictionPressureTransitionPeriod.
  • Kubernetes. kube-controller-manager — --terminated-pod-gc-threshold.
  • Kubernetes, исходный код kubelet: eviction/helpers.go — текст сообщения о выселении; printers.go — как kubectl выбирает значение STATUS.

Читать дальше