Maîtriser l'Affinité et l'Anti-Affinité Pod/Node

thumbernail Kubernetes, tutos

Kubernetes : Maîtriser l'Affinité et l'Anti-Affinité Pod/Node

Kubernetes est un formidable orchestrateur, mais par défaut, son scheduler place les pods là où il y a des ressources disponibles, sans forcément prendre en compte des contraintes plus fines.

C'est là qu'interviennent les mécanismes d'Affinité et d'Anti-Affinité, des outils puissants pour influencer la décision du scheduler et placer vos pods de manière plus intelligente.

Dans cet article, nous allons démystifier ces concepts, en expliquant simplement comment ils fonctionnent au niveau des Nœuds (Nodes) et des Pods, avec des exemples concrets et des cas d'usage.

Pourquoi aller au-delà du Scheduling par Défaut ?

Le scheduler de Kubernetes fait un bon travail pour répartir la charge, mais parfois, vous avez besoin de plus :

  • Performance : Placer un pod applicatif près de sa base de données ou de son cache pour réduire la latence réseau.

  • Haute Disponibilité (HA) : S'assurer que les réplikas d'une application critique ne tournent pas tous sur la même machine physique (ou dans la même zone de disponibilité) pour éviter un point unique de défaillance (SPOF).

  • Contraintes Matérielles/Logicielles : Forcer un pod à tourner sur un nœud disposant d'un matériel spécifique (GPU, SSD rapide) ou d'une licence particulière.

  • Isolation : Éviter que des pods très consommateurs en ressources ne se retrouvent sur le même nœud et ne s'impactent mutuellement.

  • Réglementation/Localisation : Contraindre des pods à s'exécuter dans une région géographique spécifique.

C'est pour répondre à ces besoins que l'Affinité et l'Anti-Affinité existent.

Comprendre les Concepts Clés

  • Affinité (Affinity) : Règle qui attire des pods vers certaines cibles (nœuds ou autres pods). "Je veux tourner ici, ou près de ce pod."

  • Anti-Affinité (Anti-Affinity) : Règle qui repousse des pods de certaines cibles. "Je ne veux pas tourner ici, ou près de ce pod."

  • Niveau Node : Les règles concernent les caractéristiques des nœuds (labels).

  • Niveau Pod : Les règles concernent la présence d'autres pods sur les nœuds.

  • Labels & Selectors : Le cœur du système. Les nœuds et les pods sont identifiés par des labels (paires clé/valeur), et les règles d'affinité/anti-affinité utilisent des selectors pour cibler ces labels.

  • topologyKey : Utilisé pour l'affinité/anti-affinité de pods. Définit le "domaine" de la règle : est-ce que les pods doivent être sur le même nœud (kubernetes.io/hostname), dans la même zone (topology.kubernetes.io/zone), ou dans la même région (topology.kubernetes.io/region) ?

  • Types de Règles :

    • requiredDuringSchedulingIgnoredDuringExecution : Règle stricte. Le pod ne sera pas schedulé si la règle ne peut être satisfaite. Si les conditions changent après le scheduling (ex: label de nœud modifié), le pod continue de tourner (IgnoredDuringExecution). C'est la plus courante.

    • preferredDuringSchedulingIgnoredDuringExecution : Règle préférentielle (soft). Le scheduler essaiera de satisfaire la règle, mais placera le pod ailleurs si ce n'est pas possible. Un poids (weight, entre 1 et 100) permet de prioriser plusieurs règles préférentielles.

(Note : Il existe aussi requiredDuringSchedulingRequiredDuringExecution, mais il est moins courant et peut causer des évictions de pods si les conditions changent, ce qui peut être perturbateur).


1. Affinité de Nœud (Node Affinity)

Objectif : Attirer un pod vers des nœuds ayant des caractéristiques spécifiques (labels).

Syntaxe : Se configure dans le spec.affinity.nodeAffinity du Pod.

apiVersion: v1
kind: Pod
metadata:
  name: mon-pod-rapide
