← К программе курса

Модуль 13 / 19

Scheduling: nodeSelector, affinity, taints, tolerations и topology

KUBERNETES

Содержание
01

1. Цели и результат модуля

Вы научитесь объяснять решение scheduler, выражать обязательные и предпочтительные правила размещения и диагностировать Pending Pod по constraints и Events.

  • nodeSelector — простое hard-условие
  • node affinity поддерживает hard и weighted soft rules
  • pod affinity связывает placement с другими Pods
  • taint отталкивает, toleration только разрешает
  • topology spread управляет skew между доменами
02

2. Теория: scheduling cycle

Scheduler обрабатывает unscheduled Pod: фильтрует неподходящие Nodes, оценивает оставшиеся, выбирает Node и записывает binding. Kubelet затем запускает Pod. Scheduled=True не означает Running или Ready; image pull, mount и probes происходят позже.

Preemption может освободить ресурсы для более приоритетного Pod, но не исправляет несовместимый label, taint или affinity rule.

03

3. Node labels и nodeSelector

nodeSelector требует точного совпадения всех указанных labels. Labels описывают архитектуру, pool, hardware или topology, но доверять произвольным labels для security нельзя. Для изоляции используют защищённые label namespaces и NodeRestriction admission.

spec.nodeName обходит scheduler и не является заменой selector: он жёстко привязывает Pod и усложняет диагностику.

04

4. Node и Pod affinity

requiredDuringSchedulingIgnoredDuringExecution — обязательный filter при scheduling; preferred... — score с weight 1–100. IgnoredDuringExecution означает, что последующее изменение label обычно не выселяет уже работающий Pod.

Pod affinity/anti-affinity сопоставляет labels Pods в topology domains. Hard anti-affinity по hostname требует разные Nodes и на одноузловом кластере закономерно блокирует вторую replica.

05

5. Taints и tolerations

Taint имеет key, optional value и effect. NoSchedule запрещает новые placements без toleration; PreferNoSchedule — мягкое предпочтение; NoExecute также выселяет нетерпимые Pods, а tolerationSeconds может отложить выселение.

Toleration не притягивает Pod к tainted Node. Для dedicated pool обычно комбинируют taint+toleration с node affinity.

06

6. Topology spread и single-node граница

topologySpreadConstraints сравнивает число matching Pods по доменам topologyKey. maxSkew ограничивает дисбаланс; DoNotSchedule — hard, ScheduleAnyway — soft. Учитываются только eligible domains и корректно размеченные Nodes.

На одном Node три replicas могут работать в одном hostname domain: это не проверка высокой доступности. Реальное распределение между Nodes/zones переносится в Advanced/HA лабораторию.

07

7. Методические указания

  • Сначала проверяйте requests и Node readiness
  • Соберите все hard filters: selector, affinity, taints, volumes
  • Отделяйте required от preferred
  • Toleration не считайте выбором Node
  • Проверяйте topology labels и eligible domains
  • Не меняйте system taints без плана rollback
  • Читайте FailedScheduling message целиком
  • Удаляйте учебные labels и taints после лаборатории
08

8. Лабораторная работа: nodeSelector и preference

Разметьте Node. selected требует label, а preferred просит отсутствующую zone лишь как preference и всё равно запускается.

terminal
mkdir -p /tmp/k8s-m13
kubectl create namespace k8s-m13
kubectl label node k8s-fundamentals-1 course.goretskiy.pro/pool=fundamentals --overwrite
cat > /tmp/k8s-m13/placement.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata: {name: selected, namespace: k8s-m13}
spec:
  nodeSelector: {course.goretskiy.pro/pool: fundamentals}
  containers: [{name: app, image: busybox:1.37, command: ["sh", "-c", "sleep 3600"]}]
---
apiVersion: v1
kind: Pod
metadata: {name: preferred, namespace: k8s-m13}
spec:
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        preference:
          matchExpressions: [{key: course.goretskiy.pro/zone, operator: In, values: [absent]}]
  containers: [{name: app, image: busybox:1.37, command: ["sh", "-c", "sleep 3600"]}]
EOF
kubectl apply --dry-run=server -f /tmp/k8s-m13/placement.yaml
kubectl apply -f /tmp/k8s-m13/placement.yaml
kubectl -n k8s-m13 wait --for=condition=Ready pod/selected pod/preferred --timeout=120s
kubectl -n k8s-m13 get pod selected preferred -o wide
09

9. Лабораторная работа: hard anti-affinity

Две replicas требуют разные hostname domains. На одном Node одна работает, вторая Pending; Event прямо сообщает несоответствие pod anti-affinity.

terminal
cat > /tmp/k8s-m13/anti-affinity.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata: {name: anti-affinity, namespace: k8s-m13}
spec:
  replicas: 2
  selector: {matchLabels: {app: anti-affinity}}
  template:
    metadata: {labels: {app: anti-affinity}}
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector: {matchLabels: {app: anti-affinity}}
            topologyKey: kubernetes.io/hostname
      containers: [{name: app, image: nginx:1.29-alpine}]
