{"componentChunkName":"component---src-templates-post-jsx","path":"/fr/post-mortem-cpu-spike-100-production-drupal","result":{"data":{"markdownRemark":{"html":"<p>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 : <em>\"Le site est inaccessible, nos équipes ne peuvent plus travailler.\"</em></p>\n<p>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.</p>\n<p>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.</p>\n<hr>\n<h2>Contexte de la plateforme</h2>\n<ul>\n<li>Drupal 11 sur infrastructure conteneurisée</li>\n<li>PHP 8.3 avec PHP-FPM</li>\n<li>Redis pour les sessions et le cache</li>\n<li>Varnish en reverse proxy interne</li>\n<li>Cloudflare en edge CDN</li>\n<li>New Relic pour l'observabilité applicative</li>\n<li>Blackfire disponible mais non actif en permanence</li>\n<li>Trafic habituel : ~4 000 visiteurs/jour, ~40 req/s en pic</li>\n</ul>\n<hr>\n<h2>Chronologie de l'incident</h2>\n<h3>09:47 — Première alerte New Relic</h3>\n<p>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.</p>\n<p>Première action : vérification du dashboard New Relic.</p>\n<p>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.</p>\n<h3>09:51 — Analyse des access logs</h3>\n<p>Connexion SSH sur l'environnement de production :</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">ssh</span> deploy@prod-web-01</code></pre></div>\n<p>Lecture des access logs en temps réel :</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">tail</span> -f /var/log/nginx/access.log <span class=\"token operator\">|</span> <span class=\"token function\">grep</span> -v <span class=\"token string\">\"varnishd\"</span></code></pre></div>\n<p>Observation immédiate : un volume anormal de requêtes sur des URLs avec des query strings variés. Exemple de pattern :</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">185.220.x.x - - [31/May/2026:09:48:12 +0000] &quot;GET /?page=1&amp;sort=asc&amp;filter=new HTTP/1.1&quot; 200 48291\n185.220.x.x - - [31/May/2026:09:48:12 +0000] &quot;GET /?page=2&amp;sort=desc&amp;filter=old HTTP/1.1&quot; 200 48301\n185.220.x.x - - [31/May/2026:09:48:13 +0000] &quot;GET /?page=1&amp;sort=asc&amp;filter=featured HTTP/1.1&quot; 200 48288</code></pre></div>\n<p>Plusieurs centaines de requêtes par minute depuis une plage d'IPs sur la même plage <code class=\"language-text\">/24</code>. Les query strings sont différents à chaque requête, ce qui empêche tout hit de cache.</p>\n<h3>09:54 — Confirmation : bot storm + cache miss systématique</h3>\n<p>Extraction des IPs les plus actives :</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">awk</span> <span class=\"token string\">'{print <span class=\"token variable\">$1</span>}'</span> /var/log/nginx/access.log <span class=\"token operator\">|</span> <span class=\"token function\">sort</span> <span class=\"token operator\">|</span> <span class=\"token function\">uniq</span> -c <span class=\"token operator\">|</span> <span class=\"token function\">sort</span> -rn <span class=\"token operator\">|</span> <span class=\"token function\">head</span> -20</code></pre></div>\n<p>Résultat : les 5 premières IPs cumulent 3 200 requêtes sur les 10 dernières minutes. Clairement non humain.</p>\n<p>Vérification côté Cloudflare : les requêtes arrivent bien depuis Cloudflare (headers <code class=\"language-text\">CF-Ray</code> présents), mais Cloudflare les transmet toutes. Aucune règle WAF ne bloque ce pattern.</p>\n<p>Vérification du cache Varnish :</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">varnishstat -1 -f MAIN.cache_hit,MAIN.cache_miss</code></pre></div>\n<p>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 <code class=\"language-text\">?page=1&amp;sort=asc</code> et <code class=\"language-text\">?page=1&amp;sort=desc</code> comme deux URLs différentes.</p>\n<p>Chaque miss remonte jusqu'à Drupal. Drupal génère la page complète. Le CPU explose.</p>\n<h3>10:02 — Première mesure d'urgence : blocage IP au niveau Cloudflare</h3>\n<p>Création d'une règle WAF Cloudflare en urgence :</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">(ip.src in {185.220.0.0/24}) =&gt; Block</code></pre></div>\n<p>Effet immédiat : les requêtes depuis ces IPs sont bloquées. Le CPU redescend à 60%. Pas encore normal, mais la pression diminue.</p>\n<p>Problème : les bots utilisent plusieurs plages d'IPs. En quelques minutes, de nouvelles IPs prennent le relais depuis d'autres /24.</p>\n<h3>10:09 — Deuxième mesure : challenge sur les user agents suspects</h3>\n<p>Plutôt que de chasser les IPs une à une, on passe à un ciblage par comportement. Règle Cloudflare ajoutée :</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">(http.user_agent contains &quot;python-requests&quot;) or \n(http.user_agent contains &quot;curl&quot;) or \n(http.user_agent eq &quot;&quot;) \n=&gt; Managed Challenge (CAPTCHA)</code></pre></div>\n<p>Résultat : le volume de requêtes chute de 60%. Les bots simples (scripts Python sans gestion de JS) ne passent plus.</p>\n<h3>10:14 — Troisième mesure : normalisation des query strings dans Varnish</h3>\n<p>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.</p>\n<p>Modification dans la configuration VCL Varnish :</p>\n<div class=\"gatsby-highlight\" data-language=\"vcl\"><pre class=\"language-vcl\"><code class=\"language-vcl\">sub vcl_recv {\n    # Normaliser l&#39;ordre des query strings\n    set req.url = regsuball(req.url, &quot;([?&amp;])(utm_[^&amp;]*)&quot;, &quot;&quot;);\n    set req.url = regsuball(req.url, &quot;([?&amp;])(sort|filter|page)=[^&amp;]*&quot;, &quot;&quot;);\n    \n    # Supprimer le ? final si tous les params ont été retirés\n    set req.url = regsub(req.url, &quot;\\?$&quot;, &quot;&quot;);\n}</code></pre></div>\n<blockquote>\n<p><strong>Note :</strong> 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.</p>\n</blockquote>\n<h3>10:31 — Retour à la normale</h3>\n<p>CPU redescend à 22%. Temps de réponse moyen de retour sous 400ms. Le client est informé. Le site est stable.</p>\n<p>Durée totale de l'incident : <strong>44 minutes</strong>.</p>\n<hr>\n<h2>Analyse des causes racines</h2>\n<p>L'incident a trois causes, pas une seule. C'est toujours le cas dans les incidents de production réels.</p>\n<h3>Cause 1 — Absence de rate limiting en amont</h3>\n<p>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.</p>\n<p>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.</p>\n<h3>Cause 2 — Varnish non configuré pour normaliser les query strings</h3>\n<p>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.</p>\n<p>Cette configuration aurait dû être en place dès le départ. Elle ne l'était pas.</p>\n<h3>Cause 3 — Pas de circuit breaker applicatif</h3>\n<p>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.\"</p>\n<p>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.</p>\n<hr>\n<h2>Ce qu'on aurait dû avoir en place</h2>\n<h3>Rate limiting Cloudflare</h3>\n<p>Règle simple, à mettre en place sur tout projet :</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">Chemin : /*\nSeuil : 100 requêtes par IP par minute\nAction : Managed Challenge\nDurée : 10 minutes</code></pre></div>\n<p>Pour les endpoints sensibles (<code class=\"language-text\">/admin</code>, <code class=\"language-text\">/user</code>, <code class=\"language-text\">/api</code>) : seuil beaucoup plus bas, 10 req/min, action Block directe.</p>\n<h3>Normalisation des query strings dans Varnish dès le départ</h3>\n<p>La configuration VCL doit être revue sur chaque projet pour :</p>\n<ul>\n<li>Supprimer les paramètres sans impact sur le contenu (UTM, fbclid, gclid, etc.)</li>\n<li>Normaliser l'ordre des paramètres qui font partie du cache key</li>\n<li>Définir explicitement la liste blanche des paramètres acceptés</li>\n</ul>\n<h3>Alerting sur le ratio cache miss Varnish</h3>\n<p>New Relic ou Cloudwatch peuvent monitorer <code class=\"language-text\">varnishstat</code>. 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%.</p>\n<h3>Liste de bots en surveillance</h3>\n<p>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.</p>\n<hr>\n<h2>Mesures mises en place après l'incident</h2>\n<h3>1. Rate limiting activé sur tous les environnements de production</h3>\n<p>Règle globale à 100 req/min/IP, règles spécifiques à 10 req/min sur :</p>\n<ul>\n<li><code class=\"language-text\">/admin/*</code></li>\n<li><code class=\"language-text\">/user/*</code></li>\n<li><code class=\"language-text\">/api/*</code></li>\n<li><code class=\"language-text\">/node/*/edit</code></li>\n</ul>\n<h3>2. VCL Varnish révisé avec normalisation des query strings</h3>\n<p>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.</p>\n<h3>3. Alerte New Relic sur le throughput</h3>\n<p>Déclenchement si le nombre de transactions/minute dépasse 3× la moyenne des 7 derniers jours pendant plus de 3 minutes.</p>\n<h3>4. Runbook de réponse aux incidents</h3>\n<p>Un document dans Confluence qui documente exactement quoi faire en cas de CPU spike :</p>\n<ol>\n<li>Ouvrir New Relic → Transaction overview → identifier les URLs les plus coûteuses</li>\n<li>Ouvrir les access logs → identifier les IPs anormales</li>\n<li>Bloquer les plages d'IPs dans Cloudflare → vérifier l'effet sur le CPU</li>\n<li>Si insuffisant → activer le mode \"Under Attack\" Cloudflare temporairement</li>\n<li>Si Drupal inaccessible → passer en mode maintenance + notifier le client</li>\n<li>Post-incident : analyser les logs, identifier la cause racine, documenter</li>\n</ol>\n<h3>5. Test de charge trimestriel</h3>\n<p>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.</p>\n<hr>\n<h2>Leçons tirées</h2>\n<p><strong>La plupart des incidents de production ne sont pas causés par un seul problème.</strong> 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.</p>\n<p><strong>L'alerting sur le CPU seul est insuffisant.</strong> À 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.</p>\n<p><strong>La configuration Varnish n'est pas \"set and forget\".</strong> 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.</p>\n<p><strong>Documenter pendant l'incident, pas seulement après.</strong> 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.</p>\n<h2>Conclusion</h2>\n<p>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.</p>\n<p>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.</p>\n<p>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.</p>","excerpt":"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…","frontmatter":{"date":"2026-05-31","metaDate":"2026-05-31","title":"Post-mortem : CPU spike à 100% en production Drupal — analyse et résolution","tags":["Drupal","Production","Performance","Post-mortem","New Relic","Cloudflare","Bot Traffic","Incident","Drupal 11"],"path":"/post-mortem-cpu-spike-100-production-drupal","cover":{"childImageSharp":{"fluid":{"base64":"data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAANABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAQAF/8QAFgEBAQEAAAAAAAAAAAAAAAAAAAIE/9oADAMBAAIQAxAAAAHIR1yVH//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEAAQUCX//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EABkQAAMAAwAAAAAAAAAAAAAAAAABEBEhMf/aAAgBAQABPyFR90KZP//aAAwDAQACAAMAAAAQwA//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAcEAADAAEFAAAAAAAAAAAAAAAAAREhMUFhcfD/2gAIAQEAAT8Qjkx5CpNqXUEGoYbU/9k=","aspectRatio":1.5001937233630376,"src":"/static/e94c64bca92ef8d77addbf763315523d/88110/cover.jpg","srcSet":"/static/e94c64bca92ef8d77addbf763315523d/0b320/cover.jpg 480w,\n/static/e94c64bca92ef8d77addbf763315523d/60b32/cover.jpg 960w,\n/static/e94c64bca92ef8d77addbf763315523d/88110/cover.jpg 1920w,\n/static/e94c64bca92ef8d77addbf763315523d/40175/cover.jpg 2880w,\n/static/e94c64bca92ef8d77addbf763315523d/e58c2/cover.jpg 3840w,\n/static/e94c64bca92ef8d77addbf763315523d/e5b5f/cover.jpg 3872w","srcWebp":"/static/e94c64bca92ef8d77addbf763315523d/d1a9d/cover.webp","srcSetWebp":"/static/e94c64bca92ef8d77addbf763315523d/bc3bf/cover.webp 480w,\n/static/e94c64bca92ef8d77addbf763315523d/39337/cover.webp 960w,\n/static/e94c64bca92ef8d77addbf763315523d/d1a9d/cover.webp 1920w,\n/static/e94c64bca92ef8d77addbf763315523d/fcbe1/cover.webp 2880w,\n/static/e94c64bca92ef8d77addbf763315523d/c136d/cover.webp 3840w,\n/static/e94c64bca92ef8d77addbf763315523d/228a9/cover.webp 3872w","sizes":"(max-width: 1920px) 100vw, 1920px"},"resize":{"src":"/static/e94c64bca92ef8d77addbf763315523d/c4f3a/cover.jpg"}}}}}},"pageContext":{"isCreatedByStatefulCreatePages":false,"pathSlug":"/post-mortem-cpu-spike-100-production-drupal","locale":"fr","prev":{"fields":{"locale":"fr"},"frontmatter":{"path":"/drupal-ai-content-transformation","title":"Pourquoi l’IA va transformer l’expérience éditoriale dans Drupal","tags":["Drupal 11","IA","Contribution","AI"]}},"next":null}}}