spec:
  affinity:
    nodeAffinity:
      # Règle STRICTE
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype # Le label du nœud
            operator: In   # L'opérateur (In, NotIn, Exists, DoesNotExist, Gt, Lt)
            values:
            - ssd       # La valeur recherchée
      # Règle PRÉFÉRENTIELLE
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 50       # Poids de la préférence (1-100)
        preference:
          matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - eu-west-1a # Préfère cette zone
      - weight: 30
        preference:
          matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - eu-west-1b # Sinon, préfère celle-ci
  containers:
  - name: mon-conteneur
    image: nginx

Explication des Exemples :

  1. required... : Ce pod doit être placé sur un nœud ayant le label disktype=ssd. Si aucun nœud ne correspond, le pod restera en attente (Pending).
  2. preferred... : Le scheduler préférera (avec un poids de 50) placer le pod sur un nœud dans la zone eu-west-1a. S'il ne peut pas, il essaiera (avec un poids de 30) la zone eu-west-1b. S'il ne peut toujours pas, il le placera sur n'importe quel autre nœud disponible répondant à la règle required.

Schéma Conceptuel :

 Pod (veut disktype=ssd)   ─────►   Node 1 (disktype=ssd)  [OK]
                              │
                              └─X──►   Node 2 (disktype=hdd)  [Ignoré par 'required']

 Pod (préfère zone=a)      ───(Poids 50)───► Node 3 (zone=a) [Choix préféré]
                              │
                              ───(Poids 30)───► Node 4 (zone=b) [Second choix]
                              │
                              ───(Poids 0)────► Node 5 (zone=c) [Dernier recours]

Cas d'Usage :

  • Placer des pods nécessitant des I/O rapides sur des nœuds avec SSD (disktype=ssd).

  • Placer des pods nécessitant des GPU sur des nœuds équipés (hardware=gpu-tesla-v100).

  • Contraindre des pods à une zone ou région spécifique pour des raisons de latence ou de conformité (topology.kubernetes.io/zone=us-east-1).

  • Utiliser des nœuds avec des licences logicielles spécifiques.


2. Anti-Affinité de Nœud (Node Anti-Affinity)

Objectif : Empêcher un pod d'être schedulé sur des nœuds ayant certaines caractéristiques.

Note importante : Il n'existe pas de section nodeAntiAffinity dédiée. L'anti-affinité de nœud est réalisée en utilisant nodeAffinity avec les opérateurs NotIn ou DoesNotExist.

Syntaxe : Toujours dans spec.affinity.nodeAffinity.

apiVersion: v1
kind: Pod
metadata:
  name: pod-sans-gpu
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: hardware # Le label du nœud
            operator: DoesNotExist # Ne doit PAS avoir ce label
        - matchExpressions:
          - key: gpu-type # Ou, si le label existe...
            operator: NotIn # Sa valeur ne doit PAS être celles-ci
            values:
            - nvidia-a100
            - nvidia-v100
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
          - key: maintenance # Label indiquant une maintenance prévue
            operator: DoesNotExist # Préfère éviter les nœuds en maintenance
  containers:
  - name: mon-conteneur
    image: busybox
    command: ["sleep", "3600"]

Explication des Exemples :

  1. required... : Ce pod ne doit pas être placé sur un nœud ayant le label hardware (quelque soit sa valeur) OU sur un nœud dont le label gpu-type a la valeur nvidia-a100 ou nvidia-v100. Utile si le pod n'a pas besoin de GPU et que vous voulez réserver ces nœuds coûteux.
  2. preferred... : Le scheduler évitera (avec un poids élevé de 80) de placer le pod sur des nœuds ayant le label maintenance. S'il ne trouve que des nœuds avec ce label (et qui satisfont la règle required), il le placera quand même là.

Schéma Conceptuel :

 Pod (ne veut pas gpu=nvidia) ───X──► Node 1 (gpu-type=nvidia-a100) [Rejeté par 'required']
                              │
                              └───►   Node 2 (pas de label gpu)   [OK]


 Pod (préfère éviter maintenance=true) ───(Poids 80 Éviter)───► Node 3 (maintenance=true) [Choix évité si possible]
                                     │
                                     ───(Poids 0)──────────► Node 4 (pas de label maintenance) [Choix préféré]

Cas d'Usage :

  • Éviter de placer des pods "normaux" sur des nœuds coûteux (GPU, grosse mémoire) réservés à des tâches spécifiques.

  • Éviter des nœuds marqués pour maintenance (maintenance=true).

  • Éviter des nœuds avec une architecture CPU spécifique si l'application n'est pas compatible (kubernetes.io/arch != arm64).


3. Affinité de Pod (Pod Affinity)

Objectif : Attirer un pod vers des nœuds où d'autres pods (correspondant à certains labels) sont déjà en cours d'exécution. Idéal pour la co-localisation.

Syntaxe : Se configure dans le spec.affinity.podAffinity du Pod. Nécessite un topologyKey.

apiVersion: v1
kind: Pod
metadata:
  name: web-server
  labels:
    app: frontend # Label pour être ciblé par d'autres
spec:
  affinity:
    podAffinity:
      # Règle STRICTE
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app         # Cherche des pods avec ce label...
            operator: In
            values:
            - cache         # ...et cette valeur (le pod cache)
        topologyKey: kubernetes.io/hostname # Doit être sur le MÊME NŒUD
      # Règle PRÉFÉRENTIELLE
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 70
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: app
              operator: In
              values:
              - api-gateway
          topologyKey: topology.kubernetes.io/zone # Préfère être dans la MÊME ZONE que l'API Gateway
  containers:
  - name: web-container
    image: nginx

Explication des Exemples :

  1. required... : Ce pod web-server doit être schedulé sur un nœud (topologyKey: kubernetes.io/hostname) où au moins un pod ayant le label app=cache est déjà en cours d'exécution. Si aucun nœud ne remplit cette condition, le pod restera Pending.
  2. preferred... : Le scheduler préférera (poids 70) placer ce pod dans la même zone de disponibilité (topologyKey: topology.kubernetes.io/zone) qu'un pod ayant le label app=api-gateway.

Schéma Conceptuel :

 Pod Web (veut app=cache, même nœud)   ─────► Node 1 (contient Pod Cache (app=cache)) [OK]
                                       │
                                       └─X──► Node 2 (pas de Pod Cache)         [Ignoré par 'required']


 Pod Web (préfère app=api, même zone) ───(Poids 70)───► Node 3 (Zone A, contient Pod API (app=api)) [Choix préféré]
                                      │
                                      ───(Poids 0)────► Node 4 (Zone B, pas de Pod API)      [Moins préféré]

Cas d'Usage :

  • Performance : Placer un serveur web (app=frontend) sur le même nœud (topologyKey: kubernetes.io/hostname) qu'un cache en mémoire (app=cache) pour une latence minimale.

  • Localité des données : Placer des pods de traitement analytique (app=analytics) dans la même zone (topologyKey: topology.kubernetes.io/zone) que les pods de base de données (app=database) qu'ils interrogent fréquemment.

  • Regrouper des microservices très communicants.


4. Anti-Affinité de Pod (Pod Anti-Affinity)

Objectif : Empêcher un pod d'être schedulé sur des nœuds où d'autres pods (correspondant à certains labels) sont déjà en cours d'exécution. Idéal pour la haute disponibilité et l'isolation.

Syntaxe : Se configure dans le spec.affinity.podAntiAffinity du Pod. Nécessite aussi un topologyKey.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ma-db-ha
spec:
  replicas: 3
  selector:
    matchLabels:
      app: database # Important pour que l'anti-affinité se cible elle-même
  template: # Le template du Pod
    metadata:
      labels:
        app: database # Le label que l'anti-affinité va rechercher
    spec:
      affinity:
        podAntiAffinity:
          # Règle STRICTE pour la Haute Disponibilité
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app           # Cherche des pods avec ce label...
                operator: In
                values:
                - database       # ...et cette valeur (les autres réplikas de moi-même)
            topologyKey: kubernetes.io/hostname # NE PAS être sur le MÊME NŒUD
          # Règle PRÉFÉRENTIELLE pour encore plus de résilience
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - database
              topologyKey: topology.kubernetes.io/zone # Préfère NE PAS être dans la MÊME ZONE
      containers:
      - name: db-container
        image: postgres:15

