Learning Platform
Глоссарий Troubleshooting
Урок 03.04 · 25 мин
Продвинутый
kube-schedulerSchedulingTaintsTolerationsAffinityBinding

kube-scheduler: filtering, scoring, binding

kube-scheduler — отдельный процесс, чья единственная задача: назначить ноду каждому Pod-у, у которого её ещё нет. Это решение не делается жадно — для каждого Pod-а сначала прогоняется фильтр (какие ноды вообще подходят), затем скоринг (какая лучшая). Результат — POST на API server c полем .spec.nodeName. Дальше работает kubelet.

В этом уроке проходим scheduling cycle до уровня плагинов, разбираем разницу между nodeSelector, nodeAffinity и taints/tolerations, и понимаем почему DaemonSet теперь идёт через scheduler.


Что делает scheduler

Очень простая последовательность.

Scheduling cycle
1. Watch unscheduled podsScheduler делает WATCH на /api/v1/pods?fieldSelector=spec.nodeName= (пустой nodeName). Видит все Pod-ы, у которых ещё нет ноды. Каждый такой Pod помещается в внутреннюю очередь — activeQ.
2. Filtering phase (predicates)Для одного Pod-а из очереди: пробежать по всем нодам кластера, отбросить те, что не подходят. Критерии: достаточно CPU/memory? matches nodeSelector? toleration на taint? volume binding satisfied? Если 0 нод подходят — Pod помечается Unschedulable, попадает в backoff queue.
3. Scoring phase (priorities)Для каждой пригодной ноды посчитать score (0–100). Несколько плагинов скорят независимо, потом веса суммируются. Лучшая нода — с максимальным score. При равенстве выбирается случайно из лучших.
4. Reserve + PermitScheduler внутренне резервирует ресурсы на выбранной ноде (in-memory кэш) — чтобы следующий Pod в очереди не получил ту же ноду, если ресурсы уже заняты. Permit-плагины могут отложить или отменить binding (используется для gang scheduling).
5. BindPOST /api/v1/namespaces/<ns>/pods/<name>/binding с объектом Binding, в котором .target.name = выбранная нода. Apiserver обновляет .spec.nodeName в Pod-объекте. После этого kubelet на той ноде увидит Pod через watch.
NOTE

Scheduler работает по одному Pod-у за раз. Это не параллельный планировщик. Параллелизация была бы сложна из-за shared state (“сколько уже забронировано на ноде X в этом цикле”). Один поток процессит десятки Pod-ов в секунду — для большинства кластеров достаточно.


Планировщик ОС: preemptive vs cooperative, time slice

Filtering: какие ноды подходят

Фильтры (исторически они назывались predicates) — это серия плагинов, каждый из которых для пары (pod, node) возвращает true/false. Если хоть один сказал false — нода отбрасывается.

Главные built-in плагины фильтрации:

NodeResourcesFit

Проверяет, достаточно ли свободных CPU, memory, ephemeral-storage и других resources (включая GPU, hugepages) на ноде для requests Pod-а. Если Pod просит 2 CPU, а на ноде свободно 1.5 — нода фильтруется.

NodeAffinity

Проверяет nodeSelector (точное совпадение labels) и nodeAffinity (выражения через operator In, NotIn, Exists, …). Если есть requiredDuringSchedulingIgnoredDuringExecution — нода обязана совпасть, иначе фильтруется.

TaintToleration

Если нода имеет taint с эффектом NoSchedule, и у Pod-а нет соответствующего toleration — нода фильтруется. PreferNoSchedule не фильтрует, а только понижает score.

VolumeBinding / VolumeRestrictions

Проверяет, что все PersistentVolumeClaim Pod-а могут быть привязаны к volume-ам, доступным с этой ноды (учитывая topology — zone, region). Например, EBS volume в us-east-1a недоступен с ноды в us-east-1b.

PodTopologySpread

Проверяет topologySpreadConstraints Pod-а — например, “не более 1 Pod-а из этого Deployment в каждой AZ”. Если на ноде в зоне X уже есть один такой Pod, и max skew = 1 — нода фильтруется.

InterPodAffinity

Проверяет podAffinity и podAntiAffinity — должны ли определённые Pod-ы быть рядом (affinity) или вдали (anti-affinity) от других Pod-ов в определённой topology (например, hostname, zone).


Scoring: выбор лучшего из подходящих

После фильтрации остаётся набор подходящих нод. Scoring плагины присваивают каждой score от 0 до 100. Итоговый score — взвешенная сумма по всем плагинам. Выбирается нода с максимальным score (при равенстве — случайно).

Главные scoring плагины:

  • NodeResourcesBalancedAllocation — поощряет ноды, где CPU и memory utilization будут сбалансированы после размещения. Не любит ноды, где CPU остаётся 5%, а memory 80%.
  • NodeResourcesFit (scoring mode) — два режима: LeastAllocated (выше score — меньше используется, classic spreading) или MostAllocated (выше score — больше используется, bin-packing для cluster autoscaling).
  • ImageLocality — выше score, если на ноде уже скачан образ Pod-а. Экономит время на pull.
  • NodeAffinity (scoring mode) — preferredDuringSchedulingIgnoredDuringExecution суммирует веса всех matched preferences.
  • InterPodAffinity (scoring mode) — preferredDuringSchedulingIgnoredDuringExecution для pod affinity/anti-affinity.
  • PodTopologySpread (scoring mode) — поощряет более равномерное распределение.
  • TaintToleration (scoring mode) — учёт PreferNoSchedule taints — нода с такими taints получает penalty.
TIP

В v1.35 scheduler-плагины конфигурируются через KubeSchedulerConfiguration. Можно создать профиль с custom набором/весами плагинов, или вообще написать свой scheduler (отдельный процесс с уникальным schedulerName). Pod-ы с .spec.schedulerName: custom будет планировать только этот scheduler.


nodeSelector, nodeAffinity, taints — три разных механизма

Все три влияют на размещение Pod-ов, но логика и направление разные. Запомните различия — на CKAD-экзамене это частая ловушка.

Сравнение механизмов размещения
nodeSelectorПростейший механизм. Pod указывает map labels — нода должна иметь ВСЕ эти labels. Никаких операторов кроме точного match. Используется для простых случаев типа disk=ssd, gpu=nvidia.
nodeAffinityБогаче чем nodeSelector. Операторы: In, NotIn, Exists, DoesNotExist, Gt, Lt. Два режима: required (hard, как nodeSelector) и preferred (soft, влияет на scoring). Несколько nodeSelectorTerms — OR между ними, AND внутри каждого.
taint на нодеНода ставит знак: 'этот pool — мой, не для всех'. Без toleration ни один Pod не попадёт. Это ИНВЕРСИЯ — нода отталкивает Pod-ы, не наоборот.
toleration на Pod-еPod говорит: 'я готов терпеть такой taint'. Не требует, не выбирает — просто разрешает планировщику не отбросить эту ноду. Можно tolerate-ить taint, но это не значит, что Pod ПОПАДЁТ на эту ноду — нужен ещё nodeSelector/affinity для выбора.

Пример nodeSelector vs nodeAffinity

# nodeSelector — простой, exact match всех labels
apiVersion: v1
kind: Pod
metadata:
  name: gpu-job
spec:
  nodeSelector:
    accelerator: nvidia-a100
    zone: us-east-1a
  containers:
  - name: trainer
    image: tensorflow:gpu
---
# nodeAffinity — то же + soft preferences
apiVersion: v1
kind: Pod
metadata:
  name: gpu-job-v2
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: accelerator
            operator: In
            values: ["nvidia-a100", "nvidia-h100"]
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 50
        preference:
          matchExpressions:
          - key: zone
            operator: In
            values: ["us-east-1a"]      # предпочтительно
  containers:
  - name: trainer
    image: tensorflow:gpu

Семантика отличается: nodeSelector требует обе label-ы, nodeAffinity принимает любой из A100/H100 (operator In) и предпочитает (но не требует) зону.


Taints и tolerations

Taint — это label-подобная штука на ноде, но со специальной семантикой repulsion.

# Поставить taint на ноду
kubectl taint nodes node-1 dedicated=ml-team:NoSchedule

# Убрать (минус в конце)
kubectl taint nodes node-1 dedicated=ml-team:NoSchedule-

