Демистификация планирования Kubernetes: глубокое погружение в предикаты и приоритеты

Демистификация планирования Kubernetes: глубокое погружение в предикаты и приоритеты

Сегодняшняя статья в основном знакомит с этапами процесса планирования, на которых стратегии планирования Predicates и Priorities вступают в игру.

Predicates

Сначала давайте рассмотрим Predicates. Роль Predicates в процессе планирования можно понимать как фильтр, то есть: он «фильтрует» из всех узлов текущего кластера ряд узлов, удовлетворяющих условиям, в соответствии с политикой планирования. Эти узлы являются потенциальными хостами для пода, ожидающего планирования. В Kubernetes есть четыре политики планирования по умолчанию.

Первый тип называется GeneralPredicates.

Как следует из названия, этот набор правил фильтрации отвечает за наиболее базовые политики планирования. Например, PodFitsResources вычисляет, достаточно ли ресурсов CPU и памяти на хосте. Конечно, как я упоминал ранее, PodFitsResources проверяет только поле requests пода. Следует отметить, что планировщик Kubernetes не определяет конкретные типы ресурсов для аппаратных ресурсов, таких как GPU, а вместо этого использует формат Key-Value, называемый Extended Resource, для их описания. Например:

apiVersion: v1
kind: Pod
metadata:
  name: extended-resource-demo
spec:
  containers:
  - name: extended-resource-demo-ctr
    image: nginx
    resources:
      requests:
        alpha.kubernetes.io/nvidia-gpu: "2"
      limits:
        alpha.kubernetes.io/nvidia-gpu: "2"

Видно, что наш под объявляет использование двух GPU типа NVIDIA с помощью определения alpha.kubernetes.io/nvidia-gpu=2. В PodFitsResources планировщик на самом деле не знает, что значение этого ключа — GPU, а напрямую использует последующее значение для вычислений. Конечно, в поле Capacity узла также необходимо добавить общее количество GPU на этом хосте, например: alpha.kubernetes.io/nvidia-gpu=4. Я подробно расскажу об этих процессах позже при объяснении Device Plugin.

PodFitsHost проверяет, соответствует ли имя хоста spec.nodeName пода. PodFitsHostPorts проверяет, не конфликтуют ли запрошенные подом порты хоста (spec.nodePort) с уже используемыми портами. PodMatchNodeSelector проверяет, соответствует ли узел, указанный в nodeSelector или nodeAffinity пода, проверяемому узлу, и так далее.

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

Второй тип — правила фильтрации, связанные с Volume.

Этот набор правил фильтрации отвечает за политику планирования, связанную с постоянными томами (persistent volumes) контейнера.

Среди них NoDiskConflict проверяет, нет ли конфликта между постоянными томами, объявленными несколькими подами. Например, тома типа AWS EBS не могут одновременно использоваться двумя подами. Поэтому, если том EBS с именем A уже смонтирован на каком-то узле, другой под, также объявляющий использование этого тома A, не может быть запланирован на этот узел.

MaxPDVolumeCountPredicate проверяет, не превышает ли количество постоянных томов определенного типа на узле заданное число. Если да, то под, объявляющий использование постоянных томов этого типа, больше не может быть запланирован на этот узел.

VolumeZonePredicate проверяет, соответствует ли метка зоны (Zone, домен высокой доступности) постоянного тома метке зоны проверяемого узла.

Кроме того, существует правило под названием VolumeBindingPredicate. Оно отвечает за проверку того, соответствует ли поле nodeAffinity PV, соответствующего поду, метке какого-либо узла. Локальные постоянные тома (Local Persistent Volume) должны использовать nodeAffinity для привязки к конкретному узлу. Это фактически означает, что на этапе Predicates Kubernetes должен иметь возможность планировать на основе атрибутов Volume пода.

Кроме того, если PVC пода еще не привязан к конкретному PV, планировщик также отвечает за проверку всех PV, которые должны быть привязаны. Когда есть доступный PV и его nodeAffinity соответствует проверяемому узлу, это правило возвращает «успех». Например:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-local-pv
spec:
  capacity:
    storage: 500Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /mnt/disks/vol1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - my-node

Видно, что постоянный каталог, соответствующий этому PV, появится только на хосте с именем my-node. Поэтому любой под, использующий этот PV через PVC, должен быть запланирован на my-node, чтобы работать правильно. Именно здесь VolumeBindingPredicate принимает это решение.

Третий тип — правила фильтрации, связанные с хостом.

Этот набор правил в основном проверяет, удовлетворяет ли планируемый под определенным условиям самого узла.

Например, PodToleratesNodeTaints проверяет механизм «taint» (пометок) узла, который мы часто используем. Только когда поле Toleration пода соответствует полю Taint узла, этот под может быть запланирован на узел.

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

Четвертый тип — правила фильтрации, связанные с подами.

Этот набор правил частично пересекается с GeneralPredicates. Особенностью является PodAffinityPredicate. Роль этого правила — проверять отношения сродства и анти-сродства между планируемым подом и существующими подами на узле. Например:

