Aller au contenu
English
Conteneurs & Orchestration medium

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

70 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

Quatre familles se dégagent, et les nommer évite les trois quarts des confusions. nodeSelector et nodeAffinity servent à cibler des nœuds. taints et tolerations servent à protéger ou réserver des nœuds, ce qui est l'opération inverse. podAffinity et podAntiAffinity gèrent les relations entre Pods, sans rien dire des nœuds. Et topologySpreadConstraints répartit un groupe de replicas entre des domaines. La section 8 revient sur ce choix, une fois les six mécanismes détaillés.

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:1.0.0
nodeName: my-node-1

L'affirmation « aucune vérification n'a lieu » se vérifie en une manipulation. Un nœud est protégé par un taint NoSchedule, et un Pod sans la moindre toleration lui est adressé par nodeName :

Fenêtre de terminal
kubectl taint nodes worker-3 reserve=oui:NoSchedule
kubectl run direct --image=nginx:1.31.3-alpine3.24@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752 \
--overrides='{"spec":{"nodeName":"worker-3"}}'
Sortie
direct 1/1 Running worker-3

Le Pod tourne, sur un nœud qui aurait refusé n'importe quel autre. Le même Pod visant ce nœud par nodeSelector reste, lui, en Pending. C'est la démonstration la plus nette de ce que nodeName contourne.

Un nœud inexistant : le Pod ne reste pas Pending, il disparaît

Section intitulée « Un nœud inexistant : le Pod ne reste pas Pending, il disparaît »

C'est le comportement le plus déroutant de ce champ, et le seul qui laisse le lecteur sans rien à diagnostiquer. Un nodeName mal orthographié produit bien un Pod Pending, mais pas durablement :

MomentCe que montre kubectl get pods
CréationPending, avec spec.nodeName déjà renseigné
Environ 1 minute plus tardle Pod n'existe plus

Le ramasse-miettes du kube-controller-manager supprime de force les Pods liés à un nœud absent du cluster. La suppression n'écrit aucun événement sur l'objet : kubectl describe pod répond NotFound, et la méthode de diagnostic habituelle, lire les events, ne rend rien du tout.

Dans un Deployment, la conséquence est pire : le contrôleur recrée un Pod, le ramasse-miettes le supprime une minute plus tard, et le cycle ne s'arrête jamais. Un âge de Pod qui ne dépasse jamais la minute est la signature de ce défaut.

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.

Un 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:1.0.0
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

Le suffixe IgnoredDuringExecution est la partie la plus informative de ces noms à rallonge : la règle est évaluée une seule fois, au moment du placement. Si le nœud perd ensuite le label qui l'avait rendu éligible, le Pod reste où il est. Un seul mécanisme de cette page échappe à cette logique, le taint en effet NoExecute, qui expulse des Pods déjà en place.

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 autre message que l'événement FailedScheduling.

apiVersion: v1
kind: Pod
metadata:
name: pod-gpu-required
spec:
containers:
- name: my-container
image: my-image:1.0.0
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, départage les nœuds candidats quand plusieurs règles s'appliquent. Le Pod est toujours planifié, 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:1.0.0
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, et c'est là que se cache un piège de diagnostic.

Un nœud dont le label vaut coeurs=quatre est simplement écarté, comme s'il ne portait pas le label du tout. Aucun message ne signale que la valeur n'était pas un nombre : l'événement reste le générique node(s) didn't match Pod's node affinity/selector. Mieux vaut le savoir, car rien dans la sortie ne vous mettra sur la piste.

La valeur écrite dans la règle n'est pas validée non plus. Un values: ["beaucoup"] derrière un Gt est accepté par l'API, le Pod est créé, et il reste en Pending avec ce même message générique. Vérifiez la valeur vous-même, l'API ne le fera pas pour vous.

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:1.0.0
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:1.0.0

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œudStrict, c'est son usagePossible, mais ce n'est pas son usage
Répartir équitablement entre zonesComplexe à exprimerConçu pour
Tolérer un léger déséquilibreImpossiblemaxSkew
Répartir sur zones ET nœudsComplexe à exprimerPlusieurs contraintes empilées

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:1.0.0

Pourquoi un maxSkew de 1 refuse parfois un Pod que vous croyiez plaçable

Section intitulée « Pourquoi un maxSkew de 1 refuse parfois un Pod que vous croyiez plaçable »

Voici la mesure qui surprend le plus, faite sur un cluster de 4 nœuds, un control-plane tainté et 3 workers. Un Deployment de 4 replicas avec maxSkew: 1 sur kubernetes.io/hostname et DoNotSchedule :

Sortie
worker-1 Running
worker-2 Running
worker-3 Running
Pending
Événement du Pod restant
0/4 nodes are available: 1 node(s) had untolerated taint(s),
3 node(s) didn't match pod topology spread constraints.

Trois workers, quatre replicas, une répartition 2/1/1 : l'écart vaut 1, la contrainte devrait passer. Elle ne passe pas, et la raison tient à un réglage implicite. Le champ nodeTaintsPolicy vaut Ignore par défaut : le nœud de control-plane, bien qu'inaccessible à ce Pod, compte quand même comme un domaine. La répartition réelle est donc 2/1/1/0, et l'écart vaut 2.

Deux façons de débloquer, mesurées sur le même cluster :

Champ ajouté à la contrainteRésultat
nodeTaintsPolicy: Honor4 replicas placés, les domaines taintés sont exclus du calcul
whenUnsatisfiable: ScheduleAnyway4 replicas placés, la contrainte devient une préférence

