Leçon 4 — Livraison progressive et observabilité

Observer le service avec Prometheus et Grafana

8 min de lecture

Observer le service avec Prometheus et Grafana

La leçon précédente s'appuie sur une hypothèse : Prometheus collecte les bonnes séries, avec la bonne étiquette de version, assez souvent. Si l'une de ces conditions tombe, l'analyse canary ne voit rien et laisse tout passer. Et l'incident du ConfigMap partagé ne s'est vu ni dans Argo CD, ni dans Argo Rollouts : seulement dans les métriques. Cette leçon rend l'observabilité vérifiable : ce qui est collecté, les requêtes d'astreinte, le tableau de bord, et deux alertes testées contre un incident réel.

Le mécanisme

flowchart LR
  POD["Pod inference : /metrics (prometheus_client)"] -->|"toutes les 15 s"| PROM["Prometheus (kube-prometheus-stack)"]
  SM["ServiceMonitor (chart) : port http, /metrics, podTargetLabels"] -.->|"lu par l'opérateur"| OP["prometheus-operator"]
  OP -.->|"génère la configuration de collecte"| PROM
  PR["PrometheusRule (chart) : 2 alertes"] -.-> OP
  PROM --> GRAF["Grafana : tableau de bord (ConfigMap grafana_dashboard)"]
  PROM --> AR["Argo Rollouts : AnalysisRun"]
  PROM --> AM["Alertmanager (désactivé dans le lab)"]

L'application expose trois métriques (main.py) : inference_requests_total{status} (compteur), inference_request_seconds (histogramme), inference_inflight_requests (jauge). Le ServiceMonitor du chart demande à l'opérateur d'ajouter la cible ; deux réglages de kps-values.yaml le rendent possible :

files/monitoring/kps-values.yaml
prometheus:
  prometheusSpec:
    serviceMonitorSelectorNilUsesHelmValues: false   # lire les ServiceMonitor de tous les namespaces
    ruleSelectorNilUsesHelmValues: false             # idem pour les PrometheusRule

Sans eux, Prometheus ne lit que les objets portant l'étiquette de la release kps : le ServiceMonitor existe, kubectl get le montre, et aucune série n'arrive. C'est la panne la plus fréquente avec kube-prometheus-stack.

La mise en œuvre

Vérifier la collecte

terminal
make e2e                                    # la dernière étape interroge Prometheus
make grafana-ui                             # http://localhost:3000, admin / lab-k8s-gitops

Les requêtes d'astreinte, à coller dans Grafana (Explore) ou dans l'API de Prometheus :

QuestionPromQL
Le service est-il collecté ?up{namespace="assistant"}
Taux d'erreur vu par l'utilisateursum(rate(inference_requests_total{namespace="assistant",status=~"5.."}[5m])) / sum(rate(inference_requests_total{namespace="assistant"}[5m]))
Taux de succès par versionsum by (rollouts_pod_template_hash) (rate(inference_requests_total{status="200"}[1m])) / sum by (rollouts_pod_template_hash) (rate(inference_requests_total{status=~"200|5.."}[1m]))
Volume par version et code depuis 30 minsum by (rollouts_pod_template_hash, status) (increase(inference_requests_total{namespace="assistant"}[30m]))
Pods saturésmax by (pod) (inference_inflight_requests) comparé à MAX_INFLIGHT (32)

Le tableau de bord

Le tableau de bord est un ConfigMap (dashboard-assistant.yaml) que le sidecar de Grafana charge grâce à l'étiquette grafana_dashboard. Il vit dans Git comme le reste : un tableau de bord modifié à la main dans l'interface est perdu au prochain redémarrage de Grafana.

Tableau de bord de l'assistant pendant les démonstrations canary : trafic par code, taux de succès par hash de version (le hash stable 8476bcfd4 tombe à 90 % pendant l'incident du ConfigMap partagé), latences, requêtes en cours (capture réelle du 2026-10-16)

Les alertes

Le chart émet une PrometheusRule (prometheusrule.yaml) :

files/gitops-source/chart/inference/templates/prometheusrule.yaml
        - alert: InferenceErreursElevees
          expr: |
            sum(rate(inference_requests_total{namespace="assistant",status=~"5.."}[5m]))
            / sum(rate(inference_requests_total{namespace="assistant"}[5m])) > 0.05
          for: 2m
          labels: { severity: page }
        - alert: InferenceAucunPodCollecte
          expr: absent(up{namespace="assistant",job="inference"} == 1)
          for: 5m

La première mesure ce que subit l'utilisateur, tous pods confondus : c'est elle qui aurait réveillé l'astreinte pendant l'incident du ConfigMap partagé, que ni Argo CD ni Argo Rollouts ne signalaient. La seconde couvre le cas où il n'y a plus rien à mesurer : sans elle, un service à terre ne produit aucune série, donc aucune alerte d'erreur.

Une alerte qui n'a jamais sonné n'est pas testée. Évaluez son expression aux instants de l'incident, avec le paramètre time de l'API :

terminal
Q='sum(rate(inference_requests_total{namespace="assistant",status=~"5.."}[5m])) / sum(rate(inference_requests_total{namespace="assistant"}[5m]))'
kubectl --context kind-lab-k8s-gitops get --raw \
  "/api/v1/namespaces/monitoring/services/kps-prometheus:9090/proxy/api/v1/query?time=2026-10-16T15:05:00Z&query=$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))' "$Q")"

Ce qui casse en production

SymptômeCauseParade
ServiceMonitor présent, aucune sériesélecteur de kube-prometheus-stack limité à sa releaseserviceMonitorSelectorNilUsesHelmValues: false, ou étiquette release: kps sur le ServiceMonitor
p50 à 25 ms et p95 à 47,5 ms constants alors que la réponse prend moins d'une millisecondehistogram_quantile interpole linéairement dans le premier bucket (0 à 50 ms) : il ne peut rien dire de plus finbuckets adaptés à la latence réelle ; pour Gemini (secondes), les buckets 0,5 à 32 s du code sont corrects, pour le mode simulé ils ne le sont pas
Prometheus en manque de mémoire après l'ajout d'une étiquettecardinalité : une étiquette par utilisateur ou par question multiplie les sériesjamais d'identifiant libre en étiquette ; ici 47 séries pour 3 pods, contrôlées par count({namespace="assistant"})
Alerte silencieuse alors que le service est à terreexpression en ratio : plus de trafic, plus de série, plus d'alertealerte absent(up == 1) en complément
Le hash de version manque dans les sériespodTargetLabels absent, ou Deployment au lieu de Rollouttest de rendu test_servicemonitor_exports_version_label

La règle de décision

  • Dans le lab et sur un cluster autogéré : kube-prometheus-stack. Sur GKE : Google Cloud Managed Service for Prometheus, activé par défaut sur Autopilot ; le ServiceMonitor y devient un PodMonitoring, les règles un objet Rules, et l'analyse canary interroge son frontend (valeur prometheusAddress de l'environnement gke).
  • Deux alertes qui réveillent (taux d'erreur utilisateur, absence de collecte) valent mieux que vingt qui s'ignorent. Le reste va au tableau de bord.
  • À surveiller sur Prometheus lui-même : nombre de séries actives et durée de collecte.

Pratique

Vous ajoutez l'étiquette user_id au compteur inference_requests_total pour savoir qui utilise l'assistant (900 salariés). Quelle conséquence anticipez-vous ?

L'alerte InferenceErreursElevees n'a jamais sonné en six mois. Qu'en concluez-vous avant de la garder telle quelle ?

Exercice

Ajoutez au tableau de bord un panneau « Part de 503 de délestage » qui montre, par pod, la part des requêtes refusées par MAX_INFLIGHT. Écrivez la requête, puis dites ce qu'une valeur durablement supérieure à 1 % signifie pour l'HPA.

Voir la solution
sum by (pod) (rate(inference_requests_total{namespace="assistant",status="503"}[5m]))
/ sum by (pod) (rate(inference_requests_total{namespace="assistant"}[5m]))

Ajoutez un objet de panneau au JSON de monitoring/dashboard-assistant.yaml (copie du panneau 1 avec cette expression, unit: percentunit), puis kubectl apply -f monitoring/dashboard-assistant.yaml ; le sidecar recharge Grafana en quelques secondes.

Une part de 503 durable signifie que des pods atteignent leur limite de requêtes simultanées avant que l'HPA ne réagisse : le CPU n'est pas le bon signal pour un service qui attend un modèle distant. C'est l'argument de la métrique inference_inflight_requests comme cible de l'HPA (autoscaling.inflight dans le chart), expliquée dans le projet précédent.

Résultat obtenu

Exécution du 2026-10-16, après les démonstrations canary :

-- cibles collectées
up{job="inference", pod="inference-6cd4d8d86c-lpjdg", rollouts_pod_template_hash="6cd4d8d86c"} 1
up{job="inference", pod="inference-6cd4d8d86c-6chj6", rollouts_pod_template_hash="6cd4d8d86c"} 1
up{job="inference", pod="inference-6cd4d8d86c-bdbvl", rollouts_pod_template_hash="6cd4d8d86c"} 1
-- volume par version et code sur 30 min
8476bcfd4  200  224363     8476bcfd4  502  10437    <- hash STABLE : erreurs du ConfigMap partagé
7cb69f66b5 200   12011     7cb69f66b5 502   4944    <- canary 0.2.2, chart 0.2.0
c5f8b7776  200   12421     c5f8b7776  502   5451    <- canary 0.2.2, chart 0.3.0
6cd4d8d86c 200  140804                              <- version stable actuelle
-- séries de l'application : 47
-- mémoire par pod (working set) : 44 à 53 Mio

-- règles chargées
InferenceErreursElevees inactive ok
InferenceAucunPodCollecte inactive ok

-- expression de InferenceErreursElevees aux instants de l'incident (UTC)
2026-10-16T15:00:00Z vide          (aucun trafic)
2026-10-16T15:03:00Z 0.0542
2026-10-16T15:05:00Z 0.0853        <- au-dessus de 5 % plus de 2 minutes : l'alerte aurait sonné
2026-10-16T15:10:30Z 0.0512        (second essai, chart corrigé : pic bref)

$ make e2e
...
[OK] Prometheus collecte les métriques du service : 1 requêtes comptées
e2e : tout est vert

La ligne 8476bcfd4 502 10437 est la preuve chiffrée de l'incident de la leçon 3 : des erreurs attribuées au hash stable, ce qu'aucun outil de déploiement n'avait signalé.

Je sais faire

  • Vérifier qu'un ServiceMonitor produit des séries, et corriger le sélecteur sinon
  • Écrire les requêtes d'astreinte : taux d'erreur, succès par version, saturation
  • Versionner un tableau de bord Grafana dans Git
  • Écrire une alerte de taux d'erreur et une alerte d'absence, et tester la première sur un incident passé
  • Reconnaître une latence fausse due aux buckets de l'histogramme
Suivant