Распределение подов по узлам

Можно настроить под так, чтобы тот мог запускаться только на определенном(-ых) узле(-ах) или предпочитал определенную группу узлов. Сделать это можно несколькими способами, при этом все рекомендуемые подходы используют селекторы лейблов для выбора. Зачастую нужды в подобных ограничениях нет; планировщик автоматически размещает поды оптимальным образом (например, распределяет их по узлам, чтобы они не оказались все на одном узле с дефицитом ресурсов). Однако в некоторых обстоятельствах возможность контролировать, куда именно попадет под, может пригодиться. Например, она поможет запланировать под на узел с быстрым SSD-хранилищем или разместить два активно взаимодействующих друг с другом пода в одной зоне доступности.

Для планирования подов на определенные узлы можно использовать любой из методов:

Лейблы узлов

Как и у многих других объектов Kubernetes, у узлов есть лейблы. Их можно навешивать вручную. Kubernetes также навешивает стандартный набор лейблов на все узлы кластера.

Изоляция узла/ограничение его использования

Лейблы узлов позволяют размещать поды на определенные узлы или группы узлов. С их помощью можно планировать поды на узлы с определенными требованиями к изоляции, безопасности или соответствию нормативным положениям.

При использовании лейблов для изоляции узлов следует выбирать ключи лейблов, которые kubelet не может изменить. В этом случае взломанный узел не сможет навесить на себя эти лейблы в надежде, что планировщик разместит на него рабочие нагрузки.

Admission-плагин NodeRestriction не позволяет kubelet'у устанавливать или изменять лейблы с префиксом node-restriction.kubernetes.io/.

Чтобы использовать этот префикс для изоляции узла:

  1. Убедитесь, что используется авторизатор узлов и включен admission-плагин NodeRestriction.
  2. Добавьте лейблы с префиксом node-restriction.kubernetes.io/ к узлам и используйте их в селекторах узлов. Например, example.com.node-restriction.kubernetes.io/fips=true или example.com.node-restriction.kubernetes.io/pci-dss=true.

nodeSelector

nodeSelector — простейшая рекомендуемая форма настроек выбора узлов. Можно добавить поле nodeSelector в спецификацию пода и перечислить в нем лейблы узлов, которые подходят для развертывания пода. В этом случае Kubernetes будет планировать под только на узлы со всеми указанными лейблами.

Дополнительную информацию см. в разделе Размещение подов на узлах.

Правила совместного/раздельного существования (affinity и anti-affinity)

nodeSelector — самый простой способ развернуть поды на узлах с определенными лейблами. Правила совместного/раздельного существования расширяют типы ограничений, которые можно накладывать. Вот некоторые из их преимуществ:

  • Язык правил affinity/anti-affinity более выразителен. nodeSelector выбирает узлы только со всеми указанными лейблами. Правила affinity/anti-affinity расширяют логику выбора и делают ее более гибкой.
  • Правило может быть мягким (soft) или предпочтительным (preferred). В этом случае планировщик все равно запланирует под, даже если подходящего узла для него не найдется.
  • При планировании пода планировщик может учитывать лейблы других подов, запущенных на узле (или в иной топологической области), а не только лейблы самого узла. Это позволяет формулировать правила, определяющие сосуществование подов на узле.

Правила совместного существования (affinity) бывают двух типов:

  • Правила для узлов (node affinity) работают подобно полю nodeSelector, но более выразительны. Кроме того, можно задавать мягкие правила.
  • Правила для подов (inter-pod affinity и anti-affinity) позволяют при планировании учитывать лейблы других подов.

Правила совместного существования для узлов (node affinity)

Правила совместного существования для узлов концептуально похожи на nodeSelector. С помощью лейблов они позволяют ограничивать список узлов, на которые может быть запланирован под. Существует два типа таких правил:

  • requiredDuringSchedulingIgnoredDuringExecution: Планировщик не может запланировать под, если правило не выполнено. Работает как nodeSelector, но с более выразительным синтаксисом.
  • preferredDuringSchedulingIgnoredDuringExecution: Планировщик пытается найти узел, который соответствует правилу. Если подходящий узел не удается найти, планировщик все равно планирует под.

Задавать правила совместного существования для узлов можно с помощью поля .spec.affinity.nodeAffinity в спецификации пода.

В качестве примера рассмотрим следующую спецификацию пода:

apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - antarctica-east1
            - antarctica-west1
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
            - another-node-label-value
  containers:
  - name: with-node-affinity
    image: registry.k8s.io/pause:2.0

В этом примере применяются следующие правила:

  • У узла должен быть лейбл с ключом topology.kubernetes.io/zone, и значение этого лейбла должно быть либо antarctica-east1, либо antarctica-west1.
  • Предпочтительно, чтобы у узла был лейбл с ключом another-node-label-key и значением another-node-label-value.

Можно использовать поле operator для указания логического оператора, который Kubernetes будет применять при интерпретации правил. Доступны In, NotIn, Exists, DoesNotExist, Gt и Lt.

Узнать больше о том, как они работают, можно в подразделе Операторы.

NotIn и DoesNotExist позволяют задавать правила раздельного существования (anti-affinity) для узла. Кроме того, можно использовать taint'ы узлов, чтобы "отвадить" поды от определенных узлов.

Дополнительную информацию см. в разделе Размещаем поды на узлы с помощью Node Affinity.

Вес правил совместного существования

Для каждого правила типа preferredDuringSchedulingIgnoredDuringExecution можно указать вес weight в диапазоне от 1 до 100. Найдя несколько узлов, удовлетворяющих всем остальным требованиям для планирования пода, планировщик перебирает предпочтительные правила, которым удовлетворяет узел, и суммирует их веса.

Итоговая сумма добавляется к оценке, полученной при анализе других параметров, влияющих на приоритет узла. Принимая решение о размещении пода, планировщик отдает предпочтение узлам с наибольшей суммарной оценкой.

В качестве примера рассмотрим следующую спецификацию пода:

apiVersion: v1
kind: Pod
metadata:
  name: with-affinity-anti-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/os
            operator: In
            values:
            - linux
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: label-1
            operator: In
            values:
            - key-1
      - weight: 50
        preference:
          matchExpressions:
          - key: label-2
            operator: In
            values:
            - key-2
  containers:
  - name: with-node-affinity
    image: registry.k8s.io/pause:2.0

Если правилу preferredDuringSchedulingIgnoredDuringExecution соответствуют два узла (один — с лейблом label-1:key-1, другой — с label-2:key-2), планировщик считает вес weight каждого узла и добавляет его к другим оценкам для этого узла. Под планируется на узел с наивысшей итоговой оценкой.

Правила совместного существования и профили планирования

СТАТУС ФИЧИ: Kubernetes v1.20 [beta]

При настройке нескольких профилей планирования можно связать профиль с правилами совместного существования для узлов (это удобно, когда профиль применяется к определенному набору узлов). Для этого необходимо добавить addedAffinity в поле args плагина NodeAffinity в конфигурации планировщика. Например:

apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration

profiles:
  - schedulerName: default-scheduler
  - schedulerName: foo-scheduler
    pluginConfig:
      - name: NodeAffinity
        args:
          addedAffinity:
            requiredDuringSchedulingIgnoredDuringExecution:
              nodeSelectorTerms:
              - matchExpressions:
                - key: scheduler-profile
                  operator: In
                  values:
                  - foo

Правило addedAffinity применяется ко всем подам с полем .spec.schedulerName, имеющим значение foo-scheduler, в дополнение к NodeAffinity, заданному в PodSpec. Таким образом, подходящие для пода узлы должны удовлетворять параметрам addedAffinity и правилам .spec.NodeAffinity пода.

Поскольку конечные пользователи не видят addedAffinity, результат применения этих правил может быть для них неожиданным. Используйте лейблы узлов, которые однозначно соотносятся с именем профиля планировщика.

Правила совместного/раздельного существования подов

Правила совместного/раздельного существования подов позволяют выбирать узлы для планирования в зависимости от лейблов подов, которые уже на этих узлах работают (вместо лейблов самих узлов).

Алгоритм работы правил совместного/раздельного существования подов можно описать так: "данный под должен (или не должен в случае раздельного (anti-affinity) существования) размещаться на X, если на нем уже работает один или несколько подов, удовлетворяющих правилу Y", где X — топологический домен, например, узел, стойка, зона/регион облачного провайдера и т. п., а Y — правило, которое Kubernetes пытается удовлетворить.

Для задания правил Y используются селекторы лейблов с необязательным связанным списком пространств имен. Для подов в Kubernetes указываются пространства имен, соответственно, их лейблы также оказываются неявно связаны с этими же пространствами имен. Любые селекторы лейблов для лейблов подов должны содержать пространства имен, в которых Kubernetes должен искать эти лейблы.

