Планировщик Kubernetes: как поды распределяются по нодам и как управлять этим в 2026 | AdminWiki

Планировщик Kubernetes: как поды распределяются по нодам и как управлять этим в 2026

26 сентября 2026 13 мин. чтения

kube-scheduler решает одну задачу: найти ноду, на которой под может запуститься, и записать эту привязку в API-сервер. Решение принимается в два прохода. Фильтрация (Filter) отбрасывает ноды, которые не подходят по ресурсам, меткам, taints и топологии. Скоринг (Score) ранжирует оставшихся кандидатов по сумме баллов от плагинов. Побеждает нода с максимальным баллом, после чего планировщик вызывает Bind.

Размещением подов управляют декларативно, без правки кода планировщика: requests в манифесте, метки нод, nodeAffinity, podAntiAffinity, taints и topologySpreadConstraints задают, куда под попадёт и куда не попадёт никогда. Место планировщика среди компонентов control plane и путь пода после привязки разобраны в статье Архитектура Kubernetes: компоненты кластера и путь пода от манифеста до запуска.

Дальше по шагам: цикл планирования, готовые манифесты для привязки к нодам, работа с taints, ресурсами и топологией, плюс чек-лист диагностики, если под завис в Pending.

Как kube-scheduler выбирает ноду: этапы фильтрации и скоринга

Планирование идёт по этапам фреймворка, каждый этап обслуживают плагины. Порядок такой:

  1. Планировщик достаёт под из очереди (activeQ) и заводит для него цикл планирования.
  2. PreFilter и Filter: ноды, не прошедшие проверки по ресурсам, affinity, taints и топологии, отсеиваются. Остаётся список кандидатов.
  3. PostFilter: если кандидатов нет, включается вытеснение. Под с высоким priority может вытеснить поды с низким priority на подходящей ноде.
  4. Score: каждый плагин выставляет нодам-кандидатам баллы, планировщик складывает их с учётом весов.
  5. Reserve, Permit, PreBind и Bind: лучшая нода резервируется, затем привязка записывается в API-сервер. kubelet на этой ноде видит под и запускает контейнеры.

При выборе ноды планировщик сравнивает requests подов с allocatable ноды. Limits в расчёте не участвуют. Под с requests.cpu=2 не встанет на ноду, где allocatable.cpu=1.5, даже если реальная загрузка этой ноды близка к нулю.

Фильтрация: какие ноды отсеиваются сразу

Этап Filter выполняют несколько плагинов, каждый проверяет своё условие:

  • NodeResourcesFit: сумма requests всех подов ноды плюс requests нового пода не должна превышать allocatable по CPU, памяти и ephemeral-storage.
  • NodeAffinity: проверяет правила requiredDuringSchedulingIgnoredDuringExecution из spec.affinity.nodeAffinity и поле nodeSelector.
  • TaintToleration: у пода должно быть toleration для каждого taint ноды с эффектом NoSchedule и NoExecute.
  • NodeUnschedulable: нода, переведённая в cordon (spec.unschedulable=true), выпадает из кандидатов.
  • PodTopologySpread: при whenUnsatisfiable: DoNotSchedule нода отбрасывается, если размещение нарушит maxSkew.
  • InterPodAffinity, NodeName, NodePorts, VolumeBinding, NodeVolumeLimits: правила совместного размещения, привязка к имени ноды, конфликты портов и томов.

Пример: на ноде стоит taint node-role.kubernetes.io/control-plane:NoSchedule, а в манифесте пода нет соответствующего toleration. Нода отсеивается, и в событиях пода появляется строка node(s) had untolerated taint.

Посмотреть состояние ноды целиком: kubectl describe node worker-1. В выводе есть Taints, Capacity, Allocatable и таблица Allocated resources с занятыми requests.

Скоринг: как выбирается лучшая нода из оставшихся

