Kubernetes prakticky: advanced scheduling

Azure Kubernetes Service automaticky dává vaše Pody na vhodné Nody včetně zajištění vysoké dostupnosti rozhazováním Deploymentů na Nody z různých fault domén. Přesto někdy můžete potřebovat scheduler ovlivnit a implementovat svoje specifické potřeby kde se mají vaše Pody objevit. Pojďme se podívat na Node affinitu, Pod affinitu a anti-affinitu, Taint a Pod prioritu.

Node affinity

Máte potřebu učinit nějaký Node atraktivnější pro váš Pod? AKS v tuto chvíli nepodporuje víc jak jeden typ VM v clusteru, ale velmi brzy bude. Pak vám může dávat smysl namířit svoje Pody na nějaký typ VM. Představme si například, že pro účely testování nebo pro nenáročné Pody chceme použít levnou B-series, ale produkční náročné Pody chceme na plné D-series. Jak to zařídit?

Starší koncept je NodeSelector, ale Node Affinity umí totéž a spoustu věcí navíc. Budeme se tedy soustředit na druhou možnost, která je novější a v tuto chvíli lepší cesta.

U Nodů můžeme využívat automaticky generované Labely, ale také si můžeme přidat vlastní. Pojďme mému 3-nodovému clusteru přiřadit příznak barva a dva udělat modré a jeden červený.

kubectl label nodes aks-nodepool1-38238592-0 color=blue
kubectl label nodes aks-nodepool1-38238592-1 color=blue
kubectl label nodes aks-nodepool1-38238592-2 color=red
kubectl get nodes --show-labels
NAME                       STATUS    ROLES     AGE       VERSION   LABELS
aks-nodepool1-38238592-0   Ready     agent     2d        v1.11.1   agentpool=nodepool1,beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=Standard_B2s,beta.kubernetes.io/os=linux,color=blue,failure-domain.beta.kubernetes.io/region=westeurope,failure-domain.beta.kubernetes.io/zone=0,kubernetes.azure.com/cluster=MC_aksgroup_akscluster_westeurope,kubernetes.io/hostname=aks-nodepool1-38238592-0,kubernetes.io/role=agent,storageprofile=managed,storagetier=Premium_LRS
aks-nodepool1-38238592-1   Ready     agent     10h       v1.11.1   agentpool=nodepool1,beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=Standard_B2s,beta.kubernetes.io/os=linux,color=blue,failure-domain.beta.kubernetes.io/region=westeurope,failure-domain.beta.kubernetes.io/zone=0,kubernetes.azure.com/cluster=MC_aksgroup_akscluster_westeurope,kubernetes.io/hostname=aks-nodepool1-38238592-1,kubernetes.io/role=agent,storageprofile=managed,storagetier=Premium_LRS
aks-nodepool1-38238592-2   Ready     agent     10h       v1.11.1   agentpool=nodepool1,beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=Standard_B2s,beta.kubernetes.io/os=linux,color=red,failure-domain.beta.kubernetes.io/region=westeurope,failure-domain.beta.kubernetes.io/zone=1,kubernetes.azure.com/cluster=MC_aksgroup_akscluster_westeurope,kubernetes.io/hostname=aks-nodepool1-38238592-2,kubernetes.io/role=agent,storageprofile=managed,storagetier=Premium_LRS

V definici Podu můžeme určit jeho Node affinity tak, že vyžadujeme, aby scheduler vybral jen Nody vyhovující našemu filtračnímu kritériu. Například pokud bych použil label kubernetes.io/hostname můžu určit konkrétní node. V následujícím příkladu budu požadovat umístění na červený node.

kind: Pod
apiVersion: v1
metadata:
  name: mypod
spec:
  containers:
    - name: mypod
      image: nginx:alpine
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: color
            operator: In
            values:
            - red

Pustíme to a uvidíme, že Pod běží na Nodu s číslem dva, protože to je jediný červený, co máme.

