Configurations concernées

Ces conseils ont été rédigés pour au moins la configuration suivante :

  • Apache 2.2.3
  • PHP 5.1.6
  • MySQL 5.0.24
  • FreeBSD 6.1-STABLE

Bien entendu, ils sont plus ou moins valables pour d'autres écarts de configurations, mais peuvent tout de même vous donner des pistes de recherche.

Performances de MySQL

Un des défaut de MySQL sous FreeBSD, et qui apparemment ne se produit pas sous GNU/Linux, est l'effondrement de ses performances lors d'un trop grand nombre de requête simultanées. Dans mon cas, avec un Celeron 1,8 GHz et 1,2 Go de mémoire vive, les performances s'écroule à partir d'une trentaine de requêtes simultanées, bloquant la machine.

Le problème semble corrigé avec FreeBSD 7.0, mais il demeure possible d'atténuer ces problèmes pour les autres version de ce système d'exploitation.

Quelques pistes sont possibles:

  • Compiler MySQL avec le support des threads Linux. Il suffit à l'installation de passer le paramètre « WITH_LINUXTHREADS=yes » lors de l'installation (la façon peut varier selon la procédure d'installation : portupgrade, make install dans l'arbre des ports ou installation depuis les sources).
  • Compiler MySQL avec le paramètre « BUILD_OPTIMIZED=yes ». Ce paramètre passe des paramètres de compilations plus optimisés que les paramètre par défaut afin d'obtenir un exécutable plus performant. Difficile de chiffre précisément les améliorations apportés, elles doivent dépendre du système.
  • Compiler MySQL avec le paramètre « BUILD_STATIC=yes ». Ce paramètre compile MySQL avec les liens vers les bibliothèques externes en statique. Chaque fois qu'un appel à ces bibliothèques sera effectué, il n'y aura pas besoin de chercher la bibliothèque, le lien étant en dur dans l'exécutable. En revanche, lors d'une mise à jour des bibliothèques dont dépend MySQL, il y a un risque de casser ces liens statiques, ce qui nécessite une recompilation de MySQL.
  • Lancer MySQL en désactivant la résolution DNS lors de chaque connexion. En effet, à chaque fois qu'un client se connecte, MySQL effectue une résolution DNS inverse afin de connaître le nom d'hôte à partir de l'IP. Lors de nombreuses connexions simultanées, MySQL doit donc attendre que le serveur de nom réponde avant d'effectuer la requête, réponse qui peut être plus ou moins longue selon l'état du réseau ou la charge système. Pour désactiver la résolution DNS inverse, il faut passer le paramètre « --skip-name-resolve » lors du lancement de MySQL. Pour FreeBSD, rajoutez simplement dans le fichier « /etc/rc.conf » une ligne « mysql_args="--skip-name-resolve" » avant de redémarrer MySQL.
  • Paramétrer correctement l'utilisation mémoire de MySQL. En effet, plus une base de donnée à de mémoire à sa disposition, plus gros peut être son cache, et donc plus rapides peuvent être l'exécutions de requêtes (il est plus rapide d'aller chercher les résultats précédents en mémoire que d'accéder au disque dur). En revanche, trop allouer de mémoire à MySQL va pénaliser les autres processus du système. Il faut donc trouver un juste milieu. Il existe donc dans le répertoire « /usr/local/share/mysql/ » une série de fichiers à l'extension « .cnf » qui correspondent à des pré-réglages de MySQL pour certaines utilisations pré-définies. Il suffit de choisir le fichier le plus proche de ses besoins, et de le copier sous le nom « /etc/my.cnf », puis éventuellement de l'adapter plus finement à ses besoins. Puis, il faut relancer MySQL pour prendre en compte les nouveaux réglages.
  • Augmentez la taille de la mémoire allouée au tables temporaires (paramètre « tmp_table_size » dans le fichier « /etc/my.cnf »). Cette zone mémoire sert à stocker les tables temporaires créées lors de certaines requêtes. Si ce paramètre n'est pas assez grand, MySQL se servira du disque dur, plus lent que la mémoire vive. Il est par défaut paramétré à 32 Mo.

MPM et PHP

Apache à partir de sa version 2 supporte plusieurs MPM distincts. Les plus couramment rencontrés sont les suivants :

  • prefork : à chaque nouvelle connexion, un nouveau processus Apache est créé à partir de l'un des processus père. Ce nouveau processus chargera en mémoire tous les modules Apache présents en configuration. C'est la méthode old school, qui existe dans Apache 1.3.
  • worker : à chaque nouvelle connexion, un nouveau thread sera créé à partir d'un processus père. Cette méthode à pour avantage d'être plus rapide, car il n'est pas nécessaire de lancer un processus avec tous ses modules associés. Les modules étants partagés entre tous les threads d'un même processus père, l'occupation mémoire s'en retrouve diminuée.

Au vu de la liste ci-dessus, on va naturellement vouloir se tourner vers le MPM worker : plus réactif, moins de mémoire consommée, pourquoi hésiter. Le problème est que PHP ne supporte que le MPM prefork, et n'est absolument pas garanti avec le MPM worker. Ca peut fonctionner, comme ne pas le faire. Personnellement, ça a très bien fonctionné avec PHP 5.0.x, mais pas du tout avec PHP 5.1.x. Les symptômes rencontrés étaient des pertes de session, ou bien des pages blanches.

Donc pour éviter de perdre vos cheveux prématurement pour cause d'arrachage, MPM prefork uniquement. On pourrait se dire que dans ce cas là autant rester avec Apache 1.3. C'est effectivement quelque chose à penser, mais il faut se souvenir que cette version sera un jour ou l'autre abandonnée, et plus vite que la version 2.

PHP et MySQL

Vous avez deux méthodes pour créer des connexions à une base de données : les connexions permanentes, et les connexions non permanentes. Une connexion permanente est une connexion qui restera ouverte tant qu'elle n'est pas explicitement fermée (c'est à dire par l'instruction idoine, ou bien à la fin du script). Ça peut être utile pour gagner du temps en évitant de réouvrir la connexion quand on en a besoin, mais si plusieurs personnes consultent le site, le service MySQL risque de saturer rapidement sous la charge des connexions ouvertes.
À l'inverse, la connexion non permanente s'ouvre et se ferme à chaque instruction SQL envoyée au service MySQL. Les fonctions d'ouverture et de fermeture de connexion de PHP servent dans ce cas à créer ou supprimer la variable gérant les connexions. Contrairement à la connexion permanente, il y a une légère perte de temps lors de l'ouverture et de la fermeture de la connexion, mais ce dernier est négligeable (sauf peut-être si vous avez des scripts qui ont besoin de souvent lancer des requêtes). Dans le cas de la connexion non permanente, vous avez moins de chances de saturer le serveur si celui-ci a de nombreuses visites simultanées.
Pour désactiver les connexions permanentes dans PHP, il faut placer le paramètre « mysql.allow_persistent » à « Off » dans le fichier « /usr/local/etc/php.ini ».

PHP et occupation mémoire

Cela risque de paraître une évidence, mais PHP est lourd. Après quelques essais rapides, un processus Apache occupe 7 Mo en mémoire sans PHP, et 20 Mo avec. Et n'oubliez pas : plus vous aurez de connexions, plus vous aurez de processus Apache ouverts. A coup de 20 Mo par processus Apache, l'occupation mémoire grimpe assez rapidement.

Pour éviter de consommer trop de mémoire inutilement, n'installez que les module PHP qui vous sont nécessaires. De plus, si par hasard un module est problématique, vous aurez plus de chances de trouver lequel est concerné avec peu de modules installés qu'avec beaucoup. Quand je parle de module problématique, c'est dans le sens : j'accède à une page qui ne demande aucune fonction de ce module, mais Apache plante avec un code d'erreur 11. Pour trouver le module gênant, désactivez les modules un à un, puis relancez Apache et rechargez votre page jusqu'à ce que Apache ne plante plus. A une époque j'avais le module PHP Tidy qui me plantait Apache sans raison particulière.

PHP et performances.

PHP est un langage interprété, c'est à dire qu'à chaque fois qu'un script PHP va être lu et exécuté, l'interpréteur PHP va devoir contrôler sa syntaxe, puis le compiler en langage semi-natif ( appelé « bytecode »), puis l'exécuter. Pour un unique fichier PHP, le temps de contrôle puis compilation est négligeable. En revanche, pour un serveur web qui peut traiter plusieurs centaines de fichiers à la seconde, ce temps commence à peser lourd par rapport à celui de l'exécution.

Pour accélérer les choses, il existe ce que l'on appelle des « accélérateurs PHP » dont le rôle principal est d'optimiser le chargement des scripts PHP. Grosso modo, quand un script PHP sera demandé une première fois, sa syntaxe sera contrôlé, il sera compilé en bytecode de manière optimisée, et ce bytecode sera sauvé puis exécuté. Lors de la prochaine demande de ce même script, au lieu de tout recommencer à zéro, l'interpréteur PHP réutilisera le bytecode précédemment généré. Bref, un gain de temps qui peut être non négligeable.

Il existe plusieurs accélérateurs PHP, donc les deux plus connus sont :

  • ZendOptimizer. Il s'agit d'un accélérateur propriétaire, créé par Zend, la société qui fait aussi le Zend Engine (le moteur de PHP).
  • eAccelerator. Il s'agit d'un accélérateur PHP libre, dont les performances avoisinent paraît-il celles de ZendAccelerator.

J'ai d'abord essayé eAccelerator, mais j'ai eu quelques problèmes avec : cache non mis à jour, certains liens renvoyaient vers la page d'origine, etc. Avec ZendOptimizer, j'ai toujours ce genre de problèmes, mais de manière moins fréquente. Donc, je préfère n'en utiliser aucun.

En revanche, pour ceux qui veulent sauter le pas, il est possible d'accélérer légèrement ZendOptimizer si l'on ne se sert pas de fichiers cryptés par ZendGuard. Il faut rajouter dans son fichier « /usr/local/etc/php.ini », dans la section correspondant à ZendOptimizer, les lignes suivantes :

zend_optimizer.enable_loader=0
zend_optimizer.disable_licensing=0

Apache et AcceptFilter

AcceptFilter est une fonctionnalité basé sur des options du noyau permettant d'optimiser les connexions HTTP (voir le module noyau « accf_http » pour FreeBSD). Sur le papier, ça a l'air très bien. En revanche en pratique je suis loin d'être convaincu : occupation énorme en mémoire des processus (ils montaient à 45 Mo sans sourciller), processus ouverts mais jamais fermés (ce qui n'aide pas non plus pour la consommation mémoire). Le seul avantage que j'avais trouvé, quand le système n'était pas saturé, était que les connexions semblaient plus réactives. Donc mon conseil est de désactiver AcceptFilter. Pour FreeBSD, c'est facile, placez une ligne

apache22_http_accept_enable="NO"

dans « /etc/rc.conf » (le « apache22 » pouvant changer selon votre version d'Apache).

Apache et les limites en connexion.

Pour en revenir à l'occupation mémoire, quelques pistes pour éviter de mettre son serveur à genoux :

  • Prévoyez de la RAM, beaucoup de RAM. Pensez qu'Apache ne sera pas le seul processus à tourner sur votre machine, et que si vous utilisez un SGBD, celui-ci sera à l'aise avec beaucoup de mémoire pour son cache. Dans mon cas, le serveur a 1,2 Go de RAM installé, et MySQL est paramétré pour en utiliser 512 Mo à des fins de cache. À ces 1,2 Go de RAM sont associés 512 Mo de swap disque.
  • Prévoyez des limites en nombre de processus Apache. Ca ne sert à rien de vouloir laisser un nombre immense de connexions se produire si votre système ne peut pas les prendre en charge. En général, prévoyez environ une occupation swap de 200 Mo maximum, histoire de ne pas mettre la machine à genou lorsqu'elle passera son temps à swapper. Dans mon cas, je suis parti sur ces paramètres :
    • 512 Mo de RAM pour les processus Apache (1,2 Go - 512 Mo pour MySQL - 176 Mo pour les autres processus). À cela je rajoute 200 Mo de marge en swap. Ca me laisse donc 712 Mo de mémoire pour Apache.
    • 30 Mo par processus Apache (je prends un peu de marge) : 712 / 30 = 24 connexion simultanés. J'ai donc paramétré Apache pour ne pas dépasser les 25 processus lancés.

Conclusion

J'espère que ces quelques recettes pourront répondre à vos question. Elles ne sont pas parfaites car venues avec l'expérience.