Retenez surtout le principe : un domaine vide compte dans le calcul de l'écart. C'est vrai du control-plane comme de toute zone où aucun Pod ne peut aller, et c'est la cause la plus fréquente d'un spread qui refuse sans raison apparente.

Quatre champs suffisent à décrire une contrainte de répartition. Celui qui décide 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
nodeTaintsPolicyIgnore par défaut : les nœuds taintés comptent comme domaines
nodeAffinityPolicyHonor par défaut : les nœuds exclus par l'affinité ne comptent pas

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

Pour le cas précis de la répartition de replicas, topologySpreadConstraints est généralement le meilleur outil : il exprime un déséquilibre toléré avec maxSkew, là où une anti-affinité stricte ne connaît que l'interdiction pure, et il se combine sur plusieurs niveaux de topologie, zone puis nœud.

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:1.0.0
tolerations:
- key: "accelerator"
operator: "Equal"
value: "nvidia-gpu"
effect: "NoSchedule"
# Pour CIBLER le nœud, ajoutez aussi un selector
nodeSelector:
accelerator: nvidia-gpu

Les deux champs se répartissent le travail : la toleration ouvre la porte, le nodeSelector indique le chemin. Retirez le selector et le Pod reste autorisé sur le nœud réservé, mais rien ne l'y envoie : il ira où le scheduler trouve de la place.

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.

Combien de temps dure réellement une expulsion NoExecute

Section intitulée « Combien de temps dure réellement une expulsion NoExecute »

Trois Pods posés sur le même nœud, avec trois tolerations différentes, puis un taint NoExecute appliqué à l'instant zéro :

Toleration du PodExpulsé à
Aucunet + 1 s
tolerationSeconds: 20 sur ce taintt + 21 s
operator: Exists sans key ni effectjamais, toujours en place après 46 s

L'expulsion sans toleration est donc immédiate, pas différée : ne comptez sur aucun délai de grâce pour réagir. Et la dernière ligne confirme l'avertissement de la section précédente : une toleration réduite à operator: Exists avale tous les taints, y compris ceux que Kubernetes pose quand un nœud tombe. Un Pod écrit ainsi restera sur un nœud en panne.

Kubernetes pose lui-même des taints sur les nœuds en difficulté, sans que vous interveniez. 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

Dans la plupart des clusters installés par kubeadm, les nœuds de control plane portent un taint qui écarte les charges de travail ordinaires. C'est une pratique courante, pas une règle universelle, et son nom se relève ainsi :

Fenêtre de terminal
kubectl get node <control-plane> -o jsonpath='{.spec.taints[*].key}{":"}{.spec.taints[*].effect}'
Sortie
node-role.kubernetes.io/control-plane:NoSchedule

Sa valeur est vide, ce qui a une conséquence pratique : une toleration écrite avec operator: Equal et value: "" fonctionne, mais operator: Exists sur la seule clé est la forme habituelle et la plus lisible.

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 rencontrés 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 oublier le 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

La question à se poser est celle du besoin, pas du mécanisme. Vous voulez cibler un nœud : nodeSelector si une égalité suffit, nodeAffinity dès qu'il faut un opérateur ou une préférence. Vous voulez protéger ou réserver un nœud : taints et tolerations, en n'oubliant pas d'ajouter de quoi diriger le Pod. Vous voulez répartir des replicas : topologySpreadConstraints. Vous voulez exprimer une relation entre Pods : podAffinity ou podAntiAffinity. Ces besoins se combinent souvent, et c'est l'objet de la section précédente.

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 tels que Kubernetes 1.37 les écrit : cherchez le vôtre plutôt que de raisonner par déduction. Les formulations ont changé au fil des versions, et une recherche sur un ancien libellé ne rend rien.

Message dans les eventsCause probableSolution
0/4 nodes are available:En-tête, toujours suivi du détail par causeLire la suite de la ligne
node(s) didn't match Pod's node affinity/selectorLabel manquant sur les nœudskubectl label nodes
node(s) had untolerated taint(s)Toleration manquanteAjouter la toleration au Pod
node(s) didn't match pod anti-affinity rulesPlus de replicas que de domaines libresRéduire les replicas ou ajouter des nœuds
node(s) didn't match pod topology spread constraintsÉcart dépassé, souvent à cause d'un domaine videnodeTaintsPolicy: Honor, ou ScheduleAnyway
Insufficient cpu, Insufficient memoryRessources insuffisantesAjuster les requests ou ajouter des nœuds

Le message complet comporte deux parties séparées par un point, et la seconde est presque toujours ignorée :

Événement complet
0/4 nodes are available: 1 node(s) had untolerated taint(s),
3 node(s) didn't match pod anti-affinity rules.
preemption: 0/4 nodes are available: 1 Preemption is not helpful for
scheduling, 3 No preemption victims found for incoming pod.

La partie preemption: dit si le scheduler aurait pu libérer de la place en expulsant un Pod moins prioritaire. No preemption victims found signifie qu'il a cherché et n'a trouvé personne à sacrifier : inutile d'espérer que la situation se débloque seule.

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)
  6. Un domaine vide compte dans le calcul de maxSkew, control-plane compris
  7. Un nodeName erroné ne laisse pas un Pending : le Pod est supprimé au bout d'une minute

Sept questions sur ce qui distingue les six mécanismes de placement, et surtout sur le seul d'entre eux qui peut déplacer un Pod déjà en cours d'exécution.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

7 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

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