Optimisations apache/php/postgres pour site à haut volume

J’ai basculé récemment le site Les souterrains de Delain vers un hébergement Dedibox V3, une solution d’hébergement bien plus légère que le serveur utilisé auparavant. J’ai choisi un OS Ubuntu serveur 10.04 pour cette occasion. Ce système d’exploitation est bien conçu pour des sites à petite/moyenne charge, mais sur un site comme delain, très rapidement, la machine s’est effondrée et n’a pas réussi à suivre l’afflux des requêtes. Voici un petit aperçu des choses que j’ai faites, et qui m’ont permis de faire tourner quand même ce site malgré les performances moindres de la machine.

Les chiffres

La base de données pèse environ 5Go, certaines tables atteignent les 15 millions d’enregistrements. Le site génère environ 2500 visites par jour, 250 000 pages/jour, pour un trafic échangé qui se situe entre 2 et 3 Go/jour.

Optimisation des requêtes sql

Je ne vais pas revenir sur ce point, nous en avons déjà parlé sur ce blog. Quelle que soit la puissance de la machine, si vos requêtes sont mal écrites, si les index ne sont pas bien positionnés, si il n’y a pas régulièrement des vacuum analyze sur votre base de données postgres, vous ne vous en sortirez pas. Il faut absolument analyser ce qui se passe, utiliser les vues pg_stat_activity et pg_locks pour analyser le comportement de votre base, voir ce qui bloque, et retravailler vos requêtes, pour les rendre les plus fluides possibles. Dans mon cas, beaucoup de traitements se lançaient plusieurs fois pour rien, causant de nombreux ralentissements. De même, vérifier à toujours bien cibler les zones à mettre à jour. Un update sans condition sur une grosse table va forcément demander beaucoup de ressources.

Optimisation du php

On le sait, ce qui prend le plus de temps machine, c’est souvent les entrées/sortie sur le disque. Si votre code php n’est pas optimisé, il risque de générer de nombreuses écritures inutiles. Par exemple, dans une page, si un paramètre (disons action) est passé en url (méthode GET), on fait une action, sinon une autre. Si le code ressemble à ceci :


if ($_GET['action'] == '')
instruction 1;
else
instruction 2;

Ce code va fonctionner. Toutefois, si on ne passe rien dans l’url, il va générer dans le log apache un warning car la variable $_GET[‘action’] n’est pas définie (plus précisément, l’index ‘action’ du tableau $_GET n’existe pas). Si cela arrive à chaque page appelée dans votre site, cela peut générer des dizaines de milliers de lignes, et participer au ralentissement du système. Dans notre cas, il vaudra mieux écrire :


