← Back to notes

kube-scheduler: размещение Pod-ов
на нодах


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):

  1. Scheduler ищет node, где можно вытеснить Pod-ы с более низким PriorityClass
  2. Учитывает PDB victim-ов — предпочитает не нарушать PDB
  3. Victim-ы получают graceful termination
  4. Pod получает nominatedNodeName и возвращается в очередь
  5. После завершения 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/]


20: Authn/Authz/Admission | 22: Controller Manager

kube-scheduler: размещение Pod-ов на нодах | Aleksandr Suprun