WordPress 504 Gateway Timeout : base de données ou PHP ? Comment le prouver

Cet article vous a aidé ?

Les 504 sont le pire type d’indisponibilité : ni un plantage clair, ni une jolie page d’erreur, juste un proxy qui hausse les épaules devant vos utilisateurs pendant que votre Slack se remplit de « le site tourne en rond ». WordPress rend la chose encore plus amusante parce que la panne peut se situer à trois endroits à la fois : le proxy web, PHP-FPM et la base de données, qui se renvoient la responsabilité.

Ceci est une approche orientée production pour prouver si votre 504 Gateway Timeout vient de la couche base de données (MySQL/MariaDB) ou de la couche PHP (PHP-FPM / mod_php). Pas de conjectures. Pas de « je l’ai redémarré et c’est reparti ». Des preuves que vous pouvez coller dans un canal d’incident et à partir desquelles prendre une décision.

Le seul modèle mental dont vous avez besoin pour les 504

Un 504 Gateway Timeout n’est presque jamais la vraie défaillance. C’est le messager. Le proxy (Nginx, Apache en reverse proxy, Cloudflare, un load balancer) a attendu une réponse d’un upstream et a manqué de patience.

Dans un hébergement WordPress typique, le chemin de la requête ressemble à ceci :

  • Client → CDN/WAF (optionnel) → Nginx/Apache (reverse proxy)
  • Proxy → gestionnaire PHP (PHP-FPM ou mod_php)
  • PHP → base de données (MySQL/MariaDB) et autres dépendances (Redis, API externes, SMTP, passerelles de paiement)
  • Les réponses reviennent par le même chemin

Donc quand vous obtenez un 504, la question est : qui n’a pas répondu à temps ? Ce « qui » peut être PHP (bloqué, lent, saturé) ou la base de données (requêtes lentes, verrous, blocages IO), ou les deux avec une chaîne causale (DB lente → travailleurs PHP s’accumulent → proxy expire).

Voici la règle opérationnelle qui vous fait gagner des heures : un 504 est un problème d’enfilement tant que le contraire n’est pas prouvé. Quelque chose s’amoncelle : des requêtes attendent des workers PHP, des workers PHP attendent la base de données, la base de données attend le disque, ou tout le monde attend un verrou.

Petite plaisanterie courte : Un 504, c’est comme une réunion qui « a manqué de temps » — personne n’admet qu’il n’a pas fait le travail, mais tout le monde accepte de reporter.

Ce que « problème de base de données » signifie (opérationnellement)

« La base de données est lente » n’est pas un diagnostic. Opérationnellement, cela signifie l’un des cas suivants :

  • Les requêtes sont lentes parce qu’elles scannent trop (index manquants, mauvais motifs de requêtes).
  • Les requêtes sont lentes parce que le SGDB est bloqué (verrous, verrous de métadonnées, longues transactions).
  • Les requêtes sont lentes parce que la base ne peut pas lire/écrire assez vite (saturation IO, blocages fsync).
  • Les requêtes sont lentes parce que la base est CPU-contraint (tris complexes, regex, jointures lourdes).
  • Les requêtes sont lentes parce que les connexions sont un goulot (max_connections, thread pool, pics de connexions).

Ce que « problème PHP » signifie (opérationnellement)

« PHP est lent » signifie généralement l’un des cas :

  • Le pool PHP-FPM est saturé (tous les workers occupés ; les requêtes font la queue).
  • Les workers sont bloqués (deadlocks dans le code, appels API externes avec timeouts longs, problèmes DNS).
  • Les workers meurent / recyclent (OOM kills, max_requests trop bas, fuites mémoire).
  • Cache d’opcode absent/mal configuré (chaque requête compile trop de code).
  • IO fichier lent (NFS bloqué, EBS et crédits burst, stockage surchargé).

L’astuce n’est pas de débattre ces théories. L’astuce est de rassembler suffisamment de signaux pour condamner une couche.

Playbook de diagnostic rapide (vérifier 1/2/3)

Voici la séquence « j’ai 10 minutes avant que la direction découvre que la page status est aussi WordPress ». Vous n’optimisez pas ; vous identifiez le goulot et vous arrêtez l’hémorragie.

1) Commencez par la périphérie : confirmez que c’est un timeout d’origine, pas une crise CDN

  • Si Cloudflare/ALB renvoie 504, vérifiez si l’origine est joignable et si les logs du proxy montrent des timeouts upstream.
  • Si seules certaines pages font 504, suspectez l’application/la base de données. Si toutes les pages font 504, y compris les assets statiques, suspectez le web/proxy ou le réseau.

2) Vérifiez le log d’erreurs du proxy pour des détails sur le timeout upstream

  • Nginx vous dira littéralement : « upstream timed out » (PHP n’a pas répondu) ou « connect() failed » (PHP indisponible).
  • Cela ne prouve pas encore DB vs PHP ; cela prouve « PHP n’a pas répondu au proxy ». Ensuite vous demandez : PHP attendait-il la DB ?

3) Vérifiez PHP-FPM : longueur de file d’attente et saturation de max_children

  • Si PHP-FPM a une listen queue qui se construit et des processus au maximum de max_children, PHP est saturé (la cause peut quand même être la DB).
  • Si PHP a des workers libres mais que les requêtes expirent quand même, cherchez des appels bloquants (verrous DB, API externes, blocages filesystem).

4) Vérifiez MySQL/MariaDB : requêtes actives, attentes de verrou, et pics de slow query

  • Si vous voyez beaucoup de threads « Waiting for table metadata lock » ou des SELECT/UPDATE de longue durée, vous avez une contention DB.
  • Si vous voyez des waits InnoDB fsync/IO élevés et des temps de requête croissants, suspectez le stockage.

5) Faites un mouvement de stabilisation, pas un mouvement aléatoire

  • Si DB verrouillée : tuez la transaction bloquante, pas toute la base (sauf si vous aimez prolonger la panne).
  • Si PHP saturé : augmentez temporairement les workers PHP seulement si la DB peut le supporter ; sinon vous vous DDoSerez vous-même la base de données.
  • Si un endpoint de plugin fait fondre tout : limitez le débit ou désactivez-le temporairement.

Faits intéressants et brève histoire (pourquoi les 504 se manifestent ainsi)

  1. 504 est défini dans la spec HTTP comme « Gateway Timeout » — il concerne explicitement les intermédiaires (proxies/gateways) qui expirent, pas l’application d’origine qui décide d’expirer.
  2. Nginx a popularisé le langage « upstream » dans les logs et la doc, et cette formulation a orienté la manière dont les équipes déboguent : « quel upstream ? » est devenu la première question.
  3. PHP-FPM est devenu le choix par défaut pour beaucoup de stacks WordPress parce qu’il isole les processus PHP, offre des contrôles de pool (pm.max_children), et évite le gonflement mémoire d’Apache prefork sur les sites chargés.
  4. MySQL/InnoDB a remplacé MyISAM pour la plupart des installations WordPress car le verrouillage au niveau ligne et la récupération après crash importent quand vous avez des écritures concurrentes (commentaires, paniers, sessions).
  5. Le schéma WordPress est volontairement générique (postmeta, usermeta tables clé/valeur). Flexible, oui. Aussi une source de problèmes de performance si vous interrogez les meta sans bons index.
  6. Les requêtes lentes n’apparaissent pas toujours comme des pages lentes tant que la concurrence n’augmente pas. Une requête d’1s est gênante. Une requête d’1s répétée 200 fois devient un déni de service que vous payez.
  7. Les valeurs par défaut des timeouts sont rarement alignées : timeout CDN, timeout proxy, fastcgi timeout, PHP max_execution_time et MySQL net_read_timeout peuvent tous diverger. Le désalignement produit des 504 « mystères ».
  8. Les changements d’index peuvent verrouiller des tables plus longtemps que prévu, surtout avec de grandes tables et certains ALTER TABLE. Cela se manifeste par des verrous de métadonnées soudains et des 504 en cascade.
  9. Historiquement, « ajoutez des workers » fonctionnait quand les CPU étaient bon marché et la charge DB légère. À l’échelle, cette approche se transforme en essaims autogénérés frappant la base de données.

À quoi ressemble une « preuve » : établir la responsabilité sans impressions

« C’est la base de données » est une affirmation. « C’est PHP » est une affirmation. La preuve est une chaîne d’horodatages et d’éléments corrélés qui montre où le temps est passé.

Dans un compte-rendu d’incident propre, vous voulez au moins deux signaux indépendants qui pointent vers le même coupable :

  • Preuve couche proxy : timeouts upstream, temps de réponse upstream, ratios 499/504, pics d’erreurs.
  • Preuve couche PHP : état PHP-FPM (actif/idle, max_children atteint), slowlog avec stack traces, temps CPU des workers, longueur des files d’attente.
  • Preuve couche DB : slow query log corrélé avec la fenêtre d’incident, attentes de verrou, transactions longues, waits IO, threads en hausse.
  • Preuve hôte : CPU steal, charge moyenne vs threads exécutables, iowait, latence disque, retransmissions réseau.

Ne négligez pas la chaîne de dépendances : PHP peut être « celui qui a expiré », tandis que la base de données est « celle qui a causé cela ». Votre travail est d’identifier la première ressource contrainte dans la chaîne.

Deuxième plaisanterie courte : Redémarrer des services pour réparer un 504, c’est comme monter le volume de la radio pour corriger un bruit moteur bizarre — cela change votre ressenti, pas la physique.

L’état d’esprit fiabilité (une citation)

L’espoir n’est pas une stratégie. — souvent attribuée à la culture ops ; traitez-la comme une idée paraphrasée utilisée dans les cercles de fiabilité.

Traduction : rassemblez des preuves, puis agissez.

Tâches pratiques (commandes, sorties, décisions)

Ce sont des tâches réelles que vous pouvez exécuter sur un hôte Linux typique pour WordPress. Chaque tâche inclut : commande, ce que signifie la sortie, et quelle décision prendre ensuite. Exécutez-les dans l’ordre si vous paniquez ; choisissez-en si vous êtes calme.

Task 1: Confirm the 504 and measure where time is spent (client-side)

