Планировщик: affinity, taints, topology spread

Рейтинг: 74.1% · 11 голосов
Kubernetes для разработчиков: поды, деплойменты, сервисы, ingress, конфиги и отладка. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
anton_k8s
Сообщения: 55
Зарегистрирован: 12 май 2026, 03:23

Планировщик: affinity, taints, topology spread

Сообщение anton_k8s »

Оглавление курса (48)
  1. Зачем нужен Kubernetes и из чего состоит кластер
  2. Поднимаем локальный кластер: minikube и kind
  3. Поды: базовая единица запуска
  4. Deployment и ReplicaSet: управляем репликами
  5. Service: сетевой доступ к подам
  6. ConfigMap и Secret: выносим конфигурацию
  7. Ingress: пускаем трафик снаружи
  8. Хранилище: Volumes и PersistentVolumeClaim
  9. Namespaces, requests и limits
  10. Health checks: liveness и readiness пробы
  11. Отладка: почему под не стартует
  12. Helm: пакетный менеджер для Kubernetes
  13. Базовая безопасность: RBAC и доступы
  14. Job и CronJob: разовые и периодические задачи
  15. StatefulSet и DaemonSet: stateful-нагрузки и системные агенты
  16. Стратегии обновления и планирование: rollout и rollback, graceful shutdown, nodeSelector, affinity, taints
  17. Автомасштабирование: HPA по метрикам, обзор VPA и Cluster Autoscaler
  18. Наблюдаемость: логи, метрики, events, обзор Prometheus и Grafana
  19. Безопасность глубже: securityContext, Pod Security Standards, NetworkPolicy, шифрование секретов
  20. Архитектура control plane: api-server, etcd, scheduler
  21. Узел кластера: kubelet, kube-proxy и container runtime
  22. Декларативная модель: reconciliation, контроллеры, CRD
  23. kubectl профессионально: get, describe, explain, jsonpath
  24. Под глубже: init, sidecar, lifecycle hooks и QoS
  25. Метки, селекторы и организация ресурсов
  26. Сервисы и kube-proxy глубже: типы, IPVS, EndpointSlices
  27. Ingress, ingress-контроллеры и Gateway API
  28. Секреты в кластере: шифрование, External Secrets, Vault
  29. Хранилище глубже: PV, StorageClass, CSI и StatefulSet
  30. Сеть кластера: CNI, NetworkPolicy и CoreDNS
  31. Планировщик: affinity, taints, topology spread (вы здесь)
  32. Ресурсы, QoS и вытеснение: requests, limits, eviction
  33. Helm глубже: шаблоны, хуки, зависимости, OCI
  34. Kustomize и управление конфигурацией без шаблонов
  35. RBAC и аутентификация глубже
  36. Admission и Pod Security: контроль на входе
  37. Policy as code: Kyverno и OPA Gatekeeper
  38. GitOps: Argo CD и Flux
  39. Операторы и CRD: расширяем Kubernetes
  40. Сервис-меш: Istio, Linkerd и когда он нужен
  41. Эксплуатация кластера: апгрейд, узлы, бэкап etcd
  42. Где запускать кластер: managed, self-hosted, k3s, Deckhouse
  43. Траблшутинг кластера: Pending, CrashLoop, узлы NotReady
  44. Стоимость и эффективность кластера: FinOps
  45. CI/CD в Kubernetes: сборка, деплой, прогрессивные релизы
  46. Лучшие практики и антипаттерны Kubernetes
  47. Сквозной проект и путь дальше: от манифеста до прода
  48. Деплой приложений через Argo CD: Application, sync и App-of-Apps
Боль, с которой ты столкнёшься рано или поздно

Деплоишь сервис в три реплики, радуешься отказоустойчивости. Ночью падает один узел - и все три реплики уезжают в перезапуск, потому что kube-scheduler по своей логике поставил их на один и тот же узел. HA на бумаге, ноль HA в реальности. Или другой классический случай: под висит в Pending часами, kubectl describe молчит загадками, а причина - taint на узлах, для которого ты забыл toleration. Или GPU-узел за большие деньги забивается обычными nginx-подами, потому что ничто не мешает им туда сесть.

