K8s Q A Node Affinity Taints Tolerations

Материал из noname.com.ua
Перейти к навигацииПерейти к поиску


Распределяем pod-ы по машинам в kubernetes

Зачем управлять распределением POD-ов?

Зачем вообще нужно привязывать поды к определенным узлам?
Это может быть связано с производительностью, безопасностью или надежность.
Например – pod может требовать доступ к специфическому железу (видеокарты и ML-ускорители для задач машинного обучения, аппаратные криптоускорители).
Это может быть продиктовано безопасностью: критические части проекта будут размещаться на машинах, где физически не может быть ничего, кроме них. Это снижает шансы на то, что удачный взлом, скажем, сервиса регистраций раскроет данные о платежах.
Не оторые стандарты безопасности (включая PCI DSS) имеют даже требования к физической безопасности серверов – датчики вскрытия, пломбы на корпусках, запрет на доступ.


Отдельная удобная особенность – tier-инг. Нагрузку в кластере можно разделить на “важную” и “не очень”. Под важную выделять мощные современные машины с резервированием PSU, горячей замены дисков и памяти, под “не очень” – соскрести какой-нибудь хлам.
В облаках это делается даже проще за счет spot instances. Такие инстансы дешевле (порой радикально), но их работу никто не гарантирует – инстанс может отключится в любой момент (вместо него появится новый). Это вызовет пересоздание POD-ов, но для чего-то маловажного это, может – и не страшно совсем.

Способы управления

NodeSelector

Это самый простой способ управления аллокацией. Он предельно прямолинеен – запутаться в нем невозможно. Выполняется в 2 этапа. Сначала надо поставить метки на node командой label:

kubectl label nodes snowflake3 disk=hdd

Проверить, какие метки уже есть можно через

kubectl describe nodes

Теперь можно указать pod-у требование на привязку к конкретной метке. Для аллокации пода будут использоваться только помеченые узлы, то есть при включении nodeSelector для пода ноды без меток будут игнорироваться:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
  nodeSelector:
    disk: hdd

Для deployment nodeSelector передается в шаблон pod-а, как обычно.
Не смотря на удобство и прямолинейность подхода – nodeSelector имеет три минуса:

  • nodeSelector применяется в момент аллокации пода и бесполезен, если под уже аллоцирован. Если вам нужно “освободить” ноду – придется поставить на нее метку и затем выкинуть оттуда поды командой drain
  • nodeSelector не особенно гибкий и работает по принципу “один к одному”. К примеру, можно сделать метки для машин small, medium и large и указать поду, что он должен развернуться на машине класса small. Но нельзя – на машине класса medium или large – возможен только один вариант.
  • nodeSelector не запрещает аллокаций. То есть на машине с меткой могут размещаться поды без nodeAffinity. Для решения этой проблемы придуман иной подход.

Taints and Tolerations

Taints – это NodeAffinity наоборот. Если nodeAffinity говорит scheduler-у, где он должен размещать pod-ы, то taint говорит, где pod-ы размещать нельзя. Любой taint запрещает размещение на машине любых подов (есть одно исключение, про него дальше). Однако можно создать под, который будет игнорировать (tolerate) этот запрет – и данный pod запустится на данной машине. Даже если у вас есть совершенно пустой нормальный кластер kubernetes – у вас уже есть taint. По умолчанию kubernetes запрещает размещать обычные поды на master nodes – это taint node-role.kubernetes.io/master

taint создается с помощью команды kubectl taint.
Общий вид:

kubectl taint nodes nodeName taintKey=taintValue:taintEffect

taintKey и taintValue – это просто метки, они могут быть произвольными.
У taintEffect есть 3 возможных значения:

  • NoSchedule – новые поды не будут аллоцироваться, однако существующие продолжат свою работу
  • PreferNoSchedule – новые поды не будут аллоцироваться, если в кластере есть свободное место
  • NoExecute – все запущенные поды без tolerations должны быть убраны

Теперь о том, как прописываются tolerations. Язык tolerations слегка сложнее прямолинейного подхода nodeAffinity:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
  tolerations:
  - key: "pft-env"
    operator: "Exists"
    effect: "NoSchedule"

в данном примере мы создадим под, который будет игнорировать taint, созданный вот такой командой:

kubectl taint nodes pft-node-1 pft-env=true:NoSchedule

Есть более сложный вариант – можно учитывать не только факт наличия метки, но и ее значение.


Создадим пару taint-ов:

kubectl taint nodes secure-1 secGroup=secure:NoSchedule
kubectl taint nodes insecure-2 secGroup=unsafe:NoSchedule
apiVersion: v1
kind: Pod
metadata:
  name: vault
  labels:
    env: test
spec:
  containers:
  - name: vault
    image: vault
  tolerations:
  - key: "secGroup"
    operator: "Equals"
    value: "secure"
    effect: "NoSchedule"

