Blog

Écureuils sous stéroïdes

Spip et stéroïdes

Ou comment passer de Spip3 sous LAMP à Spip3 sous nginx+php-fpm+xcache+mysql

Si vous utilisez spip, vous avez très probablement déployé la configuration recommandée à base de LAMP. Puis vous avez eu une montée en charge assez violente et vous commencez à noter des ralentissements.

Vous avez donc décidé de cacher votre site pour en améliorer la disponibilité et le temps de réponse, arrivant sur une configuration qui est généralement nginx-apache2-php nginx ne servant que de cache pour apache2 qui fait toujours tout le travail.

Je vous propose de passer au niveau suivant et de tuer les indiens, et donc de se débarasser d'apache. Et de donner de la drogue aux écureils parce que les écureils défoncés au speed c'est toujours plus drôle.

La configuration est une debian stable - wheezy donc, tout sur un seul serveur, y compris la base de donnée. On commence par la fin de la chaine et on remonte vers le haut

Installation de php5-fpm

Php fpm permet d'utiliser FastCGI pour php. En gros, le principe est d'utiliser un interpréteur php plus performant (et oui, capable de multi
thread aussi) pour l'exécuter au lieu de passer par les modules serveurs.

Et avec nginx, on a pas de moduls serveur. Nginx ne fait que serveur http, il n'interprète pas de code, mais il fait très bien serveur de cache et proxy. Et il sait faire du fastcgi aussi.

La configuration de ph-fpm par défaut est bien faites et les fichiers sont largement documentés. Prenons un terminal et allons-y:

sudo apt-get install php5-fpm

Il s'agît ensuite de modifier un peu le fichier de configuration. Toute la config de php5-fpm se trouve dans /etc/php5/fpm avec un php.ini, des répertoires de configuration spécifiques conf.d/ et des pool définies dans pool.d/. Chaque pool correspond à une application php, et peut donc être configurées différement (si vous avez plusieurs sites, une pool par site peut-être intéressante).

Le seul fichier que nous allons modifier est le fichier /etc/php5/fpm/pool.d/www.conf qui correspond à la pool par défaut que nous
allons utiliser.

Modifier les lignes correspondantes:

listen = /var/run/php5-fpm.sock # Nous allons écouter sur un socket unix, vu que tout est sur la me machine, pas besoin de configuration réseau.
php_flag[display_errors] = off # On affiche pas les erreurs php
php_admin_flag[log_errors] = on # Par contre on les log
php_admin_value[error_log] = /var/log/fpm-php.www.log # et on les logs dans ce fichier.

On active ensuite php5-fpm au démarrage de la machine, et on le redémarre:

sudo update-rc.d php5-fpm defaults && sudo service php5-fpm restart

Maintenant tout process parlant au socket unix /var/run/php5-fpm.sock pourra envoyer des ordres à php-fpm et récupérer les résultats. Ce fichier est possédé par www-data:www-data (du moins dans debian) ce qui est parfait.

Installation et configuration de nginx

On passe ensuite à l'installation et à la configuration de nginx.

sudo apt-get install nginx

La configuration est découpée en plusieurs fichiers. J'aime bien garder les fichiers système le plus proche possible de la version du paquet, ça facilite le travail de mise à jour, et nginx propose d'inclure de la config dans conf.d/ et dans sites-enabled/

Nous allons commencer par mettre en place du cache au niveau de nginx avant de s'attaquer au gros morceau. Nous allons donc créer un fichier pour ça:

/etc/nginx/conf.d/cache.conf

##
# Let's use some cache
##

fastcgi_cache_path /var/lib/nginx/cache levels=1:2 keys_zone=example_com:10m inactive=7d max_size=800m;
fastcgi_temp_path /var/lib/nginx/tmp;
fastcgi_cache_key $scheme$host$request$is_args$args;

