Aller au contenu
Conteneurs & Orchestration medium

Scheduling avancé : Affinity, Taints, Tolerations et Topology Spread

60 min de lecture

logo kubernetes

Kubernetes ne programme pas les Pods "au hasard". Vous pouvez contrôler précisément où s'exécutent vos workloads grâce à plusieurs mécanismes complémentaires. Ce guide présente les outils modernes de scheduling : du simple nodeSelector jusqu'aux topologySpreadConstraints pour la haute disponibilité.

  • Cibler des nœuds spécifiques avec nodeSelector et nodeAffinity
  • Gérer les relations entre Pods avec podAffinity / podAntiAffinity
  • Répartir proprement les replicas avec topologySpreadConstraints
  • Protéger et réserver des nœuds avec taints / tolerations
  • Débugger les Pods bloqués en Pending

Kubernetes propose sept mécanismes de placement, et la confusion vient de ce qu'ils ne répondent pas à la même question. Certains désignent où poser un Pod, d'autres définissent qui a le droit d'entrer sur un nœud, d'autres encore organisent la répartition d'un groupe de replicas. La colonne « Agit sur » est le meilleur repère : un mécanisme qui agit sur le nœud ne se combine pas de la même façon qu'un mécanisme qui agit sur les Pods entre eux.

MécanismeObjectifAgit sur
nodeNameForcer un nœud précis (bypass scheduler)Nœud
nodeSelectorFiltrer par labels (simple)Nœud
nodeAffinityFiltrer par labels (avancé, préférence/contrainte)Nœud
podAffinityRapprocher des Pods liésPods
podAntiAffinityÉloigner des PodsPods
topologySpreadConstraintsRépartir les replicas entre domainesTopologie
taints / tolerationsProtéger/réserver des nœudsNœud

Kubernetes définit des labels standardisés pour la topologie. Préférez-les aux labels custom quand ils existent :

LabelDescription
kubernetes.io/hostnameNom du nœud
topology.kubernetes.io/zoneZone de disponibilité
topology.kubernetes.io/regionRégion
node.kubernetes.io/instance-typeType d'instance (cloud)
Fenêtre de terminal
# Voir les labels d'un nœud
kubectl get nodes --show-labels

Le champ nodeName lie directement un Pod à un nœud, sans passer par le scheduler. C'est le seul mécanisme de cette page qui ne formule pas une contrainte à satisfaire, mais une affectation figée : le kubelet du nœud nommé prend le Pod tel quel. Aucune vérification de ressources, de taint ou de label n'a lieu, ce qui explique les comportements déroutants quand le nœud ne convient pas.

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

Cas d'usage :

  • Dépannage ou tests sur un nœud précis
  • Situations très spécifiques où vous savez exactement où placer le Pod

À éviter en production, préférez nodeSelector ou nodeAffinity pour des règles dynamiques.

nodeSelector est la méthode la plus simple pour contraindre un Pod à des nœuds ayant un label spécifique. Le scheduler ne retient que les nœuds portant tous les labels demandés, avec une égalité stricte. La mise en place se fait en deux temps : poser le label sur les nœuds concernés, puis le réclamer dans le Pod.

Le label est une paire clé/valeur libre stockée sur l'objet Node ; il persiste aux redémarrages mais disparaît si le nœud est retiré puis réenregistré dans le cluster.

Fenêtre de terminal
kubectl label nodes node-gpu accelerator=nvidia-gpu

Le champ se place directement sous spec, au même niveau que containers, sans passer par le bloc affinity.

apiVersion: v1
kind: Pod
metadata:
name: pod-gpu
spec:
containers:
- name: my-container
image: my-image
nodeSelector:
accelerator: nvidia-gpu
  • Seuls les nœuds ayant accelerator=nvidia-gpu peuvent accueillir ce Pod
  • Si aucun nœud ne correspond, le Pod reste en Pending
  • Limitation : égalités strictes uniquement (key=value)

