kube-scheduler решает одну задачу: найти ноду, на которой под может запуститься, и записать эту привязку в API-сервер. Решение принимается в два прохода. Фильтрация (Filter) отбрасывает ноды, которые не подходят по ресурсам, меткам, taints и топологии. Скоринг (Score) ранжирует оставшихся кандидатов по сумме баллов от плагинов. Побеждает нода с максимальным баллом, после чего планировщик вызывает Bind.
Размещением подов управляют декларативно, без правки кода планировщика: requests в манифесте, метки нод, nodeAffinity, podAntiAffinity, taints и topologySpreadConstraints задают, куда под попадёт и куда не попадёт никогда. Место планировщика среди компонентов control plane и путь пода после привязки разобраны в статье Архитектура Kubernetes: компоненты кластера и путь пода от манифеста до запуска.
Дальше по шагам: цикл планирования, готовые манифесты для привязки к нодам, работа с taints, ресурсами и топологией, плюс чек-лист диагностики, если под завис в Pending.
Как kube-scheduler выбирает ноду: этапы фильтрации и скоринга
Планирование идёт по этапам фреймворка, каждый этап обслуживают плагины. Порядок такой:
- Планировщик достаёт под из очереди (activeQ) и заводит для него цикл планирования.
- PreFilter и Filter: ноды, не прошедшие проверки по ресурсам, affinity, taints и топологии, отсеиваются. Остаётся список кандидатов.
- PostFilter: если кандидатов нет, включается вытеснение. Под с высоким priority может вытеснить поды с низким priority на подходящей ноде.
- Score: каждый плагин выставляет нодам-кандидатам баллы, планировщик складывает их с учётом весов.
- 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-критичных сервисов |
| Burstable | Requests заданы, но хотя бы у одного контейнера 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. Ответ почти всегда лежит в событиях пода.
Чек-лист команд для быстрой диагностики
- kubectl describe pod web-1 | grep -A 10 Events. Строка FailedScheduling содержит список причин после двоеточия, например 0/3 nodes are available.
- kubectl get events --field-selector reason=FailedScheduling. Покажет то же самое по всем подам сразу.
- kubectl describe node worker-1 | grep -A 8 "Allocated resources". Сверьте requests с allocatable.
- kubectl describe node worker-1 | grep Taints. Проверьте, не мешает ли taint.
- kubectl get nodes --show-labels. Убедитесь, что метки для nodeSelector и nodeAffinity существуют.
- kubectl get nodes. Статус Ready и отсутствие SchedulingDisabled закрывают вопрос с cordon.
- kubectl get pods --show-labels. Проверьте метки, которые ищут podAffinity и podAntiAffinity.
- 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/selector | Required-правило nodeAffinity не выполняется | Расширить список значений или сменить режим на preferred |
| node(s) didn't match pod anti-affinity rules | Свободных доменов для размещения нет | Смягчить правило до preferred, добавить ноды |
| node(s) didn't match pod topology spread constraints | maxSkew нарушен | Заменить DoNotSchedule на ScheduleAnyway, добавить домены |
| node(s) were unschedulable | Нода в cordon | kubectl uncordon worker-1 |
| pod has unbound immediate PersistentVolumeClaims | PVC не привязан или том в другой зоне | Проверить 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.