J’ai eu à mettre en place Varnish récemment. Je ne connaissais que de nom, cela m’a permis de découvrir l’outil dans le détail, de connaître mieux son paramétrage et les modifications qu’il a fallu faire sur le site pour tirer parti au mieux de ce système de cache HTTP. Je vous fais part de ce retour d’expérience, n’hésitez pas à partagez en commentaire vos expériences sur le sujet.

Cela faisait longtemps que je n’avais pas posté sur ce blog. Un retour d’expérience sur le mise en place de Varnish est une très bonne raison de le faire.

Varnish : Un cache HTTP

Varnish est un système de cache HTTP. Dans une architecture, il vient se mettre devant les frontaux web, qui sont eux mêmes devant le serveur de base de données. Quand l’internaute demande une ressource via une url, Varnish va voir si cette ressource le concerne, et si oui s’il l’a en cache. Si la page est dans son cache, il renvoie directement la page, sans même passer par le serveur Apache par exemple, ou une requête MySQL. Les temps de réponse sont donc forcément beaucoup plus rapides. D’autant qu’il est possible de configurer Varnish pour stocker le cache en RAM, ce qui fait qu’il n’y a même pas besoin d’une lecture disque pour avoir accès à l’information. S’il n’a pas la page en cache, il passe la main aux frontaux qui traiteront l’information comme ils ont l’habitude de le faire. Et quand les frontaux vont répondre, Varnish en profitera pour mettre cette réponse en cache.

Installation, fonctionnement et paramétrage de Varnish

Varnish se trouve dans les dépôts Linux, par exemple sous Debian vous pouvez l’installer par un simple apt-get install varnish.
Varnish tourne via un démon et peut être configuré par un fichier .vcl. Ce fichier vlc va permettre de définir les backend, ainsi que le comportement de Varnish dans chacune des phases d’exécution d’une requête.

Pour Varnish, les backend sont les frontaux web qui du coup se trouvent derrière lui. Ils se déclarent dans le fichier VCL comme suit :


backend web1 {
.host = "192.168.1.10";
.port = "80";
.connect_timeout = 1s;
.first_byte_timeout = 30s;
.probe = {
.url = "/";
.timeout = 15s;
.interval = 15s;
.window = 5;
.threshold = 2;
}
}

On peut de cette manière lister tous les backend (web1, web2, web3… si vous en avez plusieurs). On définit ainsi leur IP, le port à utiliser pour les joindre, ainsi que les paramètres qui vont permettre de voir l’état de santé des frontaux. Si un des frontaux par exemple ne répond plus plusieurs fois de suite dans le timeout, il sera temporairement sorti du pool.

Si vous voulez voir à quoi ressemble le cycle de vie d’une requête Varnish, vous pouvez aller voir le schéma sur le site officiel.
Vous voyez qu’il existe différentes étapes (nous y reviendrons plus en détail par la suite) :

  • vlc_recv : une requête utilisateur arrive
  • vcl_hash : Varnish constitue une clé de hash qui va lui permettre d’identifier son cache
  • vcl_hit : la ressource a été trouvée dans le cache
  • vcl_miss : la ressource n’a pas été trouvée dans le cache
  • vcl_pass : on a une entrée périmée, ou le pass a été forcé par la configuration
  • vcl_fetch : on a passé auparavant la main au backend (les frontaux web) et on obtient la réponse
  • vcl_deliver : on envoie la réponse à l’utilisateur

Dans le fichier de configuration VCL, on va pouvoir définir ce que Varnish doit faire pour chacune de ces étapes, via une fonction comme suit :


sub vcl_recv {
...
}

Gestion de l’expiration des ressources

Pour pouvoir gérer l’expiration de vos différentes ressources, il va falloir indiquer dans vos entêtes HTTP le max-age souhaité pour chacune de vos pages. Cela permettra de dire à Varnish le temps que vous souhaitez qu’il les garde en cache. Cela se fait via l’entête HTTP Cache-Control. Ainsi, vous pouvez spécifier la chose suivante :


