Kubernetes дозволяє дуже гнучко керувати тим, як його Pods будуть розташовані на серверах, тобто WorkerNodes.
Це може бути корисним, якщо вам треба запускати под на специфічній конфігурації ноди, наприклад – WorkerNode повинна мати GPU, або SSD замість HDD. Інший приклад, це коли вам потрібно розміщати окремі поди поруч, щоб зменшити затримку їхньої комунікації, або зменшити cross Availability-zone трафік (див. AWS: Grafana Loki, InterZone трафік в AWS, та Kubernetes nodeAffinity).
І, звісно, це важливе для побудування High Availability та Fault Tolerance архітектури, коли вам потрібно розділити поди по окремим нодам або Availability-зонам.
Ми маємо чотири основних підходи для контролю того, як Kubernetes Pods будуть розміщатись на WorkerNodes:
- налаштувати Nodes таким чином, що вони будуть приймати тільки окремі поди, які відповідають заданим на ноді крітеріям
taintsandtolerations: на ноді задаємо taint, для якого поди повинні мати відповідний toleration, щоб запустись на цій ноді
- нашалтувати сам Pod таким чином, що він буде вибирати тільки окремі Nodes, які відповідають заданим у поді крітеріям
- для цього використовуємо
nodeName– вибирається нода тільки с заданним ім’ям - або
nodeSelectorдля вибору ноди з відповіднимиlabelsі їх значеннями - або
nodeAffinityтаnodeAntiAffinity– правила, за якими Kubernetes Scheduler буде вибирати ноду, на якій запустить под, в залежності від параметрів цієї ноди
- для цього використовуємо
- налаштувати сам Pod таким чином, що він буде вибирати Node в залежності від того, як запущені інші Pods
- для цього використовуємо
podAffinityтаpodAntiAffinity– правила, за якими Kubernetes Scheduler буде вибирати ноду, на якій запустить под, в залежності від інших подів на цій ноді
- для цього використовуємо
- і окрема тема – Pod Topology Spread Constraints, тобто правила розміщення Pods по failure-domains – регіонам, Availability-зонам чи нодам
Зміст
kubectl explain
Ви завжди можете прочитати відповідну документацію по будь-якому параметру або ресурсу, використовуючи kubectl explain:
[simterm]
$ kubectl explain pod
KIND: Pod
VERSION: v1
DESCRIPTION:
Pod is a collection of containers that can run on a host. This resource is
created by clients and scheduled onto hosts.
...
[/simterm]
Або:
[simterm]
$ kubectl explain Pod.spec.nodeName
KIND: Pod
VERSION: v1
FIELD: nodeName <string>
DESCRIPTION:
NodeName is a request to schedule this pod onto a specific node. If it is
non-empty, the scheduler simply schedules this pod onto that node, assuming
that it fits resource requirements.
[/simterm]
Node Taints та Pods Tolerations
Отже, перший варіант – це задати на нодах обмеження того, які поди на ній можуть бути запущені з використанням Taints та Tolerations.
Тут taint “відштовхує” поди які не мають відповідної toleration від ноди, а toleration – “тягне” под до специфічної ноди, яка має відповідний taint.
Наприклад, ми можемо створити ноду, на якій будуть запускатись тільки поди з якимись критичними сервісами типу контроллерів.
Задаємо tain з effect: NoSchedule – тобто, забороняємо створювати нові поди на цій ноді:
[simterm]
$ kubectl taint nodes ip-10-0-3-133.ec2.internal critical-addons=true:NoSchedule node/ip-10-0-3-133.ec2.internal tainted
[/simterm]
Далі, створюємо под, якому вказуємо toleration з ключем "critical-addons":
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: my-container
image: nginx:latest
tolerations:
- key: "critical-addons"
operator: "Exists"
effect: "NoSchedule"
І перевіряємо поди на цій ноді:
[simterm]
$ kubectl get pod --all-namespaces -o wide --field-selector spec.nodeName=ip-10-0-3-133.ec2.internal NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES default my-pod 1/1 Running 0 2m11s 10.0.3.39 ip-10-0-3-133.ec2.internal <none> <none> dev-monitoring-ns atlas-victoriametrics-loki-logs-zxd9m 2/2 Running 0 10m 10.0.3.8 ip-10-0-3-133.ec2.internal <none> <none> ...
[/simterm]
Але звідки тут Loki? Тому що поки задали Taint – Scheduler встиг перенести на цю ноди под з Loki.
Щоб запобігти цьому – в Tain додаємо ключ NoExecute – тоді scheduler виконає Pod eviction, тобто “виселить” вже запущені поди з цієї ноди:
[simterm]
$ kubectl taint nodes ip-10-0-3-133.ec2.internal critical-addons=true:NoExecute
[/simterm]
Перевіряємо taints тепер:
[simterm]
$ kubectl get node ip-10-0-3-133.ec2.internal -o json | jq '.spec.taints'
[
{
"effect": "NoExecute",
"key": "critical-addons",
"value": "true"
},
{
"effect": "NoSchedule",
"key": "critical-addons",
"value": "true"
}
]
[/simterm]
Для нашого поду додаємо другий toleration, інакше і він буде evicted з цієї ноди:
...
tolerations:
- key: "critical-addons"
operator: "Exists"
effect: "NoSchedule"
- key: "critical-addons"
operator: "Exists"
effect: "NoExecute"
Деплоїмо, та перевіряємо поди ще раз:
[simterm]
$ kubectl get pod --all-namespaces -o wide --field-selector spec.nodeName=ip-10-0-3-133.ec2.internal NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES default my-pod 1/1 Running 0 3s 10.0.3.246 ip-10-0-3-133.ec2.internal <none> <none> kube-system aws-node-jrsjz 1/1 Running 0 16m 10.0.3.133 ip-10-0-3-133.ec2.internal <none> <none> kube-system csi-secrets-store-secrets-store-csi-driver-cctbj 3/3 Running 0 16m 10.0.3.144 ip-10-0-3-133.ec2.internal <none> <none> kube-system ebs-csi-node-46fts 3/3 Running 0 16m 10.0.3.187 ip-10-0-3-133.ec2.internal <none> <none> kube-system kube-proxy-6ztqs 1/1 Running 0 16m 10.0.3.133 ip-10-0-3-133.ec2.internal <none> <none>
[/simterm]
Тепер на цій ноді тільки наш под, та поди з DaemonSets, які по дефолту мають запускатись на всіх нодах і мають відповідні tolerations, див. How Daemon Pods are scheduled.
Окрім Equal в умовах toleration, яка тільки перевіряє наявність заданої лейбли, можна виконати і перевірку значення ціїє лейбли.
Для цього в operator замість Exists вказуємо Equal, і додаємо value з потрібним значенням:
...
tolerations:
- key: "critical-addons"
operator: "Equal"
value: "true"
effect: "NoSchedule"
- key: "critical-addons"
operator: "Equal"
value: "true"
effect: "NoExecute"
Щоб видалити tain – додаємо в кінці мінус:
[simterm]
$ kubectl taint nodes ip-10-0-3-133.ec2.internal critical-addons=true:NoSchedule- node/ip-10-0-3-133.ec2.internal untainted $ kubectl taint nodes ip-10-0-3-133.ec2.internal critical-addons=true:NoExecute- node/ip-10-0-3-133.ec2.internal untainted
[/simterm]
Вибір ноди подом – nodeName, nodeSelector та nodeAffinity
Інший підхід, коли ми налаштовуємо пд таким чином, що “він” вибирає на якій ноді йому запускатись.
Для цього маємо nodeName, nodeSelector, nodeAffinity та nodeAntiAffinity. Див. Assign Pods to Nodes.
nodeName
Найпростіший способ. Має перевагу над всіма іншими:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: my-container
image: nginx:latest
nodeName: ip-10-0-3-133.ec2.internal
nodeSelector
З nodeSelector можемо вибирати ті ноди, які мають відповідні labels.
Додаємо лейблу:
[simterm]
$ kubectl label nodes ip-10-0-3-133.ec2.internal service=monitoring node/ip-10-0-3-133.ec2.internal labeled
[/simterm]
Перевіряємо її:
[simterm]
$ kubectl get node ip-10-0-3-133.ec2.internal -o json | jq '.metadata.labels'
{
...
"kubernetes.io/hostname": "ip-10-0-3-133.ec2.internal",
"kubernetes.io/os": "linux",
"node.kubernetes.io/instance-type": "t3.medium",
"service": "monitoring",
...
[/simterm]
В маніфесті поду задаємо nodeSelector:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: my-container
image: nginx:latest
nodeSelector:
service: monitoring
Якщо поду задано кілька лейбл в nodeSelector – то відповідна нода повинна мати всі ці лейбли, щоб на ній запустився цей под.
nodeAffinity та nodeAntiAffinity
nodeAffinity та nodeAntiAffinity діють так само, як і nodeSelector, але мають більш гнучкі можливості.
Наприклад, можна задати hard або soft ліміти запуску – для soft-ліміту scheduler спробує запустити под на відповідній ноді, а якщо не зможе – то запустить на іншій. Відповідно, якщо задати hard-ліміт, і scheduler не зможе запустити под на обраній ноді – то под залишиться в статусі Pending.
Hard-ліміт задається в полі .spec.affinity.nodeAffinity за допомогою requiredDuringSchedulingIgnoredDuringExecution, а soft – з preferredDuringSchedulingIgnoredDuringExecution.
Наприклад, можемо запустити под в AvailabiltyZone us-east-1a або us-east-1b, використовуючи node-label topology.kubernetes.io/zone:
[simterm]
$ kubectl get node ip-10-0-3-133.ec2.internal -o json | jq '.metadata.labels'
{
...
"topology.kubernetes.io/region": "us-east-1",
"topology.kubernetes.io/zone": "us-east-1b"
}
[/simterm]
Задаємо hard-limit:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: my-container
image: nginx:latest
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1a
- us-east-1b
Або софт-ліміт. Наприклад, з неіснуючою лейблою:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: my-container
image: nginx:latest
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: non-exist-node-label
operator: In
values:
- non-exist-value
У такому випидаку под все одно буде запущено на будь-якій найбільш вільній ноді.
Також можна комбінувати умови:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: my-container
image: nginx:latest
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1a
- us-east-1b
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: non-exist-node-label
operator: In
values:
- non-exist-value
При використанні декількох умов в requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms – буде обрано перше, що збіглось з лейблами ноди.
При використанні декількох умов в matchExpressions – вони мають відповідати всі.
В operator можна використовувати оператори In, NotIn, Exists, DoesNotExist, Gt (greater than) та Lt (less than).
soft-limit та weight
У preferredDuringSchedulingIgnoredDuringExecution за допомогою weight можна задати “вагу” умови, задавши значення від 1 до 100.
В такому випадку, якщо всі інші умови співпали, scheduler вибере ту ноду, умова якої має найбільший weight:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: my-container
image: nginx:latest
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1a
- weight: 100
preference:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1b
Цей под буде запущено на ноді в регіоні us-east-1b:
[simterm]
$ kubectl get pod my-pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES my-pod 1/1 Running 0 3s 10.0.3.245 ip-10-0-3-133.ec2.internal <none> <none>
[/simterm]
І зона цієї ноди:
[simterm]
$ kubectl get node ip-10-0-3-133.ec2.internal -o json | jq -r '.metadata.labels."topology.kubernetes.io/zone"' us-east-1b
[/simterm]
podAffinity та podAntiAffinity
Аналогічно до вибору ноди за допомогою hard- та soft-лімітів, можна налаштувати Pod Affinity в залежності від того, які лейбли будуть у подів, які вже запущені на ноді. Див. Inter-pod affinity and anti-affinity.
Наприклад, є три поди Grafana Loki – Read, Write та Backend.
Ми хочемо запускати Read та Backend в одній AvailabilityZone, щоб уникнути cross-AZ трафіку, але при цьому хочемо, що вони не запускались на тих нодах, де є поди з Write.
Поди Loki мають відповідні до компоненту лейбли – app.kubernetes.io/component=read, app.kubernetes.io/component=backend та app.kubernetes.io/component=write.
Тож для Read задаємо podAffinity до подів з лейблою app.kubernetes.io/component=backend, та podAntiAffinity до подів з лейблою app.kubernetes.io/component=read:
...
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app.kubernetes.io/component
operator: In
values:
- backend
topologyKey: "topology.kubernetes.io/zone"
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app.kubernetes.io/component
operator: In
values:
- write
topologyKey: "kubernetes.io/hostname"
...
Тут в podAffinity.topologyKey ми вказуємо, що хочемо розміщати поди, використовуючи домен topology.kubernetes.io/zone – тобто topology.kubernetes.io/zone для Read має співпадати з подом Backend.
А в podAntiAffinity.topologyKey задаємо kubernetes.io/hostname – тобто не розміщати на WorkerNodes, де є поди з лейблою app.kubernetes.io/component=write.
Деплоїмо, та перевіряємо де є под з Write:
[simterm]
$ kubectl -n dev-monitoring-ns get pod loki-write-0 -o json | jq '.spec.nodeName' "ip-10-0-3-53.ec2.internal"
[/simterm]
Та в якій AvailabilityZone ця нода:
[simterm]
$ kubectl -n dev-monitoring-ns get node ip-10-0-3-53.ec2.internal -o json | jq -r '.metadata.labels."topology.kubernetes.io/zone"' us-east-1b
[/simterm]
Перевіряємо де знаходиться под з Backend:
[simterm]
$ kubectl -n dev-monitoring-ns get pod loki-backend-0 -o json | jq '.spec.nodeName' "ip-10-0-2-220.ec2.internal"
[/simterm]
І його зона:
[simterm]
$ kubectl -n dev-monitoring-ns get node ip-10-0-2-220.ec2.internal -o json | jq -r '.metadata.labels."topology.kubernetes.io/zone"' us-east-1a
[/simterm]
І тепер под з Read:
[simterm]
$ kubectl -n dev-monitoring-ns get pod loki-read-698567cdb-wxgj5 -o json | jq '.spec.nodeName' "ip-10-0-2-173.ec2.internal"
[/simterm]
Нода інша, ніж у пода Write або Backend, але:
[simterm]
$ kubectl -n dev-monitoring-ns get node ip-10-0-2-173.ec2.internal -o json | jq -r '.metadata.labels."topology.kubernetes.io/zone"' us-east-1a
[/simterm]
Та сама AvailabilityZone, що й в поду Backend.
Pod Topology Spread Constraints
Ми можемо налаштувати Kubernetes Scheduler таким чином, щоб він розподіляв под по “доменам”, тобто – по нодам, регіонам або Availability-зонам. Див. Pod Topology Spread Constraints.
Для цього в полі spec.topologySpreadConstraints задаються параметри, які описують як саме будуть створені поди.
Наприклад, у нас є 5 WorkerNode в двох AvailabilityZone.
Ми хочемо запустити 5 подів, і задля fault tolerance ми хочемо, щоб кожен под був розміщений на окремій ноді.
Тоді наш конфіг для Deployment може виглядати так:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
spec:
replicas: 5
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-container
image: nginx:latest
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: my-app
Тут:
maxSkew: максимальна різниця в кількості подів в одному домені (topologyKey)- грає роль тільки якщо
whenUnsatisfiable=DoNotSchedule, приwhenUnsatisfiable=ScheduleAnywayпод буде створено незалежно від умов
- грає роль тільки якщо
whenUnsatisfiable: може мати значенняDoNotSchedule– не дозволяти створювати поди, абоScheduleAnywaytopologyKey: WorkerNode label, по якій буде обрано домен, тобто по якій лейблі групуємо ноди, на яких розраховується розміщення подівlabelSelector: які поди враховувати при розміщенні подів (наприклад, якщо поди з різних Deployment, але мають розміщувати однаково – то в обох Deployment налаштовуємоtopologySpreadConstraintsз взаємнимиlabelSelector)
Крім того, можна задати параметри nodeAffinityPolicy та/або nodeTaintsPolicy зі значеннями Honor або Ignore – враховувати nodeAffinity або nodeTaints при розрахунку розміщення подів, чи ні.
Деплоїмо та перевіряємо ноди цих подів:
[simterm]
$ kk get pod -o json | jq '.items[].spec.nodeName' "ip-10-0-3-53.ec2.internal" "ip-10-0-3-22.ec2.internal" "ip-10-0-2-220.ec2.internal" "ip-10-0-2-173.ec2.internal" "ip-10-0-3-133.ec2.internal"
[/simterm]
Всі розміщені на окремих нодах.