Три эффекта:

  • NoSchedule — Pod без toleration не будет запланирован на эту ноду (но существующие Pod-ы остаются).
  • PreferNoSchedule — soft вариант, понижает score, не блокирует полностью.
  • NoExecute — самый строгий. Pod без toleration не только не планируется — существующие Pod-ы выгоняются с ноды. Используется для аварийных ситуаций (“эта нода нездорова, эвакуируйте всё”).

Built-in taints

Kubernetes сам ставит taints на ноды в специальных ситуациях:

TaintКогда
node.kubernetes.io/not-readyНода NotReady (NodeCondition Ready != True)
node.kubernetes.io/unreachableNodeController не может связаться с нодой
node.kubernetes.io/memory-pressureНизко свободной памяти
node.kubernetes.io/disk-pressureНизко свободного диска
node.kubernetes.io/pid-pressureНизко свободных PID
node.kubernetes.io/unschedulablekubectl cordon поставил
node.kubernetes.io/network-unavailablenetwork plugin сообщает проблему

Все они с эффектом NoSchedule или NoExecute. Если хотите, чтобы ваш критичный Pod (мониторинг, log collector) запускался даже на нездоровых нодах — добавляйте соответствующие tolerations.

spec:
  tolerations:
  - key: node.kubernetes.io/not-ready
    operator: Exists
    effect: NoExecute
    tolerationSeconds: 300              # терпеть до 5 минут, потом выгнать
  - key: node.kubernetes.io/unreachable
    operator: Exists
    effect: NoExecute
    tolerationSeconds: 300

tolerationSeconds — специальное поле для NoExecute: как долго Pod может оставаться на ноде после появления taint, прежде чем будет выгнан.

WARNING

DaemonSet pods автоматически получают tolerations для большинства built-in taints — иначе log collector упал бы при первой проблеме на ноде. Но это не магия: посмотрите .spec.template.spec.tolerations любого DaemonSet — там целый список.


Pod affinity и anti-affinity

Эти конструкции выражают отношение Pod-а к другим Pod-ам, а не к нодам. Логика: «хочу быть рядом с Pod-ами label app=cache» или «не хочу быть в той же зоне с другим Pod-ом этого же Deployment».

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: web
    spec:
      affinity:
        podAntiAffinity:                # 3 реплики в РАЗНЫХ зонах
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: web
            topologyKey: topology.kubernetes.io/zone
        podAffinity:                    # держаться рядом с кешем
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchLabels:
                  app: redis
              topologyKey: kubernetes.io/hostname
      containers:
      - name: web
        image: nginx

topologyKey — фундаментальная штука. Это label на ноде, который определяет topology domain. kubernetes.io/hostname — каждая нода — отдельный домен (anti-affinity → разные ноды). topology.kubernetes.io/zone — каждая AZ — отдельный домен (anti-affinity → разные зоны).

topologySpreadConstraints — современный спред

Anti-affinity сложный и плохо масштабируется (O(n²) проверок). С v1.19 появился topologySpreadConstraints — более явный механизм:

spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule    # или ScheduleAnyway (soft)
    labelSelector:
      matchLabels:
        app: web

maxSkew: 1 — максимальная разница между «зона с самым большим количеством Pod-ов с этим label» и «зона с самым маленьким». Если у вас 3 зоны и 6 реплик — будет 2/2/2. Если 7 — будет 3/2/2 (skew=1). Если 8 — будет 3/3/2 (skew=1). Если 9 — будет 3/3/3.

Это более предсказуемо, чем anti-affinity, и работает быстрее на больших кластерах.


Bind: финальный шаг

После выбора ноды scheduler делает специальный POST:

POST /api/v1/namespaces/default/pods/nginx-7d4-abc/binding HTTP/2
Content-Type: application/json

{
  "apiVersion": "v1",
  "kind": "Binding",
  "metadata": {
    "name": "nginx-7d4-abc",
    "namespace": "default"
  },
  "target": {
    "apiVersion": "v1",
    "kind": "Node",
    "name": "worker-3"
  }
}

Apiserver получает Binding, обновляет .spec.nodeName в самом Pod-объекте до worker-3, делает Put в etcd. После этого:

  • kubelet на worker-3 через watch-stream видит изменение Pod-а на свою ноду → начинает запускать.
  • scheduler видит, что Pod больше не unscheduled (есть nodeName), убирает его из своей очереди.

