Strace Et PHP
Quand un script PHP est lent, on pense souvent direct au code applicatif, à MySQL, au cache, etc. Mais parfois, le souci est plus bas niveau : DNS qui traîne, disque qui mouline, appels réseau bloquants, ou un sleep oublié.
strace est l’outil parfait pour ça : il te montre les appels système (syscalls) que ton process fait au noyau Linux. En gros, tu vois la vraie vie du process.
On va travailler sur une Debian avec un LAMP, mais l’idée est la même ailleurs.
1) Pré-requis
Article réservé aux curieux ayant déjà l’habitude de gérer des stacks LAMP.
Installer ce qu’il faut (grosso modo)
Tu peux tester dans un container en le lançant comme ceci (ici on autorise SYS_PTRACE, ce qui va te permettre d’utiliser strace)
docker run -it --name debian --hostname debian --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --restart unless-stopped debian:trixie-slim bash
Puis installer ce qu’il faut (dans le container) :
apt update
apt install -y strace php-cli php-cgi php-fpm apache2 ca-certificates
Petit rappel de contexte
php-cli: PHP en mode ligne de commandephp-cgi: exécution “façon web” (CGI), pratique pour simuler une requêtephp-fpm: le pool de workers en prodstrace: l’outil qu’on va utiliser
2) Notions à connaître :
C’est quoi un syscall ?
Un syscall (appel système), c’est quand ton programme demande au noyau de faire un truc qu’il ne peut pas faire tout seul.
Exemples typiques :
- ouvrir un fichier :
openat() - lire/écrire :
read(),write() - parler au réseau :
connect(),sendto(),recvfrom() - attendre :
nanosleep() - allouer de la mémoire (en vrai) :
mmap(),brk()
PHP “pur” fait plein de boulot en userspace, mais dès que tu touches au disque, au réseau, au temps, à la mémoire gérée par l’OS, tu retombes sur des syscalls.
C’est quoi la libc ?
La libc (souvent glibc sur Debian), c’est la grosse bibliothèque standard C du système. Beaucoup de langages (dont PHP via ses libs) passent par elle.
Important :
- beaucoup de fonctions “simples” cachent des syscalls
- exemple :
fopen()côté C va souvent finir enopenat()côté kernel
Unix : “everything is a file” (et les file descriptors)
Sur Unix, plein de choses se manipulent comme des fichiers :
- fichiers disque
- sockets réseau
- pipes
- stdout/stderr
Un file descriptor (FD), c’est juste un petit entier (3, 4, 5…) qui représente “un truc ouvert”.
Exemples :
0: stdin1: stdout2: stderr3+: fichiers, sockets, etc
strace peut t’aider à voir ce que cache un FD (genre “FD 7 = socket vers 93.184.216.34:443”).
3) Strace : c’est quoi exactement ?
strace colle un micro sur un process et log :
- chaque syscall
- ses arguments
- sa valeur de retour
- souvent le temps passé dedans
C’est ultra utile pour :
- repérer un DNS lent
- voir des I/O disque qui prennent cher
- trouver un
connect()qui bloque - détecter un process externe lancé par
system() - comprendre “pourquoi ça attend”
4) Déterminer les lenteurs d’un script PHP
Le script d’exemple
cat << 'EOF' > /var/www/test.php
<?php
$x=file_get_contents('https://ip.wtf');
$y=file_get_contents('http://perdu.com/');
system('sleep 5');
$z=file_get_contents('https://www.kokiris.com');
$data = random_bytes(100000);
for ($i = 1; $i <= 10; $i++) {
file_put_contents("/tmp/file_test{$i}", $data);
}
for ($i = 1; $i <= 10; $i++) {
$f = "/tmp/file_test{$i}";
chmod($f, 0777);
file_put_contents($f, '');
rename($f, "/tmp/file_test{$i}{$i}");
}
foreach (glob('/tmp/file_test*') as $f) { if (is_file($f)) unlink($f); }
$sizeInBytes = 16 * 1024 * 1024;
$elementSize = 8;
$numElements = intdiv($sizeInBytes, $elementSize);
$arr = array_fill(0, $numElements, 1);
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$request_uri = $_SERVER['REQUEST_URI'];
echo 'I have been called from ' . $protocol . '://' . $host . $request_uri;
EOF
Lancer strace proprement
mkdir /tmp/strace_test
strace -ff -tt -T -yyy -s 10000 -o /tmp/strace_test/demo -u www-data php8.4 /var/www/test.php
Comprendre les options principales
-f: suit les forks (indispensable avecsystem())-ff: un fichier par PID (beaucoup plus lisible)-tt: timestamp précis-T: durée du syscall (c’est la base pour chercher les lenteurs)-yyy: affiche des infos “humaines” sur les FDs (fichiers, sockets)-s 10000: augmente la taille des strings affichées-u www-data: lance la commande en tant que l’utilisateurwww-data-o /tmp/strace_test/demo: écrit les logs dans les fichiers /tmp/strace_test/demo.*
Trouver les appels web et DNS
Ce que tu vas voir côté syscalls :
- DNS : souvent des
sendto()/recvfrom()vers un resolver (port 53), et des lectures de/etc/resolv.conf,/etc/nsswitch.conf,/etc/hosts - HTTP/HTTPS :
connect()vers port 80/443, puissendto()/recvfrom()ouwrite()/read() - HTTPS : après le
connect(), tu vas voir beaucoup deread()/write()(TLS cause beaucoup d’échanges)
Filtrer réseau avec -e trace=%network
Si tu veux direct aller au but :
strace -ff -tt -T -yyy -s 10000 -e trace=%network -o /tmp/strace_test/demo -u www-data php8.4 /var/www/test.php
Repérer les connexions lentes
Tu peux chercher les connect() et les lignes avec une grosse durée à la fin ; ici je les affiche du plus rapide au plus lent :
grep -r "connect(" /tmp/strace_test/ | sed -n 's/^\(.*\) <\([0-9.]\+\)>$/\2\t\1/p' | sort -n
Trouver les opérations fichiers
Filtre “file” :
rm -f /tmp/strace_test/*
strace -ff -tt -T -yyy -s 10000 -e trace=%file -o /tmp/strace_test/demo -u www-data php8.4 /var/www/test.php
Tu vas tomber sur :
openat(): ouvrir un fichiernewfstatat()/statx(): infos fichierunlink(): suppressionchmod(): droitsrename(): renamewrite(): écritures
Et tu verras très vite tes boucles /tmp/file_test*.
ex :
grep -E "(chmod|unlink|rename|write|openat)\(.*/tmp/" /tmp/strace_test/* | awk '{print $NF "\t" $0}' | sort
Trouver le sleep et le process externe
Ton system('sleep 5') déclenche en général :
- un
fork() - puis un
execve()vers/bin/sh(ou directsleepselon cas) - puis un
nanosleep()côté processsleep
Pour repérer ça :
grep "execve" /tmp/strace_test/* | awk '{print $NF "\t" $0}' | sort
Bonus : le résumé “profiling” avec -c
strace -c te sort un tableau (temps cumulé, nombre d’appels). Super pour une première passe.
strace -c -f -u www-data -e trace=%network,%file,%process php8.4 /var/www/test.php
Tu perds le contexte ligne par ligne, mais tu gagnes une vue globale. Cela permet de spotter les appels qui sont effectués le plus de fois, ceux qui prennent du temps, etc.
5) Ok en local c’est bien, mais sur une prod comment je fais ?
Sur une prod avec PHP-FPM, si tu strace tout un pool, tu vas récupérer :
- plein de workers
- plein de clients
- du bruit partout
- et des logs énormes
Donc l’objectif c’est : isoler ton trafic et stracer le moins possible.
Technique 1 : utiliser un pool FPM dédié “trace” + un routage par IP
Idée :
- tu crées un pool FPM identique, mais sur un autre socket
- tu forces tes requêtes (depuis ton IP) à passer dessus
- tu strace uniquement ce pool
1) Créer un nouveau pool
Exemple fichier : /etc/php/8.4/fpm/pool.d/trace.conf
[trace]
user = www-data
group = www-data
listen = /run/php/php8.4-fpm-trace.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 2
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 1
Redémarre FPM :
sudo systemctl restart php8.4-fpm
2) Router selon ton IP dans Apache
Dans ton vhost, exemple simple (Apache 2.4) :
<If "%{REMOTE_ADDR} == '1.2.3.4'">
<FilesMatch "\.php$">
SetHandler "proxy:unix:/run/php/php8.4-fpm-trace.sock|fcgi://localhost/"
</FilesMatch>
</If>
<Else>
<FilesMatch "\.php$">
SetHandler "proxy:unix:/run/php/php8.4-fpm.sock|fcgi://localhost/"
</FilesMatch>
</Else>
Recharge Apache :
sudo systemctl reload apache2
Maintenant, seul ton trafic passe dans le pool “trace”.
3) Stracer uniquement les workers de ce pool
Pour attacher strace aux process du pool “isolé”
strace -ff -tt -T -yyy -s 10000 -o /tmp/strace_test/demo_fpm -p $(pgrep -d, -f 'php-fpm: pool trace')
Fais ta requête depuis ton IP, puis stop strace (Ctrl+C).
Technique 2 : simuler un appel web avec php-cgi (propre et isolé)
Ici tu exécutes le code “comme si” Apache le lançait, mais toi tu contrôles tout. Et tu peux stracer juste ce process.
Exemple :
root@debian:~# strace -u www-data -E REDIRECT_STATUS=1 -E REQUEST_METHOD=GET -E HTTP_HOST=www.example.com -E REQUEST_URI="/rewrited?token=abc" -E QUERY_STRING="token=abc" -E SCRIPT_NAME="/test.php" -E SCRIPT_FILENAME="/var/www/test.php" -E HTTPS=1 -E HTTP_X_FORWARDED_PROTO=https -E REMOTE_ADDR=127.0.0.1 -E SERVER_ADDR=127.0.0.1 -E HTTP_COOKIE="PHPSESSID=deadbeef123; consent=yes; cart=42" -ff -tt -T -yyy -s 10000 -o /tmp/strace_test/demo_cgi /usr/bin/php-cgi -d memory_limit=1024M /var/www/test.php ; echo
Content-type: text/html; charset=UTF-8
I have been called from https://www.example.com/rewrited?token=abc
Notes :
HTTP_COOKIEte permet de tester un comportement “connecté” ou avec un flag- tu peux simuler des headers via
HTTP_* - si tu veux simuler un POST, ajoute
CONTENT_TYPE,CONTENT_LENGTH, et envoie un body (c’est un peu plus long à mettre en place)
Cas d’usage : rejouer une requête authentifiée (backoffice, espace client)
L’intérêt majeur de pouvoir passer un cookie de session (PHPSESSID, ou tout autre cookie d’authentification), c’est de pouvoir rejouer exactement ce qui se passe derrière un backoffice ou un espace connecté.
En prod, tu as souvent des pages lentes accessibles uniquement après authentification : tableau de bord admin, export de données, génération de factures, etc. Impossible de les stracer via un simple curl sans être connecté.
Avec php-cgi + HTTP_COOKIE, tu peux :
- récupérer ton cookie de session depuis ton navigateur (DevTools > Application > Cookies)
- le passer à
php-cgipour simuler ta session active - stracer le script comme si tu étais connecté, sans passer par Apache
Cela te permet de debugger des lenteurs sur des pages protégées, sans avoir à modifier le code pour désactiver l’authentification.
Technique 3 : simuler avec le CLI via auto_prepend_file
Le PHP CLI ne remplit pas $_SERVER comme en web. Mais tu peux tricher : tu prepend un fichier qui construit $_SERVER, $_COOKIE, etc.
1) Créer un prepend
/tmp/prepend.php :
cat << 'EOF' > /tmp/prepend.php
<?php
// Fake environnement web minimal
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['HTTP_HOST'] = 'www.example.com';
$_SERVER['REQUEST_URI'] = '/fr/module/search/cron?token=abc';
$_SERVER['QUERY_STRING'] = 'token=abc';
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['HTTPS'] = 'on';
// Cookies (exemple : session + flags)
$_COOKIE = [
'PHPSESSID' => 'deadbeef123',
'consent' => 'yes',
'cart' => '42',
];
// GET
parse_str($_SERVER['QUERY_STRING'], $_GET);
EOF
2) Lancer ton script comme si c’était une requête web
root@debian:~# strace -u www-data -ff -tt -T -yyy -s 10000 -o /tmp/strace.cli php8.4 -d auto_prepend_file=/tmp/prepend.php /var/www/test.php ; echo
I have been called from https://www.example.com/fr/module/search/cron?token=abc
Avantage : c’est hyper simple à rejouer, tu peux faire varier $_COOKIE, $_GET, etc.
6) Mini checklist : que chercher dans les logs ?
Quand tu ouvres un log strace, les grosses familles utiles :
- Réseau
connect()lent vers 80/443sendto()/recvfrom()vers port 53 (DNS)poll()/epoll_wait()qui attend (souvent le process est bloqué)
- Disque
openat(),read(),write()qui prennent du tempsfsync()(si tu en vois sur du stockage réseau / disque chargé, ça peut faire très mal)
- Process externes
execve()(genre unconvert, unsleep, unwkhtmltopdf, etc)
- Mémoire
mmap()/munmap()en rafale,brk(), et parfois OOM derrière
Et surtout : garde un oeil sur les lignes avec un gros <X.XXXXXX>.
7) Deux conseils de bon sens
stracepeut logger des trucs sensibles (URLs, tokens, chemins, parfois du contenu). Fais gaffe où tu stockes les logs.- Ne trace pas “tout le site”. Isole ton test. Sinon tu vas juste produire du bruit et te faire détester par le CPU.
8) Pour aller plus loin
ltrace: un outil également très puissant ; celui-ci va tracer les appels de lib (quand c’est possible), pratique mais plus fragile selon les binaires (il faut les symboles, binaires non strippés, pas de PIE)bpftrace: strace sous stéroïdes, next level 💩 :D