Intermédiaire Chapitre 15-16 / 8

Work queues et resync périodique & Le scheduler, réconciliateur de placement

Comprendre comment la work queue découple les événements de la réconciliation et comment le scheduler place les pods sur les nœuds — deux mécanismes fondamentaux de la boucle de contrôle Kubernetes.

Work queues et resync périodique

L’idée en une phrase

La work queue est la structure qui découple la réception des événements (via les informers, comme vu au chapitre 13-14) de leur traitement par la fonction Reconcile : elle accumule des clés, les déduplique, et les distribue aux workers — garantissant que la boucle de réconciliation traite l’état courant plutôt qu’un flux d’événements bruts.

Analogie : Considérons le bureau d’un médecin de garde. Les appels arrivent à la réception (les événements du Watch), mais la réceptionniste ne transmet pas chaque sonnerie au médecin. Elle tient une liste de patients à voir (la work queue). Si le même patient appelle trois fois en dix minutes, son nom n’apparaît qu’une fois sur la liste. Le médecin traite la liste dans l’ordre, et pour chaque patient, il évalue l’état actuel — pas l’historique des appels. Si la liste est vide, la réceptionniste la remplit périodiquement en vérifiant l’état de tous les patients hospitalisés (le resync).

Points clés

  • La work queue stocke des clés (namespace/name), pas des objets complets. Lorsqu’un event handler (chapitre 13-14) reçoit une notification, il extrait la clé de l’objet concerné et l’enfile. Si la même clé est déjà dans la queue, elle n’est pas dupliquée — c’est la déduplication, qui transforme naturellement un flux edge en traitement level-triggered.
  • Le rate limiter intégré contrôle la fréquence de réenfilement en cas d’échec. Lorsqu’une réconciliation échoue et que la clé est réenfilée, le délai augmente exponentiellement (par défaut : 5 ms, 10 ms, 20 ms… plafonné à 1 000 s). Ce backoff exponentiel évite de marteler l’API server sur une erreur persistante.
  • Plusieurs workers (goroutines ou threads) consomment la queue en parallèle. Chaque worker défile une clé, exécute Reconcile, puis marque la clé comme traitée (Done). Si Reconcile échoue, le worker réenfile la clé via le rate limiter (AddRateLimited). Si elle réussit, la clé est retirée définitivement.
  • Le resync périodique (configurable, souvent 30 secondes à 10 minutes) réenfile toutes les clés du cache dans la work queue à intervalles réguliers. C’est le filet de sécurité de la boucle level-triggered : même sans événement Watch, chaque objet est périodiquement réconcilié. Si un écart a échappé aux notifications, le resync le rattrape.

Exemple concret

Un Deployment api déclare 3 replicas. À t₀, l’informer reçoit trois événements MODIFIED en rafale (les trois pods passent de Pending à Running). L’event handler extrait la clé default/api et l’enfile trois fois. La work queue déduplique : une seule entrée default/api est présente. Le worker défile la clé, la fonction Reconcile lit le cache (3 pods Running, 3 désirés), constate que l’écart est nul et ne fait rien. Cinq minutes plus tard, le resync périodique réenfile default/api — Reconcile s’exécute à nouveau, vérifie que l’état réel est toujours conforme, et termine sans action. Si entre-temps un pod avait été évincé par le kubelet sans que l’événement Watch ne parvienne au contrôleur, ce resync détecterait l’écart et créerait le pod manquant.

Cycle de vie d’une clé dans la work queue

ÉtapeOpérationEffet
Réception d’un événementqueue.Add(key)La clé est enfilée (dédupliquée si déjà présente)
Traitement par un workerkey = queue.Get()La clé est défilée et marquée « en cours »
Réconciliation réussiequeue.Done(key)La clé est retirée — aucune action supplémentaire
Réconciliation échouéequeue.AddRateLimited(key)La clé est réenfilée après un délai exponentiel
Resync périodiquequeue.Add(key) pour chaque clé du cacheToutes les clés sont réenfilées pour vérification

Code YAML — configurer le resync d’un controller-manager

# kube-controller-manager — extrait des flags pertinents
# Le resync n'est pas un champ YAML d'un manifeste applicatif,
# mais un paramètre du controller-manager lui-même.
apiVersion: v1
kind: Pod
metadata:
  name: kube-controller-manager
  namespace: kube-system