Explication des Exemples :

  1. required... : Chaque nouveau pod de ce Deployment (app=database) ne doit pas être placé sur un nœud (topologyKey: kubernetes.io/hostname) où un autre pod avec le label app=database est déjà présent. Cela garantit que les 3 réplikas seront sur 3 nœuds distincts (si possible).
  2. preferred... : De plus, le scheduler tentera fortement (poids 100) d'éviter de placer les réplikas dans la même zone de disponibilité (topologyKey: topology.kubernetes.io/zone). Si vous avez assez de nœuds répartis sur plusieurs zones, cela augmente encore la résilience. Si tous les nœuds disponibles sont dans la même zone, cette règle "soft" sera ignorée, mais la règle "hard" sur les nœuds sera toujours respectée.

Schéma Conceptuel :

 Pod DB Replica 1 ────► Node 1 (Zone A)

 Pod DB Replica 2 ───X──► Node 1 (déjà un Pod DB)
                  │
                  └───► Node 2 (Zone A) [OK pour 'required', mais pas idéal pour 'preferred']


 Pod DB Replica 3 ───X──► Node 1 (déjà un Pod DB)
                  │
                  ───X──► Node 2 (déjà un Pod DB)
                  │
                  └───► Node 3 (Zone B) [OK pour 'required', et OK pour 'preferred']

Cas d'Usage :

  • Haute Disponibilité : Assurer que les réplikas d'une application stateful (ex: base de données, etcd) sont répartis sur différents nœuds physiques (topologyKey: kubernetes.io/hostname) ou même différentes zones/régions (topologyKey: topology.kubernetes.io/zone) pour survivre à une panne matérielle ou de zone. C'est le cas d'usage le plus fréquent.

  • Isolation des Ressources : Éviter que plusieurs instances d'un pod très gourmand en CPU ou mémoire ne tournent sur le même nœud et ne se concurrencent (`topologyKey:

  • Sécurité : Répartir les composants d'une application pour limiter l'impact d'une compromission d'un nœud.


Bonnes Pratiques et Points d'Attention

  • Simplicité : N'abusez pas des règles complexes. Trop de contraintes peuvent rendre le scheduling difficile, voire impossible.

  • Labels Cohérents : La puissance de ces mécanismes repose sur une stratégie de labeling claire et cohérente pour vos nœuds et vos pods.

  • Choix du topologyKey : Réfléchissez bien au domaine d'application de vos règles de Pod (Anti-)Affinity (hostname, zone, region, ou même des labels custom sur vos nœuds).

  • IgnoredDuringExecution : Rappelez-vous que ces règles sont évaluées au moment du scheduling. Si les conditions changent après (un label de nœud est retiré, un pod co-localisé est supprimé), le pod ne sera pas automatiquement déplacé ou supprimé (sauf si vous utilisez les règles RequiredDuringExecution, moins courantes).

  • Performance du Scheduler : Des règles d'anti-affinité complexes sur un grand nombre de pods peuvent ralentir le scheduler. Utilisez-les judicieusement.

  • Combinaison : Vous pouvez combiner plusieurs types d'affinité (node, pod, anti-affinité) dans un même Pod spec pour des scénarios avancés.


Conclusion

L'Affinité et l'Anti-Affinité Node/Pod sont des outils essentiels dans la boîte à outils de tout administrateur ou développeur Kubernetes souhaitant optimiser le placement de ses charges de travail.

En maîtrisant les règles required et preferred, les labelSelectors et les topologyKeys, vous pouvez aller bien au-delà du scheduling par défaut pour améliorer la performance, la résilience et l'efficacité de vos applications conteneurisées.

Découvrez les technologies d'alter way