![Image](https://blog.techiescamp.com/content/images/2024/09/scheduler-worlkflow-1.gif) ### Введение Планирование (scheduling) в Kubernetes — это механизм, который определяет **где именно будет запущен Pod**. В production-кластерах почти никогда не требуется жёсткая привязка к конкретной ноде. Вместо этого Kubernetes предлагает **декларативные и гибкие инструменты управления размещением нагрузки**: - **Node Affinity** — логика _выбора_ подходящих нод - **Taints & Tolerations** — логика _ограничения доступа_ к нодам Эти механизмы работают совместно с kube-scheduler и позволяют строить масштабируемые, отказоустойчивые и управляемые кластеры. --- ## Как kube-scheduler принимает решение В упрощённом виде scheduler проходит три логических этапа: 1. **Filter** — отбрасывает ноды, которые не подходят (ресурсы, taints, affinity) 2. **Score** — оценивает оставшиеся ноды по различным критериям 3. **Bind** — привязывает Pod к выбранной ноде Affinity и taints/tolerations участвуют именно на этапах **Filter** и **Score**, не ломая работу scheduler’а, а направляя его. --- ## Node Affinity: управление выбором нод Node Affinity — это развитие `nodeSelector`. Она позволяет описывать **условия размещения Pod’ов на нодах**, используя логические выражения и режимы строгости. ### Подготовка нод Добавим метки нодам: ```bash kubectl label nodes worker-1 zone=eu-central kubectl label nodes worker-2 zone=eu-west kubectl label nodes worker-3 zone=eu-central ``` Проверка: ```bash kubectl get nodes --show-labels ``` --- ### requiredDuringSchedulingIgnoredDuringExecution **Жёсткое требование**: Pod **не будет запущен**, если нет подходящих нод. ```yaml apiVersion: v1 kind: Pod metadata: name: app-required-affinity spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: zone operator: In values: - eu-central containers: - name: app image: nginx ``` Результат: - Pod может быть запущен **только** на `worker-1` или `worker-3` - если все такие ноды недоступны → `Pending` Это основной механизм для: - разделения по зонам - hardware-aware scheduling (SSD, GPU) - регуляторных требований --- ### preferredDuringSchedulingIgnoredDuringExecution **Мягкое предпочтение**: scheduler _старается_, но не гарантирует. ```yaml apiVersion: v1 kind: Pod metadata: name: app-preferred-affinity spec: affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: zone operator: In values: - eu-central containers: - name: app image: nginx ``` Поведение: - если есть `eu-central` → Pod пойдёт туда - если нет → будет выбран любой доступный узел Используется для: - оптимизации latency - балансировки по зонам - “best effort” размещения --- ### Несколько условий affinity ```yaml matchExpressions: - key: zone operator: In values: ["eu-central"] - key: disktype operator: In values: ["ssd"] ``` Интерпретация: - логика **AND** внутри одного `matchExpressions` - логика **OR** между `nodeSelectorTerms` --- ## Taints и Tolerations: контроль доступа к нодам Если affinity отвечает на вопрос > _«Куда можно?»_ то taints отвечают > _«Кому нельзя?»_ --- ## Что такое `nodeName` `nodeName` — это **прямое указание Kubernetes**: > «Запусти этот Pod **только** на конкретной ноде с таким именем». Когда `nodeName` задан: - kube-scheduler **полностью пропускается** - Kubernetes **не делает проверок** на ресурсы, affinity и балансировку - kubelet на указанной ноде либо запускает Pod, либо Pod зависает в `Pending` --- ## Пример Pod с `nodeName` `apiVersion: v1 kind: Pod metadata: name: nginx-fixed-node spec: nodeName: worker-1 containers: - name: nginx image: nginx` Применяем: `kubectl apply -f pod.yaml` Проверяем: `kubectl get pod nginx-fixed-node -o wide` Вывод: `NAME READY STATUS NODE nginx-fixed-node 1/1 Running worker-1` Pod **никогда** не будет запущен ни на какой другой ноде. ### Taint: пометить ноду как “закрытую” ```bash kubectl taint nodes worker-1 dedicated=infra:NoSchedule ``` Проверка: ```bash kubectl describe node worker-1 | grep Taint ``` Теперь: - **ни один Pod не будет запланирован** на эту ноду - кроме Pod’ов с соответствующей toleration --- ### Pod без toleration ```yaml apiVersion: v1 kind: Pod metadata: name: app-no-toleration spec: containers: - name: app image: nginx ``` Результат: - Pod **никогда не попадёт** на `worker-1` - scheduler исключит ноду на этапе Filter --- ### Pod с toleration ```yaml apiVersion: v1 kind: Pod metadata: name: app-with-toleration spec: tolerations: - key: "dedicated" operator: "Equal" value: "infra" effect: "NoSchedule" containers: - name: app image: nginx ``` Теперь Pod: - **имеет право** запускаться на `worker-1` - но **не обязан** — scheduler всё ещё выбирает оптимальную ноду --- ### Основные эффекты taints |Effect|Поведение| |---|---| |`NoSchedule`|новые Pod’ы не планируются| |`PreferNoSchedule`|старается не планировать| |`NoExecute`|удаляет уже запущенные Pod’ы| --- ### NoExecute и eviction ```bash kubectl taint node worker-2 maintenance=true:NoExecute ``` Pod без toleration: - будет **удалён** с ноды Pod с toleration: ```yaml tolerations: - key: "maintenance" operator: "Equal" value: "true" effect: "NoExecute" tolerationSeconds: 300 ``` Поведение: - Pod живёт 5 минут - затем удаляется Используется для: - graceful maintenance - drain нод без downtime --- ## Совместное использование Affinity и Taints Типичный production-паттерн: ```yaml affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-role operator: In values: ["infra"] tolerations: - key: "dedicated" operator: "Equal" value: "infra" effect: "NoSchedule" ``` Логика: - affinity **выбирает** infra-ноды - taint **защищает** их от случайных Pod’ов - toleration **разрешает доступ** нужным workload’ам --- # Kubernetes Scheduling на практике ### `kubectl describe pod`: реальные events + паттерны для k3s и bare-metal ![Image](https://kubernetes.io/images/docs/scheduling-framework-extensions.png) ![Image](https://www.warp.dev/static/image/r/w%3D3840%2Cq%3D80%2Cformat%3Dauto/g_A_Iu2_M_da1cc75bfa.png) --- ## Почему `kubectl describe pod` — главный инструмент для понимания scheduler’а Если Pod находится в `Pending`, **90% всей правды уже есть в Events**. `scheduler`, `kubelet`, `controller-manager` — все они пишут туда свои решения и ошибки. Команда: ```bash kubectl describe pod <pod-name> ``` Ключевой раздел: ```text Events: Type Reason Age From Message ``` Именно здесь видно: - **почему Pod не запланирован** - **какие ноды были отфильтрованы** - **какое правило affinity / taint сработало** --- ## Кейc 1: Node Affinity — нет подходящих нод ### Pod с required node affinity ```yaml affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: zone operator: In values: - eu-central ``` ### Вывод `kubectl describe pod` ```text Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 12s default-scheduler 0/3 nodes are available: 3 node(s) didn't match Pod's node affinity/selector. ``` ### Что здесь происходит - scheduler просмотрел **все 3 ноды** - ни у одной **нет label `zone=eu-central`** - фильтрация произошла на этапе **Filter** - Pod остался в `Pending` 📌 Важно: scheduler **не пытается “почти” угадать** — `required` = жёсткое правило. --- ## Кейc 2: preferred affinity — Pod запланирован, но не туда ### Affinity с предпочтением ```yaml preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: zone operator: In values: - eu-central ``` ### Events ```text Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 8s default-scheduler Successfully assigned default/app to worker-2 ``` ### Интерпретация - ноды с `eu-central` **были недоступны** - правило `preferred` **не блокирует scheduling** - Pod ушёл на `worker-2`, потому что: - ресурсы были доступны - штраф за отсутствие label меньше, чем отсутствие ресурсов 📌 Это типичное поведение для latency-оптимизаций. --- ## Кейc 3: Taint `NoSchedule` — нода исключена ### Нода с taint ```bash kubectl taint node worker-1 dedicated=infra:NoSchedule ``` ### Pod без toleration ### `kubectl describe pod` ```text Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 20s default-scheduler 0/3 nodes are available: 1 node(s) had taint {dedicated: infra}, that the pod didn't tolerate, 2 node(s) didn't have enough cpu. ``` ### Что важно заметить - scheduler **проверяет сразу все причины** - сообщения агрегируются - видно **точное количество нод по каждой причине** 📌 Это самый ценный вывод для production-debug. --- ## Кейc 4: `NoExecute` и eviction ### Нода переведена в maintenance ```bash kubectl taint node worker-2 maintenance=true:NoExecute ``` ### Pod без toleration ```text Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Killing 15s kubelet Stopping container app Warning Evicted 14s kubelet Pod was evicted due to node taint ``` ### Если есть toleration с `tolerationSeconds` ```yaml tolerations: - key: maintenance operator: Equal value: "true" effect: NoExecute tolerationSeconds: 300 ``` Pod: - остаётся **ровно 5 минут** - затем kubelet инициирует eviction --- ## Как читать Events правильно (чек-лист) При `FailedScheduling` смотри: 1. **0/N nodes are available** 2. Причины: - affinity - taints - cpu / memory - volume node affinity 3. Повторяется ли event каждые ~5 секунд → scheduler всё ещё пытается --- # Паттерны scheduling для k3s и bare-metal --- ## Паттерн 1: Master/Control-plane isolation (k3s) В k3s control-plane часто **совмещён с worker**, но его почти всегда изолируют. ### Taint control-plane ```bash kubectl taint nodes k3s-master node-role.kubernetes.io/control-plane=true:NoSchedule ``` ### Что это даёт - пользовательские workload’ы туда не попадут - system-компоненты (coredns, metrics-server) имеют tolerations 📌 Обязательный паттерн даже для single-node k3s. --- ## Паттерн 2: Infra-ноды (bare-metal) ### Ноды ```text worker-1 role=infra worker-2 role=infra worker-3 role=app worker-4 role=app ``` ### Taint infra-нод ```bash kubectl taint nodes -l role=infra dedicated=infra:NoSchedule ``` ### Workload для infra ```yaml affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: role operator: In values: ["infra"] tolerations: - key: dedicated operator: Equal value: infra effect: NoSchedule ``` Используется для: - ingress - monitoring - logging - storage (Longhorn, Ceph) --- ## Паттерн 3: Storage-aware scheduling (bare-metal) Частая ошибка: storage-Pod попал не туда. ### Label storage-нод ```bash kubectl label nodes worker-1 storage=nvme kubectl label nodes worker-2 storage=hdd ``` ### Affinity для Longhorn / Ceph ```yaml nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: storage operator: In values: ["nvme"] ``` 📌 Без этого volume может стать bottleneck’ом. --- ## Паттерн 4: Zone / rack awareness (bare-metal) ```bash kubectl label nodes worker-1 rack=rack-a kubectl label nodes worker-2 rack=rack-b ``` Используется вместе с: - podAntiAffinity - StatefulSet Результат: - реплики **физически разнесены** - отказ одного rack ≠ outage сервиса --- ## Паттерн 5: k3s + single-node fallback Для домашнего k3s: - **не использовать required affinity** - использовать `preferred` - минимально применять taints Причина: - один узел = нет альтернатив - жёсткие правила = вечный `Pending` ---