Intermédiaire Chapitre 13-14 / 8

Level-based vs edge-based & Informers, watches et cache local

Comprendre pourquoi Kubernetes raisonne sur l'état courant plutôt que sur les événements, et comment les informers maintiennent un cache local fiable via le mécanisme List-Watch.

Level-based vs edge-based

L’idée en une phrase

Un système edge-triggered réagit aux transitions (« un pod vient d’être supprimé »), tandis qu’un système level-triggered raisonne sur l’état courant (« il y a 2 pods, il en faut 3 ») — et c’est ce second modèle que Kubernetes adopte pour ses boucles de réconciliation, car il rend les contrôleurs naturellement tolérants aux événements perdus, dupliqués ou arrivés dans le désordre.

Analogie : Considérons deux façons de gérer le niveau d’eau d’une piscine. Un système edge-triggered installe un capteur qui déclenche une alarme chaque fois que le niveau baisse : si l’alarme ne retentit pas (panne du capteur, bruit ambiant), la piscine se vide sans que personne n’intervienne. Un système level-triggered mesure le niveau actuel toutes les minutes et ouvre la vanne de remplissage dès que le niveau passe sous le seuil — peu importe pourquoi il a baissé, peu importe combien de mesures ont été manquées entre-temps, la prochaine lecture suffit à corriger l’écart.

Points clés

  • Un contrôleur edge-triggered encode dans sa logique la nature de l’événement (create, update, delete) et agit différemment selon le cas. Si un événement est perdu — coupure réseau, redémarrage du contrôleur —, l’action correspondante n’est jamais exécutée et l’état réel diverge silencieusement de l’état désiré.
  • Un contrôleur level-triggered ignore le type d’événement déclencheur. Sa fonction Reconcile (comme vu au chapitre 11-12) reçoit une clé, lit l’état complet, calcule l’écart et agit. Un événement perdu retarde la réconciliation mais ne la compromet pas : le prochain déclenchement — ou le resync périodique — observe le même écart et le comble.
  • La terminologie vient de l’électronique numérique : un circuit edge-triggered se déclenche sur un front montant ou descendant du signal, un circuit level-triggered sur le niveau haut ou bas. En ingénierie logicielle, le modèle level-triggered est synonyme de convergence : le système tend vers l’état désiré quel que soit le chemin emprunté.
  • Kubernetes n’est pas purement level-triggered au sens strict — les watches transmettent bien des événements (second sous-thème de ce chapitre). Mais la fonction Reconcile, elle, traite toujours l’état courant complet. Les événements servent de signal de réveil, pas de source de vérité.

Exemple concret

Un Deployment déclare 3 replicas. À t₀, deux pods sont supprimés simultanément. Dans un modèle edge-triggered, le contrôleur recevrait deux événements DELETE et créerait deux pods — mais si le second événement est perdu (file saturée, redémarrage), il n’en crée qu’un et l’état reste à 2/3. Dans le modèle level-triggered de Kubernetes, la work queue déduplique les notifications en une seule clé (comme vu au chapitre 11-12). La fonction Reconcile observe 1 pod Running, calcule l’écart 3 − 1 = 2, et crée 2 pods. Résultat identique que l’on ait reçu un événement, deux, ou zéro (via resync) : seul l’état courant compte.

Edge-triggered vs level-triggered

CritèreEdge-triggeredLevel-triggered
DéclencheurLa transition (événement)L’état courant
Information traitée« Que s’est-il passé ? »« Quel est l’écart maintenant ? »
Événement perduAction manquée, dérive silencieuseCorrigé au prochain tick ou resync
Événements dupliquésAction exécutée deux fois (risque de surcréation)Écart recalculé, pas de double action
Complexité du codeLogique par type d’événement (switch/case)Logique unique : observer, comparer, agir

Code YAML — un état désiré indépendant de l’historique

# deployment-level.yaml — le contrôleur ne se demande pas
# "combien de pods ont été supprimés" mais "combien en manque-t-il"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: server
          image: myregistry/api:2.1
# Que 1 ou 3 pods aient disparu, la boucle level-triggered
# calcule le même écart et converge vers 3 replicas.

Code kubectl — observer la convergence level-triggered

# Supprimer tous les pods d'un coup (situation extrême)
kubectl delete pods -l app=api --wait=false
# pod "api-abc12" deleted
# pod "api-def34" deleted
# pod "api-ghi56" deleted

# Le contrôleur ne reçoit pas "3 événements à traiter séparément" :
# il observe 0 pods, calcule l'écart 3 - 0 = 3, et crée 3 pods.
kubectl get pods -l app=api -w
# NAME          READY   STATUS    AGE
# api-jkl78     0/1     Pending   0s
# api-mno90     0/1     Pending   0s
# api-pqr12     0/1     Pending   0s
# api-jkl78     1/1     Running   4s
# api-mno90     1/1     Running   4s
# api-pqr12     1/1     Running   5s
# → convergence atteinte en un seul tick de réconciliation

Piège courant : « Level-triggered signifie que le contrôleur fait du polling régulier » est un raccourci trompeur. Le contrôleur ne scrute pas l’API server en boucle — il est réveillé par les événements du watch (edge), mais traite l’état courant (level). Le modèle est hybride : les edges servent de notification efficace, le level garantit la correction. Le resync périodique n’est qu’un filet de sécurité, pas le mécanisme principal.


Informers, watches et cache local

L’idée en une phrase

Un informer est le composant qui alimente la boucle de réconciliation en données : il effectue un List initial pour remplir un cache local, puis maintient ce cache à jour via un Watch continu sur l’API server, de sorte que la fonction Reconcile peut lire l’état réel sans aucun appel réseau.

Analogie : Considérons un journal mural dans le hall d’une entreprise. Chaque matin, un employé (le reflector) recopie intégralement le tableau des projets depuis le bureau central (le List). Ensuite, un système de notification interne (le Watch) lui transmet chaque modification en temps réel — un projet ajouté, une ligne barrée. Il met à jour le journal mural au fil de la journée. Les collègues (les fonctions Reconcile) consultent le journal mural (le cache) sans jamais se déplacer au bureau central. Si le système de notification tombe, l’employé redemande la copie complète et recommence.

Points clés

  • Le mécanisme Watch repose sur une connexion HTTP long-lived (chunked transfer) vers l’API server. Le serveur envoie un flux d’événements typés — ADDED, MODIFIED, DELETED — pour chaque changement sur les objets surveillés. Chaque événement porte un resourceVersion, un compteur monotone qui identifie la version de l’objet dans etcd.
  • Le pattern List-Watch garantit la cohérence : le List initial retourne tous les objets existants avec leur resourceVersion. Le Watch reprend à partir de cette version, sans trou. Si la connexion Watch est coupée, l’informer tente de reprendre au dernier resourceVersion connu ; si etcd a compacté cette version (événements trop anciens), il refait un List complet — c’est le relist.
  • Le cache (aussi appelé Store ou Indexer) est une structure en mémoire indexée par clé (namespace/name). Il permet des lectures en O(1) et des requêtes par index (par label, par champ). La fonction Reconcile lit toujours depuis ce cache — un accès local, sans latence réseau.
  • Le SharedInformer mutualise le Watch entre plusieurs contrôleurs surveillant le même type de ressource. Dix contrôleurs intéressés par les pods partagent un seul SharedInformer (et donc un seul Watch), ce qui réduit la charge sur l’API server. Chaque contrôleur enregistre ses propres event handlers (AddFunc, UpdateFunc, DeleteFunc) auprès du SharedInformer.

Exemple concret

Le kube-controller-manager exécute une trentaine de contrôleurs dans un seul processus. Sans SharedInformer, chaque contrôleur ouvrirait son propre Watch sur les pods — 30 connexions HTTP parallèles transmettant les mêmes données. Avec le SharedInformerFactory, un seul Watch alimente un cache unique. Lorsqu’un pod passe de Pending à Running, le SharedInformer met à jour le cache et invoque les handlers de chaque contrôleur concerné (ReplicaSet controller, Endpoint controller, etc.). Chacun enfile la clé de son objet primaire dans sa propre work queue — le contrôleur ReplicaSet enfile la clé du ReplicaSet propriétaire, le contrôleur Endpoint enfile la clé du Service associé. Un seul événement Watch déclenche plusieurs réconciliations indépendantes, sans duplication de trafic réseau.

Composants du mécanisme List-Watch

ComposantRôleDétail technique
ReflectorEffectue le List initial puis le WatchMaintient le resourceVersion courant, gère la reconnexion
Store (cache)Stocke les objets en mémoire, indexés par cléLectures O(1), mises à jour atomiques par événement
SharedInformerMutualise un Watch entre plusieurs consommateursUn seul flux réseau, distribution locale des événements
Event handlerCallback enregistré par un contrôleurConvertit l’événement en clé et l’enfile dans la work queue
ResourceVersionIdentifiant de version dans etcdPermet au Watch de reprendre sans rejouer l’historique

Code YAML — l’objet tel qu’il transite dans le Watch

# Événement Watch de type MODIFIED, tel que reçu par l'informer.
# Le resourceVersion identifie précisément cette version de l'objet.
apiVersion: v1
kind: Pod
metadata:
  name: api-jkl78
  namespace: default
  resourceVersion: "48217"     # le Watch reprend après ce numéro
  labels:
    app: api
status:
  phase: Running               # transition : Pending → Running
  conditions:
    - type: Ready
      status: "True"
# L'informer met à jour le cache local avec cet objet,
# puis invoque les event handlers enregistrés.

Code kubectl — observer le Watch en temps réel

# Le flag -w (--watch) simule côté client ce que fait l'informer :
# un List initial suivi d'un Watch continu.
kubectl get pods -l app=api -w
# NAME          READY   STATUS    AGE
# api-jkl78     1/1     Running   10m    ← résultat du List initial
# api-mno90     1/1     Running   10m
# api-pqr12     1/1     Running   10m
# api-jkl78     1/1     Running   10m    ← événement Watch (MODIFIED)

# Observer le resourceVersion d'un objet — le curseur du Watch
kubectl get pod api-jkl78 -o jsonpath='{.metadata.resourceVersion}'
# 48217

# En interne, l'informer fait l'équivalent de :
# GET /api/v1/namespaces/default/pods?labelSelector=app%3Dapi
#   → List (tous les pods, avec resourceVersion de la liste)
# GET /api/v1/namespaces/default/pods?watch=true&resourceVersion=48200
#   → Watch (flux HTTP chunked à partir de cette version)

Piège courant : « Si le Watch se déconnecte, l’informer perd des événements et le cache devient incohérent » est une crainte légitime mais gérée par conception. À la reconnexion, l’informer reprend le Watch au dernier resourceVersion connu. Si ce resourceVersion a expiré (etcd a compacté l’historique, erreur 410 Gone), l’informer effectue un relist complet — il repart de zéro avec un cache garanti cohérent. C’est le même principe de réconciliation appliqué au mécanisme de cache lui-même : en cas de doute, relire l’état complet plutôt que tenter de reconstruire l’historique.


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.