Strace Et PHP

Posté le 7 Jan, 2026

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 commande
  • php-cgi : exécution “façon web” (CGI), pratique pour simuler une requête
  • php-fpm : le pool de workers en prod
  • strace : 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 en openat() 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 : stdin
  • 1 : stdout
  • 2 : stderr
  • 3+ : 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 avec system())
  • -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’utilisateur www-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, puis sendto() / recvfrom() ou write() / read()
  • HTTPS : après le connect(), tu vas voir beaucoup de read() / 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 fichier
  • newfstatat() / statx() : infos fichier
  • unlink() : suppression
  • chmod() : droits
  • rename() : rename
  • write() : é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 direct sleep selon cas)
  • puis un nanosleep() côté process sleep

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_COOKIE te 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-cgi pour 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/443
    • sendto() / recvfrom() vers port 53 (DNS)
    • poll() / epoll_wait() qui attend (souvent le process est bloqué)
  • Disque
    • openat(), read(), write() qui prennent du temps
    • fsync() (si tu en vois sur du stockage réseau / disque chargé, ça peut faire très mal)
  • Process externes
    • execve() (genre un convert, un sleep, un wkhtmltopdf, 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

  • strace peut 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