В данном примере pod vault будет создан только на ноде secure-1, потому что только на ней secGroup равен secure.

Taint-ов можно создать сколь угодно много и условия проверки можно сочетать, как в примере ниже:

apiVersion: v1
kind: Pod
metadata:
  name: processing
  labels:
    env: test
spec:
  containers:
  - name: processing
    image: processing
  tolerations:
  - key: "dedicatedNode"
    operator: "Exists"
    effect: "NoSchedule"
  - key: "secGroup"
    operator: "Equals"
    value: "secure"
    effect: "NoExecute"

В данном примере мы выделяем пул выделенных машин taint-ом “dedicatedNode” и отдельно помечаем группу максимальной безопасности значением secure для группы secGroup.

Удалить taint можно, добавив в конец знак минуса:

kubectl taint nodes secure-1 secGroup=secure:NoSchedule-
nodeAffinity 

nodeAffinity

Не смотря на простоту и эффективность механизма nodeSelector – механизм это прямолинейный и не особенно гибкий.
Авторы kubernetes предлагают более мощный, гибкий (а так же – сложный и неудобный) механизм – nodeAffinity.
Язык описания nodeAffinity предлагает несколько мощных возможностей:

логические операторы для выбора условия размещения – IN (размещать на одной из нод с разными метками) или AND (размещать на нодах, имеющих обе метки сразу) можно выбраить политики размещения pod-ов относительно друг друга: например – запретить экземплярам кэша оказываться на одной физической машине или требовать размещение приложения вместе с экземпляром кеша на одном физическом узле Минус nodeAffinity в том, что язык очень многословный и читается тяжело. Общая спецификация выглядит так:

spec:
  affinity:
    nodeAffinity:
      {affinityClass}:
        nodeSelectorTerms:
        - matchExpressions:
          - key: {affinityKey}
            operator: {affinityOperator}
            values:
            - {affinityValues}

affinityClass влияет на строгость выбора узла:

  • requiredDuringSchedulingIgnoredDuringExecution: обязательно размещать pod-ы по требованию nodeAffinity. Если разместить не получится – pod застрянет в статусе Pending
  • preferredDuringSchedulingIgnoredDuringExecution: по возможности размещать pod-ы по требованиям affinity. Если поды не влезли – scheduler разместит их “как получится”
  • affinityKey – это метка (ключ), по которой мы будем искать ноды для размещения pod-ов.
  • affinityValues – это значения метки, которые нам подойдут
  • affinityOperator – это тот логический оператор, по которому будет производится выбор метки.

Варианты:

  • In – подойдет любое из перечисленных значений
  • NotIn – противоположно In
  • Exists – метка просто есть (values игнорируется)
  • DoesNotExists – противоположно Exists
  • Gt – Greater than – значение метки больше указанного в политике числа. Сработает только для чисел
  • Lt – Less than – противоположно Gt

affinityClass preferredDuringSchedulingIgnoredDuringExecution слегка отличается – вместо nodeSelectorTerms используется поле preference (синтаксис такой же), плюс есть обязательное поле weight – оно отвечает за приоритет при выборе node.
Пример:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/e2e-az-name
            operator: In
            values:
            - e2e-az1
            - e2e-az2
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: kubernetes.io/node-tier
            operator: In
            values:
            - silver
            - bronze
  containers:
  - name: with-node-affinity
    image: k8s.gcr.io/nginx

affinity не учитывается для уже аллоцированных nodes, так что если нужно освободить node-у от всех подов которые там уже есть – поможет команда kubectl node drain


PodAffinitty и PodAntiAffinitty

Механизм, который помогает размещать pod-ы относительно нод – может так же помочь и разместить pod-ы относительно
друг друга – за это отвечают классы PodAffinity и PodAntiAffinity.
Все три класса можно сочетать друг с другом, синтаксис внутри одинаковый, по этому просто покажу пример:

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

В этом примере мы запрещаем экземплярам redis размещаться на одном узле.

  • Каждый pod в этом deployment получит метку app:store,

политика podAntiAffinity запрещает размещать второй под с меткой app=store на ноде с таким же hostname.

Важный параметр тут – topologyKey. Именно по нему scheduler понимает, какие node-ы считаются одной зоной размещения,а какие – нет.

Усложним пример, добавив web worker:

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

В этом примере мы размещаем nginx на разных node (как мы сделали с redis), но при этом требуем, чтобы nginx размещался вместе с redis. Это может быть удобно для кешей. Проверим, что получилось:

> kubectl get pods -o wide

