Что значит Pod Pending и почему это не «ошибка запуска»
Статус Pending означает, что Pod создан в API, но ещё не назначен на конкретную ноду или не перешёл к фактическому запуску. В большинстве реальных инцидентов он даже не доходит до pull образа: проблема возникает на шаге scheduling, когда планировщик (обычно default-scheduler) выбирает узел.
Важно быстро разделить два сценария:
- Pod не назначен на ноду — поле
.spec.nodeNameпустое. Это «чистая» история про scheduling: taints, affinity/selector, requests, квоты. - Pod назначен на ноду, но не стартует — это часто уже не про scheduling (тома, сеть, образ), хотя статус всё ещё может оставаться
Pending. Ниже фокус на первом сценарии, как на самом частом.
Быстрый чеклист диагностики (10 минут до корня)
1) Сразу смотрим события Pod
Почти всегда ответ уже есть в Events: планировщик прямо пишет, почему Pod не подходит ни одной ноде.
kubectl get pod -n NAMESPACE PODNAME -o wide
kubectl describe pod -n NAMESPACE PODNAME
Ищите блок Events и строки вида:
0/3 nodes are available: 3 Insufficient cpu.
0/3 nodes are available: 1 node(s) had taint {key: value}, that the pod didn't tolerate.
0/3 nodes are available: 2 node(s) didn't match Pod's node affinity/selector.
Если Events «пустые» или хотите смотреть их потоково по namespace:
kubectl get events -n NAMESPACE --sort-by=.lastTimestamp
2) Проверяем, на какие ноды вообще можно попасть
kubectl get nodes -o wide
kubectl describe node NODENAME
В describe node нас интересуют: Labels, Taints, состояние (Ready/NotReady), а также Allocated resources — сколько CPU/памяти уже «зарезервировано» запросами.
3) Сверяем требования Pod: selector/affinity, tolerations, requests
kubectl get pod -n NAMESPACE PODNAME -o yaml
Смотрите разделы spec.nodeSelector, spec.affinity, spec.tolerations и resources.requests у контейнеров.
Начинайте с одной строки из Events. Формат «из N нод доступны 0: …» — это не “шум”, а самая короткая дорожка к причине.

