Maitriser et influencer le scheduler kubernetes pour un meilleur placement de vos pods

Un environnement cloud native  généralement requiert la mise en relation de plusieurs micro-services qui vont communiquer entres elles pour fournir le service principal. Ces micro-services sont en fait des conteneurs/pods qu’il faut déployer sur les noeuds physiques en respectant les spécificités de chaque services :

  • Capacite (cpu/memoire) requise
  • Ne peut correctement fonctionner que sur des noeuds possédant des disques ssd
  • Ne peut cohabiter sur le même noeud qu’un autre pod du même service afin de satisfaire la contrainte de tolérance de pannes
  • En cas d’insuffisance de ressource physique, ne peut être arrêté car il s’agit d’un micro-service critique
  • etc…

Il va de soit que c’est impossible pour un être humain de proprement prendre en compte tous ces critères et de manuellement décider pour chaque pod sur quel noeud il doit etre placé, decision qu’il faut parfois faire sur une centaine de micro-services.

Le rôle du scheduler sera donc de placer automatiquement chaque pod sur le meilleur noeud tout en se basant sur les contraintes que vous aurez définies afin d’assurer un fonctionnement optimal de l’ensemble des composants et donc du service fourni.

  • Disponibilité d’un noeud

Une déclaration de pod contient les paramètres request.cpu et request.memory qui déterminent les quantités cpu et mémoires nécessaires pour sa bonne exécution.

Pour chaque noeud, le planificateur détermine la quantité de ressource disponible en utilisant simplement la formule ci-dessous :

Ressource disponible = Ressource totale – (somme des requests des pods qui tournent sur le noeuds) – quantité réservée pour les processus kubernetes – quantité réservée pour le système.

La valeur la quantité réservée pour les processus kubernetes est définie dans la configuration de kubernetes et permet de réserver les ressources nécessaires pour faire fonctionner les services qui implémentent kubernetes.

La valeur pour la quantité réservée pour le système est aussi définie dans la configuration de kubernetes et permet de réserver pour chaque noeuds les ressources requises pour faire fonctionner le système d’exploitation.

Il est important de bien paramétrer les 2 éléments ci-dessous, pour éviter que les pods ne saturent complètement les nodes et empêchent les services vitaux de fonctionner pouvant entraîner un plantage total du système !

Vous pouvez trouver cet information en executant describe sur un noeud

Capacity:

attachable-volumes-gce-pd:  127

cpu:                        1

ephemeral-storage:          98868448Ki

hugepages-2Mi:              0

memory:                     3785940Ki

pods:                       110

Allocatable:

attachable-volumes-gce-pd:  127

cpu:                        940m

ephemeral-storage:          47093746742

hugepages-2Mi:              0

memory:                     2700500Ki

pods:                       110

  • Politique de placement

La configuration du planificateur(scheduler) kubernetes est basée sur un ensemble de prédicats et priorité qui vont permettre de ressortir le(s) meilleur(s) noeud(s) disponible(s) pour executer toute nouvelle demande de pod.

Le processus de placement se déroule en 3 étapes principales :

  • L’application d’un filtre à l’aide des prédicats pour déterminer une liste de noeuds utilisable
  • L’application des points pour ordonner cette liste
  • La sélection du meilleur noeud

Les prédicats sont des règles qui permettent d’exclure tous les noeuds qui ne sont pas qualifiés a y faire tourner le pod. On en distingue plusieurs, entre autre :

PodFitsHostPorts

PodFitsHost

PodFitsResources

PodMatchNodeSelector

NoVolumeZoneConflict

NoDiskConflict

Ainsi, grâce au prédicat PodFitsResources, il n’est pas possible de planifier un conteneur sur un serveur ne possédant pas les capacités disponibles. Ou encore PodFitsHostPorts va filtrer la liste des noeuds dont le port souhaité par le pod est disponible.

Les priorités permettent d’affecter des poids aux neoud qui auront etre filtres, dans le but de les ordonner et utiliser le noeud qui aura le meilleur scoring :

{“name” : “LeastRequestedPriority”, “weight” : 1}

{“name” : “BalancedResourceAllocation”, “weight” : 2}

{“name” : “ServiceSpreadingPriority”, “weight” : 1}

{“name” : “EqualPriority”, “weight” : 1}

Vous pouvez consulter ici la liste des prédicats et priorités disponibles. Cette liste peut varier en fonction de votre implémentation de kubernetes

Ces éléments sont définis dans une ressource de type policy qui est utilisé par le planificateur.