NAME                           READY     STATUS    RESTARTS   AGE       IP           NODE
redis-cache-1450370735-6dzlj   1/1       Running   0          8m        10.192.4.2   kube-node-3
redis-cache-1450370735-j2j96   1/1       Running   0          8m        10.192.2.2   kube-node-1
redis-cache-1450370735-z73mh   1/1       Running   0          8m        10.192.3.1   kube-node-2
web-server-1287567482-5d4dz    1/1       Running   0          7m        10.192.2.3   kube-node-1
web-server-1287567482-6f7v5    1/1       Running   0          7m        10.192.4.3   kube-node-3
web-server-1287567482-s330j    1/1       Running   0          7m        10.192.3.2   kube-node-2

Еще один пример - своими словами

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution: null
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app.kubernetes.io/name: "parity"
            parity/chain: "mainnet"
        topologyKey: failure-domain.beta.kubernetes.io/zone

topologyKey: failure-domain.beta.kubernetes.io/zone Эта часть определяет метки которые не должны совпадать (или должны совпадать в случае podAffinity:) у node


- labelSelector:
          matchLabels:
            app.kubernetes.io/name: "parity"
            parity/chain: "mainnet"

Эта часть относится к поду и тут 2 условия итого я читаю эту запись как


  • найти все POD у которых есть одновременно 2 метки - app.kubernetes.io/name со значением "parity" и parity/chain со значением "mainnet"
  • найти все ноды на которых запущены поды из списка с шага 1
  • для всех нод из списка шага 2 составить список значений метки failure-domain.beta.kubernetes.io/zone
  • найти ноду у которой значение метки failure-domain.beta.kubernetes.io/zone не входит в список из шага 3


Еще пример с более сложной конфигурацией

affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: release_group
              operator: In
              values:
              - openstack-designate
            - key: application
              operator: In
              values:
              - designate
            - key: component
              operator: In
              values:
              - api
          topologyKey: kubernetes.io/hostname
        weight: 10

Что тут важно

  • preferredDuringSchedulingIgnoredDuringExecution - это означает что условие не обязательное,

другими словами scheduler попытается найти ноду, удовлетворяющую условиям, но если не найдет то POD все равно буltn pfgeoty

Static pod allocations

Это очень редкий случай, но не упомянуть его было бы нечестно. Pod-ы можно аллоцировать полностью статически, вручную привязав к конкретной node. В этом случае scheduler никак на них не влияет. На них не действуют taints, nodeSelector и podAffinity. Даже node drain ничего не сможет с такими подами сделать. Зачем это может потребоваться? Ну, во-первых для запуска таких pod-ов не нужен работающий scheduler или apiserver. Это делает размещение таких подов сверхнадежным – они будут работать всегда. Именно так kubeadm устанавливает свои компоненты – это не полноценные демоны, а контейнеры, которые вручную привязаны к master node.

Во-вторых такой подход может потребоваться в случае, если какой-то контейнер надо привязать к конкретной, строго определенной node вручную и ни при каких условиях не давать ему оттуда уезжать. Скажем, у вас какое-то особое шифрование и оно зависит от HSM, который физически подключен к определенной, особо защищенной машине. Вообще – это порочная практика и такой сценарий лучше решается через nodeSelector + taint, но мало ли?

Выполнить статическую аллоакцию очень просто – нужно положить манифесты pod-ов в папку со статическими подами. Этот путь можно задать двумя путями:

через аргумент командной строки kubelet: --pod-manifest-path через параметр конфига staticPodPath Если у вас kubernetes установлен через kubeadm – этот параметр там уже есть, kubelet будет искать статические манифесты по адресу /etc/kubernetes/manifests. Kubelet перечитывает папку с манифестами каждые 10 секунд. Если удалить манифест – kubernetes удалит pod.

Просто создадим манифест статического пода

cat <<EOF >/etc/kubernetes/manifests/static-web.yaml
apiVersion: v1
kind: Pod
metadata:
  name: static-web
  labels:
    role: static
spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
          protocol: TCP
EOF

И проверим, что получилось:

> kubectl get pods -l role=static

NAME                       READY     STATUS    RESTARTS   AGE
static-web-my-node1        1/1       Running   0          2m

Порядок применения

  • Первым всегда применяется static pod. Он игнорирует все (taints, affinities, node selectors).
  • Вторым по списку применяется taint. Если у pod нет toleration – он не будет размещен, по этому taint – это очень эффективный способ “разогнать” pod-ы с определенного узла (или группы узлов).
  • В случае, если есть nodeAffinity и nodeSelector – должны сработать оба условия сразу (то есть – и метка селектора и условия affinity).

Заключение

Kubernetes – мощный, богатый на возможности инструмент. Он кажется слегка неудобным, но ровно до момента понимания логики его работы. Scheduler у kubernetes практически ключевой компонент, и он достаточно гибок, пусть и не самым лучшим образом описан. Надеюсь – эта статья кому-то поможет. Высокого вам аптайма!