fastcgi_cache_path définit la façon dont est géré le cache par le système. On commence par l'endroit où sont stockés les fichiers de cache, puis le niveau de profondeur de l'arborescence (ici deux niveaux, l'un avec le dernier caractères, puis les deux précédents). On continue avec la définition d'une zone de méore partagées poru stocker les clefs de cache. elle dispose d'un nom et d'une taille, sachant que dasn 1Mo, on peut stcoker 4000 clefs, 10Mo semblent largement suffisant. Le paramètre suivant définit au bout de combien
de temps une zone de cache inactive sera supprimé et enfin max_size permets de limiter la taille sur disque du cache.

fastcgi_temp_path définit l'endroit où les ficheirs en cours de création sont stockés avant d'être déplacés.

fastcgi_cache_key est la clef de hashage unique utilisée pour différencier les différents éléments de cache. On utilise ici l'URL complète, arguments inclus.

Voilà pour le cache. Passons maintenant au gros morceau, la configuration du virtual host. Je ne fais pas de TLS ici, mais adapter la configuration est facile à faire.

On édite donc le fichier /etc/nginx/sites-available/example.org.com.conf

server {
        # Ce serveur sert simplement à rediriger example.com vers
        # www.example.com
        listen 80;
        server_name example.com;
        return 301 $scheme://www.example.com:80$request_uri/;
}

server {
        # Nous avons ici la configuration principale du virtual host
        listen 80;
        server_name www.example.com;
        access_log /var/log/nginx/www.example.com.access.log;

        # Nous rangeons tous les fichiers sous ce répertoire
        root   /srv/spip/example-com/;

        # Et si on demande à visiter un répertoire spécifique, ceci est la
        # page d'index.
        index index.php;

        location ~^/(tmp|config)/{
                # Personne ne doit accéder au contenu de www.example.com/tmp
                # ni de www.example.com/config.
                return 403;
        }

        location ~* \.(jpg|jpeg|gif|css|png|js|ico|swf|mp3|pdf)$ {
                # Le contenu statique, est signalé au navigateur comme étant
                # à garder en cache une semaine. Si il y a un proxy sur la
                # route, celui-ci est autorisé à faire une copie et à la
                # cacher.
                expires        1w;
                add_header  Cache-Control public;
        }

        location / {
                # La configuration globale du site. Tout ce qui ne va dans
                # aucune autre location vas ici.

                # Quelques fichiers standards générés par spip et devant être
                # à des URL précises.
                rewrite ^/([^/]*)/robots\.txt$    /spip.php?page=robots.txt    last;
                rewrite ^/([^/]*)/favicon\.ico$   /spip.php?page=favicon.ico   last;
                rewrite ^/([^/]*)/sitemap\.xml$   /spip.php?page=sitemap.xml   last;
                rewrite ^/([^/]*)/mobile\.html$    /spip.php?page=mobile_uk   last;

                # On essaye de voir si le fichier correspondant existe, ou si
                # c'est un répertoire. Sinon, on l'envoie à spip.php
                try_files $uri $uri/ /spip.php?q=$uri&$args;
        }

        location ~ \.php$ {
                # Ici on s'occupe de toutes les uri finissant par .php
                # On commence par inclure tout un tas de régalges par défaut
                include fastcgi_params;

                # On définit l'endroit où se trouve php5-fpm, il doit s'agir
                # de la même valeur que pour la variable listen dans la
                # configuration de la pool php5-fpm. Précédé du scheme unix:
                # si il s'agît d'un socket unix.
                fastcgi_pass unix:/var/run/php5-fpm.sock;

                # On dit à php5-fpm où sont les fichiers à évaluer. Ici ils
                # sont simplement stockés sous document_root.
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

                # On veut savoir si l'URI correspond au backend ou pas
                set $ecrire 0;
                if ($uri ~ ^/ecrire.*) {
                        set $ecrire 1;
                }

                # On définit ici la zone de cache a utiliser.
                fastcgi_cache example_org;
                # On ne mets pas en cache les pages 302? et on ne conserve
                # les 404 que 10 minutes.
                fastcgi_cache_valid 302 0;
                fastcgi_cache_valid 404 10m;
                # Si le cookie spip_session ou si la variable écrire est
                # différente de 0, on ignore le cache. Cela permets aux
                # rédacteurs de voire leurs modifiactiosn en temps réel.
                fastcgi_cache_bypass $cookie_spip_session $ecrire;

                # On a définit dans spip un cache de 30 minutes, donc pas
                # besoin que le navigateur vienne cherché du contenu si il a
                # moins de 30 minutes.
                expires 30m;
        }
}

On crée ensuite le lien dans sites-enabled et on redémarre nginx:

 sudo ln -s /etc/nginx/sites-{available,enabled}/example.com.conf && sudo service nginx reload

Et on teste que tout fonctionne.

Plus de stéroïdes!!!!

Il est maintenat temps de faire une dernière injection de stéroïdes. Le but est de cacher le résulta des instructions php. Notamment des fichiers inclus que l'on retrouve partout (par exemple, headers et footers) et dont le résultat entier est mis en cache par nginx. Dans notre config actuelle, toute la page est calculée une seule fois, même si seul un bloc diffère. Le but ici et de n'avoir à calculer que ce bloc là et donc d'accélérer la mise en cache des pages qui n'y sont pas encore.

On va utiliser pour cela notre dealer préféré: xcache.

L'installation est super facile à faire, il y a un paquet debian pour ça.

 sudo apt-get install php5-xcache

Il faut ensuite mettre en place l'interface d'administration de xcache, et donc ajouter une location à notre site et en limiter l'accès aux adresses IP de votre site de production. Toujours dans le fichier /etc/nginx/sites-available/www.example.com.conf (ou ailleurs, si vous avez un site dédié à la maintenance ou supervision de vos machines), on va ajouter ce bloc là:

        location ~ ^/xcache/ {
                root /var/www/tools/;
                index index.php;

                fastcgi_pass unix:/var/run/php5-fpm.sock;
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$uri;
                allow ADRESSE.IP.DES.ADMINS;
                deny all;
        }

Comme l'interface est en php, on utilise aussi php5-fpm. Il faut encore créer le root_directory, y mettre les bons liens et recharger nginx:

sudo mkdir -p /var/www/tools && sudo ln -s /usr/share/xcache/admin /var/www/tools/xcache && sudo service nginx reload

Il faut ensuite définir un mot de passe et un login pour protéger cette interface. Heureusement, il existe une page pour ça fournie par xcache. Allez visiter http://www.example.com/xcache/mkpassword.php choisissez un pass et récupérer le hash généré. Vous pouvez aussi le générer avec un appel à md5() de php (oui je sait, c'est pas safe. Mais comme on a limité l'accès à ce service par filtrage d'IP ça fera l'affaire).

Il nous faut ensuite configurer le reste de xcache. Il n'est nécesaire de le faire que pour php5-fpm, nous allons donc créer le fichier
/etc/php5/fpm/conf.d/30-xcache.ini

xcache.admin.user = "admin"
xcache.admin.pass = "HASH GENERE PLUS HAUT"
xcache.size  =  128M
xcache.count = 8

xcache.size est la taille en mémoire occupée par xcache, et xcache.count est le nombre de thread (un par cœur est un bon compromis). Adaptez ces deux paramètres en fonction de vos configurations. Les autres valeurs sont définies dans /etc/php5/mods-available/xcache.ini. Si vous voulez changer des valeurs par défaut, changez les dans le fichier /etc/php5/fpm/conf.d/xcache.ini

On redémarre maintenant php5-fpm et hop là, des stéroïdes vont couler à flot dans l'écureuil.

sudo service php5-fpm restart

Et voilà. En allant consulter la page www.example.com/xcache vous pourez voir l'état de mise en cache. Et voir aussi la vitesse de génération des pages.

Bonus: Redirection automatique par langue

Nous avons un site multilangue, et le plygin crayons (pourquoi c'est important? vous verrez par la suite) et nous voulons redirigez les
utilisateurs sur la page d'acceuil correspondant à leur langue par défaut (header Http-Accept-Language). Si un navigateur français arrive sur www.example.com, je veux le rediriger sur www.example.com/fr/.

La subtilité est que, avec le plugin crayon, nous avons besoin d'avoir des requètes POST sur la racine (crayon fait des POSTS sur /) et que la norme HTTP définit que si un POST se prend un 301 ou un 302, il doit être ignoré. Les navigateurs suivent généralement la 302 et renvoie un GET à l'URL de destination ce qui a pour effet de faire disparaitre les données du POST.

La solution la plus simple est donc de définir une location dans nginx qui correspondent uniquement à la racine et d'utiliser un bloc if - même si if est maléfique c'est ici un des rares cas où la seule solution est un if.

La configuration nécessite aussi de définir une variable lang dépendant du header Http-Accept-Language (la première langue précisée est celle voulue). À noter que cette configuration relativement simple ne gère pas les prioriétés. Si la langue préférée n'est aps trouvée, on chargera de l'anglais peu importe les autres préférence de l'utilisateur.

On ajoute le fichier /etc/nginx/conf.d/lang.conf qui a le contenu suivant:

map $http_accept_language $lang {
        default en;
        ~^en en;
        ~^es es;
        ~^fr fr;
        ~^fa fa;
        ~^tr tr;
        ~^ar ar;
        ~^ru ru;
}

Puis d'ajouter la redirection par langue dans
/etc/nginx/sites-available/www.example.com.conf

        location ~ ^/$ {
                # Cette location n'est valable que pour la racine. Si on a un
                # POST il ne se passe rien, sinon on renvoie la page
                # d'acceuil de la langue retrouvée. Avec un 307 (redirection
                # temporaire)
                if ($request_method = GET)
                {
                        return 307 $scheme://$host/$lang/;
                }
        }

Et on recharge nginx

sudo service nginx reload

Et voilà. Un bel écureuil sous stéroïdes avec nginx. J'ai mis un peu de temps pour arriver à cette configuration, mais elle fonctionne bien (on a 25 000 pages vues en production sur cette configuration et a marche très bien).

Comments are closed.