spec:
  containers:
    - name: kube-controller-manager
      command:
        - kube-controller-manager
        - --min-resync-period=12h0m0s    # période minimale de resync (défaut : 12h)
        - --concurrent-deployment-syncs=5 # nombre de workers parallèles par contrôleur
        - --concurrent-replicaset-syncs=5
# Chaque contrôleur choisit aléatoirement une période entre
# min-resync-period et 2× cette valeur, pour étaler la charge.

Code kubectl — observer le resync et la work queue en action

# Vérifier le nombre de workers d'un contrôleur (via les flags)
kubectl -n kube-system get pod kube-controller-manager-node01 \
  -o jsonpath='{.spec.containers[0].command}' | tr ',' '\n' | grep concurrent
# --concurrent-deployment-syncs=5

# Observer les événements de réconciliation — chaque resync
# produit un passage de Reconcile même sans changement d'état.
kubectl get events --field-selector reason=SuccessfulCreate -w
# Pas d'événement → le resync a vérifié et trouvé l'état conforme.

# Simuler un échec qui déclenche le rate limiter :
# supprimer un pod avec un finalizer bloquant
kubectl delete pod api-abc12 --grace-period=0
# Le pod reste en Terminating, le contrôleur réenfile la clé
# avec un backoff croissant jusqu'à résolution du finalizer.

Piège courant : « Le resync périodique est du gaspillage — pourquoi réconcilier des objets qui n’ont pas changé ? » est une objection fréquente. En réalité, le coût d’un resync est minimal : Reconcile lit le cache local (pas d’appel réseau, comme vu au chapitre 13-14), constate que l’écart est nul, et termine immédiatement. Le bénéfice est considérable : il garantit la convergence même si un événement Watch a été perdu ou si l’état réel a dérivé par un mécanisme externe au contrôleur. C’est le prix modique de la robustesse level-triggered.


Le scheduler, réconciliateur de placement

L’idée en une phrase

Le kube-scheduler est un réconciliateur spécialisé dont l’état désiré est « chaque pod a un nœud assigné » et l’état réel est l’ensemble des pods dont le champ spec.nodeName est vide — sa boucle de réconciliation comble cet écart en sélectionnant le nœud le plus adapté pour chaque pod non placé.

Analogie : Considérons un responsable de salle dans un restaurant. Les clients arrivent (les pods), chacun avec des préférences : table non-fumeur, près de la fenêtre, accessible en fauteuil roulant (les contraintes du pod). Le responsable consulte le plan de salle (l’état des nœuds) : quelles tables sont libres, quelles sont leurs caractéristiques, combien de couverts restent. Il élimine les tables incompatibles (le filtrage), classe les tables restantes par pertinence (le scoring), et assigne la meilleure. Si aucune table ne convient, le client attend — le responsable réessaiera au prochain tour.

Points clés

  • Le scheduler surveille les pods dont spec.nodeName est vide via un informer dédié (chapitre 13-14). Chaque pod non placé représente un écart entre l’état désiré (« ce pod doit tourner quelque part ») et l’état réel (« ce pod n’est assigné à aucun nœud »). La boucle du scheduler comble cet écart par un binding : elle écrit le nom du nœud choisi dans spec.nodeName.
  • Le placement se déroule en deux phases. La phase de filtrage (predicates) élimine les nœuds incompatibles : ressources insuffisantes, taints non tolérées, affinités non respectées, ports déjà occupés. La phase de scoring (priorities) attribue un score à chaque nœud restant selon des critères pondérés : répartition de charge, affinité inter-pods, localité des données. Le nœud avec le score le plus élevé est sélectionné.
  • Le scheduler est optimiste : il suppose que le binding va réussir et traite le pod suivant sans attendre la confirmation du kubelet. Si le nœud choisi ne peut pas accueillir le pod (race condition, nœud devenu indisponible entre le scoring et le binding), le pod revient à l’état Pending et le scheduler le retraite au prochain cycle — la même boucle de réconciliation qui recommence.
  • Depuis Kubernetes 1.19, le Scheduling Framework structure le scheduler en une série de plugins exécutés à des points d’extension (PreFilter, Filter, Score, Reserve, Bind). Chaque plugin implémente une facette du placement. Cette architecture rend le scheduler extensible sans modifier son code source — exactement comme un Operator étend le modèle de réconciliation à de nouveaux types de ressources (sujet des chapitres Avancé).

Exemple concret