if (isset($_GET['action'])')
instruction 1;
else
instruction 2;

qui va provoquer le même comportement, mais sans la ligne de warning.

Il est important d’éplucher le log apache pour traquer ces erreurs et les corriger (et non, je vous vois venir, réduire la verbosité du log php n’est pas une bonne idée…)

Postgres et la mémoire partagée (shared memory)

Par défaut, la mémoire partagée sur un système Linux est de 32M, ce qui est suffisant pour de très petites bases postgres, mais qui est très limité dès que l’on monte en charge. Il faut donc commencer par dire au système d’utiliser plus de mémoire partagée. La machine ayant 2Go de mémoire, j’ai commencé par la monter à 256 Mo.

Pour ce faire :

sudo sysctl -w kernel.shmmax=268435456
sudo sysctl -w kernel.shmall=268435456

Cela permet de modifier "à chaud" la mémoire partagée du noyau, mais ces valeurs seront perdues lors du prochain redémarrage de la machine. Pour faire en sorte qu’elles soient gardées, il faut éditer (en root) le fichier /etc/sysctl.conf pour y ajouter les lignes :

Une fois que c’est fait, nous allons dire à postgres d’utiliser plus de mémoire partagée. Pour ce faire, il faut éditer (toujours en root) le fichier /etc/postgresql/8.4/main/postgresql.conf et changer ces valeurs, avant de redémarrer postgres.


max_connections = 30
shared_buffers = 48MB

J’ai baissé le nombre de connexions maximales, en effet, je sais d’usage qu’il est rare qu’il y ait plus de 15 à 20 connexions simultanées. En revanche, j’ai augmenté la mémoire allouée à chaque connexion. Vu la mémoire partagée ajoutée, j’ai encore de la marge et je pourrais augmenter soit les shared_buffers, soit le max_connections.

Optimisations apache

Apache est très gourmand en ressources, notamment lorsqu’on le couple avec php. Les valeurs par défaut ne conviennent pas du tout à un gros volume de diffusion, et il faut absolument les revoir si on ne veut pas faire écrouler tout le système. Pour simplifier, par défaut, apache est prévu pour garder assez longtemps en mémoire les processus ouverts, de façon à réagir plus vite quand une nouvelle page est appelée. C’est très efficace dans la plupart des sites, mais si les processus en question demandent beaucoup de mémoire, il est plus efficace de les fermer pour libérer de la mémoire et d’en ouvrir un autre.

La directive qui va gérer ça est le KeepAlive dans /etc/apache2/apache2.conf. Imaginons que la première page demandée appelle un gros process php, et que celui ci prend 20Mo de mémoire, avec un KeepAlive à 30 secondes, si cette page est applée souvent, vous ne libérerez jamais la mémoire prise par apache, et vous allez vite tomber dans un cercle vicieux.

Pour ces réglages, tout dépend du site. Après plusieurs tests, j’ai mis la variable KeepAliveTimeout à 2, ce qui a donné un coup de fouet à la machine. Comme pour postgres, j’ai aussi baissé la variable MaxClients à 50 (le maximum de process constaté a été de 35, ça me laisse un peu de marge).

Gestion du swap

Le swap est un système de lecture/écriture de la mémoire sur le disque pour combler un manque de mémoire physique. C’est bien entendu bien plus lent que la mémoire physique, et c’est souvent une source de ralentissement du système. Par défaut, une machine qui a de la mémoire disponible utilise quand même du swap. Pourquoi donc ? C’est à cause de la variable swappiness du noyau. Cette variable permet au noyau de libérer des pages de sa mémoire qu’il estime non nécessaires immédiatement. Cette action libère de la mémoire, mais provoque des écritures sur le disque qui sont souvent inutiles. Par défaut, la valeur du swappiness est à 60, je l’ai réduite à 20 avec de bons résultats :

sudo sysctl vm.swappiness=20

et il faut bien entendu modifier le fichier /etc/sysctl.conf pour que cette modification soit gardée au prochain redémarrage (en rajoutant la ligne vm.swappiness=20 ) en fin de fichier.

Savoir s’arrêter à temps

Le site est également composé de traitements qui se lancent régulièrement via le cron. Un de ces traitements (intelligence artificielle des monstres, bien que ce soit plus artificiel qu’intelligent…) est très gourmand, et se lance régulièrement (toutes les 3 minutes). Il est composé de deux étapes : la première cible les montres qui doivent être joués, la seconde fait une boucle sur chacun de ces monstres. Quand ceci se lance, il se peut que cela génère une surcharge temporaire qui ralentisse tout le jeu, et qui peut amener à une situation de blocage. J’ai donc rajouté un test avant le lancement de chaque boucle :


if [ 6 -le cat /proc/loadavg | awk '{print $1}' | awk -F "." '{print $1}' ]
then
# on ne fait rien
else
# on traite la ligne fi

Ainsi, si la charge immédiate du système est supérieure à 6, je zappe les lignes, et je les traiterai au prochain lancement (si je peux). Cette valeur de 6 est arbitraire et peut être modifiée selon les besoins.

Conclusion

Il reste encore des optimisations à apporter, principalement dans le code de l’application. Les performances ne sont pas égales à l’ancien serveur, qui était un serveur de très haute qualité (quadri pro, 4 Go de ram, etc…), mais on arrive quand même à avoir quelque chose d’exploitable pour un coût presque divisé par 10.

One thought on “Optimisations apache/php/postgres pour site à haut volume

Laisser un commentaire