На этапе Score каждый плагин выставляет ноде-кандидату балл от 0 до 100, итоговое значение получается суммированием с весами. Веса плагинов Score по умолчанию: TaintToleration — 3; NodeAffinity, InterPodAffinity, PodTopologySpread и DynamicResources — 2; остальные плагины — 1. Эти значения приведены по разбору kube-scheduler в блоге Kubesimplify, а не по официальной документации Kubernetes, поэтому набор плагинов и веса могут отличаться в вашей версии — перед изменением профиля сверяйтесь с документацией своего релиза.

NodeResourcesFit в режиме LeastAllocated отдаёт баллы свободным нодам, NodeResourcesBalancedAllocation поощряет ноды, где доли занятых CPU и памяти близки друг к другу. ImageLocality добавляет балл ноде, в локальном кеше которой уже лежит образ пода. Пример: две ноды с одинаковыми свободными ресурсами, но на одной образ nginx:1.27 уже загружен. Такая нода получит больше баллов, и контейнер стартует быстрее.

Параметры отдельных плагинов задают в KubeSchedulerConfiguration (apiVersion kubescheduler.config.k8s.io/v1, kind KubeSchedulerConfiguration) через profiles и pluginConfig: например, стратегию scoringStrategy для NodeResourcesFit или args для PodTopologySpread. Каждый профиль имеет связанное имя планировщика и может иметь собственный набор плагинов. Версия v1beta3 объявлена устаревшей в v1.26 и удалена в v1.29, поэтому конфигурацию нужно держать на актуальной версии API. Прямого подтверждения вывода из эксплуатации старого policy API в доступных источниках нет, но менять поведение планировщика рекомендуется требованиями в манифестах и параметрами плагинов в актуальной KubeSchedulerConfiguration.

Синтаксис и поля конфигурации планировщика описаны в официальной документации: Scheduler Configuration и kube-scheduler Configuration (v1).

nodeSelector и nodeAffinity: как привязать поды к конкретным нодам

nodeSelector выбирает ноды только по точному совпадению меток и работает как жёсткое требование. nodeAffinity поддерживает операторы In, NotIn, Exists, DoesNotExist, Gt, Lt и два режима: requiredDuringSchedulingIgnoredDuringExecution и preferredDuringSchedulingIgnoredDuringExecution.

Пример манифеста с nodeSelector

apiVersion: v1
kind: Pod
metadata:
  name: nginx-nodeselector
spec:
  containers:
  - name: nginx
    image: nginx:1.27
    resources:
      requests:
        cpu: 250m
        memory: 256Mi
  nodeSelector:
    disktype: ssd

Под попадёт только на ноды с меткой disktype=ssd. Назначить метку: kubectl label nodes worker-1 disktype=ssd. Изменить значение уже существующей метки: добавить флаг --overwrite. Снять метку: kubectl label node worker-1 disktype-. Проверить набор меток: kubectl get nodes --show-labels.

Если ноды с такой меткой нет, под останется в Pending с событием node(s) didn't match node selector. Обходной путь планировщик искать не станет.

Пример манифеста с nodeAffinity

apiVersion: v1
kind: Pod
metadata:
  name: nginx-nodeaffinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/os
            operator: In
            values:
            - linux
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - eu-west-1a
            - eu-west-1b
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
          - key: gpu
            operator: In
            values:
            - "true"
  containers:
  - name: nginx
    image: nginx:1.27

Required-блок отсекает всё, кроме нод Linux в зонах eu-west-1a и eu-west-1b. Preferred-блок с weight 80 добавляет баллы нодам с меткой gpu=true, но не мешает запуску, если таких нод нет. Значение метки "true" берём в кавычки: в YAML без кавычек это булево значение, а метки принимают только строки.

Операторы Gt и Lt доступны в matchExpressions наравне с In, NotIn, Exists и DoesNotExist. Ограничения на их сочетание с NotIn внутри одних nodeSelectorTerms в доступных источниках не подтверждены, поэтому не полагайтесь на такое сочетание без проверки в вашей версии Kubernetes. Если required-правила не выполняются ни на одной ноде, под уходит в Pending с сообщением node(s) didn't match Pod's node affinity/selector.