cache-control: public, max-age=600

Si vous souhaitez que Varnish garde la ressource en cache pendant 600 secondes avant qu’elle n’arrive à expiration. Après, vous avez plusieurs façons de faire en fonction de votre projet pour définir les expirations de vos pages. De mon côté, j’étais sous Symfony 1 et j’ai utilisé une classe appelée par les filters Symfony qui me permet en fonction des urls de définir des expirations allant de 0 à 86400 secondes.

Gestion des ressources à ne pas cacher

Il est possible de dire à Varnish de ne pas cacher certaines ressources, en forçant un pass comme suit :


if (req.url ~ "/monCompte*") {
 return(pass);
 }

Ainsi tout ce qui concerne mon compte ne sera pas mis en cache. Mettez également l’expiration de la page à 0 pour éviter qu’elle ne soit mise en cache par le browser (possibilité à partir du moment où vous mettez le cache control à public). A savoir également que Varnish ne cache pas tout ce qui est HTTPS.

Varnish et les cookies

A partir du moment où un cookie est utilisé dans une page, Varnish va s’en servir, notamment pour générer le hash qui va lui permettre de constituer sa clé de cache afin de retrouver plus tard cet élément. Le problème, c’est que si vous avez un cookie sur toutes les pages de votre site, Varnish perd tout son intérêt car il va générer une clé de cache différente pour tous les utilisateurs. Cela pose deux problèmes : le cache ne sera pas mutualisé d’une part, et de l’autre une même page pourra être mise des dizaines ou des centaines de fois en cache, et du coup utiliser de l’espace pour rien. Heureusement, il est possible de pallier ce problème dans la configuration Varnish.

Pour des pages ne nécessitant par le cookie (des pages qui sont donc vouées à être identiques pour tous vos visiteurs, ce qui exclue de fait par exemple un espace mon compte), vous pouvez dire à Varnish de ne pas prendre le cookie. Cela se fait au travers de l’instruction unset req.http.Cookie. C’est utile également pour les images, ou les ressources médias. Un exemple dans vcl_recv :


if (req.url ~ ".(jpeg|jpg|png|gif|ico|swf|js|css|gz|rar|txt|bzip|pdf)(\?.*|)$" && req.url !~ "^/index.php?") {
unset req.http.Cookie;
return (lookup);
 }

if (req.url ~ "^/maPageStatique/")
unset req.http.Cookie;
return(lookup);
}

Cela permet de dire que pour tous les jpeg, gif, pdf… ainsi que pour l’url comprenant « maPageStatique », le cookie ne devait pas être utilisé, et ces éléments pourront être mutualisés dans le cache, même si un cookie est présent. Attention par contre, si vous avez des traitements ensuite utilisant les cookies ils ne seront plus fonctionnels.

Intégration d’éléments dynamiques à une page mise en cache : ESI ou Ajax ?

Pour tirer pleinement partie de Varnish, l’idéal est de mettre le maximum d’éléments en cache. Par contre, il est possible que vous ayiez sur vos pages des éléments dynamiques. Dans ce cas, il va falloir les appeler en ESI ou en Ajax, afin qu’il soit chargé dans la page en temps réel. Cela peut être le cas par exemple si sur toutes vos pages vous avez un panier, ou le nom de l’utilisateur connecté.
Comme Varnish travaille à la ressource, il va falloir déjà dans un premier temps que ces éléments dynamiques soient accessibles par une url. Il faudra également que vous configuriez votre VCL et vos entêtes HTTP afin que ces urls ne soient pas mises en cache (max-age à 0 et/ou un return pass dans le vcl).
Ceci étant fait, il va falloir maintenant incorporer ces éléments dans votre page en cache.
Deux solutions s’offrent à vous : ESI et Ajax.
Pour la première solution, il vous suffit d’intégrer dans votre page la balise suivante :


<esi:include src="/monDossier/maPagePasEnCache"/>