Все эти истории - про одно: kubernetes планировщик (scheduler) принимает решение, куда поставить под, и это решение можно и нужно направлять. В этом уроке разберём механику до винтика: как работает kubernetes scheduler, что такое affinity kubernetes (node и pod), как устроены taints tolerations, зачем нужен topology spread, и как priorityClass с PodDisruptionBudget спасают тебя при вытеснении и обновлениях. Это не обзор - это то, что ты будешь руками крутить в проде.

Изображение

Как kube-scheduler выбирает узел: filtering и scoring

Когда ты создаёшь под, он не появляется на узле магически. Сначала под попадает в очередь планировщика с пустым полем

Код: Выделить всё

spec.nodeName
. kube-scheduler - это отдельный компонент control plane, который крутит цикл: взять из очереди под без узла, найти ему дом, проставить nodeName через bind. Дальше уже kubelet на выбранном узле дёргает CRI (containerd) и поднимает контейнеры. Планировщик сам ничего не запускает - он только назначает.

Решение идёт в два этапа.

1. Filtering (фильтрация, она же predicates). Планировщик прогоняет все узлы через набор плагинов-фильтров и отсекает те, куда под поставить нельзя в принципе. Хватит ли на узле CPU и памяти под запросы пода (PodFitsResources)? Совпадает ли nodeSelector? Проходит ли nodeAffinity required-правила? Есть ли у пода toleration на taint узла? Свободен ли нужный порт? На выходе - список узлов-кандидатов. Если он пустой, под остаётся Pending, и это первое место, куда ты смотришь при разборе.

2. Scoring (оценка, она же priorities). Из кандидатов надо выбрать лучший. Каждый узел получает баллы от 0 до 100 по нескольким плагинам: насколько равномерно распределится нагрузка, выполняются ли preferred-правила affinity, как ложатся topology spread constraints, есть ли уже образ контейнера на узле (ImageLocality - чтобы не качать гигабайты заново). Баллы суммируются с весами, узел с максимумом побеждает. При равенстве выбор случайный среди лидеров.

Понимание этих двух фаз снимает 80 процентов вопросов. nodeSelector и required-affinity и taints - это про filtering: жёсткое да/нет. preferred-affinity и topology spread - в основном про scoring: мягкое предпочтение. Когда под Pending - сломалась фильтрация. Когда под сел не туда, куда ты хотел - не дожали scoring.

От простого к сложному: nodeSelector и node affinity

Самый примитивный инструмент - nodeSelector. Просто требуешь у пода метки на узле:

Код: Выделить всё

spec:
  nodeSelector:
    disktype: ssd
    topology.kubernetes.io/zone: ru-central1-a
Под сядет только на узлы, где есть обе метки. Это AND, логического OR тут нет, диапазонов нет, "желательно" нет. Для боевых сценариев тесновато.

nodeAffinity - это nodeSelector на стероидах. Два режима, и разница принципиальная:

Код: Выделить всё

spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values: ["ru-central1-a", "ru-central1-b"]
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
          - key: node.kubernetes.io/instance-type
            operator: In
            values: ["standard-v3"]
required работает на этапе filtering - это жёсткое условие. Нет подходящего узла - под Pending. Заметь оператор In: можно перечислить несколько зон, и это уже OR внутри ключа. Доступны операторы In, NotIn, Exists, DoesNotExist, Gt, Lt - последние два дают сравнение по числу, чего nodeSelector не умеет.

preferred работает на scoring. Это пожелание с весом от 1 до 100. Нет узлов нужного типа - под всё равно сядет, просто на менее желанный. Вес влияет на итоговый балл узла.