Причина №1: Insufficient cpu / memory из-за resource requests
Планировщик опирается не на фактическое потребление, а на requests. Если вы запросили 2 CPU, Pod запланируется только на ноду, где по расчёту осталось минимум 2 CPU свободных по сумме requests всех уже размещённых Pod.
Типичный кейс: кластер «живой», мониторинг показывает низкое реальное потребление CPU, но Pod висит в Pending с Insufficient cpu. Это означает, что CPU уже зарезервирован requests’ами других Pod.
Как быстро подтвердить
Смотрите Events Pod и «Allocated resources» на ноде:
kubectl describe pod -n NAMESPACE PODNAME
kubectl describe node NODENAME
В выводе ноды будет примерно так:
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
cpu 3900m (97%) 6000m (150%)
memory 7Gi (88%) 10Gi (125%)
Если requests по CPU/памяти близки к 100% — планировщик честно не видит места.
Что делать
- Скорректировать requests под реальную потребность (особенно у фоновых/батч задач).
- Добавить ёмкость: больше нод или больше ресурсов на нодах.
- Подумать про HPA/VPA, но сначала убрать заведомо завышенные requests.
Если вы упёрлись в «не хватает железа», проще планировать расширение пула заранее. Частый путь для продакшена — вынести ресурсоёмкие компоненты на отдельные VDS (под ваш контролируемый нод-пул) и масштабировать кластер предсказуемо.
Пример: аккуратные requests/limits
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
Если requests не заданы, планировщик считает их нулевыми — Pod легко «помещается», но дальше повышается риск нестабильности на ноде и eviction при давлении ресурсов.
Причина №2: LimitRange «подкладывает» requests/limits, и вы не замечаете
Pod может попасть в Pending из-за requests, которые вы явно не задавали. Виновник — объект LimitRange в namespace: он способен проставить default requests/limits контейнерам при создании, если вы их не указали.
Как проверить LimitRange
kubectl get limitrange -n NAMESPACE
kubectl describe limitrange -n NAMESPACE LIMITRANGENAME
Затем сравните с тем, что реально оказалось в Pod после admission:
kubectl get pod -n NAMESPACE PODNAME -o jsonpath='{.spec.containers[*].resources}'
Если вы ожидали «без requests», а получили, например, cpu: 1000m — объяснение найдено.
Типичные решения
- Явно задайте разумные
resources.requestsв Deployment/StatefulSet. - Пересмотрите политику LimitRange: дефолты должны соответствовать большинству workload в namespace (и не быть «на всякий случай в 1 CPU»).
Причина №3: ResourceQuota не даёт создать или масштабировать Pod
ResourceQuota ограничивает суммарное потребление ресурсов в namespace: CPU, память, количество Pod, PVC и т. д. При превышении Kubernetes может запретить создание новых Pod или требовать обязательные requests/limits.
Как распознать
Иногда это видно в Events Pod, но чаще — по факту того, что ReplicaSet/Deployment не может создать Pod (поэтому важно смотреть describe у контроллеров).
kubectl get resourcequota -n NAMESPACE
kubectl describe resourcequota -n NAMESPACE QUOTANAME
kubectl describe deploy -n NAMESPACE DEPLOYNAME
kubectl describe rs -n NAMESPACE RSNAME
Ищите сообщения про превышение лимита или требование указать requests/limits.
Что делать
- Увеличить квоту (если вы владелец платформы).
- Уменьшить requests/limits или число реплик.
- Разнести нагрузку по namespace, если политика это допускает.
Причина №4: taints и отсутствующие tolerations
Taints на ноде говорят: «сюда нельзя, если Pod не умеет это терпеть». Tolerations в Pod — явное разрешение. Это частый источник Pending в production: выделенные ноды, системные ноды, GPU-пулы, spot/preemptible и т. п.
Как увидеть taints на нодах
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints
kubectl describe node NODENAME
Событие планировщика часто выглядит так:
0/5 nodes are available: 2 node(s) had taint {dedicated: infra}, that the pod didn't tolerate.
Как правильно добавить toleration
Если политика действительно разрешает запуск на tainted-нодах, добавьте toleration. Пример для taint dedicated=infra:NoSchedule:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "infra"
effect: "NoSchedule"
Важное различие эффектов:
NoSchedule— не планировать новые Pod без toleration.PreferNoSchedule— по возможности не планировать, но допускается при отсутствии альтернатив.NoExecute— дополнительно выселяет уже запущенные Pod без toleration (с возможным grace черезtolerationSeconds).
Не лечите Pending добавлением toleration «на всё подряд». Это ломает изоляцию пулов и быстро превращает кластер в «всё запускается везде».
Причина №5: nodeSelector, nodeName и несовпадающие labels
nodeSelector — жёсткое требование: Pod должен попасть на ноду с указанными label. Ошибка в ключе/значении, смена label после апгрейда или перенос кластера — и вы получаете 0 подходящих нод.
Диагностика
kubectl describe pod -n NAMESPACE PODNAME
kubectl get nodes --show-labels
kubectl get node NODENAME -o jsonpath='{.metadata.labels}'
В Events будет что-то вроде:
0/4 nodes are available: 4 node(s) didn't match Pod's node affinity/selector.
Типовой пример nodeSelector
nodeSelector:
nodepool: "apps"
Проверьте, что на целевых нодах label действительно есть и совпадает по регистру и значению.
Причина №6: nodeAffinity (required vs preferred) и «слишком умные» правила
affinity даёт более гибкие условия, чем nodeSelector. Но из-за гибкости там часто прячутся ошибки: неверные matchExpressions, оператор In с пустым списком, или «required»-правила, которым в реальности не соответствует ни одна нода.
Проверьте requiredDuringSchedulingIgnoredDuringExecution
Если вы используете requiredDuringSchedulingIgnoredDuringExecution, это железобетонное условие. Для отладки иногда полезно временно перевести часть требований в preferredDuringSchedulingIgnoredDuringExecution, чтобы Pod хотя бы запланировался и вы подтвердили гипотезу.
Пример: required affinity по архитектуре и пулу
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: "kubernetes.io/arch"
operator: "In"
values: ["amd64"]
- key: "nodepool"
operator: "In"
values: ["apps"]
Сверьте labels на нодах: если у части нод ключ nodepool отсутствует или называется иначе — Pod будет в Pending бесконечно.