kubectl apply -f podNodeAffinityRequired.yaml
kubectl get pods -o wide
NAME      READY     STATUS              RESTARTS   AGE       IP        NODE
mypod     0/1       ContainerCreating   0          0s        <none>    aks-nodepool1-38238592-2

Co se stane pokud červený Node mít vůbec nebudeme nebo bude plně obsazený? Nasazení podu selže. Někdy nechceme mít pravidlo takhle silné, ale spíš definovat naší preferenci. V následujícím příkladě je mojí největší prioritou dostat Pod na oranžový Node. Takový já aktuálně nemám, takže scheduler zkusí dle mého pořadí preferencí další možnost a to je červený Node. Pokud by ani to nebylo možné, dá to na jakýkoli jiný.

kind: Pod
apiVersion: v1
metadata:
  name: mypod2
spec:
  containers:
    - name: mypod
      image: nginx:alpine
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 10
        preference:
          matchExpressions:
          - key: color
            operator: In
            values:
            - orange
      - weight: 1
        preference:
          matchExpressions:
          - key: color
            operator: In
            values:
            - red

Pustíme a zjistíme, že Pod je znovu na červeném Nodu.

kubectl apply -f podNodeAffinityPreferred.yaml
kubectl get pods -o wide
NAME      READY     STATUS              RESTARTS   AGE       IP              NODE
mypod     1/1       Running             0          3m        192.168.0.224   aks-nodepool1-38238592-2
mypod2    0/1       ContainerCreating   0          2s        <none>          aks-nodepool1-38238592-2

Taint a tolerace

V předchozím případě jsme lákali Pody na určité Nody. Taints umožňují opačný postup, tedy dávání Nodům „věcné břemeno“. Je to trochu podobné jako whitelist vs. blacklist. Pokud máte 100 nodů a jeden z nich nechcete běžně používat je jednoduší dát „blacklist“ na jeden, než 99 whitelistů. Navíc je tu druhá zásadní věc. Jakmile přidáte Nodu věcné břemeno Pody v defaultní konfiguraci si na něj nesednou. Nemusím tedy nic v Podu specifikovat a on se Nodu vyhne. Je to opačně – Podu můžu explicitně definovat toleranci na určitý Taint. Existují tři režimy Taintů. PreferNoSchedule je soft pravidlo, které říká pokud to bude možné, vyhni se mu. NoSchedule je tvrdé pravidlo, tedy pokud nemáš explicitní toleranci, nikdy tam nechoď. A je tu ještě NoExec, které dokonce funguje i zpětně, tedy pokud aktuálně na Nodu existují nějaké Pody bez tolerance (vzniklé před nastavením Taintu) budou evakuovány jinam.

Kdy něco takového použít? Tak například potřebujete vyřadit nějaký Node z provozu, protože ho jdete updatovat nebo odebírat. Dáte-li mu Taint NoExec, na který není nikdo tolerantní, všechny Pody Nodu se přesunou jinam. Toho používá AKS například při rolling upgrade clusteru nebo škálování dolu. Další příklad je označení Nodů, které nejsou vhodné pro produkci. Například vezměme předchozí myšlenku s B-series a D-series VM. Nechceme riskovat, že u produkčního Podu zapomeneme na Node affinitu, tak raději na B-series Nody dáme Taint a necháme naše testovací Pody tuto skutečnost explicitně tolerovat. Další situace je virtuální Node s Virtual Kubelet (o tom jindy). Jde o to, že virtuální Node reprezentuje například Azure Container Instances („serverless“ kontejnery) nebo vaše IoT zařízení přes IoT Hub. Asi nechceme, aby se produkční Pody nasadily omylem na vašem Raspberry v továrně. Dáme tedy Taint a kontejnery skutečně určené do IoT zařízení budou mít toleranci. Poslední příklad co mě napadá je situace, kdy máme v clusteru nějaké velmi drahé Nody, například stroje s GPU, na kterých provádíme náročné výpočty třeba s využitím Azure Machine Learning. Nechceme, aby běžné Pody zabíraly zbytečně místo na tomto drahém Nodu, což by stalo, pokud by měli defaultní konfiguraci, která jim to nezakazuje. Dáme na něj tedy Taint a naše machine learning Pody na něj budou mít toleranci a navíc Node affinitu.

