← 20: Authn/Authz/Admission | 22: Controller Manager →
Роль scheduler в control plane
Scheduler превращает сохранённый в etcd PodSpec в решение о размещении. Pod без spec.nodeName — это задача для scheduler. Пока scheduler не назначит node, kubelet не узнает о Pod и контейнеры не запустятся.
Scheduler получает информацию через watch: подписывается на Pod объекты без spec.nodeName и реагирует на каждое новое добавление. После выбора node он делает bind — записывает spec.nodeName через API server в etcd.
[source: kubernetes.io/docs/concepts/scheduling-eviction/kube-scheduler/]
Полный цикл scheduling
Pod создан в etcd (spec.nodeName пуст)
│
▼
┌──────────────────────────────────────────────┐
│ 1. QUEUE │
│ Pod попадает в activeQ (сортировка по │
│ PriorityClass value, FIFO внутри priority)│
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 2. FILTER │
│ Проверка каждого node: │
│ - достаточно ли ресурсов (CPU, memory)? │
│ - taints/tolerations совпадают? │
│ - nodeSelector / nodeAffinity выполнены? │
│ - podAffinity/antiAffinity выполнены? │
│ Результат: список feasible nodes │
└──────────────────┬───────────────────────────┘
│
feasible nodes > 0?
│ │
нет да
│ │
▼ ▼
┌─────────────┐ ┌──────────────────────────────┐
│ PostFilter │ │ 3. SCORE │
│ (Preemption)│ │ Каждый node получает оценку│
└─────────────┘ │ - баланс ресурсов │
│ - topology spread │
│ - preferred affinity │
│ Node с max score побеждает│
└──────────────┬──────────────┘
│
▼
┌──────────────────────────────┐
│ 4. RESERVE │
│ Резервирование ресурсов в │
│ in-memory cache (не etcd) │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 5. BIND │
│ Записать spec.nodeName │
│ через API → etcd │
│ kubelet подхватит по watch│
└──────────────────────────────┘
Filter stage: жёсткие ограничения
Filter отсекает nodes, которые не могут принять Pod. Каждый plugin — бинарный фильтр: pass или reject.
Ресурсы
Pod запрашивает через resources.requests. Scheduler сравнивает с allocatable на node минус уже зарезервированное. Если не хватает CPU или memory — node отсекается.
# Посмотреть allocatable на node
kubectl describe node <node-name> | grep -A5 "Allocatable:"
# Посмотреть текущее использование ресурсов
kubectl top nodes
Taints и Tolerations
Taints на nodes, tolerations на Pod-ах. Если taint не покрыт toleration — Pod не попадёт на node.
Только NoExecute выгоняет уже работающие Pod-ы. kubectl drain помечает node unschedulable и инициирует eviction через Eviction API с учётом PDB — он не ставит taint NoExecute.
[source: kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/]
nodeSelector
Простейший жёсткий фильтр по labels на node:
spec:
nodeSelector:
disk: ssd
Если ни один node не имеет label disk: ssd — Pod остаётся Pending навсегда.
nodeAffinity
Расширенный nodeSelector с операторами и soft-вариантом:
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # hard → Filter
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: [eu-west-1a, eu-west-1b]
preferredDuringSchedulingIgnoredDuringExecution: # soft → Score
- weight: 80
preference:
matchExpressions:
- key: node-type
operator: In
values: [compute-optimized]
IgnoredDuringExecution — если label на node изменился после размещения, Pod не выселяется.
[source: kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/]
podAffinity и podAntiAffinity
Размещение относительно других Pod-ов, а не node labels.
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web
topologyKey: topology.kubernetes.io/zone
Hard podAntiAffinity по зонам гарантирует, что реплики окажутся в разных зонах. Но при 4 репликах и 3 зонах — 4-я реплика Pending навсегда.
topologySpreadConstraints
Равномерное распределение Pod-ов по доменам:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # hard
labelSelector:
matchLabels:
app: web
topologySpread vs podAntiAffinity:
[source: kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/]
Score stage: выбор лучшего node
Из feasible nodes scheduler выбирает лучший, суммируя оценки от scoring plugins:
- LeastAllocated — предпочитает nodes с большим свободным ресурсом (spread)
- MostAllocated — предпочитает nodes с меньшим свободным ресурсом (bin-packing)
- InterPodAffinity — бонус за preferred podAffinity
- NodeAffinity — бонус за preferred nodeAffinity
- TopologySpread — бонус за равномерное распределение
Каждый plugin возвращает score 0-100, умноженный на weight. Node с максимальной суммой побеждает.
Preemption: когда feasible nodes нет
Если Filter не нашёл ни одного подходящего node, scheduler переходит к PostFilter (Preemption):
- Scheduler ищет node, где можно вытеснить Pod-ы с более низким PriorityClass
- Учитывает PDB victim-ов — предпочитает не нарушать PDB
- Victim-ы получают graceful termination
- Pod получает
nominatedNodeNameи возвращается в очередь - После завершения eviction — scheduler повторяет цикл
Если preemption невозможен (нет Pod-ов с более низким priority, или PDB блокирует) — Pod уходит в unschedulableQ. Возврат в activeQ event-driven: при изменении cluster state (новый node, удалённый Pod, изменённый taint).
[source: kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/]
PriorityClass
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: critical-service
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "Для critical workloads"
Системные приоритеты:
Без PriorityClass все Pod-ы имеют priority 0 — preemption фактически не работает.
Pod Disruption Budgets (PDB)
PDB ограничивает voluntary disruptions — операции, которые кластер может контролировать.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
namespace: app-ns
spec:
minAvailable: 2 # или maxUnavailable: 1
selector:
matchLabels:
app: web
# kubectl drain с PDB (minAvailable: 2, replicas: 3):
# drain удаляет Pod-ы по одному, ждёт пока replacement запустится,
# прежде чем удалить следующий.
kubectl drain node-1 --ignore-daemonsets
# Без PDB: drain убирает ВСЕ Pod-ы с node-1 одновременно.
# Если все реплики на node-1 → полный outage.
[source: kubernetes.io/docs/tasks/run-application/configure-pdb/]
Leader election
В HA-кластерах запущены несколько экземпляров kube-scheduler, но активен только один (leader). Остальные — в standby. Leader election через Lease object в kube-system namespace.
scheduler-1 (leader) ← выполняет scheduling
scheduler-2 (standby) ← ждёт
scheduler-3 (standby) ← ждёт
scheduler-1 падает:
→ scheduler-2 или scheduler-3 перехватывает Lease
→ пауза scheduling ~1-3 секунды
→ новый leader начинает обрабатывать очередь
Level-triggered design: после failover новый leader видит все unscheduled Pod-ы и обрабатывает их. Пропущенные события не теряются.
Диагностика Pending
# Узнать причину Pending
kubectl describe pod <pod-name> | grep -A10 Events:
# Посмотреть очередь scheduler (если scheduler pod доступен)
kubectl get pods -n kube-system -l component=kube-scheduler
Сигналы мониторинга
Когда использовать
nodeSelector:
- Простые случаи: выбрать nodes по одному label (disk type, GPU presence).
- Нет операторов (In, NotIn) — только exact match.
nodeAffinity:
- Когда нужны операторы или несколько conditions.
preferred— мягкое предпочтение без Pending при недоступности.
podAntiAffinity (hard):
- Stateful workloads, где важна строгая изоляция: не более одной реплики БД на zone.
- Помните: 4 реплики + 3 зоны + hard antiAffinity = 4-я реплика Pending навсегда.
topologySpreadConstraints:
- Stateless workloads: равномерное распределение, никаких Pending из-за дисбаланса.
- Предпочтительно перед hard podAntiAffinity для большинства случаев.
PDB:
- Всегда для production workloads с replicas > 1. Защищает от drain-induced outage.
minAvailable: 1— абсолютный минимум; лучшеmaxUnavailable: 1.
PriorityClass:
- Когда в кластере есть workloads разной критичности: system services > business-critical > batch.
- Без PriorityClass preemption не работает (все priority = 0).
Типичные ошибки
1. Hard podAntiAffinity с replicas > зон
4 реплики, 3 зоны, requiredDuringScheduling antiAffinity — 4-я реплика Pending навсегда. Используйте topologySpreadConstraints с maxSkew: 1.
2. Нет PDB для stateless production
kubectl drain node без PDB убивает все Pod-ы сервиса одновременно, если они на одной ноде. Даже одна строка minAvailable: 1 спасает от полного outage.
3. Не смотреть на events при Pending
kubectl get pods показывает Pending, но не объясняет почему. kubectl describe pod → Events секция — там причина: Insufficient cpu, didn't match node affinity, 0/3 nodes are available.
4. Overcommit без LimitRange
Без requests scheduler считает Pod как "бесплатный". Если все Pod-ы без requests, scheduler разместит их куда угодно, ignoring реальные ресурсы. Node уходит в OOM, Pod-ы evict-ируются.
5. Неправильное понимание PreferNoSchedule
Это soft constraint, не hard. При нехватке других nodes, scheduler всё равно разместит Pod на tainted node. Если нужна строгая изоляция — используйте NoSchedule.
Альтернативы
Descheduler: scheduler не перемещает запущенные Pod-ы при изменении условий. Descheduler периодически evict-ирует Pod-ы, нарушающие current constraints (imbalance, taints, affinity). Отдельный deployment.
Cluster Autoscaler: когда Filter не находит feasible nodes, добавляет nodes в node group (AWS ASG, GCP MIG). Работает совместно со scheduler.
Karpenter: альтернатива Cluster Autoscaler. Реагирует на unschedulable Pod-ы напрямую, создаёт node с нужными параметрами (instance type, AZ, spot/on-demand). Быстрее и гибче.
[source: kubernetes.io/docs/tasks/extend-kubernetes/configure-multiple-schedulers/]