Aller au contenu

Optimiser l’assignation des pods dans Kubernetes

Mise à jour :

Dans un cluster Kubernetes, les pods sont généralement programmés sur n’importe quel nœud disponible, selon la capacité et la charge du cluster. Cependant, certaines applications nécessitent une assignation spécifique à des nœuds dotés de caractéristiques particulières.

Par exemple :

  • Workloads nécessitant des GPU : Certains pods, comme ceux exécutant des modèles d’intelligence artificielle, doivent être déployés sur des nœuds équipés de cartes graphiques (NVIDIA, AMD).
  • Applications nécessitant un stockage haute performance : Bases de données ou systèmes distribués doivent être placés sur des nœuds avec des disques SSD NVMe.
  • Isolation et sécurité : Certains workloads sensibles nécessitent une exécution sur des nœuds dédiés, isolés des autres charges de travail.

Kubernetes propose plusieurs mécanismes de contraintes pour orienter l’assignation des pods sur des nœuds spécifiques. Ces mécanismes permettent de :

  • Restreindre un pod à une catégorie de nœuds (ex: uniquement les nœuds avec GPU).
  • Éviter que plusieurs instances d’un même service tournent sur le même nœud.
  • Réserver des nœuds pour certains workloads.

Comment assigner des pods à des nœuds spécifiques ?

Kubernetes offre plusieurs solutions pour affiner l’assignation des pods :

  1. NodeName : Assigner un pod à un nœud spécifique.
  2. Node Selector : Une approche plus simple pour filtrer sur des labels.
  3. Node Affinity : Contraintes avancées basées sur les labels des nœuds.
  4. Pod Affinity et Anti-Affinity : Regrouper ou éloigner certains pods.
  5. Taints et Tolerations : Réserver des nœuds pour des workloads spécifiques.

1. NodeName : assigner un pod à un nœud précis

Le champ nodeName permet de forcer directement un pod à s’exécuter sur un nœud spécifique du cluster.

Cas d’usage typiques :

  • Dépannage ou tests sur un nœud précis.
  • Assignation manuelle d’un pod à un nœud spécifique sans passer par des labels.
  • Déploiement de workloads critiques sur un nœud contrôlé.

Comment utiliser NodeName ?

Il suffit de spécifier le nom exact du nœud dans la spec du pod :

apiVersion: v1
kind: Pod
metadata:
name: pod-on-specific-node
spec:
containers:
- name: my-container
image: my-image
nodeName: my-node-1

Explication :

  • Kubernetes ignorera complètement le scheduler et placera directement le pod sur my-node-1.
  • Si my-node-1 n’existe pas ou est indisponible, le pod restera en attente (Pending).

Limitations et bonnes pratiques

  • Ne pas utiliser nodeName en production sauf dans des cas spécifiques.
  • Si le nœud devient indisponible, Kubernetes ne reprogrammera pas automatiquement le pod ailleurs.
  • Préférer nodeSelector ou nodeAffinity pour des règles plus dynamiques.

2. Node Selector

Le nodeSelector est la méthode la plus simple et directe pour contraindre un pod à être programmé sur un nœud spécifique, en se basant sur les labels des nœuds.

Cas d’usage typiques :

  • Déployer des pods nécessitant un GPU uniquement sur des nœuds équipés de cartes graphiques.
  • Assigner un service de base de données sur un nœud optimisé pour le stockage.
  • Séparer certains types de workloads sur des nœuds dédiés (ex : nœuds pour CI/CD, applications sensibles, etc.).

Comment ça fonctionne ? :

Le nodeSelector fonctionne en associant un pod à un nœud possédant un label donné. Contrairement à nodeAffinity,que nous verrons juste après, il n’accepte que des égalités strictes (key=value).

Poser un label sur un nœud

Avant d’utiliser nodeSelector, il faut ajouter un label au nœud cible. Par exemple, pour identifier un nœud avec un GPU :

Terminal window
kubectl label nodes node-gpu accelerator=nvidia-gpu

💡 On peut afficher les labels d’un nœud avec :

Terminal window
kubectl get nodes --show-labels

Forcer un pod sur un nœud spécifique

Si je veux que mon pod s’exécute uniquement sur les nœuds équipés de GPU, j’ajoute le nodeSelector dans mon manifeste :

apiVersion: v1
kind: Pod
metadata:
name: pod-gpu
spec:
containers:
- name: my-container
image: my-image
nodeSelector:
accelerator: nvidia-gpu

Explication :

  • Seuls les nœuds ayant le label accelerator=nvidia-gpu pourront accueillir ce pod.
  • Si aucun nœud ne correspond, le pod restera en attente (Pending).

Forcer un workload sur un groupe de nœuds

On peut également utiliser nodeSelector pour orienter un service entier (ex: un cluster de bases de données) vers des nœuds spécialisés.

Exemple :

On peut déployer une base PostgreSQL sur des nœuds optimisés pour le stockage.

Terminal window
kubectl label nodes node-storage disk=ssd

Puis on définit le nodeSelector dans le Deployment :

apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres-db
spec:
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:14
nodeSelector:
disk: ssd

Explication :

  • PostgreSQL ne pourra être programmé que sur des nœuds possédant des SSD (disk=ssd).
  • Utile pour optimiser les performances des bases de données et limiter l’usure des disques.

2. Node Affinity : assigner les pods en fonction des nœuds

Le mécanisme Node Affinity permet de contraindre Kubernetes à programmer un pod sur un type spécifique de nœud, en fonction des labels définis sur ces derniers. C’est une solution plus flexible et avancée que nodeSelector, qui permet d’exprimer des préférences et des règles obligatoires.

Cas d’usage typiques :

  • Exécuter des workloads nécessitant un GPU uniquement sur des nœuds équipés de cartes graphiques.
  • Restreindre certains pods à des nœuds haute performance (ex: CPU optimisés).
  • Déployer des applications sur des nœuds géographiquement proches (ex: multi-datacenters).

Comment ça fonctionne ?

L’affinité de nœud repose sur les labels attribués aux nœuds. On peut définir deux types de règles dans la section affinity.nodeAffinity d’un pod :

  • requiredDuringSchedulingIgnoredDuringExecutionObligatoire : si aucune correspondance, le pod ne sera pas programmé.
  • preferredDuringSchedulingIgnoredDuringExecutionOptionnelle : Kubernetes essaiera de respecter la règle, mais peut la contourner si aucun nœud ne correspond.

IgnoredDuringExecution signifie que ces contraintes ne sont prises en compte qu’au moment du scheduling du pod, mais pas après (si un nœud devient inéligible plus tard, le pod ne sera pas déplacé automatiquement).

Exemple : Affinité obligatoire pour des nœuds GPU :

Si je veux que mon pod ne soit programmé que sur des nœuds équipés de GPU, j’ajoute un label à ces nœuds :

Terminal window
kubectl label nodes node-gpu accelerator=nvidia-gpu

Puis, dans mon manifest de pod, j’ajoute une contrainte d’affinité obligatoire :

apiVersion: v1
kind: Pod
metadata:
name: pod-gpu
spec:
containers:
- name: my-container
image: my-image
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: accelerator
operator: In
values:
- nvidia-gpu

Explication :

  • La clé accelerator doit avoir la valeur nvidia-gpu sur le nœud.
  • Si aucun nœud ne correspond, le pod restera en attente (Pending).

Exemple : Affinité préférée pour un nœud spécifique :

Si je veux favoriser un nœud GPU, mais autoriser le pod à tourner ailleurs si nécessaire :

apiVersion: v1
kind: Pod
metadata:
name: pod-gpu-preferred
spec:
containers:
- name: my-container
image: my-image
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: accelerator
operator: In
values:
- nvidia-gpu

Explication :

  • weight: 100 → Kubernetes essaiera fortement de programmer le pod sur un nœud GPU, mais ne le bloquera pas en cas d’indisponibilité.

Bonnes pratiques et limites

  • Étiqueter correctement les nœuds (kubectl label nodes <node-name> key=value).
  • Tester avec kubectl describe pod pour voir pourquoi un pod ne se programme pas.
  • Combiner avec des Taints/Tolerations pour réserver certains nœuds (ex: GPU exclusifs).
  • Les règles sont statiques : Kubernetes ne reprogramme pas un pod si un meilleur nœud devient disponible après coup.

4. Pod Affinity et Pod Anti-Affinity : regrouper ou éloigner les pods

Contrairement à nodeSelector et nodeAffinity, qui s’appliquent aux nœuds, les règles Pod Affinity et Pod Anti-Affinity influencent la répartition des pods les uns par rapport aux autres.

Cas d’usage typiques :

  • Pod Affinity → Regrouper certains pods sur les mêmes nœuds pour améliorer la communication et les performances (ex : services fortement interconnectés).
  • Pod Anti-Affinity → Répartir des pods sur plusieurs nœuds pour éviter les SPOF (Single Point of Failure) et améliorer la tolérance aux pannes.

Ces contraintes utilisent les labels des pods et une clé spéciale topologyKey pour définir les limites de la contrainte (par exemple : au niveau du nœud, de la zone, du datacenter, etc.).

Pod Affinity : rapprocher des pods ensemble

Avec Pod Affinity, on peut forcer ou préférer l’exécution de pods sur le même nœud qu’un autre pod possédant un label donné.

Exemple : Placer des pods d’une même application sur le même nœud :

Si une application Web et son cache Redis doivent être exécutés sur le même nœud pour réduire la latence, on applique une affinité obligatoire (requiredDuringSchedulingIgnoredDuringExecution) :

apiVersion: v1
kind: Pod
metadata:
name: web-app
labels:
app: web
spec:
containers:
- name: web-container
image: my-web-image
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- redis
topologyKey: "kubernetes.io/hostname"

Explication :

  • Ce pod sera programmé sur le même nœud qu’un autre pod portant le label app=redis.
  • La topologie clé kubernetes.io/hostname signifie que l’affinité s’applique au niveau du nœud.

On peut aussi utiliser preferredDuringSchedulingIgnoredDuringExecution pour exprimer une préférence sans rendre la contrainte obligatoire.

Pod Anti-Affinity : répartir les pods pour éviter les SPOF

Avec Pod Anti-Affinity, on évite que plusieurs pods similaires s’exécutent sur le même nœud, ce qui améliore la résilience et la haute disponibilité.

Exemple : Éviter que plusieurs instances d’un service tournent sur le même nœud :

Si une application Web possède plusieurs réplicas, on veut éviter qu’ils s’exécutent tous sur le même nœud, pour améliorer la disponibilité en cas de panne.

apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web
topologyKey: "kubernetes.io/hostname"
containers:
- name: web-container
image: my-web-image

Explication :

  • Kubernetes empêchera plusieurs pods ayant app=web de tourner sur le même nœud.
  • La topologie clé kubernetes.io/hostname force la répartition au niveau des nœuds.
  • Utile pour éviter un SPOF si un nœud tombe en panne.

On peut ajuster topologyKey pour répartir les pods sur des zones de disponibilité (topology.kubernetes.io/zone) ou des datacenters.

Bonnes pratiques et limites

  • Utiliser des labels cohérents pour identifier les pods concernés.
  • Ajuster topologyKey pour appliquer les règles au bon niveau (nœud, zone, datacenter).
  • Préférer preferredDuringSchedulingIgnoredDuringExecution si la contrainte peut être relâchée en cas de besoin.
  • Les contraintes trop strictes peuvent bloquer l’ordonnancement des pods (vérifier avec kubectl describe pod <pod>).

5. Taints et Tolerations : réserver des nœuds pour des workloads spécifiques

Contrairement à nodeAffinity, qui privilégie certains nœuds, les taints et tolerations fonctionnent à l’inverse :

  • Un taint (“souillure”) appliqué à un nœud empêche les pods d’y être programmés.
  • Une toleration sur un pod lui permet d’ignorer ce taint et de s’y exécuter.

Cas d’usage typiques :

  • Réserver des nœuds spécialisés (ex : nœuds avec GPU, stockage haute performance).
  • Empêcher des workloads généraux d’être programmés sur certains nœuds.
  • Isoler certains workloads sensibles (ex : workloads critiques sur des nœuds dédiés).

