notes/apache-prefork-150-workers
150 workers Apache sur 6 Go : le site tue sa propre base
Le MaxRequestWorkers par défaut de Debian promet 9 Go de mémoire à une machine qui en a 6. Et ps te donne le mauvais chiffre pour le recalculer.
- apache
- php
- mémoire
Le MaxRequestWorkers livré par Debian vaut 150. Presque personne n'y touche. En MPM prefork, chaque requête occupe un processus entier qui charge PHP et ses extensions, une soixantaine de mégaoctets pièce. La configuration par défaut promet donc 9 Go à des machines qui en ont souvent la moitié.
Tant que le trafic reste calme, rien ne se voit. Le jour où une affluence remplit les 150 places, la mémoire et le swap se vident ensemble et le noyau abat le plus gros processus qui reste. C'est la base de données. Le site vient de tuer sa propre base, et il recommencera à la prochaine affluence.
Pour recalculer la bonne valeur, il faut le poids d'un worker, et ps te le donne faux. Mesuré le 16 septembre 2026 sur une machine de production : ps annonçait 223 Mo de moyenne par processus Apache. Le dump du noyau au moment de la panne en donnait 57. ps compte dans chaque processus le code PHP partagé entre tous. Le chiffre utile est la mémoire privée, et le noyau la consigne pour chaque processus quand il tue :
journalctl -k --since "<date du kill>" --until "<deux minutes après>" \
| grep -oE "[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +apache2$" \
| awk '{print $2*4/1024}' | sort -n
La deuxième colonne est en pages de 4 Kio. Ce jour-là : 151 processus, médiane à 61 Mo, maximum à 75 Mo, 8 578 Mo cumulés sur une machine de 5 925 Mio.
Le calcul prend le pire cas, pas la moyenne. 5 925 moins 2 000 gardés pour la base, les autres conteneurs et le système, il reste environ 3 900. Divisé par 75, ça fait 52 places. J'ai posé 50. Le swap ne compte pas : au moment du kill il était plein à 99 %.
La valeur se vérifie sous charge. Soixante requêtes simultanées sur 127.0.0.1 avec l'en-tête Host du site, ce qui évite le CDN et fail2ban, et un comptage pendant :
for i in $(seq 1 60); do curl -s -o /dev/null -H "Host: <site>" http://127.0.0.1/ & done
docker exec <conteneur-web> sh -c 'ps -C apache2 --no-headers | wc -l'
Le bon résultat est 51 : la valeur posée, plus le processus parent.
Reste le détail qui annule tout. Sur une stack Docker, la configuration d'Apache vit dans l'image, et seul le répertoire du site est monté. Un réglage posé à la main dans le conteneur disparaît à la première recréation, et le 150 revient sans prévenir. Le fichier se pose sur l'hôte et se monte en lecture seule depuis le compose.
Avec 50 places, une grosse affluence fait patienter des visiteurs. Après la bascule, tu regardes la file d'attente et tu remontes d'un cran si le site rame.
retour aux notes