Concevoir des Systemes pour la Panne (Parce qu'Ils Vont Tomber en Panne)
Patterns de reponse aux pannes pour les systemes en production. Circuit breakers, strategies de retry, degradation gracieuse, dead letter queues, budgets de timeout et chaos engineering pour petites equipes.
La Taxonomie des Pannes
Les systemes tombent en panne de quatre facons. Chacune necessite une reponse differente.
| Type | Description | Exemple | Reponse correcte |
|---|---|---|---|
| Transitoire | Glitch bref, se resout tout seul | Timeout reseau, reset de connexion | Retry avec backoff |
| Permanente | Casse jusqu'a intervention humaine | Config invalide, mismatch de schema | Fail fast, alerte, pas de retry |
| Partielle | Certaines fonctionnalites marchent | Un fournisseur sur trois est down | Degrader gracieusement, servir ce qui marche |
| En cascade | Une panne en declenche d'autres | Surcharge de base de donnees qui fait timeout tous les services | Circuit breaker, delester la charge |
L'erreur la plus dangereuse : traiter toutes les pannes de la meme maniere. Retenter une panne permanente gaspille des ressources. Ne pas retenter une panne transitoire degrade l'experience utilisateur. Ne pas detecter une panne en cascade fait tomber tout le systeme.
Pour la gestion des pannes specifiquement dans les systemes event-driven, consulte notre guide d'architecture event-driven. Pour les pannes de systemes IA, voir notre guide des modes de defaillance IA.
Circuit Breakers
Un circuit breaker empeche un service defaillant de faire tomber tout ce qui en depend. Quand un service en aval echoue de maniere repetee, le circuit breaker "s'ouvre" et court-circuite les requetes immediatement au lieu d'attendre des timeouts.
class CircuitBreaker {
private failures = 0;
private lastFailure = 0;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private threshold: number = 5, // pannes avant ouverture
private resetTimeout: number = 30000, // ms avant de reessayer
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailure > this.resetTimeout) {
this.state = 'half-open'; // Essayer une requete
} else {
throw new CircuitOpenError('Circuit is open');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = 'closed';
}
private onFailure() {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) {
this.state = 'open';
}
}
}
// Utilisation
const supplierBreaker = new CircuitBreaker(5, 30000);
async function checkAvailability(productId: string) {
return supplierBreaker.execute(async () => {
return await supplierApi.checkAvailability(productId);
});
}
L'Erreur Classique
La plupart des equipes implementent le circuit breaker mais ne gerent pas l'etat ouvert. Quand le circuit est ouvert, qu'est-ce que l'utilisateur voit ? Une erreur 500, c'est la mauvaise reponse. La bonne reponse depend du contexte :
| Contexte | Quand le circuit s'ouvre | Comportement correct |
|---|---|---|
| Recherche produit | Un fournisseur down | Afficher les resultats des autres fournisseurs |
| Verification de prix | Service de pricing down | Afficher le prix en cache avec un label "au X" |
| Checkout | Passerelle de paiement down | Mettre la commande en file, traiter plus tard |
| Recommandation | Service ML down | Afficher les produits populaires a la place |
| Service d'images | CDN down | Afficher une image placeholder |
Strategies de Retry
Tous les retries ne se valent pas. La strategie depend du type de panne.
interface RetryConfig {
maxAttempts: number;
strategy: 'immediate' | 'fixed' | 'exponential' | 'none';
baseDelay: number; // ms
maxDelay: number; // ms
jitter: boolean; // randomiser pour eviter le thundering herd
}
const RETRY_CONFIGS: Record<string, RetryConfig> = {
// Transitoire : retry avec backoff exponentiel
network_timeout: {
maxAttempts: 3,
strategy: 'exponential',
baseDelay: 1000, // 1s, 2s, 4s
maxDelay: 10000,
jitter: true,
},
// Rate limited : retry avec delai fixe
rate_limited: {
maxAttempts: 5,
strategy: 'fixed',
baseDelay: 5000, // attendre 5s entre les tentatives
maxDelay: 5000,
jitter: false,
},
// Conflit de lock optimiste : retry immediat
lock_conflict: {
maxAttempts: 3,
strategy: 'immediate',
baseDelay: 0,
maxDelay: 0,
jitter: false,
},
// Panne permanente : pas de retry
validation_error: {
maxAttempts: 1,
strategy: 'none',
baseDelay: 0,
maxDelay: 0,
jitter: false,
},
};
async function retryWithStrategy<T>(
fn: () => Promise<T>,
config: RetryConfig,
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt === config.maxAttempts - 1) break;
if (config.strategy === 'none') break;
let delay = config.baseDelay;
if (config.strategy === 'exponential') {
delay = Math.min(config.baseDelay * Math.pow(2, attempt), config.maxDelay);
}
if (config.jitter) {
delay += Math.random() * delay * 0.5; // 0-50% de jitter
}
await sleep(delay);
}
}
throw lastError;
}
Le jitter est critique. Sans lui, tous les clients retentent en meme temps apres une panne (thundering herd). Le jitter repartit les retries sur une fenetre de temps, reduisant le pic sur le service en cours de recuperation.
Degradation Gracieuse
Quand une dependance tombe en panne, sers ce que tu peux au lieu de planter completement.
async function getProductPage(productId: string): Promise<ProductPageData> {
// Donnees critiques : doit reussir
const product = await productService.getById(productId);
if (!product) throw new NotFoundError();
// Donnees non critiques : degrader gracieusement
const [reviews, recommendations, availability] = await Promise.allSettled([
reviewService.getForProduct(productId),
recommendationService.getSimilar(productId),
inventoryService.checkStock(productId),
]);
return {
product,
reviews: reviews.status === 'fulfilled' ? reviews.value : [],
recommendations: recommendations.status === 'fulfilled' ? recommendations.value : [],
availability: availability.status === 'fulfilled'
? availability.value
: { status: 'unknown', message: 'Check availability in store' },
};
}
Promise.allSettled est la cle. Contrairement a Promise.all, elle n'echoue pas si une promesse est rejetee. Chaque resultat est resolu independamment. La page produit s'affiche avec les donnees disponibles.
Des Donnees Perimees Valent Mieux que Pas de Donnees
async function getProductPrice(productId: string): Promise<PriceInfo> {
try {
const livePrice = await pricingService.getPrice(productId);
await cache.set(`price:${productId}`, livePrice, { ttl: 300 });
return livePrice;
} catch (error) {
// Service de pricing down : servir le prix en cache
const cached = await cache.get(`price:${productId}`);
if (cached) {
return { ...cached, stale: true, staleSince: cached.cachedAt };
}
// Pas de cache non plus : retourner le prix catalogue
const product = await productService.getById(productId);
return { price: product.listPrice, stale: true, approximate: true };
}
}
Un prix perime du cache vaut mieux qu'une erreur 500. Un prix catalogue approximatif vaut mieux que pas de prix du tout. Aie toujours un fallback, meme s'il est moins precis.
Budgets de Timeout
Chaque operation a un budget de temps. Si le budget est depasse, echoue rapidement au lieu de faire attendre l'utilisateur indefiniment.
const TIMEOUT_BUDGETS = {
api_request: 5000, // 5s au total pour toute requete API
database_query: 2000, // 2s pour toute requete base de donnees
external_api: 3000, // 3s pour les appels de services externes
llm_generation: 30000, // 30s pour la generation IA (streaming)
search_query: 1000, // 1s pour la recherche
cache_operation: 100, // 100ms pour lecture/ecriture cache
};
async function withTimeout<T>(promise: Promise<T>, budget: number, label: string): Promise<T> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => reject(new TimeoutError(`${label} exceeded ${budget}ms budget`)), budget);
});
return Promise.race([promise, timeout]);
}
// Utilisation
const results = await withTimeout(
searchService.query(userQuery),
TIMEOUT_BUDGETS.search_query,
'product_search',
);
Protection Contre les Timeouts en Cascade
Quand le service A appelle le service B qui appelle le service C, chaque service doit soustraire son propre temps de traitement du budget restant :
API Gateway (budget de 5s)
└── Verification auth (100ms utilise, 4.9s restant)
└── Service produit (200ms utilise, 4.7s restant)
└── Service pricing (timeout : 4.7s, pas les 5s d'origine)
Sans budgets en cascade, le service de pricing utilise un timeout complet de 3s meme si le gateway n'a plus que 4.7s. Si le pricing prend 3s, le gateway fait timeout avant que la reponse n'arrive, gaspillant tout le travail effectue.
Tester les Pannes
Chaos Engineering pour Petites Equipes
Tu n'as pas besoin du Chaos Monkey de Netflix pour tester la gestion des pannes. Commence par de l'injection de fautes simple :
// Middleware : injecter des pannes en staging
function chaosMiddleware(req: Request, res: Response, next: NextFunction) {
if (process.env.NODE_ENV !== 'staging') return next();
const chaos = req.headers['x-chaos'];
if (chaos === 'latency') {
setTimeout(next, 3000); // Ajouter 3s de latence
} else if (chaos === 'error') {
res.status(500).json({ error: 'Chaos: injected failure' });
} else if (chaos === 'timeout') {
// Ne pas repondre du tout (simuler un service bloque)
} else {
next();
}
}
Scenarios de test qui comptent :
| Scenario | Comment tester | Quoi verifier |
|---|---|---|
| Base de donnees down | Arreter la base de donnees en staging | Le circuit breaker s'ouvre, les donnees en cache sont servies |
| Dependance lente | Injecter 5s de latence | Le timeout se declenche, une reponse degradee est retournee |
| File d'attente pleine | Remplir la file avec des messages de test | La backpressure s'applique, pas de perte de donnees |
| Pression memoire | Limiter la memoire du conteneur | Gestion OOM, redemarrage gracieux |
| Expiration de certificat | Utiliser un certificat court en staging | L'alerte se declenche avant l'expiration |
Pieges Courants
-
Meme strategie de retry pour toutes les pannes. Un timeout a besoin de backoff. Un input invalide a besoin de zero retries. Un rate limit a besoin d'un delai fixe.
-
Circuit breaker sans fallback. Ouvrir le circuit et retourner 500, ce n'est pas de la tolerance aux pannes. Sers des donnees en cache, des resultats degrades ou une reponse mise en file.
-
Pas de jitter sur les retries. Tous les clients retentent en meme temps, submergeant le service en cours de recuperation. Ajoute du jitter aleatoire.
-
Timeouts infinis. Une requete qui attend indefiniment bloque une connexion et un thread. Chaque operation a besoin d'un budget de timeout.
-
Tester uniquement le happy path. Si tu n'as jamais teste ce qui se passe quand la base de donnees est down, tu ne sais pas si tes fallbacks fonctionnent.
-
Pannes en cascade a cause de dependances partagees. Si les services A, B et C dependent tous de la meme base de donnees, et que cette base est lente, les trois services deviennent lents. Des circuit breakers sur les dependances partagees empechent la cascade.
Points Cles
-
Classifie les pannes avant de reagir. Transitoire (retry), permanente (fail fast), partielle (degrader), en cascade (circuit break). Chaque type necessite une strategie differente.
-
Les circuit breakers ont besoin de fallbacks. Ouvrir le circuit n'est pas la solution. Servir des donnees en cache, des resultats alternatifs ou des reponses mises en file, ca c'est la solution.
-
Le jitter empeche le thundering herd. Randomise les delais de retry. Sans jitter, les retries synchronises rendent la recuperation plus difficile.
-
Des donnees perimees valent mieux que pas de donnees. Un prix en cache d'il y a 5 minutes vaut mieux qu'une erreur 500. Aie toujours un chemin de fallback.
-
Les budgets de timeout sont en cascade. Chaque service dans la chaine soustrait son temps de traitement du budget restant. Ne laisse pas les services internes utiliser plus de temps que le service externe n'en a.
-
Teste les pannes en staging. De l'injection de fautes simple (latence, erreurs, connexions bloquees) verifie que tes patterns de resilience fonctionnent vraiment.
Nous concevons des systemes resilients dans le cadre de nos pratiques de logiciels sur mesure et cloud. Si tu as besoin d'aide en ingenierie de fiabilite, parle a notre equipe ou demande un devis.
Sujets couverts
Guides connexes
Modes de Defaillance de l'IA : Guide d'Ingenierie Production
Guide technique sur les defaillances IA en production. Decouvre les hallucinations, limites de contexte, injection de prompt et derive du modele.
Lire le guideArchitecture Event-Driven en pratique : ce qui tourne vraiment mal
Patterns réels d'architecture event-driven en production. Event storms, boucles de sync bidirectionnelle, dead letters, stores d'idempotence, et choix entre Kafka, RabbitMQ, BullMQ et Symfony Messenger.
Lire le guideGuide Entreprise des Systèmes d'IA Agentiques
Guide technique des systemes d'IA agentiques en entreprise. Decouvre l'architecture, les capacites et les applications des agents IA autonomes.
Lire le guidePrêt à construire des systèmes IA prêts pour la production ?
Notre équipe est spécialisée dans les systèmes IA prêts pour la production. Discutons de comment nous pouvons aider.
Démarrer une conversation