Trouver la configuration optimale d'Apache : bonnes pratiques et analyses

le jeudi 12 mars 2020

À la demande générale et sous la pression populaire nous abordons aujourd'hui l'état de l'art pour une configuration d'Apache satisfaisante.

Cet article fait suite à notre dernier billet ou nous vous parlions d'Apache pour ses 25 ans.

Old school

Prenez une Debian stable (9/stretch au moment où ces mots sont écrits), installez Apache et PHP sans trop réfléchir :

$ docker run --rm -ti debian:stretch bash
# apt update && apt install apache2 libapache2-mod-php curl
# echo '<?php echo "Working.\n"; ?>' >/var/www/html/test.php
# service apache2 start
# curl -s localhost/test.php
Working.

Mais si vous essayez d'avoir plus de 150 connexions TCP simultanées, ça marchera mal. Notez qu'en HTTP/1.1 la norme pour les navigateurs consiste à utiliser 'Keep-Alive' (maintenir une connexion TCP(+TLS) ouverte le plus longtemps possible pour y faire passer N requêtes HTTP) et en moyenne 6 de ces connexions simultanées sont maintenues par navigateur. A partir de HTTP/2 la donne change, mais HTTP/1.1 reste largement déployé et utilisé et cet effet multiplicateur de 6 n'est pas à négliger : 150 connexions simultanées, c'est peut être 25 utilisateurs simultanés maximum.

Ca coince

Nous allons faire un test simple en générant un fichier quelconque de 1 MB et en générant 300 clients HTTP simultanés qui téléchargent ce fichier en boucle :

# dd if=/dev/urandom of=/var/www/html/dummy bs=1M count=1
# curl -sL https://storage.googleapis.com/hey-release/hey_linux_amd64 >hey && chmod +x hey
# ./hey -c 300 -z 30s -t 10 http://localhost/dummy
...
Response time histogram:
  0.001 [1]     |
  0.987 [106660]        |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  1.974 [378]   |
  2.961 [362]   |
...
Status code distribution:
  [200] 107824 responses

Error distribution:
  [278] Get http://localhost/dummy: net/http: request canceled (Client.Timeout exceeded while awaiting headers)

Il y a des erreurs : leur taux semble faible, mais en fait statistiquement la moitié des clients n'a pas pu obtenir de connexion (Apache n'en accepte que 150); plus exactement ils ont attendu 10s avant de décider qu'il s'agissait d'un échec et ceci est arrivé 278 fois en 30 secondes.

Reformulons : ce que ab/hey ne vous disent pas c'est que beaucoup de clients ne sont pas satisfaits par le temps de réponse voire la disponibilité de ce serveur; seuls ceux qui ont obtenu une connexion (dans les temps) ont reçu un service acceptable.

La distribution du temps de réponse est bien concentrée sur une valeur - servir un fichier statique déjà en cache est une activité stable et prévisible, ouf - et vous verrez dans le test suivant que ce système dont la file d'attente des connexions est toujours pleine est déjà deux fois plus lent.

La particularité de cette situation est que seuls les clients perçoivent le problème, le serveur ne loggera pas des connexions qui n'ont jamais été établies. Vous aurez par contre un signal à ne pas rater :

# tail /var/log/apache2/error.log 
[Tue Mar 03 13:56:00.673702 2020] [mpm_prefork:error] [pid 10333] AH00161: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting

Ca explose

Dans le cas des fichiers statiques, vos utilisateurs souffrent mais à l'issue du test votre conteneur/système se porte très bien. Avec une application PHP vous devriez vous attendre à voir également votre serveur souffrir, voir être menacé de :

  • La mort par le CPU : il n'y a pas de magie, si vous avez par exemple 16 interpréteurs PHP qui combattent pour l'accès à 8 CPUs, leur exécution sera 2 fois plus lente qu'en régime normal. Les applications PHP ne sont pas des consommatrices à 100% de CPU (elles attendent aussi souvent une réponse du serveur SQL), mais 50% est assez courant. La prudence consiste à ne pas faire tourner plus d'interpréteurs PHP que 2 fois le nombre de CPUs; avec Apache MaxClient=150 par défaut, nous vous souhaitons d'avoir un serveur avec ~75 CPUs !
  • La mort par la RAM : chaque interpréteur n'ira pas forcément consommer jusqu'à PHP's max_memory (128M par défaut), mais si votre serveur n'a pas quelque chose de l'ordre de 150*128M - 19 GB de RAM - il peut être sujet à d'atroces souffrances appelées swapping.

New school

Ceci demande un tout petit plus de configuration. Nous vous présentons ici la recette "FastCGI", il y a aussi la "PHP-FPM" qui est proche mais demande plus de configuration. Il y a des pour et contre subtils entre FastCGI et PHP-FPM, mais leur architecture est essentiellement la même.

La différence consiste à utiliser mod_fcgid pour lancer les interpréteurs PHP (qui ne sont donc plus embarqués dans Apache) :

$ docker run --rm -ti debian:stretch bash
# apt update && apt install apache2 libapache2-mod-fcgid php-cgi curl
# cat >/etc/apache2/mods-enabled/fcgid.conf
FcgidMaxProcesses 16
FcgidWrapper /usr/bin/php-cgi
AddHandler fcgid-script .php
<EOF>

# cat >/etc/apache2/mods-enabled/mpm_event.conf
ServerLimit        128
ThreadLimit         64
ThreadsPerChild     64
MaxRequestWorkers 8192
<EOF>

# cat >>/etc/apache2/sites-available/000-default.conf
<Directory /var/www/html>
    Options +ExecCGI
</Directory>
<EOF>

# echo '<?php echo "Working.\n"; ?>' >/var/www/html/test.php
# service apache2 start
# curl -s localhost/test.php
Working.

Cette configuration est minimaliste, prenez le temps de lire la documentation complète de mod_fcgid et ajustez les limites à vos usages - certains paramètres on en particulier des valeurs par défaut un peu fantaisistes (comme FcgidMaxProcesses = 1000 ou FcgidMaxRequestLen = 128kB).

Vous noterez que dans cette situation Debian a le bon goût de choisir par défaut le MPM Event et non le "vieux" MPM Worker. Par contre il vient avec des réglages conservateurs et toujours 150 connexions simultanées max, d'où les ajustements proposés ci-dessus. De même, consultez la documentation de ce MPM et ne mettez pas aveuglément en production cet exemple incomplet.

# dd if=/dev/urandom of=/var/www/html/dummy bs=1M count=1
# curl -sL https://storage.googleapis.com/hey-release/hey_linux_amd64 >hey && chmod +x hey
# ./hey -c 300 -z 30s -t 10 http://localhost/dummy
...
Response time histogram:
  0.001 [1]     |
  0.527 [102783]        |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  1.053 [64]    |
  1.579 [13]    |
...
Status code distribution:
  [200] 102879 responses

Il n'y a plus d'erreur, toutes les requêtes ont trouvé une réponse 200 dans le temps imparti. Plus précisément 99,99% ont été servies en environ une demi-seconde. C'est deux fois plus rapide que le test précédent (car il n'y quasiment pas de file d'attente qui se forme) mais surtout tous les clients sont servis.

Changement de paradigme

Nous n'avons pas fait de mesure sur une application PHP (il faudrait un article nettement plus long). Si vous le faites, vous allez probablement être déçu en première approche avec le modèle Event/FastCGI ou Event/FPM : vos mesures montreront moins de clients servis et pas mal d'erreurs 503. Par contre votre serveur se portera comme un charme, quel que soit la charge que vous lui soumettez.

Bienvenue dans le monde de la régulation. Vous avez le contrôle, vous pouvez enfin résoudre la contrainte impossible : je veux accepter plein de connexions simultanées (MaxClients > 5000) mais je veux limiter le nombre d'interpréteur PHP simultanés car ma capacité CPU/RAM est finie (MaxClients < 50).

Par contre les limites sont plus nettes : avant vos clients formaient des files d'attentes plus ou moins longues et les surcharges étaient amorties progressivement, en ralentissant l'ensemble de votre serveur - souvent jusqu'à des profondeurs insondables.

Maintenant si le lot d'interpréteur PHP que vous allouez est entièrement occupé, Apache répondra immédiatement 503 et vous aurez un message explicite par requête rejetée dans vos logs :

[Tue Mar 03 13:49:01.591711 2020] [fcgid:warn] [pid 11675:tid 139776078243584] [client 127.0.0.1:42182] mod_fcgid: can't apply process slot for /usr/bin/php-cgi

Vous avez donc le contrôle et les informations sur ce que perçoivent les utilisateurs. Ce n'est que le début de la solution. Il vous reste à analyser, optimiser, architecturer, "scaler"... Tout un métier. Celui de Bearstech.

Service Conseil et accompagnement apache

Bearstech vous propose ses services Conseil et accompagnement apache

Découvrir ce service

Partager cet article

Flux RSS

flux rss

Bearstech recrute

Administratrice/administrateur système GNU/Linux (5 ans d'expérience)

Bearstech recrute en CDI à temps plein un administrateur système H/F bénéficiant d’une expérience d’au moins 5 ans dans le domaine de l’administration système GNU/Linux.

Découvrir cette offre

Partager cet article :

Nos expertises technologiques