EOF
kubectl apply --dry-run=server -f /tmp/k8s-m13/anti-affinity.yaml
kubectl apply -f /tmp/k8s-m13/anti-affinity.yaml
sleep 5
kubectl -n k8s-m13 get pods -l app=anti-affinity -o wide
pending=$(kubectl -n k8s-m13 get pods -l app=anti-affinity --field-selector=status.phase=Pending -o jsonpath='{.items[0].metadata.name}')
kubectl -n k8s-m13 describe pod "$pending"
10

10. Лабораторная работа: taint и toleration

Временно добавьте NoSchedule. Обычный Pod блокируется, tolerated запускается. Снимите taint и убедитесь, что первый Pod теперь размещён.

terminal
kubectl taint node k8s-fundamentals-1 course.goretskiy.pro/dedicated=lab:NoSchedule
cat > /tmp/k8s-m13/taints.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata: {name: blocked-by-taint, namespace: k8s-m13}
spec:
  containers: [{name: app, image: busybox:1.37, command: ["sh", "-c", "sleep 3600"]}]
---
apiVersion: v1
kind: Pod
metadata: {name: tolerated, namespace: k8s-m13}
spec:
  tolerations:
  - {key: course.goretskiy.pro/dedicated, operator: Equal, value: lab, effect: NoSchedule}
  containers: [{name: app, image: busybox:1.37, command: ["sh", "-c", "sleep 3600"]}]
EOF
kubectl apply --dry-run=server -f /tmp/k8s-m13/taints.yaml
kubectl apply -f /tmp/k8s-m13/taints.yaml
sleep 5
kubectl -n k8s-m13 get pod blocked-by-taint tolerated -o wide
kubectl -n k8s-m13 describe pod blocked-by-taint
kubectl taint node k8s-fundamentals-1 course.goretskiy.pro/dedicated:NoSchedule-
kubectl -n k8s-m13 wait --for=condition=Ready pod/blocked-by-taint --timeout=120s
11

11. Topology и диагностика FailedScheduling

Мягкий spread не создаёт новые failure domains: все replicas окажутся на единственном Node.

terminal
cat > /tmp/k8s-m13/spread.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata: {name: spread, namespace: k8s-m13}
spec:
  replicas: 3
  selector: {matchLabels: {app: spread}}
  template:
    metadata: {labels: {app: spread}}
    spec:
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: ScheduleAnyway
        labelSelector: {matchLabels: {app: spread}}
      containers: [{name: app, image: nginx:1.29-alpine}]
EOF
kubectl apply --dry-run=server -f /tmp/k8s-m13/spread.yaml
kubectl apply -f /tmp/k8s-m13/spread.yaml
kubectl -n k8s-m13 rollout status deployment/spread --timeout=120s
kubectl -n k8s-m13 get pods -l app=spread -o custom-columns=POD:.metadata.name,NODE:.spec.nodeName
kubectl get nodes -L kubernetes.io/hostname,topology.kubernetes.io/zone

Затем потребуйте отсутствующий label. Диагностируйте через Pod conditions, Events и фактические Node labels, после чего удалите только неисправный Pod.

terminal
kubectl -n k8s-m13 run impossible --image=busybox:1.37 --overrides='{"spec":{"nodeSelector":{"course.goretskiy.pro/pool":"missing"},"containers":[{"name":"impossible","image":"busybox:1.37","command":["sh","-c","sleep 3600"]}]}}'
sleep 3
kubectl -n k8s-m13 get pod impossible -o wide
kubectl -n k8s-m13 describe pod impossible
kubectl get nodes --show-labels
kubectl -n k8s-m13 get events --field-selector involvedObject.name=impossible --sort-by=.metadata.creationTimestamp
kubectl -n k8s-m13 delete pod impossible
12

12. Контроль, критерии освоения и cleanup

  • Что делает scheduler после фильтрации?
  • Scheduled равно Ready?
  • nodeSelector — hard или soft?
  • Что значит IgnoredDuringExecution?
  • Почему вторая anti-affinity replica Pending?
  • Toleration выбирает Node?
  • Различия трёх taint effects?
  • Что такое topology domain и skew?
  • Почему single-node не доказывает spread?
  • Почему preemption не исправит label mismatch?
terminal
kubectl taint node k8s-fundamentals-1 course.goretskiy.pro/dedicated:NoSchedule- 2>/dev/null || true
kubectl label node k8s-fundamentals-1 course.goretskiy.pro/pool-
kubectl delete namespace k8s-m13 --wait=true
rm -rf /tmp/k8s-m13
kubectl get namespace k8s-m13 2>&1 || true

Модуль освоен, если вы прогнозируете placement четырёх сценариев, находите блокирующий hard constraint и оставляете Node без учебных taints/labels.