Aller au contenu

SLO, error budget & alerting burn rate

Objectifs pédagogiques

  • Distinguer SLI, SLO et SLA sur un service réel
  • Choisir des SLI de disponibilité et de latence pertinents
  • Calculer et piloter un error budget
  • Instrumenter une API Node.js et écrire les recording rules associées
  • Alerter sur les symptômes avec des règles burn rate multi-fenêtres
  • Poser les bases de la robustesse applicative : probes et arrêt propre

Mesurer ce que vit l'utilisateur


Pourquoi des SLO ?

  • 100 % de disponibilité n'est ni atteignable ni souhaitable : chaque « 9 » supplémentaire coûte très cher
  • L'utilisateur ne perçoit pas la différence entre 99,99 % et 100 %, son réseau mobile est moins fiable que ça
  • Sans objectif chiffré, fiabilité et vélocité s'opposent à coups d'opinions
  • Un SLO transforme le débat en décision : tant que l'objectif est tenu, on livre
  • Il donne un état stable mesurable, indispensable avant tout test de robustesse ou de chaos

SLI, SLO, SLA

  • SLI (Service Level Indicator) : la mesure, un ratio de bons événements sur le total
  • SLO (Service Level Objective) : l'objectif interne fixé sur un SLI et une fenêtre de temps
  • SLA (Service Level Agreement) : l'engagement contractuel envers le client, avec pénalités
  • Le SLO est toujours plus strict que le SLA : il laisse une marge avant la pénalité
  • Un SLI se mesure, un SLO se décide, un SLA se négocie

Exemple : une API de paiement

Notion Exemple
SLI disponibilité Requêtes de paiement sans erreur 5xx / requêtes de paiement totales
SLI latence Requêtes de paiement servies en moins de 300 ms / requêtes totales
SLO 99,5 % de succès et 95 % des requêtes sous 300 ms, sur 30 jours glissants
SLA Disponibilité mensuelle inférieure à 99 % : 10 % de l'abonnement remboursé
  • La consommation CPU d'un pod n'est pas un SLI : l'utilisateur ne la voit pas

Choisir ses SLI

  • Partir des parcours critiques : payer, se connecter, consulter son historique
  • Disponibilité : ratio de requêtes sans erreur serveur, les 4xx du client sont généralement exclues
  • Latence : ratio de requêtes sous un seuil, plus robuste qu'une moyenne
  • Mesurer au plus près de l'utilisateur : load balancer ou ingress plutôt que l'intérieur du pod
  • Peu de SLI par service, deux ou trois suffisent
  • Un bon SLI baisse quand les utilisateurs souffrent, et seulement dans ce cas

Fenêtres de mesure

  • Fenêtre glissante (28 ou 30 jours) : reflète l'expérience récente, recommandée pour piloter
  • Fenêtre calendaire (mois civil) : alignée sur la facturation, adaptée aux SLA
  • Une fenêtre trop courte rend le SLO nerveux, une fenêtre trop longue masque les régressions
  • La rétention de Prometheus doit couvrir la fenêtre, ou les ratios doivent être pré-agrégés
  • En TP, raisonnez sur des fenêtres de quelques heures : le principe reste identique

RED et USE : deux grilles complémentaires

Méthode S'applique à Signaux
RED Services orientés requêtes (API, frontend) Rate, Errors, Duration
USE Ressources (CPU, mémoire, disque, réseau) Utilization, Saturation, Errors
  • RED détecte l'impact utilisateur : c'est la matière première des SLI
  • USE explique la cause : saturation CPU, throttling, pression mémoire d'un nœud
  • On alerte sur RED, on diagnostique avec USE

Error budget


Le budget d'erreurs

  • Error budget = 100 % − SLO, la part d'échec que l'on s'autorise
  • Exprimé en temps sur 30 jours (30 × 24 × 60 = 43 200 minutes) :
SLO Budget Indisponibilité tolérée sur 30 jours
99 % 1 % 7 h 12
99,5 % 0,5 % 3 h 36
99,9 % 0,1 % 43 min
99,95 % 0,05 % 21 min 36
99,99 % 0,01 % 4 min 19

Le budget en événements

  • Pour un SLI de requêtes, le budget se compte aussi en requêtes
  • 10 millions de paiements par mois avec un SLO de 99,5 % : 50 000 échecs tolérés
  • Une panne totale de 10 minutes un soir creux coûte moins qu'une heure à 5 % d'erreurs en pleine journée
  • Le budget mesure l'impact réel, pas la durée apparente de l'incident

Politique d'error budget

  • Budget disponible : on livre, on expérimente, on lance des expériences de chaos
  • Budget en tension : déploiements plus prudents, canary et analyse automatique obligatoires
  • Budget épuisé : gel des fonctionnalités, priorité aux actions de fiabilité et aux postmortems
  • La politique est signée par le produit et l'équipe SRE avant le premier incident
  • Elle remplace les arbitrages au cas par cas par une règle connue de tous

Instrumenter et calculer


Instrumenter Express avec prom-client

const client = require('prom-client');
client.collectDefaultMetrics();

const httpRequests = new client.Counter({
  name: 'http_requests_total',
  help: 'Requêtes HTTP traitées',
  labelNames: ['method', 'route', 'code'],
});
const httpDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Durée de traitement des requêtes HTTP',
  labelNames: ['method', 'route', 'code'],
  buckets: [0.05, 0.1, 0.2, 0.3, 0.5, 1, 2, 5],
});

app.use((req, res, next) => {
  const end = httpDuration.startTimer();
  res.on('finish', () => {
    const route = req.route ? req.baseUrl + req.route.path : 'unmatched';
    const labels = { method: req.method, route, code: res.statusCode };
    httpRequests.inc(labels);
    end(labels);
  });
  next();
});

Exposer /metrics sans exploser la cardinalité

app.get('/metrics', async (req, res) => {
  res.set('Content-Type', client.register.contentType);
  res.end(await client.register.metrics());
});
  • Le label route porte le gabarit (/api/payments/process), jamais l'URL brute avec ses identifiants
  • Les routes inconnues sont regroupées sous unmatched pour résister aux scans
  • Un bucket doit tomber exactement sur le seuil du SLO (0,3 s) pour un ratio exact
  • Chaque combinaison de labels crée une série : pas d'ID utilisateur, d'IP ni de timestamp

Les SLI en PromQL

Ratio d'erreurs sur les routes de paiement

sum(rate(http_requests_total{job="securepay-api", route=~"/api/payments/.*", code=~"5.."}[5m]))
/
sum(rate(http_requests_total{job="securepay-api", route=~"/api/payments/.*"}[5m]))

Ratio de requêtes sous 300 ms

sum(rate(http_request_duration_seconds_bucket{job="securepay-api", route=~"/api/payments/.*", le="0.3"}[5m]))
/
sum(rate(http_request_duration_seconds_count{job="securepay-api", route=~"/api/payments/.*"}[5m]))

Latence p95, pour les dashboards

histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket{job="securepay-api"}[5m])))

Recording rules

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: securepay-slo-recording
  namespace: monitoring
spec:
  groups:
    - name: securepay-sli
      rules:
        - record: securepay:sli_errors:ratio_rate5m
          expr: |
            sum(rate(http_requests_total{job="securepay-api", route=~"/api/payments/.*", code=~"5.."}[5m]))
            /
            sum(rate(http_requests_total{job="securepay-api", route=~"/api/payments/.*"}[5m]))
        # Même règle déclinée pour 30m, 1h, 6h et 3d
  • Pré-calculer les ratios rend les alertes et les dashboards rapides et cohérents
  • Convention de nommage : niveau:métrique:opérations

Alerter sur les symptômes


Symptômes plutôt que causes

  • « CPU > 80 % » réveille l'astreinte sans prouver qu'un utilisateur souffre
  • Les causes sont innombrables, les symptômes peu nombreux : erreurs et lenteur
  • Une alerte doit être actionnable, urgente et liée à un impact utilisateur
  • Les signaux de cause (CPU, mémoire, saturation) restent sur les dashboards et dans les runbooks
  • Exception utile : les causes qui précèdent toujours un impact (disque plein dans 4 h, certificat qui expire)

Le burn rate

  • Vitesse de consommation du budget : taux d'erreur observé / (1 − SLO)
  • Burn rate 1 : le budget est épuisé exactement à la fin de la fenêtre de 30 jours
  • Burn rate 14,4 : 2 % du budget mensuel partent en une heure
  • Temps avant épuisement = durée de la fenêtre / burn rate
  • Avec un SLO de 99,5 %, un taux d'erreur de 7,2 % correspond à un burn rate de 14,4
  • On alerte sur la vitesse de consommation, pas sur un seuil d'erreur arbitraire

Les limites d'une fenêtre unique

Approche Défaut
Taux d'erreur > SLO sur 5 min Beaucoup de faux positifs, alerte sur des pics sans importance
Taux d'erreur sur 30 jours Détection beaucoup trop lente
Burn rate élevé sur 1 h seule Bonne précision, mais l'alerte reste active longtemps après la fin de l'incident
  • On cherche à la fois précision, rappel, détection rapide et retour rapide au calme
  • Réponse du SRE Workbook : plusieurs burn rates, chacun vérifié sur deux fenêtres

Multi-window, multi-burn-rate

Budget consommé Fenêtre longue Fenêtre courte Burn rate Action
2 % 1 h 5 min 14,4 Page
5 % 6 h 30 min 6 Page
10 % 3 jours 6 h 1 Ticket
  • La fenêtre longue garantit que la consommation est significative
  • La fenêtre courte vérifie qu'elle est toujours en cours : l'alerte se lève dès la fin de l'incident
  • Les incidents brutaux réveillent l'astreinte, les dégradations lentes créent un ticket

Règle d'alerte burn rate

- alert: SecurePayErrorBudgetBurn
  expr: |
    (
      securepay:sli_errors:ratio_rate1h > (14.4 * 0.005)
      and
      securepay:sli_errors:ratio_rate5m > (14.4 * 0.005)
    )
    or
    (
      securepay:sli_errors:ratio_rate6h > (6 * 0.005)
      and
      securepay:sli_errors:ratio_rate30m > (6 * 0.005)
    )
  labels:
    severity: critical
    component: securepay-api
  annotations:
    summary: "Error budget de securepay-api consommé trop vite"
    runbook_url: "https://git.example.com/securepay-platform/-/blob/main/docs/runbooks/error-budget-burn.md"

Outillage SLO


Sloth : générer les règles depuis une spécification

version: "prometheus/v1"
service: "securepay-api"
slos:
  - name: "payments-availability"
    objective: 99.5
    sli:
      events:
        error_query: sum(rate(http_requests_total{job="securepay-api",route=~"/api/payments/.*",code=~"5.."}[{{.window}}]))
        total_query: sum(rate(http_requests_total{job="securepay-api",route=~"/api/payments/.*"}[{{.window}}]))
    alerting:
      name: SecurePayPaymentsAvailability
      page_alert:
        labels:
          severity: critical
      ticket_alert:
        labels:
          severity: warning
  • sloth generate produit les recording rules et les alertes multi-fenêtres
  • Sloth existe aussi en contrôleur Kubernetes (CRD PrometheusServiceLevel)

Pyrra : SLO en CRD avec interface

apiVersion: pyrra.dev/v1alpha1
kind: ServiceLevelObjective
metadata:
  name: securepay-payments-availability
  namespace: monitoring
spec:
  target: "99.5"
  window: 4w
  indicator:
    ratio:
      errors:
        metric: http_requests_total{job="securepay-api",route=~"/api/payments/.*",code=~"5.."}
      total:
        metric: http_requests_total{job="securepay-api",route=~"/api/payments/.*"}
  • Pyrra génère les PrometheusRules et fournit une interface listant budgets et burn rates
  • Écrire les règles à la main une fois, puis outiller : on comprend ce que l'outil génère

Dashboard error budget

  • SLI sur la fenêtre : disponibilité et ratio de latence sur 30 jours
  • Budget restant : 1 - (ratio_erreurs_30j / 0.005), en pourcentage
  • Burn rate sur 1 h et 6 h, avec les seuils 14,4 et 6 matérialisés
  • RED : débit, taux d'erreur et latence p95 par route
  • Annotations : déploiements et expériences de chaos, pour corréler les chutes
  • Le dashboard sert de preuve : une capture horodatée vaut mieux qu'une affirmation

La robustesse commence dans l'application


Probes : trois questions différentes

Probe Question posée Effet en cas d'échec
startupProbe L'application a-t-elle fini de démarrer ? Les autres probes attendent, redémarrage si le délai expire
readinessProbe Peut-elle recevoir du trafic maintenant ? Retrait des endpoints du Service, sans redémarrage
livenessProbe Est-elle bloquée sans espoir de retour ? Redémarrage du conteneur par le kubelet
  • Une liveness qui teste une dépendance externe provoque des redémarrages en cascade
  • La readiness peut refléter les dépendances, la liveness jamais

Probes dans le chart

startupProbe:
  httpGet: { path: /health, port: http }
  periodSeconds: 2
  failureThreshold: 30
readinessProbe:
  httpGet: { path: /ready, port: http }
  periodSeconds: 5
  failureThreshold: 2
livenessProbe:
  httpGet: { path: /health, port: http }
  periodSeconds: 10
  failureThreshold: 3
  • /health répond tant que le processus tourne, /ready vérifie qu'il peut servir
  • Des seuils trop serrés transforment une latence passagère en redémarrage

Arrêt propre : la séquence de terminaison

  1. Le pod passe en Terminating et son retrait des endpoints démarre en parallèle
  2. Le hook preStop s'exécute : une courte pause laisse le temps au retrait de se propager
  3. Le conteneur reçoit SIGTERM : il arrête d'accepter des connexions et termine les requêtes en cours
  4. Passé terminationGracePeriodSeconds (30 s par défaut), SIGKILL coupe tout

  5. Sans gestion de SIGTERM, chaque déploiement ou scale-down produit des erreurs 5xx

  6. Ces erreurs consomment l'error budget à chaque mise en production

Arrêt propre : implémentation

const server = app.listen(PORT);
let shuttingDown = false;

app.get('/ready', (req, res) =>
  shuttingDown ? res.status(503).end() : res.status(200).json({ status: 'READY' }));

process.on('SIGTERM', () => {
  shuttingDown = true;
  server.close(() => process.exit(0));
  setTimeout(() => process.exit(1), 25000).unref();
});
lifecycle:
  preStop:
    sleep:
      seconds: 5
terminationGracePeriodSeconds: 30
  • L'action sleep native évite d'embarquer un binaire sleep dans une image distroless (activée par défaut depuis Kubernetes 1.30)

À retenir

  • Un SLO chiffre ce que vit l'utilisateur et arbitre entre fiabilité et vélocité
  • L'error budget se calcule, se suit et se dépense consciemment
  • On alerte sur la vitesse de consommation du budget, sur deux fenêtres à la fois
  • Recording rules d'abord, outillage (Sloth, Pyrra) ensuite
  • Probes justes et arrêt propre : sans eux, aucun test de robustesse n'est concluant

Pour aller plus loin

  • Site Reliability Engineering et The Site Reliability Workbook (Google), chapitres SLO et « Alerting on SLOs »
  • Documentation de Sloth et de Pyrra
  • Le cours Grafana k6 pour mettre ces SLO à l'épreuve