kind: “Policy”

version: “v1”

predicates:

– name: “PodFitsPorts”

– name: “PodFitsResources”

– name: “NoDiskConflict”

– name: “MatchNodeSelector”

– name: “HostName”

priorities:

– name: “LeastRequestedPriority”

weight: 1

– name: “BalancedResourceAllocation”

weight: 1

– name: “ServiceSpreadingPriority”

weight: 1

La modification de cette ressource est fonction de votre implémentation kubernetes, et peut se faire soit via une custom ressource ou directement dans des fichiers de configuration.

  • Quelques techniques importantes de placement
    • nodeName

Dans les spécifications d’un pod se trouve un attribut “nodeName” qui représente le noeud qui doit héberger le pod. La présence de cette valeur va obliger le planificateur a placer le pod uniquement sur ce noeud, et donc ignorer tous les autres prédicats et priorités vus plus haut.

apiVersion: v1

kind: Pod

metadata:

name: myapp-pod

labels:

app: myapp

spec:

containers:

– name: myapp-container

image: busybox

command: [‘sh’, ‘-c’, ‘echo Hello Kubernetes! && sleep 3600’]

nodeName: node-1

Quelques éléments à prendre en compte :

  • Si le noeud mentionne n’existe pas le pod ne sera jamais execute
  • Si le noeud est saturé il ne pourra non plus execute le pod, ce dernier restera donc en attente.

Cette technique est le plus souvent pratiquée pour faire des tests sur un noeud ou alors lorsqu’il y a des contraintes techniques qui exigeraient de ne exécuter le pod que sur ce noeud (par exemple si le pod doit monter un disque en hostPath pour lire un fichier qui ne se trouve que sur ce noeud)

Cette technique est bien évidemment à utiliser avec précautions car elle introduit le concept de dépendance à une ressource, qui va en contradiction avec les fondement de kubernetes et du cloud-native.

  • NodeSelector

Il est aussi possible de filtrer les noeuds a partir du nodeSelector, qui est un champs disponible dans les spécifications du pod. Ce champs permet de définir un ou plusieurs labels(cle – valeur) qui doivent être aussi présentes sur les noeuds qui pourront accueillir ce pod. Un noeud ne possédant pas ces labels ne pourront donc pas l’accueillir.

Pour définir un label sur un noeud, il suffit d’exécuter la commande

kubectl label node <nom_du_noeud> <cle>=<valeur>

Pour consulter la liste des noeuds possédant un label spécifique

kubectl get nodes -l “cle=valeur”

Et ci-dessous notre pod avec le nodeSelector

apiVersion: v1

kind: Pod

metadata:

name: myapp-pod

labels:

app: myapp

spec:

containers:

– name: myapp-container

image: busybox

command: [‘sh’, ‘-c’, ‘echo Hello Kubernetes! && sleep 3600’]

nodeSelector:

env: production

Cette méthode est très utilisée et permet de faire une segmentation des noeuds afin de faire par exemple tourner les pod de production sur les noeuds ayant un label production, ces noeuds fournissant un maximum de capacité et de performance .

  • Node affinity et antiaffinity

Ceci utilise le même principe que la nodeSelector vu plus haut, mais est un peu plus avancée. En fait elle va permettre via des opérateurs fournir possibilité d’affiner son filtre de sélection de noeud, aussi en fonction du type d’affinité souhaite on peut indiquer au scheduler soit de préférer cet algorithme de sélection quand c’est possible, ou alors de l’exiger. On a donc 2 attributs possibles lors de la configuration du node affinity ou anti affinity :

  1. PreferredDuringSchedulingIgnoredDuringExecution

Supposons que nous souhaitions faire exécuter nos pods sur certains noeuds optimizes a cet effet mais en même temps nous ne pouvons tolérer qu’ils restent en statut “en attente” faute de noeud disponible.

De ce fait, nous pouvons l’option “PreferredDuringSchedulingIgnoredDuringExecution” pour instruire au planificateur d’utiliser dans le meilleur des cas les attributs de sélection de neoud, mais au cas ou aucun des noeuds préférés n’est disponible alors planifier le pod sur n’importe quel autre noeud

  1. RequiredDuringSchedulingIgnoredDuringExecution

Contrairement à l’attribut vu précédemment, cet attribut exige de choisir uniquement un noeud qui respecte les critères de sélection de noeuds définis sur le pod.

Le pod défini ci-dessous va s’exécuter uniquement sur les noeuds qui sont soit de preprod ou de test (qui possèdent les labels env:dev ou env:preprod)