Au moment du chargement de la page en cache, un appel ESI sera fait pour charger le contenu dynamique. Cette solution est élégante, par contre le gros problème avec l’ESI dans Varnish c’est qu’il n’est (pour l’instant) pas parallélisable (à surveiller quand même, car il est possible que dans les futures versions de Varnish, les appels ESI puissent être parallélisables, ce qui les rendraient tout de suite plus attrayants). Cela pose des soucis de performances, et des cassures dans le chargement de votre page, notamment si vous avez plusieurs appels ESI. C’est la solution que j’avais choisi au départ, mais j’ai changé pour des appels Ajax (après, je n’ai pas testé l’inverse, à savoir une ressource dynamique et l’intégration dans celle-ci d’éléments dynamiques par ESI).
Dans ce dernier cas (Ajax), vous procédez comme vous avez l’habitude. Dans votre code HTML vous aurez un élément du DOM (par exemple une balise div ou span) dans lequel vous viendrez par appel Ajax intégrer le résultat html d’une url non mise en cache (dans notre exemple précédent /monDossier/maPagePasEnCache).

Purger du cache Varnish

Une fois votre cache correctement configuré, vous aurez sans doute besoin de vider de temps à autre certains éléments pour rafraîchir une page avant l’expiration de son cache.
Il existe plusieurs méthodes en Varnish pour le faire.

– Vous pouvez demander un vidage de cache par une requête HTTP qui ne sera pas une requête GET mais une requête PURGE. Il faut par contre pour des raisons de sécurité restreindre les possibilités de faire ces purges, en filtrant par IP autorisées. Sinon, n’importe qui peut être capable de lancer une telle requête de PURGE. Un exemple de VCL pour gérer la purge :

acl purge {
 "localhost";
 "127.0.0.1";
}

sub vcl_recv {
 if (req.request == "PURGE") {
 if (!client.ip ~ purge) {
 error 405 "Not allowed.";
 }
 return(lookup);
 } else {
 return(lookup);
 }
}

sub vcl_hit {
 if (req.request == "PURGE") {
 purge;
 error 200 "Purged.";
 }
}

sub vcl_miss {
 if (req.request == "PURGE") {
 purge;
 error 200 "Not in cache.";
 }
}

sub vcl_pass {
 if (req.request == "PURGE") {
 error 502 "PURGE on a passed object";
 }
}
<pre>

– Il est possible de demander également à Varnish de ne pas se servir du cache dont il dispose en bannissant celui-ci.
Cela force Varnish à recréer un nouveau cache dont il pourra ensuite se servir. L’inconvénient est que l’ancien cache reste en RAM, mais jusqu’à son expiration. Il pourra également être supprimé par un thread dédié à ce nettoyage nommé « ban lurker ». Cela évite également d’empiler de nombreuses règles de bans qui deviennent longues à parser.
L’avantage des bans par rapport aux purges est qu’on peut utiliser des expressions réfulières et ainsi bannir un ensemble d’urls et non plus seulement une url.
Ces requêtes de bans ou de purge sont des commandes shell, mais on peut très bien les appeler en PHP via un exec pour ainsi se batîr un backoffice d’administration du cache.
Voici quelques exemples de code pour bannir une url, un ensemble d’url via une regex, ou vider complètement le cache :


// Bannir une url
exec('echo "ban req.http.host == \"www.example.com\" && req.url == \"'.$url.'\"" | nc -q 1 '.IP_VARNISH);

// Bannir des urls par regex. Par exemple bannir du cache toutes les images d'un dossier
// l'url sera : /images/.*/NomDuDossier/.*.jpg$
exec('echo "ban req.http.host == \"www.example.com\" && req.url ~ \"'.$url.'\"" | nc -q 1 '.IP_VARNISH);

// Vider complètement le cache
exec('echo "ban.url /*" | nc -q 1 '.IP_VARNISH);

Ressources

Quelques ressources pour approfondir sur le sujet :

Des tutos Varnish sur le site binbash

Des tutos Varnish sur le blog de jeremm

Un article intéressant pour démarrer

Varnish appliqué à WordPress sur le site nicolargo

La documentation officielle sur le langage VCL