Dejme Taint na Node číslo 2.

kubectl taint nodes aks-nodepool1-38238592-2 devonly=goaway:NoSchedule

Udělejme Deployment bez tolerance.

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: notoleration
spec:
  replicas: 5
  template:
    metadata:
      labels:
        app: notoleration
    spec:
      containers:
      - name: myweb
        image: nginx:alpine

A také Deployment s tolerancí.

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: toleration
spec:
  replicas: 5
  template:
    metadata:
      labels:
        app: toleration
    spec:
      containers:
      - name: myweb
        image: nginx:alpine
      tolerations:
      - key: "devonly"
        operator: "Exists"
        effect: "NoSchedule"

Pošleme to tam a uvidíme, že Pody bez tolerance se nedostanou na Node 2, ale ty s tolerancí klidně ano.

kubectl apply -f deploymentNoToleration.yaml
kubectl apply -f deploymentToleration.yaml
kubectl get pods -o wide
NAME                            READY     STATUS              RESTARTS   AGE       IP              NODE
notoleration-7d99cc44b5-5gdsh   0/1       ContainerCreating   0          19s       <none>          aks-nodepool1-38238592-1
notoleration-7d99cc44b5-c5rb2   0/1       ContainerCreating   0          19s       <none>          aks-nodepool1-38238592-1
notoleration-7d99cc44b5-drgfh   0/1       ContainerCreating   0          19s       <none>          aks-nodepool1-38238592-1
notoleration-7d99cc44b5-nm7g2   1/1       Running             0          19s       192.168.0.114   aks-nodepool1-38238592-1
notoleration-7d99cc44b5-p8fgn   1/1       Running             0          19s       192.168.0.173   aks-nodepool1-38238592-1
toleration-65697bd4d8-djw9c     0/1       ContainerCreating   0          1s        <none>          aks-nodepool1-38238592-2
toleration-65697bd4d8-gk4sr     0/1       ContainerCreating   0          1s        <none>          aks-nodepool1-38238592-1
toleration-65697bd4d8-k5tcc     0/1       ContainerCreating   0          1s        <none>          aks-nodepool1-38238592-1
toleration-65697bd4d8-l66vq     0/1       ContainerCreating   0          1s        <none>          aks-nodepool1-38238592-2
toleration-65697bd4d8-nfhsm     0/1       ContainerCreating   0          1s        <none>          aks-nodepool1-38238592-2

Funguje. Pojďme Taint i Deploymenty zase zrušit.

kubectl taint nodes aks-nodepool1-38238592-2 devonly:NoSchedule-
kubectl delete -f deploymentNoToleration.yaml
kubectl delete -f deploymentToleration.yaml

Pod affinity a anti-affinity

Někdy mě konkrétní Node vůbec nezajímá, ale jde mi o to, aby Pody byly buď u sebe nebo naopak daleko od sebe.

Představme si například, že mám Pod s cache a Pod s aplikací. Pro zvýšení výkonu bych rád, aby se objevily na stejném Nodu, aby tam byla nižší latence a nemuselo se přes síť.

Nejprve nasadíme jeden Pod běžným způsobem.

kind: Pod
apiVersion: v1
metadata:
  name: first
  labels:
    app: first
spec:
  containers:
    - name: nginx
      image: nginx:alpine

Teď přidáme druhý s tím, že bychom chtěli, aby byl na stejném Nodu, jako tenhle první.

kind: Pod
apiVersion: v1
metadata:
  name: second-affinity
  labels:
    app: second-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - first
        topologyKey: kubernetes.io/hostname
  containers:
    - name: nginx
      image: nginx:alpine

