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 таким чином, що вони будуть приймати тільки окремі поди, які відповідають заданим на ноді крітеріям
taints
andtolerations
: на ноді задаємо 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
– не дозволяти створювати поди, абоScheduleAnyway
topologyKey
: 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]
Всі розміщені на окремих нодах.