← Kubernetes: разбор поломок
7 мин · средне · Урок 3 из 12

Pod в Pending из-за CPU

Что означает FailedScheduling, чем requests отличаются от limits и почему нода с 4 ядрами не даст 4 ядра.

Pod создан, но не запускается. Минуту, пять, час - статус не меняется.

NAME    READY  STATUS    RESTARTS  AGE
heavy   0/1    Pending   0         2m

Pending означает «объект в API есть, контейнера ещё нет». Между этими двумя точками стоит scheduler - компонент, который выбирает ноду для Pod. Если подходящей ноды нет, Pod будет ждать сколько угодно долго, без ошибок и без рестартов.

Из-за чего Pod застревает в Pending

  1. Scheduler не нашёл ноду. Не хватает ресурсов, мешают taints, не сошлись affinity или nodeSelector.
  2. Образ ещё тянется. Долгий pull большого образа.
  3. Том не нашёлся. Pod просит PVC, который ещё не привязан к PersistentVolume.
  4. Init-контейнер работает. Pod ждёт его завершения.

Первый пункт - самый частый, и он же самый непонятный на вид: с Pod всё в порядке, просто его некуда положить.

Что показывает диагностика

kubectl describe pod heavy

Главная секция Events. При проблеме со scheduling там видно:

Events:
  Type     Reason            Message
  Warning  FailedScheduling  0/1 nodes are available: 1 Insufficient cpu.

Формат сообщения всегда одинаковый:

Типичные причины:

ТекстЧто значит
Insufficient cpuСумма requests.cpu существующих Pod плюс новый больше, чем свободно на ноде
Insufficient memoryТо же самое про память
node(s) had untolerated taintНа нодах есть taint, у Pod нет соответствующего toleration
node(s) didn't match Pod's node affinitynodeSelector или affinity не сошёлся
node(s) didn't have free portshostPort уже занят
pod has unbound immediate PersistentVolumeClaimsPVC не привязан

Requests против limits

Различие тут решающее.

requests - сколько Pod гарантированно получит. Scheduler смотрит только на requests, когда выбирает ноду. Сумма requests.cpu всех Pod на ноде не может превысить allocatable.

limits - потолок, выше которого контейнер не поднимется во время работы. На выбор ноды limits не влияют вообще.

Если Pod просит requests.cpu: 64, scheduler ищет ноду, где свободно минимум 64 vCPU. В кластере из нод по 4 ядра такой ноды не будет никогда, и Pod останется в Pending навсегда.

Значение requests больше, чем allocatable самой большой ноды, не запланируется никогда. Ошибки при этом нет: Pod просто висит в Pending, пока его не удалят.

Единицы CPU и памяти

CPU:

m это millicore, тысячная доля ядра. К мегагерцам отношения не имеет.

Память:

Без суффикса для CPU подразумеваются целые ядра, для памяти байты. Поэтому cpu: 64 читается как «дай мне 64 ядра целиком», а cpu: 64m это шестьдесят четыре тысячных ядра. Одна буква меняет запрос в тысячу раз.

Capacity против allocatable

kubectl describe node показывает два разных числа:

Capacity:
  cpu:                4
  memory:             8Gi
Allocatable:
  cpu:                3800m
  memory:             7Gi

Capacity - сколько у ноды физически. Allocatable меньше: часть зарезервирована под kubelet, kube-proxy и системные демоны. Scheduler считает по allocatable.

Ниже в том же выводе идёт то, что уже занято:

Allocated resources:
  Resource           Requests   Limits
  cpu                500m (13%) 1 (26%)
  memory             256Mi (3%) 512Mi (7%)

Свободное место считается как Allocatable - Allocated (Requests). Если новый Pod просит больше, scheduler отвечает Insufficient.

allocatable  3800m  ████████████████████
занято        500m  ███
свободно     3300m  █████████████████
Pod просит  64000m  не влезает

Как чинят

Путей два.

Первый: уменьшить requests до реального потребления. Ориентиры грубые, но рабочие: 100m для лёгкого сервиса, 500m для среднего, 2 для тяжёлого. Точные цифры берут из метрик, например из kubectl top pod на работающей копии.

Второй: добавить ноду. В облаке это делает cluster autoscaler, который как раз реагирует на Pod в состоянии Pending.

Отдельная тонкость: resources у существующего Pod изменить нельзя, поле иммутабельное. Самостоятельный Pod приходится удалять и применять заново:

kubectl delete pod heavy
kubectl apply -f heavy.yaml   # тот же манифест, но с адекватным requests.cpu

Если Pod создан Deployment, правят шаблон Deployment, и Pod пересоздаются сами.

Полезные команды при разборе:

# Причина в конце describe
kubectl describe pod heavy | tail -10

# Сколько на ноде занято
kubectl describe node | grep -A5 "Allocated resources"

# Сколько просит Pod
kubectl get pod heavy -o jsonpath='{.spec.containers[0].resources}'

Как не допустить

Requests ставят по фактическому потреблению, а не «с запасом на всякий случай». Завышенные requests съедают место на нодах, и кластер упирается в лимиты, будучи загруженным на 20 процентов.

В namespace ставят LimitRange с дефолтными и максимальными значениями: тогда Pod без requests получит разумные числа, а Pod с cpu: 64 отвалится сразу на admission, с внятной ошибкой вместо тихого Pending.

Коротко

Прошли материал до конца? Отметьте урок пройденным.