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

  • - podAffinityTerm - это элемент списка (list, [ ... ] ), которых может быть более одного
  • matchExpressions - это список условий, которые перечисляют, какие метки (labelSelector) должны быть у пода и какие значения должны быть у этих меток. ВАЖНО что под, от которого будет происходить "отталкивание" должен удовлетворять ВСЕМ условиям, т е у него должны быть все метки release_group application component каждая с соответствующим значением
  • topologyKey в данном примере это нода, те скедулер попробует выбрать ноду на которой нет ПОДа соответвующего условиям, однако это может быть и другой ключ, например rack или datacenter


Если нужно выбрать ноду, на которой не должно быть нескольких подов, то конфигурация будет следующая

affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: label_1_name_for_pod_1
              operator: In
              values:
              - label_1_value_for_pod_1

... тут сколько угодно еще таких конструкций - key: ...

            - key: label_N_name_for_pod_1
              operator: In
              values:
              - label_N_value_for_pod_1
          topologyKey: kubernetes.io/hostname
        weight: 10


... тут сколько угодно еще таких конструкций - podAffinityTerm:...


      - podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: label_1_name_for_pod_M
              operator: In
              values:
              - label_1_value_for_pod_M

... тут сколько угодно еще таких конструкций ...

            - key: label_N_name_for_pod_M
              operator: In
              values:
              - label_N_value_for_pod_M
          topologyKey: kubernetes.io/hostname
        weight: 10


В этом примере scheduler попытается найти ноду (по тому что topologyKey: kubernetes.io/hostname) которая удовлетворяет условиям: 1. на ней нет ПОДа с метками label_1_name_for_pod_1 и значением этой метки label_1_value_for_pod_1 и с далее с перечисленными метками вплоть до последне метки label_N_name_for_pod_1 со значением label_N_value_for_pod_1 2 И так далее проверяя все условия из списка preferredDuringSchedulingIgnoredDuringExecution: [ ... ] в плоть до последнего условия, которое требует что бы не было ПОДА со списком меток label_1_name_for_pod_M ... label_N_name_for_pod_M которые имеют соответственно значения label_1_value_for_pod_M ... label_N_value_for_pod_M

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 практически ключевой компонент, и он достаточно гибок, пусть и не самым лучшим образом описан. Надеюсь – эта статья кому-то поможет. Высокого вам аптайма!