Та самая длинная приписка IgnoredDuringExecution - не украшение. Она означает: правило проверяется только при планировании. Если узел уже после размещения пода поменял метки и перестал подходить - под не выселяется, продолжает работать. Гарантия только на момент посадки.

Pod affinity и anti-affinity: ставим поды рядом или врозь

nodeAffinity привязывает под к свойствам узла. А podAffinity и podAntiAffinity - к расположению других подов. Это ключевой инструмент для HA.

podAntiAffinity - "не ставь меня рядом с такими же". Главный рецепт против "все реплики на одном узле":

Код: Выделить всё

spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: api
        topologyKey: kubernetes.io/hostname
Читается так: под с меткой app=api не должен оказаться на узле (topologyKey = hostname), где уже есть под с app=api. Три реплики - три разных узла, иначе третья встанет в Pending. topologyKey тут критичен: hostname - врозь по узлам, topology.kubernetes.io/zone - врозь по зонам ДЦ.

Грабли с required-anti-affinity: если узлов меньше, чем реплик, лишние реплики навсегда зависнут в Pending. Поэтому на практике для anti-affinity чаще берут preferred - "постарайся развести, но если некуда, ставь вместе":

Код: Выделить всё

      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              app: api
          topologyKey: kubernetes.io/hostname
podAffinity - наоборот, "поставь меня рядом". Например, кэш-под держать на том же узле, что и приложение, ради низкой задержки. Только осторожно: required podAffinity легко загоняет все поды в одну точку отказа. И ещё - podAffinity дороже по вычислениям для планировщика, на больших кластерах с тысячами подов это заметная нагрузка на CPU scheduler-а.

Taints и tolerations: выделенные узлы и почему под висит в Pending

Affinity - это про притяжение со стороны пода. taints tolerations - механика отталкивания со стороны узла. taint вешается на узел и говорит: "не пускай сюда никого, кроме тех, кто явно согласен". Согласие пода - это toleration.

Ставим taint на узел:

Код: Выделить всё

kubectl taint nodes worker-gpu-1 dedicated=gpu:NoSchedule
Формат key=value:effect. Три эффекта:
  • NoSchedule - новые поды без toleration не сядут. Уже запущенные остаются.
  • PreferNoSchedule - мягкая версия, планировщик старается избегать, но может и поставить.
  • NoExecute - не только не пускает новых, но и выселяет уже работающие поды без toleration. Это то, что Kubernetes делает сам, когда узел становится NotReady: вешает node.kubernetes.io/unreachable:NoExecute.
Toleration на поде, который ДОПУСКАЕТСЯ на GPU-узел:

Код: Выделить всё

spec:
  tolerations:
  - key: "dedicated"
    operator: "Equal"
    value: "gpu"
    effect: "NoSchedule"
Важнейший нюанс, на котором путаются все новички: toleration НЕ притягивает под к узлу. Он только снимает запрет. Под с этим toleration может сесть и на обычный узел без taint. Чтобы GPU-поды садились именно на GPU-узлы и больше никуда, taint+toleration комбинируют с nodeAffinity или nodeSelector. taint отгоняет чужих, affinity притягивает своих.

Так же работают и control plane узлы: на них висит node-role.kubernetes.io/control-plane:NoSchedule, поэтому твои рабочие поды туда не лезут. А системные компоненты (kube-proxy, CNI-агенты Cilium/Calico) несут tolerations и спокойно там живут.

Диагностика Pending из-за taint. Самая частая загадка прода:

Код: Выделить всё

kubectl describe pod api-7d9f-xk2
...
Events:
  Warning  FailedScheduling  default-scheduler
  0/5 nodes are available: 3 node(s) had untolerated taint
  {dedicated: gpu}, 2 Insufficient cpu.
Тут планировщик прямым текстом разложил фильтрацию: 3 узла отсёк taint, 2 - нехватка CPU. Эта строка "X nodes are available" - первое, что читаешь при любом Pending.

Topology spread constraints: равномерно по зонам и узлам