Синтаксис Pod и Deployment, включая блоки affinity и resources, разобран в гайде Манифесты Kubernetes 2026: структура YAML, примеры Pod и Deployment. Полный перечень операторов и примеры правил приведены в официальной документации Assigning Pods to Nodes.

Поле spec.nodeName привязывает под к конкретной ноде в обход планировщика. Такой под не проходит проверки ресурсов, taints и affinity, поэтому для продакшена способ не подходит.

podAffinity и podAntiAffinity: управление совместным размещением подов

podAffinity притягивает под к нодам, где уже работают поды с нужными метками. podAntiAffinity действует наоборот и разводит поды по разным нодам или зонам. Обязательный параметр topologyKey задаёт домен: kubernetes.io/hostname ограничивает правило уровнем ноды, topology.kubernetes.io/zone уровнем зоны. По умолчанию правила видят только поды своего namespace, для остальных нужны поля namespaces или namespaceSelector.

affinity:
  podAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchLabels:
          app: cache
      topologyKey: kubernetes.io/hostname

Под с этим фрагментом разместится только на ноде, где уже есть под app=cache. Если кеша на ноде нет, под будет ждать в Pending до его появления. Для большинства задач жёсткий вариант избыточен: preferredDuringSchedulingIgnoredDuringExecution с weight даёт похожий эффект без риска вечного Pending.

Пример манифеста с podAntiAffinity для распределения реплик

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: web
            topologyKey: kubernetes.io/hostname
      containers:
      - name: web
        image: nginx:1.27
        resources:
          requests:
            cpu: 200m
            memory: 256Mi

Каждая реплика получит отдельную ноду. В кластере из двух воркеров третий под останется в Pending с событием node(s) didn't match pod anti-affinity rules. Проверить размещение: kubectl get pods -l app=web -o wide. Метки в labelSelector берутся из самого пода, а не из Deployment, поэтому шаблон обязан содержать labels app: web.

Суффикс IgnoredDuringExecution означает, что правило действует только на этапе планирования. Смена метки у уже запущенного пода размещение не изменит.

Taints и tolerations: как запретить или разрешить размещение на нодах

Taint на ноде отталкивает поды, у которых нет подходящего toleration. Эффектов три:

ЭффектПоведениеТипичное применение
NoScheduleНовые поды без toleration не размещаютсяНоды под GPU, control-plane
PreferNoScheduleПланировщик избегает ноды, но разместит под, если свободных мест нетМягкая разгрузка ноды
NoExecuteВытесняет уже запущенные поды без tolerationВывод ноды из эксплуатации

Назначить taint: kubectl taint nodes worker-gpu-1 nvidia.com/gpu=true:NoSchedule. Снять: тот же вызов с дефисом в конце, kubectl taint nodes worker-gpu-1 nvidia.com/gpu=true:NoSchedule-. Посмотреть: kubectl describe node worker-gpu-1 | grep Taints.

tolerations:
- key: nvidia.com/gpu
  operator: Equal
  value: "true"
  effect: NoSchedule

Для эффекта NoExecute полезен tolerationSeconds: он задаёт, сколько секунд под может оставаться на ноде после появления taint. Без этого параметра под вытесняется сразу, с tolerationSeconds: 300 получит пять минут на корректное завершение.

tolerations:
- key: dedicated
  operator: Exists
  effect: NoExecute
  tolerationSeconds: 300

Operator Exists совпадает с любым значением taint по этому ключу, поэтому поле value не нужно. Пустой key с operator Exists и конкретным effect делает под толерантным ко всем taints этого эффекта, такой приём пригодится демонсетам.

Ресурсы, лимиты и QoS: как requests влияют на планирование