Un pod ml-training demande 4 CPU et 16 Go de mémoire, avec une tolérance pour le taint gpu=true:NoSchedule. Le cluster comporte 5 nœuds. Phase de filtrage : le nœud 1 (2 CPU) est éliminé (ressources insuffisantes), le nœud 2 (pas de taint gpu) est éliminé (le pod tolère le taint mais le nœud n’a pas de GPU — un nodeSelector l’exclut), le nœud 5 est en état NotReady et éliminé. Restent les nœuds 3 et 4, tous deux avec GPU et ressources suffisantes. Phase de scoring : le nœud 3 utilise déjà 70 % de sa mémoire, le nœud 4 n’en utilise que 30 %. Le plugin LeastRequestedPriority attribue un score supérieur au nœud 4. Le scheduler effectue le binding : spec.nodeName = node-4. Le kubelet du nœud 4 observe ce changement, tire l’image du conteneur et démarre le pod.

Phases du scheduling

PhaseRôleExemple
Filtrage (predicates)Éliminer les nœuds incompatiblesNœud avec 2 CPU éliminé pour un pod demandant 4 CPU
Scoring (priorities)Classer les nœuds restants par pertinenceLeastRequestedPriority favorise le nœud le moins chargé
ReserveRéserver les ressources sur le nœud choisi (optimiste)Mémoire et CPU comptabilisés avant le binding effectif
BindÉcrire spec.nodeName sur l’objet pod dans etcdLe kubelet prend le relais pour démarrer le conteneur

Code YAML — pod en attente de placement

# pod-ml.yaml — un pod avec des contraintes de placement
apiVersion: v1
kind: Pod
metadata:
  name: ml-training
spec:
  # spec.nodeName est ABSENT → le scheduler doit le remplir
  nodeSelector:
    accelerator: gpu           # contrainte : nœud avec GPU
  tolerations:
    - key: gpu
      operator: Equal
      value: "true"
      effect: NoSchedule       # tolère le taint des nœuds GPU
  containers:
    - name: trainer
      image: myregistry/ml-trainer:3.2
      resources:
        requests:
          cpu: "4"
          memory: 16Gi         # le scheduler vérifie la capacité
# À l'application, le pod passe en Pending. Le scheduler observe
# ce pod sans nodeName, filtre, score, et écrit spec.nodeName.

Code kubectl — observer le scheduler en action

# Appliquer le pod et observer le placement
kubectl apply -f pod-ml.yaml
kubectl get pod ml-training -o wide
# NAME          READY  STATUS   NODE     AGE
# ml-training   0/1    Pending  <none>   2s   ← pas encore placé

# Quelques secondes plus tard :
kubectl get pod ml-training -o wide
# NAME          READY  STATUS    NODE     AGE
# ml-training   1/1    Running   node-4   15s  ← scheduler a choisi node-4

# Voir le binding dans les événements
kubectl describe pod ml-training | grep -A2 Events
# Events:
#   Type    Reason     Age   From               Message
#   Normal  Scheduled  12s   default-scheduler  Successfully assigned default/ml-training to node-4

# Vérifier que spec.nodeName a été rempli par le scheduler
kubectl get pod ml-training -o jsonpath='{.spec.nodeName}'
# node-4

# Si aucun nœud ne convient, le pod reste Pending :
kubectl get events --field-selector reason=FailedScheduling
# ... 0/5 nodes are available: 2 Insufficient cpu, 1 node(s) had untolerated taint, 2 node(s) were not ready

Piège courant : « Le scheduler assigne définitivement un pod à un nœud — si le nœud devient sous-optimal, le pod est déplacé » est une idée reçue inexacte. Le scheduler ne déplace pas les pods déjà placés : une fois spec.nodeName écrit, seule une suppression du pod (volontaire ou par éviction) permet un nouveau placement. Le composant qui gère l’éviction des pods sur les nœuds en difficulté est le kube-controller-manager (via le node lifecycle controller), pas le scheduler. Le scheduler ne réconcilie que l’écart « pod sans nœud » — son périmètre de réconciliation est strictement limité au placement initial.


Quiz — validation des acquis
Intermédiaire 7 questions Objectif : 5/7 minimum
0/7
bonnes reponses
Objectif non atteint (minimum 5/7 requis).
Relire la fiche memo ci-dessus en pretant attention aux points manques, puis cliquer sur « Recommencer » pour retenter.