Топологический домен X задается с помощью topologyKey — ключа для лейбла узла, который система использует для обозначения домена. Примеры см. в разделе Типичные лейблы, аннотации и taint'ы.

Типы правил совместного/раздельного существования подов

По аналогии c правилами для узлов существует два типа правил для подов:

  • requiredDuringSchedulingIgnoredDuringExecution
  • preferredDuringSchedulingIgnoredDuringExecution

Например, с помощью правила совместного существования requiredDuringSchedulingIgnoredDuringExecution можно заставить планировщик размещать поды, относящиеся к разным сервисам, в одной зоне облачного провайдера, поскольку они активно обмениваются данными друг с другом.

Аналогичным образом можно использовать правило раздельного существования preferredDuringSchedulingIgnoredDuringExecution для распределения подов по нескольким зонам облачного провайдера.

Для задания правил совместного существования предназначено поле affinity.podAffinity в спецификации пода.

Для задания правил раздельного существования предназначено поле affinity.podAntiAffinity в спецификации пода.

Планирование группы подов, связанных правилами совместного существования

Если планируемый под — первый в серии подов, связанных правилами совместного существования, он может быть запланирован, если удовлетворит всем остальным правилам совместного существования. Чтобы подтвердить, что этот под — действительно первый, проводится проверка, которая должна показать, что пространство имен и селектор этого пода уникальны в кластере (то есть нет других таких подов). Кроме того, под должен соответствовать своим собственным правилам, а выбранный узел — всем запрошенным топологиям. Это предотвращает тупиковую ситуацию, когда поды не могут запланироваться из-за того, что все они связаны правилами совместного существования.

Пример правил совместного/раздельного существования для пода

Рассмотрим следующую спецификацию пода:

apiVersion: v1
kind: Pod
metadata:
  name: with-pod-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: topology.kubernetes.io/zone
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: topology.kubernetes.io/zone
  containers:
  - name: with-pod-affinity
    image: registry.k8s.io/pause:2.0

В примере задается одно правило совместного существования подов, и одно — раздельного. Для совместного правила используется жесткий тип requiredDuringSchedulingIgnoredDuringExecution, для раздельного — мягкий preferredDuringSchedulingIgnoredDuringExecution.

Правило совместного существования гласит, что планировщик может разместить под на узел, только если тот находится в зоне с одним или более подами с лейблом security=S1. Например, если есть кластер с выделенной зоной, назовем ее "V", состоящей из узлов с лейблом topology.kubernetes.io/zone=V, планировщик может назначить под на любой узел зоны V только если в этой зоне уже есть хотя бы один под с лейблом security=S1. И наоборот, если в зоне V нет подов с лейблом security=S1, планировщик не сможет назначить под на какой-либо из узлов в этой зоне.

Правило раздельного существования гласит, что планировщик при возможности не должен размещать под на узел, если тот находится в зоне с одним или более подами с лейблом security=S2. Например, если есть кластер с выделенной зоной, назовем ее "R", состоящей из узлов с лейблами topology.kubernetes.io/zone=R. Планировщик должен избегать назначать поды на узлы из зоны R, если в ней уже есть по крайней мере один под с лейблом security=S2. Соответственно, если в зоне R нет подов с лейблами security=S2, правило раздельного существования не будет влиять на планирование подов в эту зону.

Больше примеров правил совместного/раздельного существования для подов можно найти в рабочей документации.

Поле operator пода поддерживает значения In, NotIn, Exists и DoesNotExist при задании правил совместного/раздельного существования.

Узнать больше о том, как они работают, можно в подразделе Операторы.

В принципе, topologyKey может быть любым разрешенным лейблом-ключом со следующими исключениями по соображениям производительности и безопасности:

  • При задании правил совместного/раздельного существования для подов пустое поле topologyKey не допускается как для requiredDuringSchedulingIgnoredDuringExecution, так и для preferredDuringSchedulingIgnoredDuringExecution.
  • Для правил раздельного существования типа requiredDuringSchedulingIgnoredDuringExecution admission-контроллер разрешает использовать только kubernetes.io/hostname в качестве topologyKey. Для работы с кастомными топологиями admission-контроллер можно дополнить или совсем отключить его.