nodeAffinity offre plus de flexibilité que nodeSelector : il accepte des opérateurs (appartenance à une liste, existence d'une clé, comparaison numérique) et distingue une contrainte bloquante d'une simple préférence. Les noms de ces deux modes sont longs mais se décomposent en deux parties, ce qui s'applique au scheduling et ce qui est ignoré pendant l'exécution.

ModeComportement
requiredDuringSchedulingIgnoredDuringExecutionObligatoire, le Pod ne sera pas programmé si aucun nœud ne correspond
preferredDuringSchedulingIgnoredDuringExecutionPréférence, Kubernetes essaie de respecter la règle, mais peut la contourner

L'opérateur In accepte plusieurs valeurs, ce que nodeSelector ne sait pas faire. Si aucun nœud ne correspond, le Pod reste en Pending indéfiniment, sans message d'erreur autre que l'event de FailedScheduling.

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

Le Pod ne sera programmé que sur un nœud avec accelerator=nvidia-gpu ou accelerator=amd-gpu.

Le mode préféré prend une liste pondérée : le champ weight (de 1 à 100) sert à départager les nœuds candidats quand plusieurs règles s'appliquent. Le Pod est toujours programmé, même si aucune préférence n'est satisfaite.

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

Kubernetes préférera un nœud GPU, mais programmera le Pod ailleurs si aucun n'est disponible.

Ces six opérateurs s'utilisent dans les matchExpressions, en mode obligatoire comme en mode préféré. Les deux derniers, Gt et Lt, comparent la valeur du label comme un entier : ils échouent silencieusement si le label contient autre chose qu'un nombre.

OpérateurDescription
InLa valeur est dans la liste
NotInLa valeur n'est pas dans la liste
ExistsLa clé existe (peu importe la valeur)
DoesNotExistLa clé n'existe pas
GtValeur supérieure à (numérique)
LtValeur inférieure à (numérique)

4. podAffinity et podAntiAffinity : relations entre Pods

Section intitulée « 4. podAffinity et podAntiAffinity : relations entre Pods »

Ces mécanismes influencent la répartition des Pods les uns par rapport aux autres, pas par rapport aux nœuds. La règle ne cite donc aucun nom de nœud : elle décrit les Pods à chercher via un labelSelector, puis indique avec topologyKey à quelle échelle appliquer le rapprochement ou l'éloignement. Le scheduler évalue cette règle à chaque placement, ce qui a un coût dont la fin de section parle.

Le rapprochement sert quand deux composants échangent beaucoup et que la latence réseau pèse, typiquement une application et son cache Redis. Les placer sur le même nœud supprime un saut réseau, au prix d'une perte de résilience si ce nœud tombe.

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

Ce Pod sera programmé sur le même nœud qu'un Pod ayant app=redis.

L'anti-affinité évite le SPOF (Single Point Of Failure, point de défaillance unique) : sans elle, rien n'empêche le scheduler de poser les trois replicas d'un service sur le même nœud, et la panne de ce nœud emporte tout le service. Notez que la règle porte sur le label du Deployment lui-même, app=web, un Pod se repousse donc de ses propres frères.

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

Kubernetes empêchera plusieurs Pods app=web de tourner sur le même nœud.

Le topologyKey est le label de nœud qui définit le périmètre de la règle. Deux nœuds portant la même valeur pour ce label appartiennent au même domaine, et la contrainte s'applique à l'intérieur de ce domaine. Choisir zone plutôt que hostname change complètement le résultat : la règle devient bien plus stricte, puisqu'elle exclut alors toute une zone d'un coup.

topologyKeyEffet
kubernetes.io/hostnameRépartition par nœud
topology.kubernetes.io/zoneRépartition par zone
topology.kubernetes.io/regionRépartition par région

5. topologySpreadConstraints : répartir proprement les replicas

Section intitulée « 5. topologySpreadConstraints : répartir proprement les replicas »

topologySpreadConstraints est le mécanisme moderne pour contrôler la répartition des Pods entre domaines de topologie (nœuds, zones, régions). Il est souvent plus adapté que podAntiAffinity pour la haute disponibilité.

La comparaison ci-dessous porte sur quatre besoins réels. La différence tient au type de règle : podAntiAffinity exprime une interdiction binaire, alors que topologySpreadConstraints exprime un écart toléré entre domaines, ce qui autorise un déséquilibre maîtrisé au lieu de bloquer le placement.

BesoinpodAntiAffinitytopologySpreadConstraints
"Jamais 2 Pods sur le même nœud"✅ Strict⚠️ Possible mais pas l'usage principal
"Répartir équitablement entre zones"⚠️ Complexe✅ Conçu pour
"Tolérer un léger déséquilibre"maxSkew
"Répartir sur zones ET nœuds"⚠️ Complexe✅ Multiple constraints

Ce Deployment répartit ses 6 replicas entre les nœuds avec un écart maximal de 1. Le labelSelector doit désigner les Pods du Deployment lui-même, sinon la contrainte compte les mauvais Pods et le calcul d'écart n'a plus de sens.

apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 6
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
containers:
- name: web-container
image: my-web-image

Quatre champs suffisent à décrire une contrainte de répartition. Celui qui décide du comportement en cas d'impasse est whenUnsatisfiable : avec DoNotSchedule, un Pod qui déséquilibrerait trop reste en Pending ; avec ScheduleAnyway, il est placé quand même sur le nœud le moins chargé.

ParamètreDescription
maxSkewÉcart maximum toléré entre domaines (1 = équilibré)
topologyKeyLabel définissant les domaines (kubernetes.io/hostname, topology.kubernetes.io/zone)
whenUnsatisfiableDoNotSchedule (strict) ou ScheduleAnyway (best-effort)
labelSelectorSélectionne les Pods concernés par le spread

On peut empiler plusieurs contraintes, qui sont alors toutes évaluées pour chaque placement. Le dosage recommandé est celui ci-dessous : strict sur les zones, où le déséquilibre coûte la haute disponibilité, et souple sur les nœuds, où l'exiger en plus rendrait souvent le placement impossible.

spec:
topologySpreadConstraints:
# D'abord répartir entre zones
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
# Puis répartir entre nœuds dans chaque zone
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: web

6. Taints et Tolerations : protéger et réserver des nœuds

Section intitulée « 6. Taints et Tolerations : protéger et réserver des nœuds »

Les taints et tolerations fonctionnent à l'inverse de l'affinité :

  • Un taint sur un nœud repousse les Pods par défaut
  • Une toleration sur un Pod lui permet d'ignorer ce taint

La clé et la valeur sont libres, l'effet doit valoir exactement l'une des trois valeurs listées juste après.

Fenêtre de terminal
kubectl taint nodes <node-name> <key>=<value>:<effect>

L'effet détermine à quel moment le taint agit. Les deux premiers ne concernent que le placement futur, le troisième s'applique aussi aux Pods déjà en cours d'exécution et provoque leur expulsion, ce qui en fait le seul des trois capable d'interrompre un service en production.

EffetComportement
NoScheduleBloque les nouveaux Pods sans toleration
PreferNoScheduleÉvite les nouveaux Pods sans toleration (best-effort)
NoExecuteBloque + expulse les Pods existants sans toleration

Un nœud GPU coûte plusieurs fois le prix d'un nœud standard, et rien n'empêche par défaut un Pod quelconque de venir l'occuper. Le taint inverse cette situation : le nœud devient inaccessible à tout ce qui ne le tolère pas explicitement.

Fenêtre de terminal
# Tainter le nœud GPU
kubectl taint nodes node-gpu accelerator=nvidia-gpu:NoSchedule

Tous les Pods standards seront bloqués sur ce nœud.

La toleration se déclare sous spec.tolerations et doit correspondre champ par champ au taint posé sur le nœud, effet compris. L'exemple ci-dessous montre la combinaison complète, avec le nodeSelector qui, lui, dirige effectivement le Pod vers le nœud GPU.

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"
# Pour CIBLER le nœud, ajoutez aussi un selector
nodeSelector:
accelerator: nvidia-gpu

Il n'existe que deux opérateurs, et Equal est celui appliqué par défaut si vous omettez le champ. Utilisez Exists avec précaution : combiné à un effect laissé vide, il tolère tous les taints du nœud, y compris ceux que Kubernetes pose en cas de panne.

OpérateurDescription
EqualLa clé et la valeur doivent correspondre
ExistsLa clé doit exister (valeur ignorée)

tolerationSeconds : tolérer temporairement NoExecute

Section intitulée « tolerationSeconds : tolérer temporairement NoExecute »

Avec NoExecute, vous pouvez permettre à un Pod de rester temporairement sur un nœud devenu tainté :

tolerations:
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 300

Le Pod restera 5 minutes sur un nœud devenu not-ready avant d'être évincé. Utile pour tolérer des perturbations réseau temporaires.

Kubernetes pose lui-même des taints sur les nœuds en difficulté, sans intervention de votre part. Savoir les reconnaître change le diagnostic : un Pod qui refuse de démarrer sur un nœud apparemment sain est souvent bloqué par l'un de ces taints automatiques, visible avec kubectl describe node.

TaintDescription
node.kubernetes.io/not-readyNœud pas prêt
node.kubernetes.io/unreachableNœud injoignable
node.kubernetes.io/disk-pressurePression disque
node.kubernetes.io/memory-pressurePression mémoire
node.kubernetes.io/unschedulableNœud en cordon

Le tiret final est ce qui distingue la suppression de l'ajout, il se place après l'effet. Un nœud pouvant porter plusieurs taints, vérifiez le résultat avec kubectl describe node plutôt que de supposer que le placement est débloqué.

Fenêtre de terminal
kubectl taint nodes <node-name> <key>:<effect>-
# Exemple :
kubectl taint nodes node-gpu accelerator:NoSchedule-

En production, un seul mécanisme suffit rarement. Les trois scénarios ci-dessous montrent les associations qui reviennent le plus souvent, et surtout pourquoi elles se combinent : un taint protège le nœud mais n'y attire personne, il faut donc lui adjoindre une affinité ou un selector pour que le Pod y aille réellement.

Objectif : réserver des nœuds GPU aux workloads ML/AI.

  1. Labelliser et tainter les nœuds GPU

    Fenêtre de terminal
    kubectl label nodes node-gpu-1 node-gpu-2 accelerator=nvidia-gpu
    kubectl taint nodes node-gpu-1 node-gpu-2 accelerator=nvidia-gpu:NoSchedule
  2. Créer le Pod ML avec toleration + nodeAffinity

    apiVersion: v1
    kind: Pod
    metadata:
    name: ml-training
    spec:
    containers:
    - name: training
    image: tensorflow/tensorflow:2.21.0-gpu@sha256:61fe1ce25bd26b0a38e310463a5588d4067d2d01b6bdb058a3ca4f5cf2e18f15
    tolerations:
    - key: "accelerator"
    operator: "Equal"
    value: "nvidia-gpu"
    effect: "NoSchedule"
    affinity:
    nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    nodeSelectorTerms:
    - matchExpressions:
    - key: accelerator
    operator: In
    values:
    - nvidia-gpu

Objectif : répartir 6 replicas équitablement entre 3 zones.

apiVersion: apps/v1
kind: Deployment
metadata:
name: web-ha
spec:
replicas: 6
selector:
matchLabels:
app: web-ha
template:
metadata:
labels:
app: web-ha
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web-ha
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: web-ha
containers:
- name: web
image: nginx:1.31.3-alpine3.24@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752

Résultat : 2 replicas par zone, répartis sur différents nœuds dans chaque zone.

Objectif : réserver des nœuds pour les DaemonSets et composants d'infrastructure.

Fenêtre de terminal
# Tainter les nœuds infra
kubectl taint nodes infra-1 infra-2 dedicated=infrastructure:NoSchedule
kubectl label nodes infra-1 infra-2 node-role=infrastructure
# DaemonSet monitoring avec toleration
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: prometheus-node-exporter
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "infrastructure"
effect: "NoSchedule"
# Ce DaemonSet tourne sur TOUS les nœuds, y compris infra
containers:
- name: exporter
image: prom/node-exporter:v1.12.1@sha256:1b4e4438faca4dd7e001dd445d161a4a2091b0fededa84093b3a8dfeae1f1be0

Partez toujours de l'objectif, jamais du mécanisme. Le tableau ci-dessous fait cette traduction pour les six besoins que l'on rencontre en pratique. Une seule ligne demande de combiner deux mécanismes, celle des nœuds réservés : le taint verrouille l'accès, l'affinité dirige le Pod, et l'oubli du second est l'erreur la plus fréquente sur ce sujet.

ObjectifSolution recommandée
Cibler un type de nœud (GPU, SSD)nodeSelector ou nodeAffinity
Réserver un nœud à certains workloadstaints + tolerations (+ affinity pour cibler)
Rapprocher des Pods liéspodAffinity
Éloigner quelques Pods sensiblespodAntiAffinity
Répartir des replicas entre nœuds/zonestopologySpreadConstraints
Tolérer temporairement un nœud instabletolerationSeconds

Un Pod reste Pending quand le scheduler ne trouve pas de nœud satisfaisant toutes les contraintes. L'information utile n'est pas dans le statut du Pod mais dans ses events, où le scheduler écrit le nombre de nœuds écartés et la raison de chaque exclusion. C'est toujours là qu'il faut regarder en premier, avant même de relire le manifest.

Ces quatre commandes se lancent dans cet ordre : d'abord la raison donnée par le scheduler, puis l'état réel des nœuds pour la confronter.

Fenêtre de terminal
# Voir pourquoi un Pod est Pending
kubectl describe pod <pod-name>
# Voir les events récents (très utile)
kubectl get events --sort-by=.lastTimestamp
# Voir les labels de tous les nœuds
kubectl get nodes --show-labels
# Voir les taints d'un nœud
kubectl describe node <node-name> | grep -A5 Taints

La première colonne reprend les messages exacts affichés dans les events : cherchez le vôtre plutôt que de raisonner par déduction. Le message générique 0/X nodes available est toujours suivi d'un détail par cause, c'est ce détail qui donne la réponse.

Symptôme dans eventsCause probableSolution
FailedScheduling: 0/X nodes availableAucun nœud ne satisfait les contraintesVérifier labels/taints/affinity
node(s) didn't match Pod's node affinity/selectorLabel manquant sur les nœudskubectl label nodes
node(s) had taint that pod didn't tolerateToleration manquanteAjouter toleration au Pod
node(s) didn't match pod topology spread constraintsContraintes de spread trop strictesRéduire maxSkew ou ajouter des nœuds
Insufficient cpu/memoryRessources insuffisantesAjuster requests ou ajouter des nœuds

Voici l'enchaînement complet sur un cas réel, du message d'erreur jusqu'à la vérification des taints nœud par nœud. La boucle finale est celle qui manque le plus souvent : kubectl describe node ne s'utilise pas sur un seul nœud quand on cherche pourquoi aucun ne convient.

Fenêtre de terminal
# 1. Identifier le problème
kubectl describe pod my-pending-pod | grep -A10 Events
# 2. Vérifier les nœuds disponibles
kubectl get nodes -o wide
# 3. Vérifier les labels
kubectl get nodes --show-labels | grep -E "accelerator|zone"
# 4. Vérifier les taints
for node in $(kubectl get nodes -o name); do
echo "=== $node ==="
kubectl describe $node | grep -A3 Taints
done
MécanismeRôleCôté
nodeSelectorFiltre simple par labelPod → Nœud
nodeAffinityFiltre avancé (préférence/contrainte)Pod → Nœud
podAffinityRapproche des PodsPod → Pod
podAntiAffinityÉloigne des PodsPod → Pod
topologySpreadConstraintsRépartit les replicasPod → Topologie
taintsRepousse les Pods non autorisésNœud
tolerationsAutorise un Pod sur nœud taintéPod

Points essentiels :

  1. topologySpreadConstraints est le mécanisme moderne pour la répartition, ne l'oubliez pas
  2. Une toleration n'attire pas un Pod, elle l'autorise seulement
  3. Pour cibler un nœud tainté : toleration + nodeSelector/nodeAffinity
  4. L'inter-pod affinity peut ralentir le scheduling dans les grands clusters
  5. Utilisez labels standardisés (kubernetes.io/hostname, topology.kubernetes.io/zone)

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn