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.
Три эффекта
# Добавить 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 — kube-apiserver автоматически ставит taint node-role.kubernetes.io/control-plane:NoSchedule. Именно поэтому обычные 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 для определения домена.
[source: kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity]
topologyKey
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
Ключевые поля:
Пример результата: 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
Сначала обязательно по зонам, предпочтительно — по нодам внутри зон.
Сводная таблица механизмов
Когда использовать
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.