Планировщик смотрит только на requests. Если сумма requests подов на ноде плюс requests нового пода превышает allocatable, нода отсеивается с сообщением Insufficient cpu или Insufficient memory. Пример: нода с allocatable.cpu=4, на ней поды с суммарными requests.cpu=3000m, новый под просит 2000m. Мест нет, под ждёт в Pending.

Проверить занятое и свободное: kubectl describe node worker-1 | grep -A 8 "Allocated resources". Посмотреть requests конкретного пода: kubectl get pod web-1 -o jsonpath='{.spec.containers[*].resources}'.

QoS-класс определяется соотношением requests и limits и влияет на порядок вытеснения при нехватке ресурсов на ноде:

QoSУсловиеПоведение при давлении на ноду
GuaranteedУ всех контейнеров requests равны limits по CPU и памятиВытесняется последним, подходит для latency-критичных сервисов
BurstableRequests заданы, но хотя бы у одного контейнера limits выше requestsВытесняется после BestEffort, если превышает свои requests
BestEffortНи requests, ни limits не заданыВытесняется первым

Пустые requests создают лишний риск: планировщик считает такой под нулевым по ресурсам и набивает ноду до отказа, а при давлении первым вытесняет именно его. Указывайте requests всегда, а limits ставьте там, где нужен предел потребления. Как requests и limits отражаются на загрузке нод и где искать перерасход, разобрано в статье Kubernetes и эффективность кластера: как оценить загрузку нод и подов без перерасхода ресурсов.

Для CPU работает ещё один механизм: kubelet ограничивает потребление контейнера значением limits через cgroup, поэтому throttling появляется даже при свободных ядрах. Метрики и настройка лимитов собраны в материале Производительность Kubernetes: метрики, лимиты и тюнинг кластера в 2026.

topologySpreadConstraints: равномерное распределение по зонам и нодам

topologySpreadConstraints контролируют расхождение в количестве подов между доменами. Параметр maxSkew задаёт допустимую разницу, topologyKey определяет домен, whenUnsatisfiable выбирает режим: DoNotSchedule запрещает размещение при нарушении, ScheduleAnyway разрешает с меньшим приоритетом.

spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: web
  containers:
  - name: web
    image: nginx:1.27

С maxSkew: 1 в трёх зонах четыре реплики распределятся как 2/1/1. Пятая сделает расклад 2/2/1, шестая 2/2/2. Разница между доменами не превысит единицу.

Отличие от podAntiAffinity в характере правила. Anti-affinity с topologyKey kubernetes.io/hostname разводит два пода с одинаковой меткой по разным нодам всегда, а spread-констрейнт допускает перекос в пределах maxSkew и потому реже приводит к Pending. В кластере из трёх нод три реплики web с ScheduleAnyway встанут по одной на ноду, четвёртая ляжет на любую свободную.

Параметры minDomains, nodeAffinityPolicy и nodeTaintsPolicy уточняют поведение. minDomains задаёт минимальное число доменов: значение должно быть больше 0 и указывается только вместе с whenUnsatisfiable: DoNotSchedule. nodeAffinityPolicy решает, как учитывать nodeAffinity и nodeSelector при расчёте перекоса: Honor включает в расчёт только подходящие ноды, Ignore — все ноды, а при null поведение эквивалентно Honor. nodeTaintsPolicy определяет учёт taints: Honor включает ноды, для которых у пода есть toleration, Ignore игнорирует taints и включает все ноды, а при null поведение эквивалентно Ignore.

Полное описание полей и примеры приведены в официальной документации Pod Topology Spread Constraints.

Диагностика Pending: чек-лист и типовые причины

Под в Pending означает, что планировщик не нашёл нод, прошедших Filter. Ответ почти всегда лежит в событиях пода.