В дополнение к labelSelector и topologyKey можно опционально указать список пространств имен, которые должен выбирать labelSelector, с помощью поля namespaces на том же уровне, что labelSelector и topologyKey. Если поле namespaces опущено или пусто, по умолчанию выбирается пространство имен пода, в котором задаются правила совместного/раздельного существования.

Селектор пространств имен

СТАТУС ФИЧИ: Kubernetes v1.24 [stable]

Подходящие пространства имен также можно выбрать с помощью namespaceSelector, который попытается найти лейбл в наборе пространств имен. Условия совместного существования применяются к пространствам имен, выбранным как селектором namespaceSelector, так и полем namespaces. Обратите внимание, что пустой селектор namespaceSelector ({}) выбирает все пространства имен, в то время как пустой или null-список namespaces и null-селектор namespaceSelector выбирает пространство имен пода, в котором правило задано.

matchLabelKeys

СТАТУС ФИЧИ: Kubernetes v1.31 [beta] (enabled by default: true)

Kubernetes включает необязательное поле matchLabelKeys для правил совместного (раздельного) существования подов. В нем указываются ключи для лейблов, которые должны совпадать с лейблами входящего пода, чтобы правила совместного (раздельного) существования выполнялись.

Ключи используются для поиска значений в лейблах подов; эти ключи-лейблы объединяются (с помощью AND) с ограничениями, задаваемыми с помощью поля labelSelector. Такая комбинированная фильтрация позволяет отобрать набор существующих подов, которые приниматься в расчет при обработке правил совместного (раздельного) существования.

Обычно matchLabelKeys используется вместе с pod-template-hash (задается для подов, которые управляются как часть деплоймента, где значение уникально для каждой ревизии). Использование pod-template-hash в matchLabelKeys позволяет нацеливаться на поды, принадлежащие к той же ревизии, что и входящий под. Таким образом, что скользящее обновление не нарушит правила совместного существования.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: application-server
...
spec:
  template:
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - database
            topologyKey: topology.kubernetes.io/zone
            # При расчете affinity подов учитываются только поды из определенного выката.
            # При обновлении Deployment'а новые поды будут следовать своим собственным правилам совместного существования
            # (если они заданы в новом шаблоне подов)
            matchLabelKeys: 
            - pod-template-hash

mismatchLabelKeys

СТАТУС ФИЧИ: Kubernetes v1.31 [beta] (enabled by default: true)

Kubernetes включает необязательное поле mismatchLabelKeys для определения правил совместного (раздельного) существования подов. В поле указываются ключи для лейблов, которые не должны совпадать с лейблами входящего пода, чтобы правила совместного (раздельного) существования подов удовлетворялись.

