K8s Q A Node Affinity Taints Tolerations: различия между версиями
Sirmax (обсуждение | вклад) |
Sirmax (обсуждение | вклад) |
||
(не показано 11 промежуточных версий этого же участника) | |||
Строка 158: | Строка 158: | ||
</PRE> |
</PRE> |
||
==nodeAffinity== |
==nodeAffinity== |
||
− | Не смотря на простоту и эффективность механизма nodeSelector – механизм это прямолинейный и не особенно гибкий. |
+ | Не смотря на простоту и эффективность механизма nodeSelector – механизм это прямолинейный и не особенно гибкий.<BR> |
− | Авторы kubernetes предлагают более мощный, гибкий (а так же – сложный и неудобный) механизм – nodeAffinity. |
+ | Авторы kubernetes предлагают более мощный, гибкий (а так же – сложный и неудобный) механизм – nodeAffinity. <BR> |
− | Язык описания nodeAffinity предлагает несколько мощных возможностей: |
+ | Язык описания nodeAffinity предлагает несколько мощных возможностей:<BR> |
<BR> |
<BR> |
||
логические операторы для выбора условия размещения – IN (размещать на одной из нод с разными метками) или AND (размещать на нодах, имеющих обе метки сразу) |
логические операторы для выбора условия размещения – IN (размещать на одной из нод с разными метками) или AND (размещать на нодах, имеющих обе метки сразу) |
||
можно выбраить политики размещения pod-ов относительно друг друга: например – запретить экземплярам кэша оказываться на одной физической машине или требовать размещение приложения вместе с экземпляром кеша на одном физическом узле |
можно выбраить политики размещения pod-ов относительно друг друга: например – запретить экземплярам кэша оказываться на одной физической машине или требовать размещение приложения вместе с экземпляром кеша на одном физическом узле |
||
Минус nodeAffinity в том, что язык очень многословный и читается тяжело. Общая спецификация выглядит так: |
Минус nodeAffinity в том, что язык очень многословный и читается тяжело. Общая спецификация выглядит так: |
||
+ | <PRE> |
||
− | |||
spec: |
spec: |
||
affinity: |
affinity: |
||
Строка 176: | Строка 176: | ||
values: |
values: |
||
- {affinityValues} |
- {affinityValues} |
||
+ | </PRE> |
||
− | affinityClass влияет на строгость выбора узла: |
||
+ | affinityClass влияет на строгость выбора узла:<BR> |
||
− | requiredDuringSchedulingIgnoredDuringExecution: обязательно размещать pod-ы по требованию nodeAffinity. Если разместить не получится – pod застрянет в статусе Pending |
||
− | preferredDuringSchedulingIgnoredDuringExecution: по возможности размещать pod-ы по требованиям affinity. Если поды не влезли – scheduler разместит их “как получится” |
||
− | affinityKey – это метка (ключ), по которой мы будем искать ноды для размещения pod-ов. affinityValues – это значения метки, которые нам подойдут affinityOperator – это тот логический оператор, по которому будет производится выбор метки. Варианты: |
||
+ | * requiredDuringSchedulingIgnoredDuringExecution: обязательно размещать pod-ы по требованию nodeAffinity. Если разместить не получится – pod застрянет в статусе Pending |
||
− | In – подойдет любое из перечисленных значений |
||
+ | * preferredDuringSchedulingIgnoredDuringExecution: по возможности размещать pod-ы по требованиям affinity. Если поды не влезли – scheduler разместит их “как получится” |
||
− | NotIn – противоположно In |
||
+ | * affinityKey – это метка (ключ), по которой мы будем искать ноды для размещения pod-ов. |
||
− | Exists – метка просто есть (values игнорируется) |
||
+ | * affinityValues – это значения метки, которые нам подойдут |
||
− | DoesNotExists – противоположно Exists |
||
+ | * affinityOperator – это тот логический оператор, по которому будет производится выбор метки. |
||
− | Gt – Greater than – значение метки больше указанного в политике числа. Сработает только для чисел |
||
− | Lt – Less than – противоположно Gt |
||
− | affinityClass preferredDuringSchedulingIgnoredDuringExecution слегка отличается – вместо nodeSelectorTerms используется поле preference (синтаксис такой же), плюс есть обязательное поле weight – оно отвечает за приоритет при выборе node. |
||
+ | Варианты: |
||
− | Пример: |
||
+ | * In – подойдет любое из перечисленных значений |
||
+ | * NotIn – противоположно In |
||
+ | * Exists – метка просто есть (values игнорируется) |
||
+ | * DoesNotExists – противоположно Exists |
||
+ | * Gt – Greater than – значение метки больше указанного в политике числа. Сработает только для чисел |
||
+ | * Lt – Less than – противоположно Gt |
||
+ | |||
+ | affinityClass preferredDuringSchedulingIgnoredDuringExecution слегка отличается – вместо nodeSelectorTerms используется поле preference (синтаксис такой же), плюс есть обязательное поле weight – оно отвечает за приоритет при выборе node. |
||
+ | <BR> |
||
+ | Пример: |
||
+ | <PRE> |
||
apiVersion: v1 |
apiVersion: v1 |
||
kind: Pod |
kind: Pod |
||
Строка 219: | Строка 226: | ||
- name: with-node-affinity |
- name: with-node-affinity |
||
image: k8s.gcr.io/nginx |
image: k8s.gcr.io/nginx |
||
+ | </PRE> |
||
+ | |||
affinity не учитывается для уже аллоцированных nodes, так что если нужно освободить node-у от всех подов которые там уже есть – поможет команда kubectl node drain |
affinity не учитывается для уже аллоцированных nodes, так что если нужно освободить node-у от всех подов которые там уже есть – поможет команда kubectl node drain |
||
− | Лирическое отступление – PodAffinitty и PodAntiAffinitty |
||
+ | ==PodAffinitty и PodAntiAffinitty== |
||
− | Механизм, который помогает размещать pod-ы относительно нод – может так же помочь и разместить pod-ы относительно друг друга – за это отвечают классы PodAffinity и PodAntiAffinity. Все три класса можно сочетать друг с другом, синтаксис внутри одинаковый, по этому просто покажу пример: |
||
+ | Механизм, который помогает размещать pod-ы относительно нод – может так же помочь и разместить pod-ы относительно <BR> |
||
+ | друг друга – за это отвечают классы <code>PodAffinity</code> и <code>PodAntiAffinity</code>. <BR> |
||
+ | Все три класса можно сочетать друг с другом, синтаксис внутри одинаковый, по этому просто покажу пример: |
||
+ | <PRE> |
||
apiVersion: apps/v1 |
apiVersion: apps/v1 |
||
kind: Deployment |
kind: Deployment |
||
Строка 252: | Строка 264: | ||
- name: redis-server |
- name: redis-server |
||
image: redis:3.2-alpine |
image: redis:3.2-alpine |
||
+ | </PRE> |
||
− | В этом примере мы запрещаем экземплярам redis размещаться на одном узле. Каждый pod в этом deployment получит метку app:store, политика podAntiAffinity запрещает размещать второй под с меткой app=store на ноде с таким же hostname. Важный параметр тут – topologyKey. Именно по нему scheduler понимает, какие node-ы считаются одной зоной размещения,а какие – нет. Усложним пример, добавив web worker: |
||
+ | В этом примере мы запрещаем экземплярам redis размещаться на одном узле. |
||
+ | * Каждый pod в этом deployment получит метку app:store, <BR> |
||
+ | политика podAntiAffinity запрещает размещать второй под с меткой app=store на ноде с таким же hostname. <BR> |
||
+ | <BR> |
||
+ | Важный параметр тут – topologyKey. Именно по нему scheduler понимает, какие node-ы считаются одной зоной размещения,а какие – нет. |
||
+ | <BR> |
||
+ | Усложним пример, добавив web worker: |
||
+ | <PRE> |
||
apiVersion: apps/v1 |
apiVersion: apps/v1 |
||
kind: Deployment |
kind: Deployment |
||
Строка 290: | Строка 310: | ||
- name: web-app |
- name: web-app |
||
image: nginx:1.16-alpine |
image: nginx:1.16-alpine |
||
+ | </PRE> |
||
− | В этом примере мы размещаем nginx на разных node (как мы сделали с redis), но при этом требуем, чтобы nginx размещался вместе с redis. Это может быть удобно для кешей. Проверим, что получилось: |
||
+ | В этом примере мы размещаем nginx на разных node (как мы сделали с redis), |
||
+ | но при этом требуем, чтобы nginx размещался вместе с redis. |
||
+ | Это может быть удобно для кешей. |
||
+ | Проверим, что получилось: |
||
+ | <PRE> |
||
> kubectl get pods -o wide |
> kubectl get pods -o wide |
||
Строка 301: | Строка 326: | ||
web-server-1287567482-6f7v5 1/1 Running 0 7m 10.192.4.3 kube-node-3 |
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 |
web-server-1287567482-s330j 1/1 Running 0 7m 10.192.3.2 kube-node-2 |
||
+ | </PRE> |
||
− | Static pod allocations |
||
+ | |||
+ | ===Еще один пример - своими словами=== |
||
+ | <PRE> |
||
+ | affinity: |
||
+ | podAntiAffinity: |
||
+ | preferredDuringSchedulingIgnoredDuringExecution: null |
||
+ | requiredDuringSchedulingIgnoredDuringExecution: |
||
+ | - labelSelector: |
||
+ | matchLabels: |
||
+ | app.kubernetes.io/name: "parity" |
||
+ | parity/chain: "mainnet" |
||
+ | topologyKey: failure-domain.beta.kubernetes.io/zone |
||
+ | </PRE> |
||
+ | |||
+ | <code>topologyKey: failure-domain.beta.kubernetes.io/zone</code> |
||
+ | Эта часть определяет метки которые не должны совпадать (или должны совпадать в случае podAffinity:) у node |
||
+ | |||
+ | |||
+ | <PRE> |
||
+ | - labelSelector: |
||
+ | matchLabels: |
||
+ | app.kubernetes.io/name: "parity" |
||
+ | parity/chain: "mainnet" |
||
+ | </PRE> |
||
+ | Эта часть относится к поду и тут 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 |
||
+ | |||
+ | |||
+ | ==Еще пример с более сложной конфигурацией== |
||
+ | <PRE> |
||
+ | 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 |
||
+ | </PRE> |
||
+ | |||
+ | Что тут важно |
||
+ | * <code>preferredDuringSchedulingIgnoredDuringExecution</code> - это означает что условие не обязательное, |
||
+ | другими словами scheduler попытается найти ноду, удовлетворяющую условиям, но если не найдет то POD все равно буltn pfgeoty |
||
+ | * <code>- podAffinityTerm</code> - это элемент списка (<code>list, [ ... ] </code>), которых может быть более одного |
||
+ | * <code>matchExpressions</code> - это список условий, которые перечисляют, какие метки (<code>labelSelector</code>) должны быть у пода и какие значения должны быть у этих меток. <B>ВАЖНО</B> что под, от которого будет происходить "отталкивание" должен удовлетворять <B>ВСЕМ</B> условиям, т е у него должны быть все метки <code>release_group</code> <code>application</code> <code>component</code> каждая с соответствующим значением |
||
+ | * <code>topologyKey</code> в данном примере это нода, те скедулер попробует выбрать ноду на которой нет ПОДа соответвующего условиям, однако это может быть и другой ключ, например <code>rack</code> или <code>datacenter</code> |
||
+ | |||
+ | <BR> |
||
+ | Если нужно выбрать ноду, на которой не должно быть нескольких подов, то конфигурация будет следующая |
||
+ | <PRE> |
||
+ | 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 |
||
+ | |||
+ | |||
+ | </PRE> |
||
+ | В этом примере <code>scheduler</code> попытается найти ноду (по тому что <code>topologyKey: kubernetes.io/hostname</code>) которая удовлетворяет условиям: |
||
+ | 1. на ней нет ПОДа с метками <code>label_1_name_for_pod_1</code> и значением этой метки <code>label_1_value_for_pod_1</code> и с далее с перечисленными метками вплоть до последне метки <code>label_N_name_for_pod_1</code> со значением <code>label_N_value_for_pod_1</code> |
||
+ | 2 И так далее проверяя все условия из списка <code>preferredDuringSchedulingIgnoredDuringExecution: [ ... ]</code> в плоть до последнего условия, которое требует что бы не было ПОДА со списком меток <code>label_1_name_for_pod_M</code> ... <code>label_N_name_for_pod_M</code> которые имеют соответственно значения <code>label_1_value_for_pod_M</code> ... <code>label_N_value_for_pod_M</code> |
||
+ | |||
+ | ==Static pod allocations== |
||
Это очень редкий случай, но не упомянуть его было бы нечестно. Pod-ы можно аллоцировать полностью статически, вручную привязав к конкретной node. В этом случае scheduler никак на них не влияет. На них не действуют taints, nodeSelector и podAffinity. Даже node drain ничего не сможет с такими подами сделать. Зачем это может потребоваться? Ну, во-первых для запуска таких pod-ов не нужен работающий scheduler или apiserver. Это делает размещение таких подов сверхнадежным – они будут работать всегда. Именно так kubeadm устанавливает свои компоненты – это не полноценные демоны, а контейнеры, которые вручную привязаны к master node. |
Это очень редкий случай, но не упомянуть его было бы нечестно. Pod-ы можно аллоцировать полностью статически, вручную привязав к конкретной node. В этом случае scheduler никак на них не влияет. На них не действуют taints, nodeSelector и podAffinity. Даже node drain ничего не сможет с такими подами сделать. Зачем это может потребоваться? Ну, во-первых для запуска таких pod-ов не нужен работающий scheduler или apiserver. Это делает размещение таких подов сверхнадежным – они будут работать всегда. Именно так kubeadm устанавливает свои компоненты – это не полноценные демоны, а контейнеры, которые вручную привязаны к master node. |
||
Строка 314: | Строка 455: | ||
Просто создадим манифест статического пода |
Просто создадим манифест статического пода |
||
+ | <PRE> |
||
− | |||
cat <<EOF >/etc/kubernetes/manifests/static-web.yaml |
cat <<EOF >/etc/kubernetes/manifests/static-web.yaml |
||
apiVersion: v1 |
apiVersion: v1 |
||
Строка 331: | Строка 472: | ||
protocol: TCP |
protocol: TCP |
||
EOF |
EOF |
||
+ | </PRE> |
||
И проверим, что получилось: |
И проверим, что получилось: |
||
+ | <PRE> |
||
− | |||
> kubectl get pods -l role=static |
> kubectl get pods -l role=static |
||
NAME READY STATUS RESTARTS AGE |
NAME READY STATUS RESTARTS AGE |
||
static-web-my-node1 1/1 Running 0 2m |
static-web-my-node1 1/1 Running 0 2m |
||
+ | </PRE> |
||
− | Порядок применения |
||
+ | ==Порядок применения== |
||
− | Первым всегда применяется static pod. Он игнорирует все (taints, affinities, node selectors). |
+ | * Первым всегда применяется static pod. Он игнорирует все (taints, affinities, node selectors). |
− | Вторым по списку применяется taint. Если у pod нет toleration – он не будет размещен, по этому taint – это очень эффективный способ “разогнать” pod-ы с определенного узла (или группы узлов). |
+ | * Вторым по списку применяется taint. Если у pod нет toleration – он не будет размещен, по этому taint – это очень эффективный способ “разогнать” pod-ы с определенного узла (или группы узлов). |
− | В случае, если есть nodeAffinity и nodeSelector – должны сработать оба условия сразу (то есть – и метка селектора и условия affinity). |
+ | * В случае, если есть nodeAffinity и nodeSelector – должны сработать оба условия сразу (то есть – и метка селектора и условия affinity). |
− | Заключение |
+ | =Заключение= |
Kubernetes – мощный, богатый на возможности инструмент. Он кажется слегка неудобным, но ровно до момента понимания логики его работы. Scheduler у kubernetes практически ключевой компонент, и он достаточно гибок, пусть и не самым лучшим образом описан. Надеюсь – эта статья кому-то поможет. Высокого вам аптайма! |
Kubernetes – мощный, богатый на возможности инструмент. Он кажется слегка неудобным, но ровно до момента понимания логики его работы. Scheduler у kubernetes практически ключевой компонент, и он достаточно гибок, пусть и не самым лучшим образом описан. Надеюсь – эта статья кому-то поможет. Высокого вам аптайма! |
||
− | |||
− | 2021-01-15 |
Текущая версия на 10:31, 18 марта 2024
Распределяем 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 практически ключевой компонент, и он достаточно гибок, пусть и не самым лучшим образом описан. Надеюсь – эта статья кому-то поможет. Высокого вам аптайма!