Pustíme a přesvědčme se, že to tak je.

kubectl apply -f podAffinity1.yaml
kubectl apply -f podAffinity2.yaml
kubectl get pods -o wide
NAME              READY     STATUS              RESTARTS   AGE       IP        NODE
first             0/1       ContainerCreating   0          1s        <none>    aks-nodepool1-38238592-2
second-affinity   0/1       ContainerCreating   0          1s        <none>    aks-nodepool1-38238592-2

Podobně jako u Nodů máme k dispozici i volnější pravidlo – preferred. Scheduler se pokusí dát je na stejný node, ale pokud to nepůjde (došla mu kapacita), dá to alespoň jinam. Ještě si všimněme jednoho atributu a tím je topologyKey. Tím definujeme „stejnost“. Pokud je to kubernetes.io/hostname říkáme, že to chceme na stejném Nodu. Klíč ale může být jiný, třeba barva. V AKS něco takového asi nepotřebuji, ale představme si, že bychom měli dvě skupiny nodů po pěti kusech s tím, že uvnitř skupiny je 10G síť, ale mezi nimi jen 1G propojení. Dávalo by pak smysl chtít Pody do stejné skupiny Nodů, ale nepotřebuji nutně stejný Node.

Existuje ještě opačná možnost – anit-affinity. Cílem je rozhodit Pody od sebe zejména z důvodu zajištění vysoké dostupnosti. To se v AKS stane samo, protože AKS vyplní standardní labely failure-domain.kubernetes.io a scheduler podle nich postupuje by default. Aby se váš Deployment automaticky rozprostřel mezi failure domény v Azure nemusíte tedy nic dělat. Pokud si to chcete vyzkoušet a například dát dva Pody tak, že ten druhý nesmí být na stejně barevném Nodu, vypadalo by to takhle:

kind: Pod
apiVersion: v1
metadata:
  name: anti1
  labels:
    app: anti1
spec:
  containers:
    - name: nginx
      image: nginx:alpine
---
kind: Pod
apiVersion: v1
metadata:
  name: anti2
  labels:
    app: anti2
spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - anti1
        topologyKey: color
  containers:
    - name: nginx
      image: nginx:alpine

Pod priorita

U Podů vždy používejte  resources omezení. Request říká scheduleru kolik má z Nodu „odečíst“, když na něj Pod umístí. Druhé nastavení je limit, kterým omezíte maximální spotřebu CPU nebo paměti (to sice neovlivňuje scheduler, ale limituje váš kontejner, aby se nezbláznil a nevyžral třeba celou paměť Nodu). Proveďme teď Deployment 10 replik, která každá chce 1 celý core, což se do mého clusteru nevejde.

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: standardpriority
spec:
  replicas: 10
  template:
    metadata:
      labels:
        app: standardpriority
    spec:
      containers:
      - name: myweb
        image: nginx:alpine
        resources:
          requests:
            cpu: "1000m"

Pošleme to tam a uvidíme, že se mi to nevejde, takže některé Pody budou ve stavu Pending.

kubectl apply -f deploymentStandardPriority.yaml
kubectl get pods
NAME                               READY     STATUS    RESTARTS   AGE
standardpriority-f86f4fdfb-256rb   1/1       Running   0          15s
standardpriority-f86f4fdfb-2gxmq   0/1       Pending   0          15s
standardpriority-f86f4fdfb-b29bb   0/1       Pending   0          15s
standardpriority-f86f4fdfb-bj9p4   0/1       Pending   0          15s
standardpriority-f86f4fdfb-fvvpj   0/1       Pending   0          15s
standardpriority-f86f4fdfb-j8g4w   1/1       Running   0          15s
standardpriority-f86f4fdfb-mm8sb   0/1       Pending   0          15s
standardpriority-f86f4fdfb-np6sd   0/1       Pending   0          15s
standardpriority-f86f4fdfb-qkwq6   1/1       Running   0          15s
standardpriority-f86f4fdfb-tx9wh   0/1       Pending   0          15s