Один из примеров использования — размещение подов определенной группы пользователей (tenant'ов) или команды в конкретном топологическом домене (узле, зоне и т. д.). То есть идея в том, чтобы избежать одновременного запуска подов от разных групп пользователей в одном топологическом домене.

apiVersion: v1
kind: Pod
metadata:
  labels:
    # Assume that all relevant Pods have a "tenant" label set
    tenant: tenant-a
...
spec:
  affinity:
    podAffinity:      
      requiredDuringSchedulingIgnoredDuringExecution:
      # следим за тем, чтобы поды, связанные с этим тенантом, попадали на нужный пул узлов
      - matchLabelKeys:
          - tenant
        topologyKey: node-pool
    podAntiAffinity:  
      requiredDuringSchedulingIgnoredDuringExecution:
      # следим за тем, чтобы поды, связанные с этим тенантом, не смогли планироваться на узлы для другого тенанта
      - mismatchLabelKeys:
        - tenant # значение лейбла "tenant" этого пода будет предотвращать
                 # планирование на узлы в пулах, на которых работают поды
                 # другого тенанта
        labelSelector:
          # Должен быть labelSelector, который выбирает поды с лейблом tenant,
          # иначе этот под также будет "ненавидеть" поды из daemonset'ов, например, 
          # те, у которых нет лейбла tenant.
          matchExpressions:
          - key: tenant
            operator: Exists
        topologyKey: node-pool

Другие примеры использования

Правила совместного/раздельного существования для подов особенно удобны, когда используются совместно с абстракциями более высокого уровня, такими как ReplicaSet, StatefulSet, Deployment и т. д. Эти правила позволяют настроить размещение рабочих нагрузок с учетом имеющейся топологии; например, пара связанных подов будет планироваться на один и тот же узел.

Представьте кластер, состоящий из трех узлов. Он используется для запуска веб-приложения и как in-memory-кэш (например, Redis). Предположим, что задержка между веб-приложением и кэшем должна быть минимальной. Правила совместного/раздельного существования для подов позволяют настроить планировщик так, чтобы тот размещал веб-серверы как можно ближе к кэшу.

В приведенном ниже примере конфигурации деплоймента с Redis его реплики получают лейбл app=store. Правило podAntiAffinity запрещает планировщику размещать несколько реплик с лейблом app=store на одном узле. В результате каждый узел получает по отдельной кэш-реплике.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-cache
spec:
  selector:
    matchLabels:
      app: store
  replicas: 3
  template:
    metadata:
      labels:
        app: store
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: redis-server
        image: redis:3.2-alpine

Конфигурация деплоймента, приведенная ниже, создает три реплики веб-сервера с лейблом app=web-store. Правило совместного существования предписывает планировщику размещать каждую реплику на узле, на котором уже имеется под с лейблом app=store. В то же время правило раздельного существования запрещает планировщику размещать несколько серверов с лейблом app=web-store на одном узле.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-server
spec:
  selector:
    matchLabels:
      app: web-store
  replicas: 3
  template:
    metadata:
      labels:
        app: web-store
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web-store
            topologyKey: "kubernetes.io/hostname"
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: web-app
        image: nginx:1.16-alpine

Развертывание ресурсов в соответствии с приведенными выше конфигурациями приведет к созданию кластера, в котором на каждом узле будет по одному веб-серверу и одной реплике Redis (всего три отдельных узла):

узел 1узел 2узел 3
webserver-1webserver-2webserver-3
cache-1cache-2cache-3

В итоге к каждому инстансу Redis'а, скорее всего, будет обращаться клиент, который работает с ним на том же узле. Подобный подход позволит минимизировать как перекос (дисбаланс нагрузки), так и задержки.

Правила совместного/раздельного существования для подов можно использовать и в других случаях.

См., например, руководство по ZooKeeper. В нем с помощью правил раздельного существования StatefulSet настраивается так, чтобы обеспечить высокую доступность (используется подход, аналогичный тому, что применен выше).

nodeName

Поле nodeName в спецификации пода — более непосредственный способ выбора узлов по сравнению с правилами совместного существования или селектором nodeSelector. Если поле nodeName не пустое, планировщик игнорирует под, а kubelet на узле с соответствующим именем пытается разместить под на этом узле. Приоритет поля nodeName выше, чем селектора nodeSelector или правил совместного/раздельного существования.

Однако у nodeName имеются и некоторые недостатки:

  • Если узел с заданным именем не существует, под не будет запущен. Кроме того, в некоторых случаях он может быть автоматически удален.
  • Если на узле с заданным именем недостаточно ресурсов для работы пода, последний будет остановлен; соответствующая причина (например, OutOfmemory или OutOfcpu) будет указана.
  • В облачных окружениях имена узлов не всегда предсказуемы или стабильны.

Ниже приведен пример спецификации пода с полем nodeName:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
  nodeName: kube-01

Такой под будет работать только на узле с именем kube-01.

Ограничения на топологию распределения подов

С помощью ограничений на топологию распределения (topology spread constraints) можно настроить размещение подов в кластере по failure-доменам, таким как регионы, зоны, узлы, или любым другим заданным топологическим конфигурациям. Это позволяет повысить производительность, ожидаемую доступность или эффективность использования ресурсов.

Дополнительные сведения о принципах их работы читайте в разделе Ограничения на топологию распределения подов.

Операторы

Ниже приведены все логические операторы, которые можно использовать в поле operator для nodeAffinity и podAffinity.

ОператорДействие
InЗначение лейбла присутствует в предоставленном наборе строк
NotInЗначение лейбла отсутствует в предоставленном наборе строк
ExistsЛейбл с таким ключом существует для объекта
DoesNotExistУ объекта не существует лейбла с таким ключом

Следующие операторы могут использоваться только с nodeAffinity.

ОператорДействие
GtВведенное значение будет обработано как целое число, и это целое число меньше, чем целое число, полученное в результате обработки значения лейбла, указанного этим селектором
LtВведенное значение будет обработано как целое число, и это целое число больше, чем целое число, полученное в результате обработки значения лейбла, указанного этим селектором

Что дальше

Изменено December 15, 2024 at 6:24 PM PST: Merge pull request #49087 from Arhell/es-link (2c4497f)