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 :
- NodeName : Assigner un pod à un nœud spécifique.
- Node Selector : Une approche plus simple pour filtrer sur des labels.
- Node Affinity : Contraintes avancées basées sur les labels des nœuds.
- Pod Affinity et Anti-Affinity : Regrouper ou éloigner certains pods.
- 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: v1kind: Podmetadata: name: pod-on-specific-nodespec: 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
ounodeAffinity
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 :
kubectl label nodes node-gpu accelerator=nvidia-gpu
💡 On peut afficher les labels d’un nœud avec :
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: v1kind: Podmetadata: name: pod-gpuspec: 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.
kubectl label nodes node-storage disk=ssd
Puis on définit le nodeSelector
dans le Deployment
:
apiVersion: apps/v1kind: Deploymentmetadata: name: postgres-dbspec: 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 :
requiredDuringSchedulingIgnoredDuringExecution
→ Obligatoire : si aucune correspondance, le pod ne sera pas programmé.preferredDuringSchedulingIgnoredDuringExecution
→ Optionnelle : 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 :
kubectl label nodes node-gpu accelerator=nvidia-gpu
Puis, dans mon manifest de pod, j’ajoute une contrainte d’affinité obligatoire :
apiVersion: v1kind: Podmetadata: name: pod-gpuspec: 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 valeurnvidia-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: v1kind: Podmetadata: name: pod-gpu-preferredspec: 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: v1kind: Podmetadata: name: web-app labels: app: webspec: 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/v1kind: Deploymentmetadata: name: web-appspec: 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 :
kubectl taint nodes <node-name> <key>=<value>:<effect>
Trois types d’effets possibles :
NoSchedule
→ Interdit 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).NoExecute
→ Expulse 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 :
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: v1kind: Podmetadata: name: pod-gpuspec: 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 :
kubectl describe nodes <node-name> # Vérifier les labels et taints du nœudkubectl describe pod <pod-name> # Comprendre pourquoi un pod reste en Pendingkubectl get nodes --show-labels # Voir tous les labels des nœuds
Le choix de la bonne stratégie dépend de votre objectif :
Objectif | Solution recommandée |
---|---|
Assigner un pod à un nœud spécifique | nodeAffinity (plus flexible) ou nodeSelector (simple) |
Regrouper certains pods ensemble | podAffinity |
Éviter que plusieurs pods tournent sur le même nœud | podAntiAffinity |
Réserver un nœud pour certains workloads | taint sur le nœud + toleration sur les pods autorisés |
Empêcher certains pods d’être déployés sur un nœud | taint 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) ! 💡