podAntiAffinity умеет только "врозь" в режиме всё-или-ничего. А что если надо "размажь 6 реплик по 3 зонам так, чтобы максимум по 2 в зоне"? Для этого есть topology spread - более тонкий и современный инструмент.

Код: Выделить всё

spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: api
    minDomains: 3
  - maxSkew: 1
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: ScheduleAnyway
    labelSelector:
      matchLabels:
        app: api
Разбираем поля:
  • maxSkew - максимально допустимая разница в числе подов между самым загруженным и самым пустым доменом. maxSkew=1 значит "почти идеально ровно".
  • topologyKey - по чему размазываем: zone или hostname.
  • whenUnsatisfiable - DoNotSchedule (жёстко, как required, под уйдёт в Pending) или ScheduleAnyway (мягко, как preferred, влияет только на scoring).
  • minDomains - минимальное число доменов, которое планировщик обязан учитывать. Поле стабильно (GA) с версии 1.30. Зачем оно: без minDomains, если eligible-зон по факту только 2, ограничение расслабляется и пустит 2 реплики в одну зону. С minDomains=3 планировщик считает, что зон должно быть как минимум три, и не даст скучковать поды, когда третья зона временно без узлов.
В примере две констрейнты одновременно: жёстко ровно по зонам и мягко ровно по узлам. Это типовая боевая конфигурация для HA-сервиса в облаке (Yandex Managed Kubernetes, VK Cloud - там зоны ru-central1-a/b/d из коробки).

Есть ещё поле matchLabelKeys (бета, по умолчанию включено с 1.32) - оно добавляет в селектор значение метки самого пода, например pod-template-hash. Польза огромная: при rolling update новые поды считают skew только среди подов своей ревизии, а не вперемешку со старыми, которые вот-вот умрут. Без этого распределение во время выкатки временно перекашивает.

priorityClass, preemption и PodDisruptionBudget

Что делает планировщик, когда важный под некуда поставить - все узлы забиты? Тут включается preemption (вытеснение). Сначала задаём приоритеты:

Код: Выделить всё

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "Критичные сервисы платежей"
Под ссылается на класс через

Код: Выделить всё

spec.priorityClassName: high-priority
. Чем выше value, тем под важнее. Если высокоприоритетный под не влезает, планировщик ищет узел, с которого можно выселить поды поменьше приоритетом, чтобы освободить место - и убивает их. Вытесненные уходят в очередь и ищут себе новое место. Так критичный сервис не стоит в Pending из-за фоновых батч-задач.

Но бесконтрольное выселение опасно. Тут страхует PodDisruptionBudget (PDB):

Код: Выделить всё

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: api
PDB говорит: при добровольных нарушениях (drain узла перед обновлением, вытеснение, scale down автоскейлером) держи живыми минимум 2 пода app=api. Можно задать наоборот maxUnavailable. PDB не мешает аварийным падениям - если узел сгорел, поды умрут несмотря на бюджет. Он защищает именно от управляемых операций. Без PDB команда kubectl drain при апгрейде узлов может разом снести все твои реплики, и сервис ляжет на ровном месте.

Типичные грабли и антипаттерны
  • Все реплики на одном узле. Нет ни anti-affinity, ни topology spread - и планировщик по scoring вполне законно ставит их кучно. Лечится preferred podAntiAffinity по hostname или topology spread с maxSkew=1.
  • required anti-affinity при нехватке узлов. Реплик 5, узлов 3 - две реплики навсегда Pending. На динамичных кластерах с автоскейлингом бери preferred либо topology spread с whenUnsatisfiable: ScheduleAnyway.
  • toleration без affinity. Думаешь, что загнал поды на выделенные узлы, а они расползлись по обычным. taint отгоняет чужих, но не притягивает своих - нужна пара с nodeAffinity.
  • Pending и непонятно почему. Всегда начинай с kubectl describe pod и строки "0/N nodes are available" - она разложит причину по фильтрам: taint, ресурсы, affinity, volume-зона.
  • Забытый PDB. Апгрейд узлов сносит все реплики разом. minAvailable обязателен для любого сервиса с SLA.
  • minAvailable равный числу реплик. PDB с minAvailable=3 при 3 репликах заблокирует drain полностью - узел не осушится никогда. Оставляй запас.
