Concours EMI study materials
Concours EMI study resources

Explosion de requêtes sur un service de commandes : cache, réplica ou sharding ?

Concours EMI · Put your knowledge to work in context.

All case studies

Explosion de requêtes sur un service de commandes : cache, réplica ou sharding ?

Une place de marché traite 2 500 commandes par seconde en pic sur un service « commandes » écrit en Java/Spring et adossé à une base PostgreSQL de 400 Go. Le SLO de latence est de 400 ms au p95 ; il est mesuré à 1 800 ms depuis trois semaines. Le pool de connexions est plafonné à 100 et saturé en permanence ; le CPU de la base tourne à 85 %. L'analyse des traces montre que l'affichage d'une commande déclenche une requête principale puis une requête par ligne d'article, avec en moyenne 40 lignes. Un cache Redis existe, avec un TTL de 30 secondes et un taux de succès de 12 %. Un incident récent a montré des commandes affichées obsolètes pendant 30 secondes après une modification, ce qui a provoqué des réclamations clients. L'équipe d'architecture propose de sharder la base par client_id. 90 % du trafic est de la lecture.

1. Quantifiez la charge réellement générée par l'endpoint et montrez en quoi le cache actuel est inadapté.

Read the worked answer

Charge réelle. 1 requête HTTP = 1 requête principale + 40 requêtes de lignes = 41 requêtes SQL. À 2 500 req/s : 2 500 × 41 ≈ 102 500 requêtes SQL par seconde.

Saturation du pool. Avec 100 connexions et un temps moyen de 2 ms par requête, le débit théorique maximal du pool est 100 / 0,002 = 50 000 req/s. On demande le double : la file d'attente s'allonge, et par la loi de Little le temps de séjour explose de façon non linéaire dès que l'utilisation dépasse 80 %. La latence de 1 800 ms n'est pas un problème de base lente, c'est un problème d'embouteillage.

Pourquoi le cache ne sert à rien ici. Trois causes cumulatives :

  • Granularité : si la clé de cache est la commande individuelle, chaque commande est consultée une fois puis n'est plus jamais relue - un taux de succès de 12 % est le signe d'une cardinalité mal alignée sur les usages réels (pages de suivi, listes d'un même client).
  • TTL de 30 s : trop court pour amortir, trop long pour la fraîcheur, il cumule les inconvénients des deux régimes.
  • Coût de fabrication : le cache n'est alimenté qu'après avoir payé les 41 requêtes. On met en cache le résultat d'un calcul qui a déjà saturé la base.

2. Proposez un ordre d'actions, en justifiant le report du sharding.

Read the worked answer

1. Corriger le N+1 (quick win, jours). Remplacer les 40 requêtes par une jointure ou un chargement par lot (fetch join, ou deux requêtes : entêtes puis lignes en IN). La charge passe de 102 500 à environ 2 500 à 2 750 req/s. Le pool de 100 connexions n'est plus saturé : le problème de latence disparaît sans changer l'architecture.

2. Recalibrer le pool et surveiller (semaines). Pool dimensionné sur la concurrence réelle, timeout court, métriques de temps d'attente. Vérifier les index sur les colonnes de filtrage des commandes.

3. Refaire le cache correctement. Clé alignée sur l'usage réel (commande par identifiant, avec invalidation à l'écriture), TTL adapté à la nature de la donnée, mesure du taux de succès avant/après.

4. Réplica de lecture pour les 90 % de lectures non critiques (listes, historique) avec une tolérance de réplication explicitement acceptée par le métier.

5. Sharding : reporté, voire rejeté. Trois arguments : 400 Go n'est pas un volume qui justifie un sharding PostgreSQL ; les requêtes traversantes par client_id deviennent coûteuses (jointures inter-shards, transactions distribuées, rééquilibrage) ; et surtout, le sharding ne corrige pas un N+1 - il multiplierait par 40 le nombre de connexions à gérer sur N shards. Sharder une requête mal écrite, c'est déplacer le problème en le rendant irréversible.

3. Comment garantir au client qu'il voit sa commande à jour, tout en conservant le bénéfice du cache ?

Read the worked answer

Le conflit vient d'avoir traité la fraîcheur comme un paramètre global (TTL de 30 s) au lieu d'une exigence par usage.

Séparer les lectures.

  • Lecture propriétaire (le client consulte sa propre commande, notamment juste après une modification) : lecture sur le primaire, ou sur le réplica uniquement si le numéro de version ou le LSN confirme que la réplication a dépassé l'écriture. C'est le principe du read-your-writes.
  • Lectures non critiques (catalogue de commandes, historique ancien) : cache et réplica acceptés, avec une tolérance de retard affichée et documentée.

Invalidation à l'écriture plutôt que TTL. Publier un événement de mise à jour (outbox transactionnelle ou CDC) qui invalide ou réécrit la clé concernée. Le TTL devient un filet de sécurité, pas le mécanisme principal.

Versionnement. Associer un numéro de version à chaque commande, renvoyé dans l'ETag ; si la version en cache est antérieure à celle que le client connaît, on va à la source. Cela rend le comportement déterministe et testable.

À énoncer explicitement au métier : la fraîcheur forte a un coût (charge sur le primaire) ; elle doit être réservée aux chemins où l'obsolescence a un impact commercial, pas appliquée à toute la plateforme. C'est cet arbitrage, écrit et validé, qui évite de reproduire l'incident.