Le SRE dans le code : timeouts, retries et alertes as code
La fiabilité se décide dans le dépôt. Ce qu'un SRE relit dans le code applicatif : timeouts, retries, health checks, rollouts, et alertes de burn rate as code.

L'essentiel de la fiabilité d'un service se décide dans son dépôt, dans la façon dont chaque fonction se comporte quand une dépendance est lente, pas dans le cluster où il tourne. Un SRE doit donc lire le code applicatif assez bien pour le relire et le modifier : trouver l'appel réseau sans timeout, la boucle de retry qui triplera la charge pendant une panne, le health check qui sortira toutes les instances du service en même temps. Ce billet est la liste de revue, avec les correctifs, et il se termine sur le seul morceau de code que le SRE possède en propre : des alertes qui réveillent sur le taux de consommation du budget d'erreur (burn rate), écrites en Terraform.
Ceci est la Partie 3 de Budget for Failure, et la première de niveau 201. La Partie 1 a défini le rôle et la Partie 2 lui a donné des chiffres : un SLO de disponibilité à 99,9 pour cent sur 30 jours, mesuré à un Application Load Balancer. Cet exemple continue ici. Les extraits de code sont en Python ; les patterns sont les mêmes dans tous les langages.
Combien de code un SRE doit maîtriser
Vous n'avez pas besoin d'être le meilleur développeur de fonctionnalités de l'équipe. Vous avez besoin de trois capacités. Lire le chemin de requête : en partant du handler, suivre une requête utilisateur à travers chaque appel réseau qu'elle fait et dire ce qui se passe si chaque appel prend trente secondes. Relire une pull request pour son comportement opérationnel, ce qui est une lecture différente de la relecture pour sa correction. Et écrire vous-même des changements de qualité production : le timeout, la métrique, le hook de rollback. Un SRE qui ouvre un ticket demandant aux développeurs d'ajouter un timeout a décrit le problème. Celui qui envoie la pull request l'a corrigé.
Chaque appel réseau a besoin d'une échéance
Le défaut de fiabilité le plus courant dans le code applicatif est un appel qui peut attendre indéfiniment. La bibliothèque Python requests n'a aucun timeout par défaut. Le SDK AWS pour Python prend par défaut 60 secondes pour la connexion et 60 secondes pour la lecture, avec des retries automatiques par-dessus. Un handler qui appelle DynamoDB avec ces défauts, derrière un load balancer dont l'idle timeout est de 60 secondes, peut monopoliser un worker pendant des minutes après que l'utilisateur a déjà reçu un 504.
Quand une dépendance ralentit, les appels sans échéance transforment une dépendance lente en service mort : chaque worker est garé en attente, le pool est épuisé, et les requêtes qui n'avaient jamais besoin de la dépendance lente échouent aussi. Posez les limites volontairement :
import boto3
from botocore.config import Config
sdk = Config(
connect_timeout=1,
read_timeout=2,
retries={"mode": "standard", "max_attempts": 3},
)
ddb = boto3.client("dynamodb", config=sdk)Choisissez le read timeout d'après la latence mesurée de la dépendance, un peu au-dessus de son 99,9e percentile, pas d'après un chiffre rond. Puis vérifiez que les limites s'emboîtent. Si le budget côté utilisateur est de 3 secondes, le handler ne peut pas faire trois appels séquentiels pouvant chacun prendre 2 secondes avec 3 tentatives. L'échéance extérieure doit être transmise vers le bas, pour que les appels internes abandonnent quand plus personne n'attend la réponse.
Les retries : le correctif qui cause des pannes
Un retry transforme un échec transitoire en succès, et un échec durable en échec plus grand. Trois règles le gardent du bon côté.
Réessayer à une seule couche. Si le client réessaie trois fois, que l'API réessaie trois fois son appel à la base de données, et que le SDK réessaie trois fois en dessous, une action utilisateur devient jusqu'à 27 requêtes contre une dépendance qui est déjà en difficulté. Choisissez la couche la plus proche de l'échec et faites échouer les autres vite.
Backoff avec jitter. Des clients qui réessaient selon un calendrier fixe arrivent ensemble et renversent de nouveau le service en cours de récupération. L'analyse de l'AWS Architecture Blog sur ce sujet est la référence standard ; sa variante « full jitter » attend un temps aléatoire entre zéro et un plafond qui croît exponentiellement :
import random, time
def call_with_retry(fn, attempts=3, base=0.1, cap=2.0, deadline=None):
for attempt in range(attempts):
try:
return fn()
except RetryableError:
if attempt == attempts - 1:
raise
pause = random.uniform(0, min(cap, base * 2 ** attempt))
if deadline and time.monotonic() + pause > deadline:
raise
time.sleep(pause)Ne réessayer que ce qui peut être répété sans danger. Une lecture qui a expiré, c'est sûr. Un « débiter la carte » qui a expiré ne l'est pas, sauf si l'opération est idempotente : le client envoie une clé unique avec la requête et le serveur stocke le résultat sous cette clé, de sorte qu'une répétition renvoie le premier résultat au lieu de débiter deux fois. En revue, tout retry autour d'une écriture est une invitation à demander où est la clé d'idempotence.
Échouer plus petit : breakers, délestage, dégradation
Timeouts et retries gèrent une dépendance brièvement souffrante. Pour une dépendance en panne, le code a besoin d'un moyen d'arrêter d'essayer et d'une décision sur ce que l'utilisateur voit pendant ce temps.
- Circuit breaker. Après une série d'échecs, cessez d'appeler la dépendance pendant un court intervalle et échouez immédiatement. Cela protège votre pool de workers et laisse à la dépendance de la place pour récupérer.
- Délestage (load shedding). Quand le service est saturé, rejetez l'excédent tôt avec un 429 ou un 503 au lieu de tout accepter et de tout servir lentement. Un rejet rapide et bon marché de 20 pour cent des requêtes vaut mieux qu'un timeout pour 100 pour cent.
- Dégradation gracieuse. Décidez par dépendance si elle est dure ou souple. Si le service de recommandations est en panne, la page produit doit se rendre sans recommandations, pas renvoyer un 500. Chaque dépendance convertie de dure à souple sort de la multiplication des disponibilités, que la Partie 4 fait en entier.
Des health checks qui disent la vérité
Un health check, c'est du code, et il est souvent faux dans l'une de deux directions. Un check superficiel qui renvoie 200 dès que le processus est vivant garde une instance cassée en rotation. Un check profond qui teste la base de données depuis chaque instance a une défaillance pire : quand la base de données a un hoquet, chaque instance échoue son check au même moment, et le load balancer n'a plus aucune target saine. Un Application Load Balancer dans cet état échoue en mode ouvert et route quand même vers toutes les targets, ce qui vous sauve, mais un orchestrateur configuré pour remplacer les instances non saines commencera à tuer une flotte en bonne santé pendant un incident de base de données.
La réponse habituelle, ce sont deux endpoints. Le liveness dit que le processus tourne et ne doit pas être redémarré. Le readiness dit que cette instance peut faire du travail utile, et ne vérifie que ce qui lui est local : configuration chargée, pool de connexions créé, caches chauds. Les dépendances partagées se surveillent via le SLI, pas via des health checks par instance.
Émettre le SLI depuis le code
Le SLI load balancer de la Partie 2 voit des codes de statut. Il ne sait pas distinguer un checkout réussi d'un 200 qui transporte un message d'erreur dans le corps, et il ne sait pas découper par niveau de client ou par opération. L'application, si. Pour chaque requête, écrivez une ligne de log structurée avec l'opération, le résultat tel que le métier le définit, la durée et le trace ID. Sur AWS, le CloudWatch embedded metric format transforme cette même ligne de log en métriques sans appel d'API supplémentaire :
{
"_aws": {
"Timestamp": 1787130000000,
"CloudWatchMetrics": [{
"Namespace": "Checkout",
"Dimensions": [["Operation"]],
"Metrics": [
{ "Name": "Requests", "Unit": "Count" },
{ "Name": "Failures", "Unit": "Count" },
{ "Name": "Duration", "Unit": "Milliseconds" }
]
}]
},
"Operation": "PlaceOrder",
"Requests": 1,
"Failures": 0,
"Duration": 184,
"trace_id": "1-66c2f1d0-3f1b2c4d5e6f7a8b9c0d1e2f",
"customer_tier": "enterprise"
}Gardez les champs à haute cardinalité comme le trace ID et le client en propriétés de log, pas en dimensions de métrique ; chaque valeur de dimension unique est une métrique facturée à part. C'est l'instrumentation qui a sa place dans le template de service de la Partie 1, pour que les développeurs l'obtiennent sans la demander.
Faire du changement le chemin sûr
Le registre de la Partie 2 montrait un seul déploiement consommant 31 pour cent du budget d'un mois. Les défenses côté code sont le travail au meilleur rendement de cette liste.
- Déploiement progressif avec rollback automatique. Envoyez 10 pour cent du trafic à la nouvelle version, comparez son ratio d'erreurs et sa latence avec l'ancienne, et revenez en arrière sans humain si elle est pire. CodeDeploy, les déploiements ECS et les alias Lambda savent tous s'arrêter et revenir en arrière sur une alarme CloudWatch. Une mauvaise release coûte alors des minutes de 10 pour cent du trafic.
- Séparer déploiement et release. Livrez le code éteint derrière un feature flag et activez-le progressivement. Couper un flag est plus rapide que n'importe quel rollback.
- Changements de schéma rétrocompatibles. Expand, migrate, contract : ajoutez la nouvelle colonne, écrivez dans les deux, déplacez les lecteurs, puis retirez l'ancienne dans une release ultérieure. Une migration qui exige que l'ancien et le nouveau code ne tournent jamais côte à côte fait de chaque rollout une panne et de chaque rollback une impossibilité.
Alertes as code : réveiller sur le burn rate, pas sur le CPU
Le dernier morceau de code qu'un SRE possède, c'est l'alerting lui-même. Le jeu d'alarmes d'un compte AWS typique réveille sur des causes : CPU au-dessus de 80 pour cent, mémoire au-dessus de 90, tout décompte de 5xx au-dessus de 10. Il est bruyant là où rien ne va mal (un autoscaling group à 95 pour cent de CPU qui tient sa cible de latence) et silencieux là où quelque chose va mal (un déploiement qui fait échouer 2 pour cent des checkouts avec toutes les métriques d'hôte à plat). Le SLO de la Partie 2 offre une meilleure condition de réveil : dépensons-nous le budget d'erreur assez vite pour que, laissé tel quel, l'objectif soit manqué ? Cette vitesse, c'est le burn rate.
burn rate = observed error ratio / (1 - SLO)
SLO 99.9% -> budget ratio 0.001
error ratio 0.1% -> burn rate 1 -> budget lasts 30 days
error ratio 0.6% -> burn rate 6 -> budget lasts 5 days
error ratio 1.44% -> burn rate 14.4 -> budget lasts 50 hours
error ratio 100% -> burn rate 1000 -> budget lasts 43 minutesUne alerte a besoin d'un seuil et d'une fenêtre, et les deux ensemble décident de la part de budget partie au moment où elle se déclenche :
budget consumed at alert = burn rate x alert window / SLO window
14.4 x 1 hour / 720 hours = 2%
6 x 6 hours / 720 hours = 5%
1 x 3 days / 30 days = 10%Ces trois lignes sont la recommandation du chapitre sur l'alerting du Google SRE Workbook, et elles sont un défaut raisonnable pour un SLO sur 30 jours : réveiller quand 2 pour cent du budget sont partis en une heure ou 5 pour cent en six heures, et ouvrir un ticket quand 10 pour cent sont partis en trois jours. Les problèmes rapides et graves obtiennent un réveil rapide. Les fuites lentes, celles qu'une alarme à seuil n'attrape jamais, obtiennent un ticket pour le prochain jour ouvré.
Pourquoi deux fenêtres
Une seule fenêtre longue a un problème de remise à zéro. Si les erreurs ont tourné à 15 pour cent pendant dix minutes puis se sont arrêtées, la moyenne sur une heure reste au-dessus du seuil de 14,4x pendant l'essentiel de l'heure suivante. L'alarme continue de réveiller un ingénieur qui regarde un système sain.
Le correctif consiste à exiger qu'une seconde fenêtre, courte, soit au-dessus du même seuil au même moment. La règle du workbook est une fenêtre courte égale à un douzième de la longue. La fenêtre longue dit que la dépense est significative ; la courte dit qu'elle est encore en cours. Ensemble :
| Action | Fenêtre longue | Fenêtre courte | Burn rate | Ratio d'erreurs à 99,9% | Budget consommé |
| Réveil | 1 heure | 5 minutes | 14.4 | 1.44% | 2% |
| Réveil | 6 heures | 30 minutes | 6 | 0.6% | 5% |
| Ticket | 3 jours | 6 heures | 1 | 0.1% | 10% |
La détection est rapide là où elle doit l'être. Lors d'une panne totale, la moyenne sur une heure franchit 1,44 pour cent après environ 52 secondes, et la fenêtre de cinq minutes est déjà au-dessus. Quand la panne se termine, la fenêtre de cinq minutes repasse sous le seuil en quelques minutes et l'alerte se lève, même si l'heure est encore polluée.
Le construire dans CloudWatch avec Terraform
CloudWatch a les deux pièces nécessaires : les alarmes metric math, pour alerter sur un ratio d'erreurs plutôt qu'un décompte brut, et les alarmes composites, pour faire un AND entre la fenêtre longue et la courte. Une alarme métrique par fenêtre, avec une seule période d'évaluation aussi longue que la fenêtre, et aucune action attachée :
locals {
slo = 0.999
budget = 1 - local.slo
lb = "app/checkout/0123456789abcdef"
windows = {
fast_long = { period = 3600, burn = 14.4 }
fast_short = { period = 300, burn = 14.4 }
slow_long = { period = 21600, burn = 6 }
slow_short = { period = 1800, burn = 6 }
}
elb_metrics = {
req = "RequestCount"
t5xx = "HTTPCode_Target_5XX_Count"
e5xx = "HTTPCode_ELB_5XX_Count"
}
}
resource "aws_cloudwatch_metric_alarm" "burn" {
for_each = local.windows
alarm_name = "checkout-burn-${each.key}"
alarm_description = "Error ratio above ${each.value.burn}x burn over ${each.value.period}s"
comparison_operator = "GreaterThanThreshold"
threshold = each.value.burn * local.budget
evaluation_periods = 1
treat_missing_data = "notBreaching"
metric_query {
id = "ratio"
expression = "(FILL(t5xx, 0) + FILL(e5xx, 0)) / req"
label = "error ratio"
return_data = true
}
dynamic "metric_query" {
for_each = local.elb_metrics
content {
id = metric_query.key
metric {
namespace = "AWS/ApplicationELB"
metric_name = metric_query.value
stat = "Sum"
period = each.value.period
dimensions = { LoadBalancer = local.lb }
}
}
}
}Le seuil, c'est le burn rate multiplié par le ratio de budget : 14,4 x 0,001 = 0,0144 pour la paire rapide, 0,006 pour la paire lente. Passez slo à 0.9995 et chaque seuil suit. Puis l'alarme composite, la seule chose qui réveille :
resource "aws_cloudwatch_composite_alarm" "slo_page" {
alarm_name = "checkout-availability-slo-page"
alarm_description = "Burning 30-day error budget at 14.4x (1h) or 6x (6h). Runbook: https://wiki.example.com/runbooks/checkout-slo"
alarm_rule = join(" OR ", [
"(ALARM(${aws_cloudwatch_metric_alarm.burn["fast_long"].alarm_name}) AND ALARM(${aws_cloudwatch_metric_alarm.burn["fast_short"].alarm_name}))",
"(ALARM(${aws_cloudwatch_metric_alarm.burn["slow_long"].alarm_name}) AND ALARM(${aws_cloudwatch_metric_alarm.burn["slow_short"].alarm_name}))",
])
alarm_actions = [aws_sns_topic.pager.arn]
ok_actions = [aws_sns_topic.pager.arn]
}Le OR à l'intérieur d'une seule alarme composite règle aussi la suppression. Une panne franche satisfait à la fois la paire rapide et la paire lente ; comme elles alimentent une seule alarme, l'ingénieur reçoit un réveil, pas deux.
Le palier ticket à trois jours est plus simple sur la voie managée. Si le SLO vit dans CloudWatch Application Signals, vous lui attachez des fenêtres de look-back de burn rate (jusqu'à 10 080 minutes, soit sept jours) et vous alertez sur la métrique de burn rate qu'il publie. La formule de seuil d'AWS est X% x SLO interval length / look-back window, et elle cache un piège : 14,4 n'est correct que pour un intervalle de 30 jours. Deux pour cent d'un budget de 28 jours en une heure, c'est 13,44. Dérivez les seuils des pourcentages ; ne copiez jamais les multiplicateurs.
La production readiness review
Mis bout à bout, voici la revue qu'un SRE mène avec les développeurs avant qu'un service ne prenne du vrai trafic, et répète quand l'architecture change :
- Chaque appel sortant a-t-il un timeout, et les timeouts tiennent-ils dans l'échéance de l'appelant ?
- Y a-t-il exactement une couche de retry, avec backoff et jitter, et les écritures réessayées sont-elles idempotentes ?
- Pour chaque dépendance : dure ou souple, et que voit l'utilisateur quand elle est en panne ?
- Les health checks évitent-ils les dépendances partagées ?
- Le service émet-il des métriques de requêtes, d'échecs et de durée par opération, et propage-t-il un trace ID ?
- Y a-t-il un SLO, un propriétaire pour lui, et une alerte de burn rate qui réveille une rotation nommée avec un lien vers un runbook ?
- Une release peut-elle être annulée en moins de cinq minutes sans changement de schéma ?
- Quelle limite est atteinte en premier à dix fois le trafic, et que se passe-t-il alors ?
Modes de défaillance et ce qu'il faut surveiller
Le faible trafic casse l'arithmétique du burn rate. À 100 requêtes par heure, un burn de 14,4x sur un SLO à 99,9 pour cent, c'est 1,44 requête échouée, donc deux timeouts malchanceux réveillent quelqu'un. Les options du SRE Workbook sont le trafic synthétique, l'agrégation de petits services dans un seul SLO, une cible plus basse, ou une fenêtre plus longue. En metric math vous pouvez conditionner au volume avec IF(req > 200, ratio, 0), au prix d'un aveuglement sous le seuil.
Les données manquantes ne sont pas une bonne nouvelle par défaut. treat_missing_data = "notBreaching" est juste pour un ratio qui disparaît faute de trafic et faux si le trafic a disparu parce que le DNS est cassé. Associez l'alarme SLO à une alarme simple sur RequestCount tombant à zéro aux heures où cela ne devrait jamais arriver.
Les buckets ne sont pas des fenêtres glissantes. Un datapoint CloudWatch avec une période d'une heure est un bucket, donc l'alarme à fenêtre longue peut réagir plus tard qu'un calcul glissant idéal. La fenêtre courte garde la détection des incidents graves rapide ; traitez le timing des slow burns comme approximatif.
Du code de résilience jamais exercé. Un circuit breaker qui ne s'est jamais ouvert en production est une supposition. L'injection de fautes, d'abord dans un environnement de test puis en production à l'intérieur du budget d'erreur, est ce qui fait gagner la confiance aux chemins de repli.
Tout ce qui précède suppose que la plateforme tient. Timeouts et retries sont écrits contre un fournisseur qui fonctionne la plupart du temps. Ce que ce fournisseur promet réellement, où sont ses domaines de défaillance, et ce qui change quand le fournisseur est votre propre data center, c'est la Partie 4.
À lire ensuite
- Partie 4 de cette série : Cloud ou on-prem : ce qu'un SRE vérifie sous le service.
- Partie 2 de cette série : SLI, SLO, SLA, error budget : les chiffres qui font tourner le SRE.
- FinOps: Budget Alerts with Terraform Before Your Next $2.5B AWS Bill : la même idée appliquée à l'argent, alerter sur le rythme de dépense plutôt que sur la facture finale.
- Agents on Call, Part 8. Production: Observability, Evals, and the Day It Lies sur ercan.ai, pour ce à quoi ressemble l'observabilité quand la chose d'astreinte est un agent.
Pour du conseil en AWS, architecture cloud, pratique SRE, pipelines CI/CD, Linux et plus encore, commencez par ercanermis.com.
Références
- Google SRE Workbook, Chapter 5: Alerting on SLOs
- Google SRE Book, Chapter 22: Addressing Cascading Failures
- AWS Architecture Blog: Exponential Backoff And Jitter
- Amazon Builders' Library: Timeouts, retries, and backoff with jitter
- Amazon Builders' Library: Implementing health checks
- Amazon CloudWatch: Embedded metric format
- Amazon CloudWatch: Service level objectives (SLOs), burn rates and burn rate alarms
- Amazon CloudWatch: Create a composite alarm
Plus d'Ercan
Deux autres sites, même auteur, terrain différent.
IA, LLMs, agents, ML appliquée.
Notes de terrain sur les charges IA. Analyse des coûts Bedrock, patterns d'agents, compromis de stockage vectoriel, modes de défaillance en production.
Visiter ercan.ai →Le hub. À propos, conseil, contact.
Hub personnel pour les deux pistes d'écriture. Qui je suis, comment fonctionne le conseil, comment me joindre.
Visiter ercanermis.com →