Les 10 points à auditer avant de scaler votre application
Scaler n'est pas seulement ajouter des serveurs. Avant de jeter des ressources sur le problème, auditez ces 10 zones qui tuent les performances en production.
La plupart des problèmes de scalabilité ne sont pas des problèmes d'infrastructure. Ce sont des problèmes de code et d'architecture. Ajouter des serveurs à une application mal optimisée la fait juste tomber plus vite, avec une facture plus élevée. Voici la checklist à passer avant de provisionner quoi que ce soit, parce que la capacité la moins chère est celle que vous gaspilliez.
Comment mener cet audit
Instrumentez d'abord. Il vous faut de vrais chiffres de production, ou un load test proche de la production, pas des suppositions. Un outil APM comme Sentry, Datadog ou New Relic pour les traces de requêtes, le query logging ou le slow query log de la base pour la couche données, et un load test avec k6 ou Locust pour reproduire la charge. Ensuite, passez les dix points ci-dessous contre ce que les données montrent vraiment, à peu près dans cet ordre, parce que c'est à peu près l'ordre d'impact.
Les dix points en un coup d'oeil
| # | Point | Comment détecter | Correction |
|---|---|---|---|
| 1 | Requêtes N+1 | Nombre de requêtes par requête HTTP dans l'APM ou la debug bar | Eager loading |
| 2 | Index manquants | EXPLAIN ANALYZE sur les requêtes les plus lentes | Indexer les colonnes de WHERE, JOIN, ORDER BY |
| 3 | Travail synchrone dans la requête | Endpoints lents qui font email, PDF, image | Le déplacer dans une queue |
| 4 | Cache absent | Pas de headers Cache-Control, requêtes identiques répétées | Cache HTTP, CDN, cache de requêtes et de pages |
| 5 | Requêtes non bornées | Endpoints sans pagination, appels all() | Toujours paginer et plafonner la taille de page |
| 6 | Session et cache sur disque local | driver file dans la config | Passer à Redis avant le second serveur |
| 7 | Caches du framework pas activés | caches config, route, view absents en prod | Les activer dans l'étape de déploiement |
| 8 | Surcoût de logging | niveau debug en production, disques qui se remplissent | niveau warning, sampling, logs envoyés hors machine |
| 9 | Résolution de services lourde | Clients coûteux construits dans chaque constructeur | Résolution paresseuse ou binding en singleton |
| 10 | Fuites mémoire dans les workers | Mémoire du worker qui grimpe sur des heures | Redémarrage max-time ou max-jobs, profiler sur 24h |
Le reste de l'article, c'est le détail derrière chaque ligne.
1. Requêtes N+1
La cause la plus courante d'une page de liste lente. Vous chargez une collection, puis vous bouclez dessus en touchant une relation, et l'ORM tire une requête par ligne. Une page qui affiche 50 commandes avec leur client fait 51 requêtes au lieu de 2. Détectez-le en comptant les requêtes par requête HTTP dans votre APM ou votre debug bar, ou activez le mode strict en développement pour que le lazy loading lève une exception au lieu de cacher le problème.
// N+1 : une requête pour les commandes, puis une par commande pour le client
$orders = Order::latest()->take(50)->get();
foreach ($orders as $order) {
echo $order->customer->name; // tire une requête à chaque itération
}
// Corrigé : deux requêtes au total, les clients chargés d'emblée
$orders = Order::with('customer')->latest()->take(50)->get();2. Index manquants
Lancez EXPLAIN ANALYZE sur vos requêtes les plus lentes et cherchez un sequential scan sur une grosse table. Indexez les colonnes sur lesquelles vous filtrez, joignez et triez, en particulier les clés étrangères, que beaucoup de frameworks n'indexent pas automatiquement. Utilisez un index composite quand vous filtrez sur plusieurs colonnes ensemble, dans l'ordre où la requête les utilise. N'indexez pas tout : chaque index ralentit les écritures et prend de la place.
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE tenant_id = 42 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;
-- Seq Scan sur une table de 10M lignes -> ajouter un index correspondant
CREATE INDEX idx_orders_tenant_status_created
ON orders (tenant_id, status, created_at DESC);3. Travail synchrone dans le cycle de requête
Tout ce dont l'utilisateur n'a pas besoin dans la réponse ne devrait pas s'exécuter avant l'envoi : envoyer un email, générer un PDF ou un rapport, redimensionner une image, appeler une API tierce lente, déclencher un webhook. Poussez-le dans une queue, Redis avec un worker ou SQS, et retournez immédiatement. Le handler de requête ne doit faire que ce qui est nécessaire pour rendre la réponse.
4. Cache HTTP et applicatif manquant
- ✓Couche HTTP : headers Cache-Control et ETag sur les réponses cachables, un CDN devant les assets statiques et devant les pages que les utilisateurs anonymes partagent.
- ✓Couche applicative : cachez le résultat des requêtes coûteuses et des valeurs calculées, et les pages entières pour le trafic déconnecté.
- ✓Invalidation : tranchez d'emblée entre un TTL court et une invalidation par événement. Un cache que vous ne savez pas invalider correctement est un bug qui attend d'être livré.
5. Requêtes et réponses non bornées
User::all() sur une table de 500k lignes, un endpoint d'API qui renvoie tous les enregistrements, un rapport qui charge une table entière en mémoire. Chacun marche très bien en staging et s'écroule en production. Paginez toujours, plafonnez toujours la taille de page, et utilisez la pagination par curseur quand l'offset devient grand.
6. Stockage de session et de cache qui ne scale pas
Les sessions et le cache sur fichier vivent sur une seule machine. Dès que vous ajoutez un second serveur applicatif, les utilisateurs rebondissent entre les deux et perdent leur session, et chaque serveur a un cache froid. Passez les sessions et le cache sur Redis ou un autre store partagé avant de scaler horizontalement, pas après l'incident.
7. Caches du framework pas activés en production
# Laravel : à lancer dans l'étape de déploiement
php artisan config:cache
php artisan route:cache
php artisan view:cacheL'équivalent ailleurs : OPcache activé avec une limite mémoire raisonnable, et le preloading si votre stack le supporte. Ce sont des gains gratuits que beaucoup de déploiements oublient tout simplement.
8. Surcoût de logging et d'observabilité
Le logging niveau debug en production écrit des millions de lignes, remplit les disques et ajoute de la latence à chaque requête. Passez le niveau à warning ou error en production, échantillonnez les événements à fort volume, et envoyez les logs hors machine vers un système que vous pouvez réellement interroger. Des logs structurés, pas du texte libre, pour pouvoir les filtrer quand quelque chose casse.
9. Travail lourd dans les constructeurs et la résolution de services
Un service qui construit un client S3, un client HTTP ou un moteur de rapport dans son constructeur paie ce coût à chaque requête qui le résout, même quand la méthode qui en a besoin n'est jamais appelée. Résolvez-le paresseusement, bindez-le en singleton pour qu'il soit construit une fois, ou injectez une closure qui diffère la construction jusqu'au premier usage.
10. Fuites mémoire dans les processus longue durée
Les workers de queue, les schedulers et les démons gardent le processus vivant à travers de nombreux jobs, donc une petite fuite invisible dans un cycle requête-réponse se cumule sur des heures jusqu'à ce que le worker soit tué. Profilez les workers sur une journée entière, pas un seul job. Réglez max-time ou max-jobs pour qu'ils redémarrent proprement sur une cadence, et surveillez la mémoire résidente comme une métrique.
Le 80/20 de la performance de scale
L'essentiel du gain est dans la couche données. Corrigez les requêtes N+1, ajoutez les index manquants, et mettez un cache devant le travail coûteux, et vous obtenez en général un gros gain de débit sans serveur supplémentaire. Le scale d'infrastructure, c'est ce que vous faites après que le code et les requêtes sont propres, pas à la place.
Instrumentez avant d'optimiser. Mettez votre effort sur le goulot d'étranglement que les données montrent, pas sur celui que l'équipe suppose. Les deux sont différents plus souvent qu'on ne le croit.
Quand vous avez vraiment besoin de plus d'infrastructure
Une fois l'audit propre et si vous atteignez encore des limites, les étapes suivantes habituelles sont : des read replicas pour une charge à dominante lecture, le scale horizontal de l'applicatif maintenant que sessions et cache sont partagés, un cluster de queue dédié, un pooler de connexions comme PgBouncer devant la base, et le sharding seulement en dernier recours quand un primaire unique ne peut vraiment plus suivre.
FAQ
Que vérifier avant de scaler une application ?
Les dix points ci-dessus, dans l'ordre : requêtes N+1, index manquants, travail synchrone dans la requête, lacunes de cache, requêtes non bornées, stockage de session non scalable, caches du framework, surcoût de logging, résolution de services lourde, et fuites mémoire dans les workers. Instrumentez d'abord pour corriger le goulot d'étranglement que les données pointent, pas celui que vous supposez.
Scaler veut-il toujours dire ajouter des serveurs ?
Non, et ça ne devrait en général pas être le premier réflexe. La plupart des problèmes de scale sont des problèmes de code et d'architecture. Ajouter des serveurs à une application non optimisée augmente la facture et déplace le goulot d'étranglement sans le supprimer.
Comment trouver le vrai goulot d'étranglement ?
Avec des données, pas de l'intuition. Un outil APM pour les traces de requêtes, le query logging ou le slow query log pour la base, et un load test pour reproduire la charge. Le goulot d'étranglement est rarement là où l'équipe regarde en premier.
Le scale est un résultat d'un code propre et d'une couche données propre, pas un substitut. Menez l'audit, corrigez ce que les chiffres pointent, et provisionnez de l'infrastructure seulement une fois que l'application l'a mérité.
Besoin d'aide sur ce sujet ? Audit Technique
Découvrir ce service →