Зачем вообще нужны topologySpreadConstraints
Когда кластер Kubernetes начинает жить «по-взрослому», вы почти неизбежно сталкиваетесь с задачей: распределять Pod равномерно. Не «в среднем», а предсказуемо — чтобы реплики одного сервиса не оказывались на одной ноде, в одной стойке или в одной зоне доступности.
Классические инструменты — pod anti affinity, nodeSelector, taints/tolerations — решают часть проблем, но у них есть типовые боли: слишком жёсткие правила, сложные выражения, неожиданные Pending при дефиците ресурсов или при появлении новых нод/зон.
topologySpreadConstraints — механизм Kubernetes, который описывает именно «распределение» реплик по доменам топологии (зона, нода, регион, любой label), а не только «запрет совместного размещения».
Как работает распределение: модель «скью»
Планировщик (scheduler) выбирает домен топологии и считает, сколько подходящих Pod уже лежит в каждом «бакете» (например, в каждой зоне). Затем пытается разместить новый Pod так, чтобы разница между самым «загруженным» и самым «пустым» бакетом не превышала maxSkew.
topologyKeyзадаёт, по какому label мы группируем ноды (например,topology.kubernetes.io/zone).maxSkewзадаёт допустимую разницу количества Pod между группами.whenUnsatisfiableопределяет поведение, если ровно распределить нельзя: не планировать или планировать «как получится».
Scheduler учитывает только те Pod, которые попадают под
labelSelectorвнутри constraints. Это позволяет балансировать реплики конкретного приложения, а не все Pod в namespace.

Если вы запускаете Kubernetes на собственных серверах или в облаке и вам нужен предсказуемый контроль над зонами/пулами нод, удобнее всего делать это на виртуальных машинах с полным доступом к настройкам сети и ядра. Для таких задач хорошо подходит VDS, особенно если вы строите multi-zone схему.
Параметры topologySpreadConstraints: что означают на практике
topologyKey — ключ домена топологии
Это label на Node, по которому строятся группы. Самые частые:
topology.kubernetes.io/zone— зона (для zone-aware размещения).topology.kubernetes.io/region— регион.kubernetes.io/hostname— конкретная нода (аналог «не класть все на одну машину»).
Можно использовать и свои лейблы, например rack или pool, если вы их дисциплинированно поддерживаете на нодах.
maxSkew — допустимая «неровность»
maxSkew: 1 означает, что разница по количеству Pod между любыми двумя доменами не должна превышать 1. Это удобно для реплик: при 3 зонах и 6 репликах вы получите 2-2-2, при 5 репликах — 2-2-1.
Если поставить maxSkew: 2, scheduler позволит баланс «помягче» (например, 3-3-1), зато меньше риск упереться в Pending при ограниченных ресурсах.
whenUnsatisfiable — стратегия при невозможности соблюдения
DoNotSchedule— жёстко: если constraint нельзя выполнить, Pod остаётсяPending.ScheduleAnyway— мягко: scheduler постарается, но если не получается — всё равно запланирует.
Практически: DoNotSchedule — про требование к отказоустойчивости (лучше не стартовать, чем нарушить модель), ScheduleAnyway — про живучесть (пусть хоть где-то запустится).
labelSelector — какие Pod распределяем
Обычно здесь указывают метки приложения: app: api, component: web, release: prod. Тогда constraint сравнивает именно реплики одного workload.
minDomains и эффект «нет второй зоны»
Если constraint задан по зонам, но реально «доступна» только одна зона (в другой зоне нет подходящих нод по ресурсам, taints или nodeSelector), поведение может удивлять. Параметр minDomains задаёт, сколько доменов должно участвовать, чтобы constraint считался выполнимым.
Это полезно, когда вы хотите строго требовать «минимум 2 зоны», иначе лучше не запускаться вовсе (например, для критичного сервиса).
matchLabelKeys — автоматическая группировка по меткам Pod
Этот параметр удобен в шаблонах/операторах, когда вы хотите автоматически разделять разные «волны» при rolling update (например, по pod-template-hash), чтобы новые и старые реплики тоже распределялись ровно.
Базовый пример: распределение реплик по зонам
Минимальный пример для Deployment с 6 репликами. Требуем равномерное распределение по зонам: topology.kubernetes.io/zone, maxSkew: 1, а при невозможности — не планируем (DoNotSchedule).
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 6
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api
containers:
- name: api
image: example/api:1.0
ports:
- containerPort: 8080
Комбинирование: зоны + запрет «все на одну ноду»
Типовой кейс: нужно распределение по зонам, но также нельзя, чтобы две реплики попали на одну и ту же ноду (особенно если реплик мало). Добавляем второй constraint по kubernetes.io/hostname.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 4
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: web
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
containers:
- name: web
image: example/web:2.3
Логика такая: по зонам часто лучше быть мягче (иначе при деградации зоны вы можете потерять запуск), а по нодам — жёстче, чтобы не складывать реплики «пирамидкой» на один хост.
topologySpreadConstraints vs pod anti affinity: когда что выбирать
pod anti affinity подходит, когда нужен запрет совместного размещения или предпочтение «не рядом». Но по мере роста правил оно становится тяжёлым в сопровождении и не всегда выражает именно «сделай равномерно».
topologySpreadConstraints лучше, когда цель — баланс и предсказуемость распределения реплик. По сути это «балансировщик» на этапе scheduling.
- Нужно «не более одной реплики на ноду» — часто достаточно constraint по
kubernetes.io/hostname(или anti-affinity, если так привычнее). - Нужно «равномерно по зонам, насколько возможно» — constraints обычно проще и понятнее.
- Нужно «никогда не вместе + сложные условия по нескольким меткам» — иногда anti-affinity остаётся уместной.
Частая рабочая схема: constraints — для равномерности, anti-affinity — точечно для «опасных соседств» (например, два Pod с одинаковым shard-id не должны попадать на одну ноду).