apiVersion: v1

kind: Pod

metadata:

name: myapp-pod

labels:

app: myapp

spec:

affinity:

nodeAffinity:

requiredDuringSchedulingIgnoredDuringExecution:

nodeSelectorTerms:

– matchExpressions:

– key: env

operator: In

values:

– dev

– preprod

containers:

– name: myapp-container

image: busybox

command: [‘sh’, ‘-c’, ‘echo Hello Kubernetes! && sleep 3600’]

Ci-dessous une liste exhaustive des opérateurs qui peuvent être utilisées (Les opérateurs Not sont employés pour le node anti-affinity):

  1. In : Indique au planificateur de planifier le pod sur les noeuds qui ont les même clé/valeurs que celles définies dans le pod
  2. NotIn : Indique au planificateur de planifier le pod sur les noeuds qui n’ont pas les même clé/valeurs que celles définies dans le pod
  3. Exists : Va sélectionner tout noeud dont la clé existe, quelque soit sa valeur.
  4. DoesNotExist : Va selectionner tout noeud dont la clé n’existe pas, quelque soit sa valeur.
  5. Gt : Ici le pod va demander au scheduler d’utiliser les noeuds dont la valeur de la clé est supérieure à celle définie sur le pod.
  6. Lt : Ici le pod va demander au scheduler d’utiliser les noeuds dont la valeur de la clé est inférieure à celle définie sur le pod.
  • Taint and tolerations

Avec les nodes anti-affinity, vous pouvez empêcher l’exécution de pods sur certain noeuds. Mais cela souffre du fait que chaque pod dans sa définition doit inclure ce paramètre d’anti affinité, si donc une personne rejoint l’équipe et déploie un pod et oublie d’y mettre l’attribut d’anti affinité, il ya des chances que son pod se retrouve sur un noeud non indiqué.

Les taints et toleration permettent d’adresser cette limitation, en effet si vous marquez un noeud avec un “taint” elle ne pourra accueillir qu’un pod qui tolère ce taint, c’est a dire seuls les pods qui dans leur définition tolère ce taint pourront atterrir sur ce noeud marque.

En marquant notre noeud on est donc sûr que pour qu’un pod y soit planifié, il faut qu’il ai une mention bien précise dans sa définition.

Vous pouvez marquez vos noeuds via la commande ci-dessous :

kubectl taint nodes server1 env=production:NoSchedule

Et sur vos pods il faudra les tolérer tel que sur l’exemple ci-dessous :

apiVersion: v1

kind: Pod

metadata:

name: myapp-pod

labels:

app: myapp

spec:

tolerations:

– key: “env”

operator: “Equal”

value: “production”

effect: “NoSchedule”

containers:

– name: myapp-container

image: busybox

command: [‘sh’, ‘-c’, ‘echo Hello Kubernetes! && sleep 3600’]

Il est important de préciser qu’un pod n’est pas obligé d’être planifié absolument sur les noeuds qu’il tolère. Il pourra tout aussi atterrir sur un pod non toléré, si vous voulez absolument qu’il ne soit exécuté que sur les noeuds qu’il tolère, il faut associer les taint-toleration aux node affinité vues plus haut.

  • Optimizations
    • Preemption and priority classes

Nous avons vu comment le planificateur place les pods sur des noeuds et aussi comment influencer sa décision. Nous avons aussi vu que en cas de saturation de tous les noeuds, le planificateur ne pourra plus placer de nouveaux pods faute de disponibilité de noeud. Ici, nous verrons qu’il est possible de légèrement influencer cela grâce au classes de priorité des pods.

Les pods à faible priorités se verront donc supprimer(kill) en pleine exécution afin de faire la place pour planifier les pods à haute priorité.

Pour le mettre en place, créez votre classe de priorité (La priorité maximale utilisable est 1000000000, les priorités supérieures a celle la sont celles définies pour les composants critiques de kubernetes):

apiVersion: scheduling.k8s.io/v1

description: Applications de production.

kind: PriorityClass

metadata:

name: applications-de-production

value: 1000000000

Et sur les pods, mentionnez à quelle classe elle devra correspondre :

apiVersion: v1

kind: Pod

metadata:

name: myapp-pod

labels:

app: myapp

spec:

priorityClassName: applications-de-production

– name: myapp-container

image: busybox

command: [‘sh’, ‘-c’, ‘echo Hello Kubernetes! && sleep 3600’]

Partager:

Facebook
Twitter
LinkedIn