/binding — subresource Pod-а, специально для этого случая. У него своя RBAC verb — bind. Это позволяет custom scheduler-у иметь права делать только bind, без права редактировать Pod-ы вообще.


DaemonSet теперь через scheduler

Исторически (до v1.17) DaemonSet работал в обход scheduler-а: DaemonSet controller сам ставил .spec.nodeName на каждый Pod, как только видел новую ноду. Это было быстрее, но создавало проблемы:

  • Не работали taints/tolerations через scheduler-плагины
  • Не учитывались resource fits (DaemonSet pod мог попасть на ноду без ресурсов)
  • Custom scheduler-логика не применялась

С v1.17 DaemonSet pods идут через обычный scheduler, но DaemonSet controller добавляет к каждому Pod-у specific nodeAffinity, который привязывает его именно к нужной ноде:

spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchFields:
          - key: metadata.name
            operator: In
            values: ["worker-3"]

Это значит, что scheduler планирует именно эту ноду — но проходит весь pipeline: filtering (есть ли ресурсы?), scoring (плагины), binding. То же самое, что обычные Pod-ы, но с принуждением к конкретной ноде.

NOTE

Static Pod-ы — другая история. Их kubelet запускает напрямую из манифестов в /etc/kubernetes/manifests/ без участия scheduler-а и api-server-а. Они потом регистрируются в apiserver как “mirror pods”, но scheduling им не нужен — нода уже известна (та, где они физически запущены).


Killer-момент: что видит scheduler в логах

Включите подробные логи scheduler-а (--v=4) и для одного Pod-а увидите что-то вроде:

I0513 09:00:01 scheduler.go:312] Attempting to schedule pod: default/nginx-7d4-abc
I0513 09:00:01 filter.go:64] Plugin NodeResourcesFit: node worker-1 result: passed
I0513 09:00:01 filter.go:64] Plugin NodeResourcesFit: node worker-2 result: rejected (insufficient cpu)
I0513 09:00:01 filter.go:64] Plugin NodeResourcesFit: node worker-3 result: passed
I0513 09:00:01 filter.go:64] Plugin TaintToleration: node worker-1 result: passed
I0513 09:00:01 filter.go:64] Plugin TaintToleration: node worker-3 result: passed
I0513 09:00:01 filter.go:64] Plugin NodeAffinity: node worker-1 result: passed
I0513 09:00:01 filter.go:64] Plugin NodeAffinity: node worker-3 result: passed
I0513 09:00:01 scorer.go:88] Pod nginx-7d4-abc — node worker-1: NodeResourcesBalancedAllocation=78, ImageLocality=0, NodeAffinity=0, total=78
I0513 09:00:01 scorer.go:88] Pod nginx-7d4-abc — node worker-3: NodeResourcesBalancedAllocation=82, ImageLocality=100, NodeAffinity=0, total=82*1+100*1=182
I0513 09:00:01 scheduler.go:421] Pod default/nginx-7d4-abc successfully bound to node worker-3

worker-3 выиграл из-за ImageLocality — образ nginx уже был там скачан, что дало +100. Это типичный кейс — scheduler любит ноды, на которых меньше работы для kubelet-а.


Проверка знанийKnowledge check
Чем отличается podAntiAffinity с topologyKey=hostname и с topologyKey=topology.kubernetes.io/zone? Что произойдёт, если у вас 3 ноды в одной зоне и Deployment с 5 репликами + podAntiAffinity по zone?
ОтветAnswer
topologyKey=hostname означает, что Pod-ы не могут оказаться на одной ноде (но могут в одной зоне). topologyKey=topology.kubernetes.io/zone — не могут оказаться в одной зоне (в реальности — на нодах с одинаковым label topology.kubernetes.io/zone). Если у вас 3 ноды в одной зоне и Deployment с 5 репликами и requiredDuringSchedulingIgnoredDuringExecution anti-affinity по zone — будет запланирован только ОДИН Pod (на любой ноде), остальные 4 останутся в Pending со статусом Unschedulable, потому что нет другой зоны. Для preferredDuringSchedulingIgnoredDuringExecution в той же ситуации все 5 будут запланированы, но scheduler выдаст warning о невозможности соблюсти preference. Это типичная CKAD-ловушка — required в anti-affinity легко создаёт hard-stuck pods.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Какие две главные фазы у scheduling cycle для одного Pod-а?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6