Диагностика: почему Pod ушёл в Pending или распределение «кривое»
1) Ноды не размечены нужным topologyKey
Если на части нод нет label topology.kubernetes.io/zone (или вы используете свой ключ), scheduler может считать домены иначе, чем вы ожидаете, или не сможет выполнить constraint.
Проверка:
kubectl get nodes -L topology.kubernetes.io/zone -L topology.kubernetes.io/region
2) labelSelector не совпадает с метками Pod
Если селектор не матчится, constraint фактически не работает: scheduler распределяет «пустое множество», и вы видите скучивание.
Быстрая проверка меток:
kubectl get pods -l app=api --show-labels
3) Слишком строгий maxSkew при дефиците ресурсов
При малом количестве нод в зоне, при CPU/Memory pressure или taints строгий maxSkew: 1 вместе с DoNotSchedule легко приводит к Pending. Это не баг — scheduler выполняет ваш контракт.
- Для некритичных сервисов временно переключите
whenUnsatisfiableнаScheduleAnyway. - Ослабьте
maxSkew. - Добавьте ноды в «пустую» зону/пул или пересмотрите requests/limits.
4) Ожидание «сразу идеально» при rolling update
Во время обновления одновременно живут старые и новые реплики. Если constraints привязаны к меткам, которые меняются между версиями (или, наоборот, не учитывают версию), баланс может казаться странным.
Часто помогает аккуратное использование matchLabelKeys или приведение меток к модели, где labelSelector включает обе «волны» Pod на время обновления.
Проверка результата: смотрим расклад по зонам и нодам
Практичный способ — вывести Pod с информацией о ноде и дальше сопоставить с зоной ноды:
kubectl get pod -l app=api -o wide
Если нужно понять, почему конкретный Pod не планируется, начинайте с событий:
kubectl describe pod -l app=api
А если вы поднимаете компактный кластер (например, k3s) и выбираете ingress-контроллер, полезно держать под рукой материал с практическими сравнениями: Ingress в k3s: Traefik, Nginx, HAProxy — что выбрать.
Рекомендованные шаблоны для продакшена
Шаблон 1: критичный stateless API
Цель: держать баланс по зонам, но не терять запуск полностью при потере одной зоны. Часто выбирают ScheduleAnyway по зонам и более строгие правила по нодам, плюс разумный PDB (это отдельная тема).
Шаблон 2: фоновые воркеры/очереди
Цель: равномерность полезна, но не ценой простоя. Здесь чаще ставят whenUnsatisfiable: ScheduleAnyway и maxSkew повыше, чтобы быстрее утилизировать кластер.
Шаблон 3: «одна реплика на ноду» для небольшого количества Pod
Если реплик примерно столько же, сколько нод, constraint по kubernetes.io/hostname с DoNotSchedule даёт очень понятное поведение. Но помните: при ремонте ноды или drain вы можете временно упереться в Pending, если свободных нод не осталось.
Для небольших прод- и тест-кластеров на виртуалках пригодится и практическая инструкция по развёртыванию: как поднять k3s на VDS.
Частые вопросы и практические замечания
Можно ли сделать «строго по 1 Pod в каждой зоне»?
В лоб — нет: constraints управляют разницей (maxSkew), а не точным числом. Но при фиксированном числе реплик и зон вы можете получить близкий эффект. Например, 3 реплики на 3 зоны при maxSkew: 1 обычно лягут 1-1-1, если ресурсы позволяют.
Что важнее: constraints или requests/limits?
С точки зрения планирования сначала нужно, чтобы Pod вообще помещался по ресурсам. Если requests завышены, никакая «красота» топологии не поможет. Поэтому сначала приводим requests/limits к реалистичным значениям, потом включаем распределение.
Как связаны constraints и Cluster Autoscaler?
С DoNotSchedule Pod может корректно зависнуть в Pending — это сигнал автоскейлеру, что нужно добавить ноды в конкретную зону/пул. С ScheduleAnyway автоскейлер может не увидеть потребность расширять «пустую» зону, потому что Pod запланировался где-то ещё.
Итоги
topologySpreadConstraints — один из самых прикладных инструментов для предсказуемого размещения Pod в Kubernetes: он решает задачу равномерности (zones/nodes/любые домены), проще читается, чем сложный pod anti affinity, и хорошо вписывается в современные практики zone-aware архитектуры.
- Используйте
topologyKeyпо зонам и/или поkubernetes.io/hostname. - Стартуйте с
maxSkew: 1и ослабляйте, если ловитеPending. - Выбирайте
whenUnsatisfiableосознанно:DoNotScheduleдля строгой модели доступности,ScheduleAnywayдля большей «живучести» при дефиците. - Первым делом проверяйте лейблы нод и корректность
labelSelector— это две самые частые причины «почему не работает».