Cluster máme tedy kompletně zaplněný (důležitá informace pro cluster autoscaler, aby ho zvětšil – ale o tom jindy). Co když ale tyto Pody nejsou zas tak důležité a jde třeba o nějaké výpočetní batch joby, které mohou počkat? Když budu chtít pustit (nebo přiškálovat) Pod, který je business critical, co se stane?

kind: Pod
apiVersion: v1
metadata:
  name: critical-standard
spec:
  containers:
  - name: nginx
    image: nginx:alpine
    resources:
      requests:
        cpu: "1000m"

Pošleme ho tam a on zůstane ve stavu Pending.

kubectl get pods
NAME                               READY     STATUS    RESTARTS   AGE 
critical-standard                  0/1       Pending   0          3s  
standardpriority-f86f4fdfb-256rb   1/1       Running   0          2m  
standardpriority-f86f4fdfb-2gxmq   0/1       Pending   0          2m  
standardpriority-f86f4fdfb-b29bb   0/1       Pending   0          2m  
standardpriority-f86f4fdfb-bj9p4   0/1       Pending   0          2m  
standardpriority-f86f4fdfb-fvvpj   0/1       Pending   0          2m  
standardpriority-f86f4fdfb-j8g4w   1/1       Running   0          2m  
standardpriority-f86f4fdfb-mm8sb   0/1       Pending   0          2m  
standardpriority-f86f4fdfb-np6sd   0/1       Pending   0          2m  
standardpriority-f86f4fdfb-qkwq6   1/1       Running   0          2m  
standardpriority-f86f4fdfb-tx9wh   0/1       Pending   0          2m

Hmm, to je problém. Pokud máte AKS s Kubernetes verzí 1.11.1 nebo vyšší je v něm k dispozici Pod prioritizace. To nám umožní definovat prioritní třídu:

apiVersion: scheduling.k8s.io/v1beta1
kind: PriorityClass
metadata:
  name: critical
value: 1000000
globalDefault: false
description: "Priority class for my business critical Pods."

Takhle by vypadal Pod, který patří do této vyšší priority.

kind: Pod
apiVersion: v1
metadata:
  name: critical-priority
spec:
  containers:
  - name: nginx
    image: nginx:alpine
    resources:
      requests:
        cpu: "1000m"
  priorityClassName: critical

Vytvořme prioritní třídu a pošleme tam tento kritický Pod. Co uvidíme je, že vytlačí nějaký s nižší prioritou.

kubectl apply -f podPriorityClass.yaml
kubectl apply -f podPriority.yaml
kubectl get pods
NAME                               READY     STATUS    RESTARTS   AGE
critical-priority                  1/1       Running   0          25s
critical-standard                  0/1       Pending   0          2m
standardpriority-f86f4fdfb-256rb   1/1       Running   0          5m
standardpriority-f86f4fdfb-2gxmq   0/1       Pending   0          5m
standardpriority-f86f4fdfb-b29bb   0/1       Pending   0          5m
standardpriority-f86f4fdfb-bj9p4   0/1       Pending   0          5m
standardpriority-f86f4fdfb-fvvpj   0/1       Pending   0          5m
standardpriority-f86f4fdfb-j8g4w   1/1       Running   0          5m
standardpriority-f86f4fdfb-mm8sb   0/1       Pending   0          5m
standardpriority-f86f4fdfb-np6sd   0/1       Pending   0          5m
standardpriority-f86f4fdfb-tx9wh   0/1       Pending   0          5m
standardpriority-f86f4fdfb-zfkfk   0/1       Pending   0          25s

 

Kubernetes scheduler je opravdu velmi chytrý a komplikovaný, nicméně díky konstruktům jako je Node a Pod affinita, Taints nebo Pod priority jsme schopni jeho chování efektivně ovlivnit aniž bychom se pouštěli do nějaké složité matematiky nebo psaní vlastního scheduleru.

Podobné příspěvky: