Kubernetes scheduler назначает Pod на Node в два этапа: Filter (отсеивает неподходящие Node) и Score (ранжирует оставшиеся по весу). Механизмы ниже влияют на один или оба этапа и позволяют контролировать, где именно в кластере запустится Pod.
source: kubernetes.io/docs/concepts/scheduling-eviction/kube-scheduler/
Taints и Tolerations
Taint — это метка на Node, которая отталкивает Pod. Toleration — разрешение на Pod, позволяющее ему игнорировать конкретный taint. Taint не притягивает Pod на ноду — он только отталкивает тех, кто не имеет toleration.
Три эффекта
| Effect | Поведение |
|---|---|
NoSchedule | Новые Pod без toleration не назначаются на эту Node |
PreferNoSchedule | Scheduler старается избегать, но размещает если нет альтернатив |
NoExecute | Не назначает новые Pod + вытесняет уже запущенные без toleration |
# Добавить taint
kubectl taint nodes node1 dedicated=gpu:NoSchedule
# Убрать taint (дефис в конце)
kubectl taint nodes node1 dedicated=gpu:NoSchedule-
# Посмотреть taints на ноде
kubectl describe node node1 | grep Taints
Toleration в Pod
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
containers:
- name: app
image: gpu-app:1.0
Два оператора:
Equal— key, value и effect должны точно совпасть с taintExists— достаточно совпадения key (value не указывается, любое значение подходит)
Toleration с tolerationSeconds для NoExecute — Pod будет вытеснен через указанное время:
tolerations:
- key: "node.kubernetes.io/unreachable"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 300
Kubernetes автоматически добавляет toleration node.kubernetes.io/not-ready и node.kubernetes.io/unreachable с tolerationSeconds: 300 ко всем Pod. Это стандартный grace period перед вытеснением при проблемах с нодой.
Где используются Taints
Control plane nodes — kubeadm при init/join ставит taint node-role.kubernetes.io/control-plane:NoSchedule (другие инсталляторы, например k3s, этого не делают). Именно поэтому обычные Pod не попадают на control plane ноды.
GPU/специализированные ноды — тaint dedicated=gpu:NoSchedule на GPU-нодах. Только Pod с GPU-запросом получают toleration, остальные Pod не занимают дорогие GPU-ноды.
Автоматические taints от node controller — при проблемах с нодой контроллер ставит node.kubernetes.io/not-ready, node.kubernetes.io/unreachable, node.kubernetes.io/memory-pressure и другие.
source: kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
nodeSelector
Простейший способ ограничить, на каких нодах может запуститься Pod. Hard constraint — если нет Node с нужными label, Pod остаётся в Pending.
# Поставить label на ноду
kubectl label nodes node1 disktype=ssd
apiVersion: v1
kind: Pod
metadata:
name: ssd-pod
spec:
nodeSelector:
disktype: ssd
containers:
- name: app
image: my-app:1.0
nodeSelector удобен для простых случаев. Как только нужны операторы (not in, greater than) или мягкие предпочтения — переходи на Node Affinity.
Node Affinity
Более гибкая альтернатива nodeSelector. Поддерживает операторы In, NotIn, Exists, DoesNotExist, Gt, Lt и два режима: hard (обязательное) и soft (предпочтительное).
source: kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity
requiredDuringSchedulingIgnoredDuringExecution (hard)
Pod не назначается, если условие не выполнено. Работает на этапе Filter. IgnoredDuringExecution означает: уже запущенные Pod не вытесняются, если нода перестала соответствовать условию.
apiVersion: v1
kind: Pod
metadata:
name: zone-pod
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- eu-west-1a
- eu-west-1b
containers:
- name: app
image: my-app:1.0
Несколько элементов в nodeSelectorTerms работают как OR. Несколько matchExpressions внутри одного term — как AND.
preferredDuringSchedulingIgnoredDuringExecution (soft)
Scheduler предпочитает Node, удовлетворяющую условию, но не обязан. Работает на этапе Score.
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: disktype
operator: In
values:
- ssd
- weight: 20
preference:
matchExpressions:
- key: network-speed
operator: In
values:
- 10gbe
weight от 1 до 100 — относительный приоритет при наличии нескольких preferred-правил. Суммируется с другими Score-факторами.
Pod Affinity и Anti-Affinity
Управляет размещением Pod относительно других Pod, а не нод. Использует topologyKey для определения домена.
topologyKey
| topologyKey | Домен |
|---|---|
kubernetes.io/hostname | Конкретная нода (уникально для каждой ноды) |
topology.kubernetes.io/zone | Availability Zone |
topology.kubernetes.io/region | Region |
podAffinity — разместить рядом
Cache-сервис рядом с frontend на одной ноде (снижение latency):
apiVersion: apps/v1
kind: Deployment
metadata:
name: cache
spec:
replicas: 2
selector:
matchLabels:
app: cache
template:
metadata:
labels:
app: cache
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- frontend
topologyKey: kubernetes.io/hostname
containers:
- name: cache
image: redis:7
podAntiAffinity — разнести реплики
3 реплики web по разным зонам:
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:
matchExpressions:
- key: app
operator: In
values:
- web
topologyKey: topology.kubernetes.io/zone
containers:
- name: web
image: nginx:1.25
Важно: required podAntiAffinity с topologyKey: kubernetes.io/hostname и 3 репликами требует минимум 3 ноды. Если нод меньше — Pod уйдут в Pending и останутся там.
topologySpreadConstraints
Более точный и гибкий контроль равномерного распределения Pod по доменам. Решает проблему, когда реплик больше, чем доменов: при required podAntiAffinity 5 реплик на 3 зоны уйдут в Pending; с topologySpreadConstraints — распределятся 2-2-1.
source: kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 5
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
containers:
- name: web
image: nginx:1.25
Ключевые поля:
| Поле | Описание |
|---|---|
maxSkew | Максимально допустимая разница в количестве Pod между самым загруженным и самым пустым доменами |
topologyKey | Label ноды, определяющий домен |
whenUnsatisfiable | DoNotSchedule (hard, не назначать если нарушает) или ScheduleAnyway (soft, назначать, но предпочитать равномерность) |
labelSelector | Какие Pod считать при подсчёте баланса |
Пример результата: 5 реплик, 3 зоны, maxSkew: 1 → распределение 2-2-1 (разница не превышает 1).
Комбинировать несколько ограничений
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: web
Сначала обязательно по зонам, предпочтительно — по нодам внутри зон.
Сводная таблица механизмов
| Механизм | Этап scheduler | Hard/Soft | Описание |
|---|---|---|---|
| Taint + Toleration | Filter | Hard/Soft | Отталкивает Pod от Node |
| nodeSelector | Filter | Hard | Привязка по label Node |
Node Affinity required | Filter | Hard | Гибкая привязка к Node (операторы) |
Node Affinity preferred | Score | Soft | Предпочтение Node |
Pod Affinity required | Filter | Hard | Сближение Pod |
Pod Affinity preferred | Score | Soft | Предпочтение сближения |
Pod Anti-Affinity required | Filter | Hard | Разнесение Pod (строго один на домен) |
Pod Anti-Affinity preferred | Score | Soft | Предпочтение разнесения |
topologySpreadConstraints DoNotSchedule | Filter | Hard | Равномерное распределение с maxSkew |
topologySpreadConstraints ScheduleAnyway | Score | Soft | Предпочтение равномерности |
Когда использовать
Taints/Tolerations — изоляция специализированных нод (GPU, bare-metal, spot/preemptible). Нода должна быть доступна только для конкретных рабочих нагрузок.
nodeSelector / Node Affinity — Pod должен работать на нодах определённого типа: SSD, конкретная архитектура (arm64/amd64), нода в конкретной зоне или с определённым количеством памяти.
Pod AntiAffinity required — критичные stateless сервисы, где каждая реплика должна быть на отдельной ноде или в отдельной зоне. Допустимо только если реплик не больше, чем доменов.
topologySpreadConstraints — предпочтительный выбор для равномерного распределения stateless сервисов с произвольным числом реплик. Работает лучше podAntiAffinity для большинства современных деплойментов.
Pod Affinity — latency-sensitive компоненты, которые должны быть физически рядом (cache и app на одной ноде).
Типичные ошибки
Pod застрял в Pending после required podAntiAffinity. Если required podAntiAffinity + topologyKey: kubernetes.io/hostname и реплик больше, чем нод — Pod никогда не запустятся. Либо уменьши replicaCount, либо перейди на preferred или topologySpreadConstraints.
# Диагностика Pending Pod
kubectl describe pod <pod-name> | grep -A 10 Events
# Events: Warning FailedScheduling 0/3 nodes available: 3 node(s) didn't match pod anti-affinity rules
Toleration с Exists на key="" — разрешает всё. Toleration без key и с operator: Exists совпадает со всеми taints кластера. Обычно это не намеренно.
# Осторожно — совпадает со всеми taints
tolerations:
- operator: "Exists"
nodeSelector и Node Affinity одновременно — AND. Если указаны оба, Pod назначается только на ноды, которые удовлетворяют обоим условиям.
Забыть label topology.kubernetes.io/zone на нодах. topologySpreadConstraints по зонам не работает, если у нод нет соответствующих label. В managed-кластерах (EKS, GKE, AKS) эти label ставятся автоматически. В bare-metal кластерах — нужно добавлять вручную.
maxSkew: 1 при малом числе реплик и нод. Если нод больше, чем реплик, некоторые домены будут пустыми. Разница между 1 Pod и 0 Pod = 1, это нарушает maxSkew: 1 при DoNotSchedule. Используй ScheduleAnyway или увеличь maxSkew.
Альтернативы
Cluster Autoscaler — вместо ручного управления размещением, автоматически добавляет ноды при нехватке ресурсов. Работает совместно с topologySpreadConstraints: может добавить ноды в нужную зону.
source: kubernetes.io/docs/concepts/cluster-administration/cluster-autoscaling/
Descheduler — отдельный компонент, который перераспределяет уже запущенные Pod для соблюдения баланса. Стандартный scheduler только размещает новые Pod, существующие не двигает. Descheduler периодически проверяет и вытесняет Pod с нарушенным балансом.
Priority Classes и PriorityPreemption — управление приоритетами Pod. Высокоприоритетный Pod может вытеснить низкоприоритетный, если нет свободных ресурсов. Это не scheduling constraint в прямом смысле, но влияет на то, какие Pod получают ноды первыми.
source: kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/
Custom Scheduler — Kubernetes позволяет запустить несколько schedulers. Для специфических задач (машинное обучение, особые hardware requirements) можно написать собственный scheduler и указать его через spec.schedulerName в Pod.