apiVersion: v1
kind: Pod
metadata:
  name: with-pod-antiaffinity
spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: kubernetes.io/hostname
  containers:
  - name: with-pod-affinity
    image: docker.io/ocpqe/hello-pod

В этом примере правило podAntiAffinity указывает, что этот под не хочет находиться на одном узле с любым подом, имеющим метку security=S2. Следует отметить, что PodAffinityPredicate имеет область действия: например, приведенное выше правило действует только для узлов с меткой с ключом kubernetes.io/hostname. В этом и заключается роль ключевого слова topologyKey.

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

При выполнении, когда планируется под, планировщик Kubernetes одновременно запускает 16 горутин для параллельного вычисления Predicates для всех узлов в кластере и в конечном итоге возвращает список хостов, которые могут запустить этот под.

Следует отметить, что при выполнении Predicates для каждого узла планировщик проверяет в фиксированном порядке. Этот порядок определяется в соответствии со смыслом самих Predicates. Например, предикаты, связанные с хостом, проверяются раньше. В противном случае вычисление PodAffinityPredicate на хосте с крайне недостаточными ресурсами не имеет смысла.

Priorities

После «фильтрации» узлов на этапе Predicates работа этапа Priorities заключается в оценке этих узлов. Диапазон оценки здесь — 0–10 баллов, и узел с наибольшим баллом является наилучшим узлом, к которому в конечном итоге будет привязан под.

Наиболее часто используемое правило оценки в Priorities — LeastRequestedPriority. Этот алгоритм фактически выбирает хост с наибольшим количеством свободных ресурсов (CPU и Memory).

Кроме того, существуют еще три Priorities: NodeAffinityPriority, TaintTolerationPriority и InterPodAffinityPriority. Как следует из названий, они схожи по смыслу и методу вычисления с предыдущими тремя Predicates: PodMatchNodeSelector, PodToleratesNodeTaints и PodAffinityPredicate. Однако, как Priority, чем больше полей узла соответствует вышеуказанным правилам, тем выше его оценка.

В наборе Priorities по умолчанию также есть стратегия под названием ImageLocalityPriority. Это новое правило планирования, включенное в Kubernetes v1.12, которое гласит: если образ, необходимый для планируемого пода, велик и уже существует на некоторых узлах, то эти узлы получат более высокую оценку.

Конечно, чтобы избежать «сталкивания» планирования из-за этого алгоритма, планировщик также оптимизирует распределение образа при вычислении оценки: если количество узлов, на которых распределен большой образ, очень мало, то вес этих узлов будет уменьшен, тем самым «компенсируя» риск возникновения сталкивания.

В целом, это основной принцип работы правил планирования по умолчанию в планировщике Kubernetes.

В реальном процессе выполнения информация о кластере и подах в планировщике кэшируется, поэтому процесс выполнения этих алгоритмов относительно быстр.

Кроме того, для более сложных алгоритмов планирования, таких как PodAffinityPredicate, при вычислении они обращают внимание не только на планируемый под и проверяемый узел, но также на информацию о всем кластере, например, перебирают все узлы и читают их метки. В этом случае планировщик Kubernetes сначала вычисляет информацию о кластере, необходимую алгоритму, прежде чем выполнять алгоритм планирования для каждого планируемого пода, а затем кэширует ее. Таким образом, при фактическом выполнении алгоритма планировщику нужно только прочитать кэшированную информацию для вычисления, что позволяет избежать необходимости повторно извлекать и вычислять информацию обо всем кластере для каждого узла при вычислении Predicates.

Заключение

Таким образом, в этой статье были рассмотрены основные алгоритмы планирования в планировщике Kubernetes по умолчанию. Важно отметить, что помимо правил, рассмотренных в этой статье, в планировщике Kubernetes на самом деле есть некоторые стратегии, которые не включены по умолчанию. Вы можете указать файл конфигурации для kube-scheduler или создать ConfigMap, чтобы настроить, какие правила включать или отключать. Кроме того, вы можете управлять поведением планировщика, устанавливая веса для Priorities.

На этом объяснение механизмов планирования по умолчанию в планировщике Kubernetes заканчивается. Стоит упомянуть, что хотя планировщик работает быстро благодаря кэшированию информации о кластере и подах, для более сложных алгоритмов планировщик должен учитывать состояние всего кластера, а не только пода и узла. Это включает предварительные вычисления и кэширование необходимой информации о кластере до фактического выполнения алгоритма планирования, что обеспечивает эффективность и точность процесса планирования.

Novita AI — это универсальная облачная платформа, которая поддерживает ваши ИИ-амбиции. Благодаря легко интегрируемым API, бессерверным вычислениям и ускорению GPU мы предоставляем экономически эффективные инструменты, необходимые для быстрого создания и масштабирования вашего бизнеса на основе ИИ. Устраните проблемы с инфраструктурой и начните бесплатно — Novita AI делает ваши ИИ-мечты реальностью.

Рекомендуемое чтение:

  1. Что нужно знать о Docker
  2. Зачем нам нужны Pods