Мини-лаба: разводим реплики руками

Нужен кластер с 3+ узлами (подойдёт kind/minikube с несколькими нодами, k3s или managed-кластер в облаке).
  • Создай Deployment nginx на 3 реплики без всяких ограничений. Глянь

    Код: Выделить всё

    kubectl get pods -o wide
    - запиши, на скольких узлах они сели. Скорее всего скучковались.
  • Добавь в spec шаблона пода preferred podAntiAffinity по topologyKey kubernetes.io/hostname (селектор по своей метке app). Передеплой, снова смотри -o wide. Реплики должны разъехаться по узлам.
  • Повесь taint на один узел:

    Код: Выделить всё

    kubectl taint nodes <node> role=infra:NoSchedule
    . Масштабируй деплой до 6 реплик. Убедись, что на этот узел поды не садятся.
  • Добавь поду toleration на этот taint - и убедись, что теперь он снова доступен для размещения.
  • Замени anti-affinity на topologySpreadConstraints с maxSkew=1 по hostname, whenUnsatisfiable: DoNotSchedule. Масштабируй больше числа узлов и поймай под в Pending. Прочитай причину в kubectl describe.
  • Создай PodDisruptionBudget с minAvailable=2, затем выполни kubectl drain на одном узле и понаблюдай, как drain соблюдает бюджет и выселяет поды по одному.
Контрольные вопросы
  • Чем отличается этап filtering от scoring, и на каком из них работают required-affinity, preferred-affinity и taints?
  • Почему под с нужным toleration всё равно может сесть на обычный узел без taint, и как заставить его садиться только на выделенные узлы?
  • В каком случае required podAntiAffinity приведёт к вечному Pending, и чем тут лучше topologySpreadConstraints?
  • Что произойдёт при kubectl drain, если PDB задан с minAvailable, равным числу реплик?
Итог

kube-scheduler фильтрует узлы-кандидаты, затем оценивает их баллами и сажает под на лучший. nodeSelector и nodeAffinity цепляют под к свойствам узла, podAffinity/anti-affinity - к соседям ради HA или локальности, taints с tolerations резервируют узлы под особые задачи, topology spread размазывает реплики ровно по зонам и нодам, а priorityClass с preemption и PodDisruptionBudget управляют тем, кого выселять и кого беречь при обновлениях. Освоишь эту связку - перестанешь ловить упавший сервис из-за того, что три реплики жили на одном узле.
👍2 ❤️5 🔥2 😄 🤔
Аватара пользователя
tracyjain
Сообщения: 1
Зарегистрирован: 28 май 2026, 04:41

Re: Планировщик: affinity, taints, topology spread

Сообщение tracyjain »

Вот про toleration который не притягивает а только разрешает - реально каждый раз забываю и потом удивляюсь почему gpu поды на обычных нодах. Спасибо что разжевали пару taint плюс affinity
👍1 ❤️1 🔥 😄 🤔2
Аватара пользователя
haskell87
Сообщения: 1
Зарегистрирован: 16 май 2026, 20:15

Re: Планировщик: affinity, taints, topology spread

Сообщение haskell87 »

А minDomains точно GA с 1.30? У нас в Yandex Managed кластер на 1.29, получается мне его пока не завезли и придётся обновляться ради ровного спреда по зонам?
👍1 ❤️ 🔥1 😄 🤔
Ответить
← Предыдущая глава
Сеть кластера: CNI, NetworkPolicy и CoreDNS
Следующая глава →
Ресурсы, QoS и вытеснение: requests, limits, eviction

Все главы курса «Kubernetes: оркестрация контейнеров от основ до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «Kubernetes на практике»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость