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
- Le pod passe en
Terminating et son retrait des endpoints démarre en parallèle - Le hook
preStop s'exécute : une courte pause laisse le temps au retrait de se propager - Le conteneur reçoit
SIGTERM : il arrête d'accepter des connexions et termine les requêtes en cours -
Passé terminationGracePeriodSeconds (30 s par défaut), SIGKILL coupe tout
-
Sans gestion de SIGTERM, chaque déploiement ou scale-down produit des erreurs 5xx
- 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