cr0x@server:~$ curl -sS -o /dev/null -w 'code=%{http_code} ttfb=%{time_starttransfer} total=%{time_total}\n' https://example.com/
code=504 ttfb=60.001 total=60.002

Signification : Time-to-first-byte (TTFB) est pratiquement égal au temps total, et la requête meurt vers ~60s. Cela sent le timeout du proxy, pas une page d’erreur générée par l’application.

Décision : Allez voir les logs du proxy pour trouver l’upstream qui a expiré et la valeur de timeout configurée.

Task 2: Check Nginx error log for “upstream timed out” (proxy evidence)

cr0x@server:~$ sudo tail -n 30 /var/log/nginx/error.log
2025/12/26 11:18:41 [error] 1842#1842: *991 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 203.0.113.10, server: example.com, request: "GET / HTTP/2.0", upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock", host: "example.com"

Signification : Nginx a attendu PHP-FPM et n’a pas reçu d’en-têtes de réponse à temps. Cela prouve que le proxy n’est pas la partie lente ; il attend.

Décision : Inspectez la saturation de PHP-FPM et les requêtes lentes. Ce n’est toujours pas suffisant pour accuser la base de données ; PHP peut être bloqué en attendant la DB.

Task 3: Confirm Nginx fastcgi timeout settings (avoid chasing ghosts)

cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -E 'fastcgi_read_timeout|proxy_read_timeout|send_timeout' | head
fastcgi_read_timeout 60s;
send_timeout 60s;

Signification : Le seuil de 60 secondes correspond à votre timing curl. Bien. Vous déboguez la bonne fenêtre, pas un timeout côté client.

Décision : Ne relevez pas les timeouts pour l’instant. Déterminez d’abord ce qui prend >60 secondes. Augmenter les timeouts masque les pannes et agrandit les files d’attente.

Task 4: Check PHP-FPM pool status (are you out of workers?)

cr0x@server:~$ sudo ss -lxp | grep php-fpm
u_str LISTEN 0 128 /run/php/php8.2-fpm.sock  44123  * 0 users:(("php-fpm8.2",pid=1640,fd=9))

Signification : PHP-FPM écoute. Si cette ligne manque, PHP-FPM est arrêté ou le chemin du socket diffère.

Décision : Si absent : restaurez le service PHP-FPM. Si présent : vérifiez s’il est saturé et si les requêtes font la queue.

Task 5: Inspect PHP-FPM for max_children saturation (the classic bottleneck)

cr0x@server:~$ sudo grep -R "pm.max_children" /etc/php/8.2/fpm/pool.d/*.conf
/etc/php/8.2/fpm/pool.d/www.conf:pm.max_children = 20

Signification : Vous avez au maximum 20 requêtes PHP concurrentes dans ce pool. Cela peut être correct ou dangereusement bas, selon le temps de requête et le trafic.

Décision : Ensuite, vérifiez si ces 20 sont toutes occupées et si les requêtes font la queue.

Task 6: Read PHP-FPM logs for “server reached pm.max_children”

cr0x@server:~$ sudo tail -n 30 /var/log/php8.2-fpm.log
[26-Dec-2025 11:18:12] WARNING: [pool www] server reached pm.max_children setting (20), consider raising it
[26-Dec-2025 11:18:13] WARNING: [pool www] server reached pm.max_children setting (20), consider raising it

Signification : PHP-FPM est saturé. Les requêtes font la queue. C’est une preuve solide que la capacité PHP est un facteur limitant.

Décision : Déterminez si PHP est lent à cause de son propre CPU/travail ou s’il attend la DB/IO. Augmenter max_children aveuglément peut écraser la BD.

Task 7: Check current PHP-FPM process count and CPU usage

cr0x@server:~$ ps -o pid,pcpu,pmem,etime,cmd -C php-fpm8.2 --sort=-pcpu | head
  PID %CPU %MEM     ELAPSED CMD
 1721 62.5  2.1       01:12 php-fpm: pool www
 1709 55.2  2.0       01:11 php-fpm: pool www
 1698 48.9  1.9       01:10 php-fpm: pool www

Signification : Si les workers affichent un CPU élevé, PHP effectue un travail lourd (ou est coincé dans des boucles). Si les workers affichent un faible CPU mais un temps écoulé long, ils attendent probablement de l’IO (DB, disque, réseau).

Décision : Si CPU élevé : profilez/optimisez PHP ou réduisez le travail (plugins, cache). Si CPU bas mais temps long : vérifiez DB et IO ensuite.

Task 8: Enable or read PHP-FPM slowlog to capture stack traces of slow requests

cr0x@server:~$ sudo grep -R "slowlog\|request_slowlog_timeout" /etc/php/8.2/fpm/pool.d/www.conf
request_slowlog_timeout = 10s
slowlog = /var/log/php8.2-fpm.slow.log
cr0x@server:~$ sudo tail -n 20 /var/log/php8.2-fpm.slow.log
[26-Dec-2025 11:18:39]  [pool www] pid 1721
script_filename = /var/www/html/index.php
[0x00007f2f0c...] mysqli_query() /var/www/html/wp-includes/wp-db.php:2056
[0x00007f2f0c...] query() /var/www/html/wp-includes/wp-db.php:1945
[0x00007f2f0c...] get_results() /var/www/html/wp-includes/wp-db.php:2932

Signification : C’est la preuve irréfutable quand elle apparaît : PHP est lent parce qu’il est dans un appel de base de données. Si vous voyez curl_exec(), file_get_contents() ou des fonctions DNS à la place, le coupable est ailleurs.

Décision : Si le slowlog montre des appels DB : passez immédiatement au diagnostic MySQL. S’il montre des appels HTTP externes : isolez ce plugin/service et ajoutez des timeouts / circuit breakers.

Task 9: Check MySQL thread states (locks and long runners)

cr0x@server:~$ sudo mysql -e "SHOW FULL PROCESSLIST\G" | egrep -A2 "State:|Time:|Info:" | head -n 40
Time: 58
State: Waiting for table metadata lock
Info: ALTER TABLE wp_postmeta ADD INDEX meta_key (meta_key)
Time: 55
State: Sending data
Info: SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts LEFT JOIN wp_postmeta ...

Signification : « Waiting for table metadata lock » est un signal rouge : une modification de schéma ou un DDL long bloque les lectures/écritures. « Sending data » long suggère de larges scans ou une IO lente.

Décision : Si lock de métadonnées : trouvez et tuez la DDL bloquante ou planifiez-la correctement. Si gros scans : vérifiez le slow query log, les index et la pression sur le buffer pool.

Task 10: Check InnoDB status for lock waits and IO stalls

cr0x@server:~$ sudo mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
=====================================
2025-12-26 11:18:45 0x7f0a4c2
TRANSACTIONS
------------
Trx id counter 12904421
Purge done for trx's n:o < 12904400 undo n:o < 0 state: running
History list length 1987
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 12904388, ACTIVE 62 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 418, OS thread handle 139682..., query id 9812 10.0.0.15 wpuser updating
UPDATE wp_options SET option_value='...' WHERE option_name='woocommerce_sessions'

Signification : Des transactions longues actives et une longueur de liste d’historique croissante indiquent un retard de purge et une contention potentielle. Les mises à jour de tables chaudes (options, sessions) causent souvent des embouteillages.

Décision : Si vous voyez une transaction longue bloquant beaucoup : identifiez-la et envisagez de la tuer (prudemment). Puis corrigez le comportement applicatif qui retient les verrous trop longtemps.

Task 11: Enable and inspect MySQL slow query log (time-correlated proof)

cr0x@server:~$ sudo mysql -e "SHOW VARIABLES LIKE 'slow_query_log%'; SHOW VARIABLES LIKE 'long_query_time';"
+---------------------+------------------------------+
| Variable_name       | Value                        |
+---------------------+------------------------------+
| slow_query_log      | ON                           |
| slow_query_log_file | /var/log/mysql/mysql-slow.log|
+---------------------+------------------------------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| long_query_time | 1.000 |
+-----------------+-------+
cr0x@server:~$ sudo tail -n 25 /var/log/mysql/mysql-slow.log
# Time: 2025-12-26T11:18:21.123456Z
# Query_time: 12.302  Lock_time: 0.000 Rows_sent: 10  Rows_examined: 2450381
SELECT * FROM wp_postmeta WHERE meta_key = '_price' ORDER BY meta_value+0 DESC LIMIT 10;

Signification : Rows_examined dans les millions pour une requête simple est le genre de chose qui transforme le trafic en timeouts. C’est une preuve solide côté DB avec horodatages.

Décision : Ajoutez/ajustez des index, réécrivez les requêtes (souvent pilotées par un plugin), ou introduisez du caching/search. Vérifiez aussi pourquoi cette requête a monté maintenant (nouveau plugin, fonctionnalité, campagne).

Task 12: Watch real-time MySQL running threads and queries (is the DB drowning?)

cr0x@server:~$ sudo mysqladmin extended-status -ri 2 | egrep "Threads_running|Questions|Slow_queries"
Threads_running            34
Questions                  188420
Slow_queries               912
Threads_running            37
Questions                  191102
Slow_queries               925

Signification : Threads_running qui monte sous charge signifie que la concurrence s’accumule dans MySQL. Si Threads_running reste bas mais que PHP expire, la DB n’est peut-être pas le goulot.

Décision : Si Threads_running est élevé : réduisez le coût des requêtes et la contention ; envisagez des réplicas en lecture pour les endpoints en lecture lourde. Si bas : concentrez-vous sur PHP/appels externes/stockage.

Task 13: Check host-level IO wait and disk latency (storage is often the quiet villain)

cr0x@server:~$ iostat -xz 1 3
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.34    0.00    4.12   28.90    0.00   54.64

Device            r/s     w/s   rKB/s   wKB/s  await  svctm  %util
nvme0n1         120.0   210.0  6400.0  8200.0  48.20   1.10  98.00

Signification : iowait proche de 30% et %util disque proche de 100% avec un await élevé signifie que le stockage est saturé. MySQL ralentira même si le CPU a l’air correct.

Décision : Si le stockage est le goulot : corrigez l’IO (disque plus rapide, meilleurs IOPS, réduire write amplification, tune InnoDB, déplacer tmpdir, réduire le volume de logs). N’ajoutez pas seulement des workers PHP.

Task 14: Check for OOM kills or kernel pressure (silent PHP death spiral)

cr0x@server:~$ sudo journalctl -k -n 50 | egrep -i "oom|killed process" | tail
Dec 26 11:17:59 server kernel: Out of memory: Killed process 1721 (php-fpm8.2) total-vm:1234567kB, anon-rss:456789kB, file-rss:0kB, shmem-rss:0kB

Signification : Si des workers PHP sont OOM-killés, le proxy voit des timeouts et des resets. Cela peut ressembler à des « 504s aléatoires ».

Décision : Réduisez l’usage mémoire PHP (bloat des plugins), limitez la mémoire par processus, ajustez la taille du pool, ajoutez de la RAM, et assurez-vous que le swap n’est pas catastrophique pour la perf.

Task 15: Confirm WordPress debug logging is not making things worse

cr0x@server:~$ grep -n "WP_DEBUG" /var/www/html/wp-config.php
90:define('WP_DEBUG', false);
91:define('WP_DEBUG_LOG', false);

Signification : Laisser WP_DEBUG_LOG activé sur un site chargé peut générer des écritures disque lourdes, transformant un petit souci en contention IO.

Décision : Gardez le debug logging désactivé en production par défaut ; activez-le temporairement avec des fenêtres serrées et une rotation des logs quand nécessaire.

Task 16: Prove whether PHP is waiting on DB using strace (surgical, not for the faint-hearted)

cr0x@server:~$ sudo strace -p 1721 -tt -T -e trace=network,read,write,poll,select -s 80
11:18:40.101203 poll([{fd=12, events=POLLIN}], 1, 60000) = 0 (Timeout) <60.000312>

Signification : Un worker PHP bloqué en poll/select pendant 60 secondes attend de l’IO réseau — souvent le socket de la base de données ou un service HTTP externe.

Décision : Si c’est le socket DB, concentrez-vous sur MySQL. Si c’est une IP externe, corrigez cette intégration (timeouts, retries, circuit breaking, mise en cache).

Vous avez maintenant assez d’outils pour prouver où le temps est passé. Suite : reconnaissance de motifs, car les signaux se regroupent de manière prévisible.

Base de données vs PHP : motifs de signaux qui les différencient

Pattern A: PHP-FPM max_children reached + PHP slowlog shows mysqli calls

Coupable le plus probable : latence ou verrous base de données provoquant l’accumulation des workers PHP.

À quoi ça ressemble :

  • Nginx : « upstream timed out while reading response header from upstream »
  • Log PHP-FPM : « server reached pm.max_children »
  • PHP slowlog : stack traces dans wp-db.php / mysqli_query()
  • MySQL : Threads_running en hausse ; pics dans le slow query log ; processlist montrant des requêtes longues ou des attentes de verrou

Faites ceci : traitez la BD comme la cause racine. Réduisez d’abord la charge DB, puis ajustez la concurrence PHP.

Pattern B: PHP-FPM max_children reached + PHP workers high CPU + DB looks calm

Coupable le plus probable : travail au niveau PHP (boucles de template, logique de plugin coûteuse, traitement d’images, cache manquant, mauvais caching d’objets).

À quoi ça ressemble :

  • Les workers PHP affichent un %CPU élevé et de longs temps écoulés
  • MySQL Threads_running modeste, slow query log non en pic
  • Les requêtes qui expirent sont souvent des endpoints spécifiques (search, admin-ajax, filtres produits)

Faites ceci : isolez l’endpoint, ajoutez du cache, activez OPcache correctement, profilez par échantillonnage (évitez le tracing complet pendant une panne).

Pattern C: PHP has idle workers, but requests still 504

Coupable le plus probable : mauvaise configuration du proxy, backlog du socket PHP, connectivité upstream, ou quelque chose en dehors de PHP/DB (DNS, API externes, système de fichiers).

À quoi ça ressemble :

  • Erreurs Nginx peuvent montrer connect() failed, recv() failed, ou resets intermittents d’upstream
  • Logs PHP-FPM peuvent montrer child exited, segfault, ou rien du tout
  • Logs hôte peuvent montrer OOM kills, blocages disque, ou problèmes réseau

Faites ceci : validez sockets, backlogs, limites kernel, et dépendances externes ; ne vous fixez pas uniquement sur MySQL.

Pattern D: DB Threads_running high + IO wait high + disk await high

Coupable le plus probable : le stockage limite la BD, qui limite PHP, provoquant des 504.

Faites ceci : corrigez l’IO. Parfois le « problème de la base de données » est « on a acheté le disque le moins cher ».

Pattern E: Sudden lock waits, especially metadata locks

Coupable le plus probable : DDL pendant le pic, migrations de plugin, ou un « petit ajout d’index » effectué en prod sans tenir compte du verrouillage.

Faites ceci : arrêtez le DDL, replanifiez avec des changements de schéma en ligne, et implémentez des garde-fous.

Trois mini-récits d’entreprise du terrain

Mini-récit 1 : l’incident causé par une mauvaise hypothèse

Ils avaient un site marketing WordPress plus une boutique WooCommerce, hébergés sur une VM « assez costaud ». Une vague soudaine de 504 est survenue pendant le lancement d’une campagne. L’ingénieur on-call a regardé le log Nginx et a vu des timeouts upstream vers PHP-FPM. Il a supposé, avec assurance, que PHP-FPM avait besoin de plus de workers.

Ils ont augmenté pm.max_children. Les 504 ont empiré. Le CPU de la base de données a grimpé, puis la latence disque est montée en flèche, puis tout le site a commencé à échouer d’une façon incohérente. Ce n’était plus seulement le checkout — même la page d’accueil expirait.

Le vrai coupable était un seul motif de requête introduit par un widget « filtrer les produits par prix ». Il utilisait postmeta d’une manière qui scannait d’énormes plages, et il s’exécutait sur chaque vue de catégorie. La base de données tenait tant que la concurrence était limitée. Augmenter les workers PHP a augmenté le nombre de requêtes concurrentes coûteuses. La BD a atteint la saturation IO. Chaque requête a ralenti. L’enfilement a explosé sur toute la pile.

Ils ont stabilisé en rollbackant le widget, en vidant les caches, et en remettant la concurrence PHP à des niveaux sensés. Le postmortem n’a pas dit « ne scalez pas PHP ». Il a dit : ne supposez pas « timeout PHP = problème PHP ». PHP était la victime. La base de données était la scène du crime.

Mini-récit 2 : l’optimisation qui a eu l’effet inverse

Une équipe a voulu être maligne : ils ont déplacé les uploads WordPress et une partie du code sur un filesystem réseau pour « simplifier les déploiements » sur deux nœuds web. Ça marchait en test. Puis le trafic prod est arrivé et les 504 ont commencé — sporadiques au début, puis corrélés à des pics (campagnes email, mises en avant home).

Tout avait l’air normal : MySQL Threads_running n’était pas fou, le CPU n’était pas saturé, PHP-FPM avait de la capacité. Mais les requêtes restaient bloquées assez longtemps pour que Nginx abandonne. Quelqu’un a affirmé que c’était « définitivement la base de données » parce que WordPress, c’est toujours la base de données. Cette affirmation a tenu une demi-journée.

Le tournant a été la capture d’un slowlog PHP-FPM : les stack traces montraient des opérations fichier et des chemins d’autoloading, pas des appels mysqli. En même temps, les métriques hôte montraient des pics d’iowait. Le filesystem réseau avait des stalls de latence périodiques et des retransmissions occasionnelles. Les workers PHP étaient idle côté CPU, bloqués sur des lectures de fichiers.

L’« optimisation » (stockage partagé) a réduit la friction de déploiement mais introduit une dépendance de latence dans chaque requête. La correction était ennuyeuse : filesystem local pour le code, stockage d’objets pour les uploads avec cache agressif, et un mécanisme de déploiement qui n’utilise pas POSIX partagé. La performance est revenue instantanément, et la base de données — étonnamment — était saine.

Mini-récit 3 : la pratique ennuyeuse mais correcte qui a sauvé la mise

Une autre société faisait tourner WordPress dans une plateforme plus large. Ils n’étaient pas parfaits, mais ils avaient une habitude qui paraissait presque désuète : chaque couche disposait d’un ensemble standard de dashboards et de logs, et ils gardaient les timeouts alignés. Timeout proxy, fastcgi timeout, PHP max_execution_time, timeouts DB. Tout consigné. Tout cohérent.

Un après-midi, ils voient une montée de 504. Le premier intervenant a vérifié les logs Nginx : upstream timed out. Puis il a vérifié PHP-FPM : max_children n’était pas atteint, mais le slowlog montrait des appels wp-db. Ils sont passés à MySQL et ont vu immédiatement des attentes de verrou autour d’un plugin de migration qui avait lancé un ALTER TABLE sur une table à fort trafic.

Parce qu’ils avaient une pratique ennuyeuse — slow query log activé avec des seuils sensés, et un calendrier de changements — ils savaient exactement ce qui avait changé et quand. Ils ont arrêté la migration, l’ont replanifiée hors pic avec des outils plus sûrs, et les 504 ont cessé. Pas de redémarrages aléatoires, pas de « scalons tout », pas de parade d’accusations d’une semaine.

Leur plus grand gain n’était pas un outil d’observabilité sophistiqué. C’était la cohérence : timeouts alignés et signaux de base toujours disponibles. En réponse aux incidents, l’ennuyeux est une fonctionnalité.

Erreurs courantes : symptôme → cause racine → correction

Voilà les motifs qui font perdre le plus de temps parce qu’ils paraissent intuitifs et sont faux en production.

1) Symptom: “Nginx shows upstream timed out, so PHP is broken.”

Cause racine : PHP est correct ; il attend MySQL sur des verrous ou des requêtes lentes.

Correction : Utilisez le slowlog PHP-FPM pour confirmer où il est bloqué. S’il est dans wp-db.php, allez directement au processlist MySQL et au statut InnoDB ; résolvez les verrous et le coût des requêtes.

2) Symptom: “Let’s increase fastcgi_read_timeout so customers stop seeing 504.”

Cause racine : Vous masquez la latence ; les files d’attente grandissent ; finalement tout s’effondre et vous obtenez des timeouts de toute façon, juste plus lents.

Correction : Gardez des timeouts stricts pour faire apparaître les pannes rapidement. Réduisez la latence en queue en corrigeant le goulot et en ajoutant cache / rate limiting quand approprié.

3) Symptom: “Raising pm.max_children made things worse.”

Cause racine : La base de données était la ressource limitante ; plus de concurrence PHP a augmenté la contention DB et l’IO.

Correction : Traitez la concurrence PHP comme un générateur de charge. Taillez-la selon ce que la BD peut servir. Réduisez les requêtes coûteuses, ajoutez des index, et mettez en cache les lectures chaudes.

4) Symptom: “Only wp-admin 504s, frontend looks okay.”

Cause racine : Les pages d’administration déclenchent souvent des requêtes plus lourdes (listes de posts avec filtres), vérifications de plugins et comportements de type cron.

Correction : Capturez le slowlog pour les endpoints admin. Vérifiez les boucles admin-ajax et appels plugin. Ajoutez du caching d’objets et auditez les plugins.

5) Symptom: “504s happen in bursts after traffic spikes.”

Cause racine : Effondrement par enfilement : misses de cache, stampedes ou pics de connexions vers la BD.

Correction : Implémentez du caching avec protection contre les stampedes, assurez-vous que les connexions DB persistantes sont raisonnables, et limitez les endpoints abusifs (xmlrpc, wp-login, admin-ajax).

6) Symptom: “Database CPU is low, so DB can’t be the issue.”

Cause racine : La BD est IO-bound ou lock-bound, pas CPU-bound.

Correction : Regardez iowait, disk await, buffer pool hit rate et lock waits. Le CPU n’est qu’une façon d’être en difficulté.

7) Symptom: “Slow query log is empty, so queries aren’t slow.”

Cause racine : slow_query_log est désactivé, long_query_time trop élevé, ou le problème est des verrous (Lock_time peut être énorme alors que Query_time semble modeste selon la config de logging).

Correction : Activez le slow query log avec un seuil réaliste (souvent 0.5–1s pour des sites chargés). Corrélez avec les lock waits et la durée des transactions.

8) Symptom: “It goes away after restart, so it was a memory leak.”

Cause racine : Le redémarrage vide les files et supprime les verrous ; vous n’avez pas corrigé le déclencheur (schéma de trafic, régression de requête, DDL, stall d’API externe).

Correction : Traitez les redémarrages comme une mitigation temporaire. Capturez des preuves avant de redémarrer : slowlog, processlist, iostat, logs d’erreurs.

Listes de contrôle / plan étape par étape

Étape par étape : prouver DB vs PHP en moins de 30 minutes

  1. Obtenez un échantillon horodaté : lancez curl avec timing pour confirmer la durée du timeout et la fréquence.
  2. Vérifiez le log du proxy : confirmez « upstream timed out » et identifiez l’upstream (socket php-fpm, hôte upstream).
  3. Vérifiez la saturation PHP-FPM : cherchez les avertissements max_children et comptez les workers occupés.
  4. Vérifiez le slowlog PHP : confirmez si les requêtes lentes sont dans des appels mysqli ou ailleurs.
  5. Vérifiez le processlist DB : cherchez des attentes de verrou, des exécutions longues et des requêtes répétitives coûteuses.
  6. Vérifiez le statut InnoDB : identifiez transactions longues et verrous bloquants.
  7. Vérifiez le slow query log : corrélez les pics de Query_time/Rows_examined avec la fenêtre d’incident.
  8. Vérifiez la santé stockage/hôte : iowait, latence disque, OOM kills, CPU steal.
  9. Faites un changement de stabilisation : tuez le bloquant, désactivez le endpoint fautif, rate limit, ou scalez temporairement le bon niveau.
  10. Enregistrez ce que vous avez vu : collez les lignes de log clés et les sorties de commandes dans la timeline de l’incident.

Checklist de stabilisation (que faire pendant l’incident)

  • Désactivez l’endpoint le plus nocif si possible (handlers admin-ajax, pages de recherche/filtrage lourdes).
  • Réduisez la concurrence côté générateur de charge si la BD fond (limitez PHP-FPM ou appliquez un rate limiting côté Nginx).
  • Arrêtez immédiatement les changements de schéma s’ils verrouillent des tables chaudes.
  • Si l’IO est saturé, stoppez les jobs background qui consomment disque (sauvegardes, indexation, logs de debug, sync de fichiers).
  • Préférez tuer ciblé (transaction bloquante) plutôt que de redémarrer MySQL à l’aveugle.

Checklist de durcissement (après l’incident)

  • Gardez le slowlog PHP-FPM configuré et testé (pas forcément bruyant tout le temps, mais prêt).
  • Gardez le slow query logging disponible (activé ou activable rapidement), et sachez où se trouve le log et comment il tourne.
  • Alignez les timeouts
  • Ajoutez un cache d’objets (Redis) et vérifiez qu’il est réellement utilisé par l’installation WordPress.
  • Auditez les plugins pour les motifs de requêtes (meta queries, recherches génériques, admin-ajax lourds).
  • Indexez de manière responsable : évitez les indexes aléatoires ; validez avec EXPLAIN et mesurez Rows_examined.
  • Planifiez les changements de schéma avec des outils à faible verrouillage et des fenêtres hors pic.
  • Surveillez la latence disque et l’iowait ; n’attendez pas que cela devienne un « incident base de données ».

FAQ

1) Si Nginx dit « upstream timed out », cela signifie-t-il que PHP est en tort ?

Non. Cela signifie que Nginx n’a pas obtenu de réponse de l’upstream (souvent PHP-FPM) à temps. PHP peut attendre MySQL, le disque ou une API externe. Utilisez le slowlog PHP-FPM pour voir où le code est bloqué.

2) Quelle est la manière la plus rapide de prouver que la base de données cause des 504 ?

Corrélez trois choses dans la même fenêtre temporelle : stack traces du slowlog PHP montrant des fonctions mysqli, processlist MySQL montrant des requêtes longues/attentes de verrou, et pics dans le slow query log.

3) Quelle est la manière la plus rapide de prouver que la capacité PHP-FPM est le goulot ?

Trouvez des avertissements « server reached pm.max_children » plus une listen queue/backlog croissante, et confirmez que MySQL n’est pas surchargé (Threads_running stable, pas de tempête de verrous). Si PHP est CPU-pegé, il effectue trop de travail par requête.

4) Dois-je augmenter fastcgi_read_timeout pour arrêter les 504 ?

Seulement comme mesure de confinement temporaire, et seulement si vous comprenez l’impact sur l’enfilement. Des timeouts longs peuvent transformer une lenteur intermittente en saturation soutenue. Corrigez la queue longue, ne la masquez pas.

5) Comment distinguer un problème de verrous d’un problème de requête lente dans MySQL ?

Les verrous apparaissent comme des états « Waiting for … lock » dans le processlist et dans les sections lock wait du statut InnoDB. Les requêtes lentes montrent un Query_time élevé et un Rows_examined important dans le slow query log, souvent avec des états « Sending data ».

6) Pourquoi les 504 surviennent-ils surtout au checkout WooCommerce ou dans le panier ?

Le checkout touche des tables à écriture lourde (sessions, orders, order meta) et peut déclencher des appels externes (passerelles de paiement, APIs taxes/livraison). Cette combinaison le rend sensible à la contention DB et à la latence externe.

7) Redis/caching d’objets peut-il réparer les 504 ?

Oui, si votre goulot est des requêtes de lecture répétées (options, postmeta) et que vous avez un bon taux de hit. Cela ne résoudra pas la contention due aux écritures ou un changement de schéma bloquant.

8) Pourquoi je vois des 504 alors que le CPU MySQL est bas ?

Parce que la BD peut être liée à l’IO (haut disk await), liée aux verrous (waiting), ou liée au réseau. Un CPU bas ne signifie pas santé. Regardez iowait, latence disque et lock waits.

9) Est-il sûr de tuer une requête MySQL pendant un incident ?

Parfois c’est la bonne décision — surtout si une transaction bloque beaucoup d’autres. Mais soyez délibéré : identifiez le bloquant, comprenez s’il s’agit d’un write critique, et attendez-vous à des erreurs applicatives pour les requêtes utilisant cette transaction.

10) Et si ni la DB ni PHP ne semblent visiblement fautifs ?

Alors suspectez les dépendances : latence DNS, appels HTTP externes, stalls du système de fichiers (stockage réseau), OOM kills du kernel, ou mauvaise configuration du proxy. Le slowlog PHP-FPM est votre boussole : il pointe la fonction où le temps disparaît.

Conclusion : prochaines étapes qui réduisent réellement les 504s

Si vous retenez une leçon opérationnelle : ne débattez pas « base de données ou PHP » en abstraction. Prouvez où le temps est passé avec des preuves horodatées du proxy, de PHP-FPM et de MySQL. Vous construisez une chaîne causale, pas une intuition.

Prochaines étapes pratiques :

  1. Gardez le slowlog PHP-FPM configuré (avec un seuil raisonnable comme 5–10 secondes) pour capturer des stack traces pendant de vrais incidents.
  2. Gardez le slow query logging utilisable (activé ou activable rapidement), et sachez où se trouve le fichier et comment il est roté.
  3. Alignez les timeouts pour qu’une requête ne meure pas mystérieusement à des couches différentes avec des horloges différentes.
  4. Traitez la concurrence PHP comme un levier aux conséquences : augmenter max_children augmente la charge sur la base. Confirmez la marge DB avant toute montée.
  5. Mesurez la latence stockage quand « la base de données est lente ». L’IO est l’axe caché que la plupart des stacks WordPress ignorent jusqu’à l’incendie.
  6. Après l’incident, éliminez le déclencheur : corrigez la requête, indexez correctement, désactivez le comportement du plugin, mettez en cache le chemin coûteux ou repensez l’endpoint.

Les 504 ne sont pas mystérieux. Elles disent simplement, poliment, qu’une partie de votre stack a cessé de suivre. La politesse cesse quand vous l’ignorez.

← Précédent
Base de données WordPress gonflée : nettoyer l’autoload wp_options sans tout casser
Suivant →
Comment lire les tests GPU : pièges 1080p, 1440p et 4K

Laisser un commentaire