Распределяем pod-ы по машинам в kubernetes
Kubernetes иногда называют “операционной системой для дата-центров” – и в этом есть логика. K8s позволяет представить группу серверов (условный ЦОД) как единое вычислительное пространство. Оператор просто бросает задания в K8s, а тот сам выбирает, где тот или иной контейнер лучше разместить. Чаще всего делает это он хорошо. Но иногда появляется необходимость как-то управлять этим процессом. Об этом я и расскажу.
Зачем управлять распределением 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
Не смотря на простоту и эффективность механизма 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
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 практически ключевой компонент, и он достаточно гибок, пусть и не самым лучшим образом описан. Надеюсь – эта статья кому-то поможет. Высокого вам аптайма!