Post-mortem : CPU spike à 100% en production Drupal — analyse et résolution

Il est 9h47 un mardi matin. Une notification New Relic arrive : CPU à 94% sur les serveurs de production. Trente secondes plus tard, une deuxième alerte : temps de réponse moyen à 8 secondes. Puis un message Slack du client : "Le site est inaccessible, nos équipes ne peuvent plus travailler."

Ce post-mortem est un scénario illustratif, construit à partir de patterns que l'on rencontre régulièrement sur des plateformes Drupal en production. La chronologie, les commandes, les décisions et surtout les erreurs sont représentatives de ce type d'incident.

L'objectif n'est pas de montrer que tout se passe toujours bien. L'objectif est de documenter comment un tel incident se déroule, pourquoi, et ce qu'on met en place pour que ça ne se reproduise plus.


Contexte de la plateforme

  • Drupal 11 sur infrastructure conteneurisée
  • PHP 8.3 avec PHP-FPM
  • Redis pour les sessions et le cache
  • Varnish en reverse proxy interne
  • Cloudflare en edge CDN
  • New Relic pour l'observabilité applicative
  • Blackfire disponible mais non actif en permanence
  • Trafic habituel : ~4 000 visiteurs/jour, ~40 req/s en pic

Chronologie de l'incident

09:47 — Première alerte New Relic

CPU à 94% sur le conteneur applicatif. L'alerte est configurée sur un seuil à 80% pendant plus de 2 minutes. Le déclenchement signifie que la situation dure depuis un moment avant même la notification.

Première action : vérification du dashboard New Relic.

Constat immédiat : le nombre de transactions web en cours a triplé par rapport à la normale. Le throughput est passé de 40 req/s à environ 140 req/s en l'espace de 15 minutes.

09:51 — Analyse des access logs

Connexion SSH sur l'environnement de production :

ssh deploy@prod-web-01

Lecture des access logs en temps réel :

tail -f /var/log/nginx/access.log | grep -v "varnishd"

Observation immédiate : un volume anormal de requêtes sur des URLs avec des query strings variés. Exemple de pattern :

185.220.x.x - - [31/May/2026:09:48:12 +0000] "GET /?page=1&sort=asc&filter=new HTTP/1.1" 200 48291
185.220.x.x - - [31/May/2026:09:48:12 +0000] "GET /?page=2&sort=desc&filter=old HTTP/1.1" 200 48301
185.220.x.x - - [31/May/2026:09:48:13 +0000] "GET /?page=1&sort=asc&filter=featured HTTP/1.1" 200 48288

Plusieurs centaines de requêtes par minute depuis une plage d'IPs sur la même plage /24. Les query strings sont différents à chaque requête, ce qui empêche tout hit de cache.

09:54 — Confirmation : bot storm + cache miss systématique

Extraction des IPs les plus actives :

awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

Résultat : les 5 premières IPs cumulent 3 200 requêtes sur les 10 dernières minutes. Clairement non humain.

Vérification côté Cloudflare : les requêtes arrivent bien depuis Cloudflare (headers CF-Ray présents), mais Cloudflare les transmet toutes. Aucune règle WAF ne bloque ce pattern.

Vérification du cache Varnish :

varnishstat -1 -f MAIN.cache_hit,MAIN.cache_miss

Ratio de cache miss anormalement élevé : 78% de miss au lieu du 15% habituel. Explication : les query strings inconnus font sortir chaque requête du cache. Varnish ne normalise pas les paramètres — il traite ?page=1&sort=asc et ?page=1&sort=desc comme deux URLs différentes.

Chaque miss remonte jusqu'à Drupal. Drupal génère la page complète. Le CPU explose.

10:02 — Première mesure d'urgence : blocage IP au niveau Cloudflare

Création d'une règle WAF Cloudflare en urgence :

(ip.src in {185.220.0.0/24}) => Block

Effet immédiat : les requêtes depuis ces IPs sont bloquées. Le CPU redescend à 60%. Pas encore normal, mais la pression diminue.

Problème : les bots utilisent plusieurs plages d'IPs. En quelques minutes, de nouvelles IPs prennent le relais depuis d'autres /24.

10:09 — Deuxième mesure : challenge sur les user agents suspects

Plutôt que de chasser les IPs une à une, on passe à un ciblage par comportement. Règle Cloudflare ajoutée :

(http.user_agent contains "python-requests") or 
(http.user_agent contains "curl") or 
(http.user_agent eq "") 
=> Managed Challenge (CAPTCHA)

Résultat : le volume de requêtes chute de 60%. Les bots simples (scripts Python sans gestion de JS) ne passent plus.

10:14 — Troisième mesure : normalisation des query strings dans Varnish

Le vrai problème de fond reste entier : Varnish ne normalise pas les query strings. Même avec les bots bloqués, un crawl Google mal configuré ou un partenaire qui ajoute des UTM à chaque requête peut reproduire le même effet.

Modification dans la configuration VCL Varnish :

sub vcl_recv {
    # Normaliser l'ordre des query strings
    set req.url = regsuball(req.url, "([?&])(utm_[^&]*)", "");
    set req.url = regsuball(req.url, "([?&])(sort|filter|page)=[^&]*", "");
    
    # Supprimer le ? final si tous les params ont été retirés
    set req.url = regsub(req.url, "\?$", "");
}

Note : cette modification a nécessité un redéploiement et a été testée en staging avant mise en production. On ne modifie pas Varnish à chaud sans filet.

10:31 — Retour à la normale

CPU redescend à 22%. Temps de réponse moyen de retour sous 400ms. Le client est informé. Le site est stable.

Durée totale de l'incident : 44 minutes.


Analyse des causes racines

L'incident a trois causes, pas une seule. C'est toujours le cas dans les incidents de production réels.

Cause 1 — Absence de rate limiting en amont

Cloudflare était configuré en mode proxy standard, sans règle de rate limiting activée. N'importe quelle IP pouvait envoyer autant de requêtes qu'elle voulait sans friction.

Le rate limiting Cloudflare est disponible sur tous les plans payants. Il n'était pas activé parce que "ça n'avait jamais posé de problème". Erreur classique.

Cause 2 — Varnish non configuré pour normaliser les query strings

Par défaut, Varnish considère chaque combinaison unique de query strings comme une URL distincte. Sans normalisation, un bot qui varie ses paramètres à chaque requête contourne le cache à 100%, même si le contenu rendu est identique.

Cette configuration aurait dû être en place dès le départ. Elle ne l'était pas.

Cause 3 — Pas de circuit breaker applicatif

Quand Drupal reçoit plus de requêtes qu'il ne peut en traiter, il continue de les accepter jusqu'à saturation du CPU. Il n'existe pas, nativement dans Drupal, de mécanisme qui dit : "Je suis saturé, je retourne un 503 temporaire plutôt que de continuer à me noyer."

Ce comportement crée une spirale : plus le CPU est chargé, plus chaque requête prend de temps à traiter, plus le nombre de requêtes en attente augmente, plus le CPU monte.


Ce qu'on aurait dû avoir en place

Rate limiting Cloudflare

Règle simple, à mettre en place sur tout projet :

Chemin : /*
Seuil : 100 requêtes par IP par minute
Action : Managed Challenge
Durée : 10 minutes

Pour les endpoints sensibles (/admin, /user, /api) : seuil beaucoup plus bas, 10 req/min, action Block directe.

Normalisation des query strings dans Varnish dès le départ

La configuration VCL doit être revue sur chaque projet pour :

  • Supprimer les paramètres sans impact sur le contenu (UTM, fbclid, gclid, etc.)
  • Normaliser l'ordre des paramètres qui font partie du cache key
  • Définir explicitement la liste blanche des paramètres acceptés

Alerting sur le ratio cache miss Varnish

New Relic ou Cloudwatch peuvent monitorer varnishstat. Un ratio de cache miss qui dépasse 40% sur une fenêtre de 5 minutes devrait déclencher une alerte, pas attendre que le CPU soit à 94%.

Liste de bots en surveillance

Maintenir une liste des user agents connus pour le scraping agressif et créer une règle Cloudflare qui les envoie systématiquement en Managed Challenge. Cette liste se construit au fil des incidents — autant commencer tôt.


Mesures mises en place après l'incident

1. Rate limiting activé sur tous les environnements de production

Règle globale à 100 req/min/IP, règles spécifiques à 10 req/min sur :

  • /admin/*
  • /user/*
  • /api/*
  • /node/*/edit

2. VCL Varnish révisé avec normalisation des query strings

Liste blanche des paramètres autorisés dans le cache key. Tout paramètre non listé est supprimé avant que la requête atteigne le cache.

3. Alerte New Relic sur le throughput

Déclenchement si le nombre de transactions/minute dépasse 3× la moyenne des 7 derniers jours pendant plus de 3 minutes.

4. Runbook de réponse aux incidents

Un document dans Confluence qui documente exactement quoi faire en cas de CPU spike :

  1. Ouvrir New Relic → Transaction overview → identifier les URLs les plus coûteuses
  2. Ouvrir les access logs → identifier les IPs anormales
  3. Bloquer les plages d'IPs dans Cloudflare → vérifier l'effet sur le CPU
  4. Si insuffisant → activer le mode "Under Attack" Cloudflare temporairement
  5. Si Drupal inaccessible → passer en mode maintenance + notifier le client
  6. Post-incident : analyser les logs, identifier la cause racine, documenter

5. Test de charge trimestriel

Un test k6 ou Gatling sur l'environnement de staging pour vérifier que la stack tient à 3× le trafic nominal. Ce test aurait révélé la faiblesse de la normalisation des query strings bien avant l'incident.


Leçons tirées

La plupart des incidents de production ne sont pas causés par un seul problème. Ici, le bot était le déclencheur, mais le vrai problème était l'absence de rate limiting et la mauvaise configuration du cache. Sans le bot, la plateforme était stable. Mais elle était fragile.

L'alerting sur le CPU seul est insuffisant. À 94% de CPU, il est trop tard. Il faut alerter en amont : ratio de cache miss, throughput anormal, temps de réponse P95 qui monte. Le CPU est une conséquence, pas la cause.

La configuration Varnish n'est pas "set and forget". Elle doit être revue à chaque projet, avec une attention particulière sur la normalisation des URLs. Un template de VCL de base devrait faire partie du kit de démarrage de tout projet Drupal avec Varnish.

Documenter pendant l'incident, pas seulement après. Pendant la résolution, on prenait des notes dans un fil Slack dédié. Ce post-mortem est construit à partir de ces notes. Sans elles, la chronologie aurait été impossible à reconstituer précisément.

Conclusion

Un CPU spike à 100% en production est stressant. Il le sera toujours. Mais la différence entre une équipe qui gère l'incident en 44 minutes avec un plan de prévention solide, et une équipe qui passe 4 heures à chercher la cause dans le noir, c'est la préparation.

Rate limiting, normalisation des query strings, alerting multi-niveaux, et runbook documenté : ce sont quatre éléments simples qui coûtent peu à mettre en place et qui peuvent épargner beaucoup de dégâts — techniques, commerciaux, et relationnels.

Les plateformes Drupal enterprise ne tombent généralement pas à cause d'un bug dans le core. Elles tombent parce qu'un élément de leur stack n'était pas configuré pour résister à un scénario imprévu. Ce post-mortem en est un exemple concret.