Чек-лист команд для быстрой диагностики

  1. kubectl describe pod web-1 | grep -A 10 Events. Строка FailedScheduling содержит список причин после двоеточия, например 0/3 nodes are available.
  2. kubectl get events --field-selector reason=FailedScheduling. Покажет то же самое по всем подам сразу.
  3. kubectl describe node worker-1 | grep -A 8 "Allocated resources". Сверьте requests с allocatable.
  4. kubectl describe node worker-1 | grep Taints. Проверьте, не мешает ли taint.
  5. kubectl get nodes --show-labels. Убедитесь, что метки для nodeSelector и nodeAffinity существуют.
  6. kubectl get nodes. Статус Ready и отсутствие SchedulingDisabled закрывают вопрос с cordon.
  7. kubectl get pods --show-labels. Проверьте метки, которые ищут podAffinity и podAntiAffinity.
  8. kubectl logs -n kube-system kube-scheduler-<имя пода> --tail=100. Логи планировщика доступны, когда control plane под вашим управлением.

Типовые сообщения и решения:

СообщениеПричинаРешение
Insufficient cpu, Insufficient memoryСумма requests превышает allocatableСнизить requests, добавить ноды, включить Cluster Autoscaler
node(s) had untolerated taintУ пода нет toleration для taintДобавить toleration или снять taint
node(s) didn't match node selectorМетки нод не совпалиПроверить kubectl get nodes --show-labels и назначить метку
node(s) didn't match Pod's node affinity/selectorRequired-правило nodeAffinity не выполняетсяРасширить список значений или сменить режим на preferred
node(s) didn't match pod anti-affinity rulesСвободных доменов для размещения нетСмягчить правило до preferred, добавить ноды
node(s) didn't match pod topology spread constraintsmaxSkew нарушенЗаменить DoNotSchedule на ScheduleAnyway, добавить домены
node(s) were unschedulableНода в cordonkubectl uncordon worker-1
pod has unbound immediate PersistentVolumeClaimsPVC не привязан или том в другой зонеПроверить StorageClass и привязку тома к зоне

Отдельная причина ожидания: preemption и PriorityClass. Если у пода задан высокий priority, планировщик на этапе PostFilter ищет ноду, с которой можно вытеснить поды с низким priority. В событиях появляются строки про Preempting, а под остаётся Pending, пока вытесненные поды завершаются. Класс указывают в spec.priorityClassName, а поле preemptionPolicy: Never отключает вытеснение других подов для этого класса.

Проверка результата: как убедиться, что поды разместились правильно

Быстрая проверка занимает три команды:

  • kubectl get pods -o wide: колонка NODE показывает фактическое размещение.
  • kubectl describe pod web-1 | grep -E "Node|Events": нода и история планирования.
  • kubectl get events --sort-by=.metadata.creationTimestamp: хронология привязок и ошибок.

Проверка anti-affinity: kubectl get pods -l app=web -o wide, все строки должны указывать разные ноды. Проверка topologySpreadConstraints: kubectl get pods -l app=web -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName, затем зоны нод через kubectl get node worker-1 -L topology.kubernetes.io/zone.

Для регулярного контроля подойдут метрики планировщика. scheduler_pending_pods показывает число подов в очереди по типу очереди: activeQ, backoffQ, unschedulable, gated и другим. Метрика числа попыток планирования разбита по результату: unschedulable означает, что под не удалось запланировать, error — внутреннюю проблему планировщика. scheduler_schedule_duration_seconds измеряет задержку попытки планирования в секундах (алгоритм планирования плюс binding). Метрики компонентов Kubernetes, включая kube-scheduler, отдаются на endpoint /metrics в формате Prometheus, а в managed-кластерах их выводят через систему мониторинга провайдера. Полный перечень метрик приведён в официальной документации Kubernetes Metrics Reference.

В CI/CD добавьте гейт: команда kubectl wait --for=condition=Ready pod -l app=web --timeout=120s завершится ошибкой, если планировщик не смог разместить поды, и деплой остановится вместо того, чтобы оставить половину реплик в Pending. Объекты API, с которыми работают эти команды, разобраны в статье Kubernetes: архитектура кластера и ключевые объекты API.

Поделиться: