ici & ailleurs

Un serveur web avec Nginx, PHP et MySQL

L'hébergement virtuel proposant des "tranches" de serveur devient de plus en plus populaire. Vous trouverez un peu partout des offres comparables à celles proposées par Gandi ou Slicehost et, de mon point de vue, elles représentent l'avenir de l'hébergement de sites web.

En regardant les offres de près, le premier réflexe est généralement d'être effrayé par les tarifs. Surtout en France, où certaines sociétés laissent croire qu'on peut fournir un hébergement de qualité pour 2 EUR par mois.

Pour une somme allant de 14 à 20 EUR par mois, vous pourrez trouver un hébergement disposant de 256 Mo de RAM, un puissance CPU raisonnable et une flexibilité que vous ne trouverez pas ailleurs. Et vous serez administrateur de votre machine ce qui n'est pas le dernier des avantages.

256 Mo

Il y a de cela 8 ans, j'avais un vieux PC dans ma cuisine, avec 192 Mo de RAM, un CPU poussif et un débit montant de 128 kbits/s. Ça ne m'empêchait pas de faire tourner un serveur web, un serveur de mail. Les début de dotclear.net se sont faits sur cette machine, à côté du frigo.

Du coup, je me suis dit, qu'il n'y avait aucune raison de ne pas faire fonctionner correctement un serveur web avec 256 Mo de RAM, un meilleur CPU et une bande passante bien plus confortable.

Afin de tester mon idée, j'ai créé une machine virtuelle avec vmware et une image Ubuntu server. Cette liste propose un grand nombre d'images de VM prêtes à l'emploi, ça vous fera gagner du temps si vous voulez faire des essais.

Au démarrage la machine virtuelle n'utilise que 16% de ses 256 Mo de mémoire. (Avec uniquement un serveur SSH).

MySQL

Sur Ubuntu, la configuration par défaut de MySQL est réputée pour consommer un peu trop de ressources. Nous pouvons améliorer ceci en modifiant un peu le fichier /etc/mysql/my.cnf. Faites en sorte d'avoir ces lignes dans le fichier :

key_buffer           = 16K
max_allowed_packet   = 1M
thread_stack         = 64K
thread_cache_size    = 8
sort_buffer_size     = 64K
read_buffer_size     = 256K
read_rnd_buffer_size = 256K
net_buffer_length    = 2K

Si vous n'avez pas l'intention d'installer Dotclear ou plus généralement, ne comptez pas utiliser InnoDB, désactivez le en ajoutant la ligne :

skip_innodb

Cette configuration me donne une utilisation de mémoire d'environ 30 Mo. Ça dépendra, bien sûr, de vos applications.

Oublions Apache

De l'ensemble LAMP, nous voulons conserver Linux, MySQL et PHP. Nous allons remplacer Apache par un autre serveur : Nginx. Nginx (on dit Engine X) est ce qu'on pourrait appeler la nouvelle génération de serveurs web. Il peut remplacer Apache dans de nombreuses circonstances. Je vous laisse lire la présentation du serveur ainsi que ce billet.

Pour installer Nginx, apt, yum ou votre gestionnaire de paquet feront l'affaire. Assurez vous simplement d'avoir une version 0.7.x. Nous verrons ensuite comment le configurer.

PHP et PHP Xcache

Nginx, contrairement à Apache, ne propose pas d'équivalent à mod_php. Vous ne pouvez pas non plus utiliser PHP simplement en CGI (tant mieux). Vous allez devoir utiliser FastCGI et indiquer à Nginx comment y accéder.

Pour faire simple, nous allons installer spawn-fcgi et écrire un script pour lancer nos processus PHP. Sur Ubuntu/Debian :

apt-get install php5-cgi spawn-fcgi

Le script permettant de lancer les processus PHP est le suivant, vous devez le créer dans /etc/init.d/php5-fcgi :

#!/bin/sh

### BEGIN INIT INFO
# Provides:       php5-fcgi
# Required-Start: $remote_fs $syslog
# Required-Stop:  $remote_fs $syslog
# Default-Start:  2 3 4 5
# Default-Stop:   0 1 6
# Short-Description: PHP5 FastCgi Spawned processes
### END INIT INFO

COMMAND=/usr/bin/spawn-fcgi
ADDRESS=127.0.0.1
PORT=9000
USER=www-data
GROUP=www-data
PHPCGI=/usr/bin/php5-cgi
PIDFILE=/var/run/fastcgi-php.pid
RETVAL=0

PHP_FCGI_MAX_REQUESTS=500
PHP_FCGI_CHILDREN=2

start() {
    export PHP_FCGI_MAX_REQUESTS PHP_FCGI_CHILDREN
    $COMMAND -a $ADDRESS -p $PORT -u $USER -g $GROUP -f $PHPCGI -P $PIDFILE
}

stop() {
    /usr/bin/killall -9 php5-cgi
}

case "$1" in
    start)
      start
      RETVAL=$?
  ;;
    stop)
      stop
      RETVAL=$?
  ;;
    restart|reload)
      stop
      start
      RETVAL=$?
  ;;
    *)
      echo "Usage: fastcgi {start|stop|restart}"
      exit 1
  ;;
esac
exit $RETVAL

Pour activer votre script (avec Ubuntu ou Debian) :

update-rc.d php5-fcgi defaults

Maintenant vos processus PHP se lanceront au démarrage. Un petit mot sur deux paramètres importants. Les processus PHP en CGI ont une tendance connue à planter de manière régulière. C'est à ceci que sert à la variable PHP_FCGI_MAX_REQUESTS que nous avons mis à 500. Tous les 500 cycles, chaque processus PHP sera relancé. Enfin, PHP_FCGI_CHILDREN=2 indique de lancer deux processus PHP. Vous pouvez en mettre plus mais n'oubliez pas que plus il y en a plus vous consommerez de mémoire.

Une fois en production, vous pourriez constater que c'est un peu lent. L'installation du paquet php5-xcache améliore grandement les performances de PHP. Consultez le site de Xcache pour en savoir plus. J'ai doublé la taille du cache (xcache.size dans /etc/php5/conf.d/xcache.ini) mais ne perdez pas de vue que cette taille est appliquée à chaque processus PHP (soit 2 fois 32M dans mon cas).

Maintenant, vous pouvez lancer votre processus à l'aide de /etc/init.d/php5-fcgi start.

Notez également que PHP FPM peut remplacer spawn-fcgi. J'en ai lu le plus grand bien mais ne l'ai pas testé.

Configuration de Nginx

Vous avez bien entendu installé Nginx avec un classique apt-get install nginx.

Nginx pour Ubuntu ou Debian fourni un fichier de configuration pour FastCGI. Nous allons le compléter. Le fichier est /etc/nginx/fastcgi_params et doit contenir ceci :

fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

fastcgi_split_path_info ^(.+\.php)(.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

Les derniers paramètres permettent d'utiliser PHP en mode PATH_INFO (à condition de bien configurer Nginx). Le tout dernier paramètre est le plus important, c'est celui qui va dire à PHP quel script exécuter.

Maintenant, testons ceci. Créez un fichier /var/www/nginx-default/test.php dans lequel vous pouvez par exemple mettre un appel à phpinfo() ou ce que vous voulez.

Ensuite, modifiez le fichier /etc/nginx/site-enabled/default (il peut se trouver ailleurs si vous n'êtes pas sur Ubuntu/Debian). Vous pouvez aussi créer un nouveau fichier, vous faites comme vous voulez.

server {
    listen   80;
    server_name localhost;
    root /var/www/nginx-default;
    index index.php index.html;

    access_log  /var/log/nginx/localhost.access.log;

    location ~ ^(.+\.php)(/.*)?$ {
        fastcgi_pass  localhost:9000;
        include /etc/nginx/fastcgi_params;
    }
}

Relancez le serveur avec /etc/init.d/nginx restart et rendez vous sur votre nouveau site pour pointer sur le fichier test.php. Votre script doit s'exécuter.

Ça ne marche pas ? Vous obtenez le très informatif "No input file specified" ? Quelque chose est mal configuré. Ça peut être un problème de permission. Commencez par essayer d'enlever la partie se chargeant d'interpréter PHP et de charger votre script. Vous pouvez également utiliser strace pour repérer quelle valeur de SCRIPT_FILENAME est passée à PHP.

Petit bonus : installation de Dotclear

Dotclear, avec la configuration que nous venons de voir, s'installe très bien. Vous pourrez même l'utiliser en PATH_INFO avec une URL du genre /index.php/...

Peut-on faire mieux ? Yes we can.

Avec Apache, vous aviez la possibilité d'utiliser Mod Rewrite pour lui dire quelque chose comme : "Si ce n'est pas un fichier ou un répertoire transforme l'URL vers tel script". Nginx propose des options de rewrite du même type avec une limite de taille ; on ne peut pas imbriquer les tests. On peut donc tester si l'URL demandée pointe vers un fichier, ou vers un répertoire mais pas les deux. C'est gênant. On peut s'en sortir en écrivant une configuration très complexe et difficile à maintenir. Sinon on peut se rappeler que Nginx n'est pas Apache et qu'il existe peut-être un autre moyen.

Nginx fournit une directive appelée try_files qui va faire exactement ce que nous ferions avec Apache rewrite en testant si le fichier et le répertoire n'existe pas.

Donc, pour votre Dotclear à la racine pointant vers /dotclear/index.php, voici la recette :

location / {
    try_files $uri $uri/ /dotclear/index.php$uri?$args;
}

La même chose est possible pour Wordpress, Drupal et consort.

Quelques tests et conclusion

Ma configuration est une machine virtuelle VMWare avec 256 Mo de RAM utilisant un seul core de CPU sur un MacBook Pro. C'est sans doute un peu plus que ce dont dispose une part d'hébergement mais ça donne une idée.

Voici quelques chiffres :

Requêtes (simultanées) Temps par requête Utilisation Mémoire
500 65ms 52%
500 (5) 342ms 56%
500 (20) 1330ms 56%
500 (50) 3436ms 56%

C'est tout à fait honorable pour du PHP avec aussi peu de ressources. L'autre aspect intéressant est que même avec 50 visiteurs au même moment, vos fichiers statiques sont servis en moins d'une demie-seconde. Et contrairement à Apache, l'utilisation mémoire n'augmente pas avec le nombre de requêtes simultanées.

La conclusion est que, oui, avec 256 Mo de RAM, vous pouvez disposer d'un serveur web tout à fait performant et vous disposez même d'une marge de manoeuvre permettant d'installer un serveur SMTP et IMAP.

N'étant pas non plus un expert sur le sujet, n'hésitez pas à signaler les erreurs. Et si vous avez des astuces concernant PostgreSQL avec une telle configuration, ça m'intéresse.