Contrairement à nodeSelector et nodeAffinity, un taint bloque les pods par défaut, sauf s’ils ont une tolérance adaptée.

Etape 1 : Ajouter un taint à un nœud

Un taint est appliqué à un nœud avec la commande suivante :

Terminal window
kubectl taint nodes <node-name> <key>=<value>:<effect>

Trois types d’effets possibles :

  • NoScheduleInterdit tout pod sans tolérance. C’est celui que l’on retrouve sur les nodes du control-plane.
  • PreferNoScheduleÉvite de programmer des pods sans tolérance (mais pas obligatoire).
  • NoExecuteExpulse immédiatement les pods qui ne tolèrent pas le taint.

**Exemple : Réserver un nœud GPU

Si je veux empêcher les pods standards d’être programmés sur un nœud GPU :

Terminal window
kubectl taint nodes node-gpu accelerator=nvidia-gpu:NoSchedule

Explication :

  • Tous les pods sans tolérance spécifique seront bloqués sur ce nœud.
  • Seuls les pods qui acceptent le taint pourront être programmés ici.

Etape 2 : Ajouter une tolérance à un pod

Un pod peut tolérer un taint via la section tolerations :

apiVersion: v1
kind: Pod
metadata:
name: pod-gpu
spec:
containers:
- name: my-container
image: my-image
tolerations:
- key: "accelerator"
operator: "Equal"
value: "nvidia-gpu"
effect: "NoSchedule"

Explication :

  • Ce pod peut être programmé sur un nœud ayant le taint accelerator=nvidia-gpu:NoSchedule.
  • Les autres pods sans tolérance ne pourront pas s’exécuter sur ce nœud.

On peut utiliser l’opérateur Exists pour tolérer un taint sans vérifier sa valeur.

Bonnes pratiques et limites

  • Utiliser kubectl describe node <node> pour vérifier les taints.
  • Tester les tolérances avec kubectl describe pod <pod>.
  • Utiliser PreferNoSchedule pour des règles moins strictes.
  • Un mauvais taint peut empêcher tout scheduling sur un nœud.

Comment choisir entre Affinity, Toleration et Taint ?

Avant d’ajouter une nouvelle règle, vérifiez celles déjà en place ! Ajouter une contrainte sans analyse préalable peut empêcher le scheduling des pods et bloquer des workloads. Utilisez :

Terminal window
kubectl describe nodes <node-name> # Vérifier les labels et taints du nœud
kubectl describe pod <pod-name> # Comprendre pourquoi un pod reste en Pending
kubectl get nodes --show-labels # Voir tous les labels des nœuds

Le choix de la bonne stratégie dépend de votre objectif :

ObjectifSolution recommandée
Assigner un pod à un nœud spécifiquenodeAffinity (plus flexible) ou nodeSelector (simple)
Regrouper certains pods ensemblepodAffinity
Éviter que plusieurs pods tournent sur le même nœudpodAntiAffinity
Réserver un nœud pour certains workloadstaint sur le nœud + toleration sur les pods autorisés
Empêcher certains pods d’être déployés sur un nœudtaint avec NoSchedule

Règle générale :

  • Besoin d’exprimer une préférence -> Affinity.
  • Besoin d’une contrainte stricte -> Taint & Toleration.
  • Besoin d’une solution simple -> nodeSelector.

Si vous hésitez, commencez par nodeAffinity pour plus de flexibilité et ajoutez des taints si nécessaire.

Conclusion

L’assignation des pods dans un cluster Kubernetes est un enjeu clé pour optimiser la performance, la disponibilité et la gestion des ressources. Grâce à Affinity, Toleration et Taint, vous pouvez adapter finement le déploiement de vos workloads selon leurs besoins spécifiques.

En combinant ces mécanismes, vous garantissez un déploiement optimisé, avec une meilleure répartition des charges et une tolérance aux pannes accrue.

Besoin d’approfondir ? Testez ces stratégies avec mes TP (en cours d’écriture) ! 💡