Причина №7: PodAntiAffinity / topology spread — вы сами запретили размещение
Ограничения на «не рядом» (anti-affinity) и распределение по зонам/нодам полезны, но могут привести к тупику: например, когда у вас всего 2 ноды, а правило требует 3 разных хоста. В Events это обычно выглядит как «didn't match pod anti-affinity rules» или «didn't match topology spread constraints».
Проверяйте:
- Количество нод/зон против желаемой топологии.
- Правильность label selector, по которому считаются «соседи».
- Жёсткость правила: required vs preferred.
Причина №8: nodeName уже задан (и это ловушка)
Если в Pod явно прописан spec.nodeName, планировщик не выбирает ноду — вы уже выбрали её сами. Любые проблемы с ресурсами, taints или доступностью этой ноды превращаются в «вечный Pending» без шанса переехать.
Проверьте:
kubectl get pod -n NAMESPACE PODNAME -o jsonpath='{.spec.nodeName}'
Если поле заполнено — разберите, кто его выставил (ручной манифест, кастомный контроллер) и можно ли убрать эту привязку.
Глубже: как читать сообщение планировщика «0/N nodes are available»
Сообщения в Events обычно суммируют причины по нодам. Удобный приём — выписать причины и пройтись по ним по порядку, после каждой правки заново читая Events (причина часто «переключается» на следующую):
- Insufficient cpu/memory — правим
resources.requestsили добавляем ёмкость. - didn't match affinity/selector — сверяем labels и правила
nodeSelector/affinity. - had taint … that the pod didn't tolerate — добавляем точечные
tolerationsили используем правильный пул нод. - exceeded quota — смотрим
ResourceQuotaи контроллер (Deployment/RS).
Мини-шпаргалка: что править в манифесте в зависимости от причины
Insufficient cpu
- Снизить
resources.requests.cpu(если завышен). - Уменьшить число реплик.
- Добавить ноды или увеличить ресурсы нод.
Taints/Tolerations
- Добавить точечный
tolerationsпод конкретный taint. - Либо убрать taint с ноды (если он лишний), но делайте это осознанно.
nodeSelector / affinity
- Исправить ключ/значение label.
- Заменить слишком жёсткий required на preferred (где допустимо).
- Проверить, что label реально присутствует на целевых нодах.
LimitRange / ResourceQuota
- Понять, какие requests/limits выставляются автоматически.
- Согласовать дефолты в namespace с реальными профилями приложений.
- Пересчитать квоты под рост нагрузки.
Практический сценарий: «Pod Pending, а в кластере CPU вроде много»
Самый частый кейс из эксплуатации:
- Pod в Pending.
- Events:
Insufficient cpu. - Мониторинг показывает низкую загрузку CPU по факту.
Алгоритм:
- Открыть
kubectl describe nodeи посмотреть «Allocated resources» по Requests. - Найти workload’ы с завышенными requests (обычно старые «на всякий случай»).
- Снижать requests безопасно: сначала небольшое уменьшение и наблюдение, затем доведение до целевого профиля.
В качестве ориентира для планирования ёмкости и типовых профилей ресурсов может пригодиться материал про выбор конфигурации сервера: как прикинуть CPU и RAM под нагрузку.
Заключение
Pod в Pending — почти всегда прозрачная история, если дисциплинированно читать Events и понимать механику scheduling: фильтры по ресурсам (requests), ограничения по размещению (nodeSelector/affinity), изоляция пулов через taints/tolerations, и политики namespace (LimitRange, ResourceQuota).
Держите цикл «Events → подтверждение на нодах → минимальная правка → снова Events». Так вы быстрее доберётесь до реальной причины и не «перелечите» кластер широкими tolerations или случайным ослаблением affinity.


