Varnish, un proxy qui vous veut du bien

GNU/Linux Magazine n° 138 | mai 2011 | Marc Falzon
Creative Commons
  • Currently 0 out of 5 Stars.
0
Thank you for rating!
You have already rated this page, you can only rate it once!
Your rating has been changed, thanks for rating!
Varnish est un reverse-proxy HTTP « accélérateur » libre (licence BSD) vous permettant de soulager vos serveurs web potentiellement mis à mal par des applicatifs web gourmands en ressources ou peu, voire pas, optimisés.

Malgré son jeune âge (la version 1.0 date de 2006), il s'est rapidement distingué de ses rares concurrents que sont l'inévitable Apache, le vénérable Squid et le désormais célèbre Nginx pour plusieurs raisons. La première, c'est que contrairement aux autres il a été conçu selon la doctrine UNIX « Écrire des programmes qui effectuent une seule chose et qui le font bien » : il assure la fonction de reverse-proxy HTTP et rien d'autre (pas même HTTPS).

Son concepteur et développeur principal -- Poul-Henning Kamp[PHK], développeur pour FreeBSD et réputé pour ses avis techniques très tranchés -- lui loue une conception résolument moderne qui exploite au mieux les capacités du matériel récent (64 bits, SMP/multi-cœurs) et explique dans ses très instructives notes d'architecture[ARNOT] que Varnish se repose largement sur les fonctionnalités du système d'exploitation pour gérer l'organisation de la mémoire (utilisation intensive de la mémoire virtuelle et de la mémoire partagée) et des threads ; je vous recommande chaudement la lecture de ces notes d'architecture, ne serait-ce que pour comprendre la différence de performance entre Squid -- décrit par PHK comme un archétype de la programmation des années 1975 -- et Varnish.

Car l'autre raison du succès de Varnish, c'est accessoirement l'un des logiciels les plus performants que j'ai eu l'occasion de voir tourner en production : il ne fait que ce pour quoi il a été créé et tout autre fonctionnalité superflue telle que la gestion des logs est déléguée à des programmes tiers qui ont accès aux données manipulées par le serveur par l'intermédiaire de segments de mémoire partagée, réduisant ainsi le nombre d'appels système effectués par le programme principal.

1. Installation

Les exemples illustrant cet article ont été testés sur une Debian Lenny 64 bits : les développeurs de Varnish proposent un paquet Debian à jour pour la version stable, la procédure d'installation est documentée sur le site officiel[VARNISH]. De l'aveu de son créateur[VARNPLF], les systèmes d'exploitation sur lesquels Varnish tourne le mieux sont GNU/Linux et FreeBSD, toutefois il a été confirmé qu'il fonctionne également sur Mac OS X, Solaris et NetBSD ; pour les autres UNIX (OpenBSD, HP-UX, AIX...) et Windows, vous pouvez oublier.

2. Configuration

Le premier aspect qui surprendra l'administrateur système chevronné lorsqu'il a terminé d'installer Varnish et qu'il dégaine Vi pour éditer le traditionnel /etc/varnish/varnish.conf, c'est qu'il n'y a pas de varnish.conf. En effet, Varnish se configure en deux temps : tous les paramètres relatifs au daemon varnishd tels que la taille du cache ou encore les interfaces réseau sur lesquelles écouter sont à passer sous forme d'options à la ligne de commandes. Deuxième effet kiss cool : la configuration déterminant le comportement du reverse-proxy vis-à-vis de l'applicatif web qu'il devra cacher se fait à l'aide de fichiers de configuration à la syntaxe unique -- le VCL, pour Varnish Configuration Language.

Un des points faibles de Varnish s'il en existe, c'est sa documentation : loin d'être inexistante[VARNISHDOC], elle est cependant mal organisée et certains exemples utilisent parfois des directives qui ont été renommées, ce qui n'aide pas à configurer ce logiciel à la syntaxe déjà pas évidente. Néanmoins, avec un peu de bonne volonté et de Google-fu, on arrive rapidement à ses fins pour peu que l'on n'ait pas des besoins très complexes.

2.1. varnishd

Lors d'une installation sous Debian, la configuration de la ligne de commandes accompagnant le lancement de varnishd se fait via le fichier /etc/default/varnish, qui contient par défaut plusieurs modèles plus ou moins complets parmi lesquels vous pourrez choisir et compléter selon vos besoins ; pour les puristes, la dernière méthode vous propose tout simplement de vous débrouiller comme un grand en vous mettant à disposition une variable vide que vous définissez avec les options de votre choix et qui sera passée telle quelle à varnishd lors du lancement via le script de démarrage /etc/init.d/varnish. Voici un premier exemple minimaliste qui permet de placer un reverse-proxy Varnish en amont de votre serveur web servant les pages web dynamiques :

DAEMON_OPTS="-a :80 \

-b localhost:8080 \

-u varnish \

-g varnish \

-s file,/var/lib/varnish/$INSTANCE/varnish_storage.bin,1G"

Cette configuration indique à varnishd qu'il doit écouter sur toutes ses interfaces réseau sur le port 80, que le backend (le serveur dit « source » ou « origine », c'est-à-dire celui qui sert les pages web dynamiques) est à interroger via la boucle locale sur le port 8080, qu'il doit changer l'UID et le GID pour adopter celui de l'utilisateur varnish et du groupe varnish et enfin qu'il dispose d'une quantité de stockage de 1 Go utilisant le fichier /var/lib/varnish/$INSTANCE/varnish_storage.bin ($INSTANCE étant défini par défaut à la valeur retournée par uname -n, correspondant au nom court de la machine) ; il est possible d'indiquer à Varnish une taille de cache exprimée en pourcentage d'espace disponible sur la partition sur laquelle le fichier est stocké en suffixant la valeur par le symbole % (par défaut 50%).

Le manuel de varnishd(1) recommande de pré-créer le fichier hébergeant le cache de Varnish afin d'éviter la fragmentation et donc une potentielle perte de performance. Le plus simple est donc de créer un fichier avec ce bon vieux copain Dédé comme suit (si vous allouez 1 Go pour le cache comme dans l'exemple précédent) :

# dd if=/dev/zero of=/var/lib/varnish/`uname \

-n`/varnish_storage.bin bs=1024 count=1000000

Démarré ainsi, Varnish accomplira sa mission sans toutefois que vous ayez de contrôle sur son comportement concernant la mise en cache : celui-ci est régi par certaines en-têtes HTTP retournées par le client et le serveur source, en particulier celles ayant trait à la gestion du cache. Voici quelques commandements de base que le reverse-proxy suivra à moins que vous ne lui explicitiez de procéder autrement :

- ne pas cacher les requêtes contenant des cookies ;

- ne pas cacher les requêtes contenant une directive Authorization (authentification HTTP) ;

- utiliser le paramètre max-age de l'en-tête Cache-Control pour déterminer combien de temps cacher un objet (TTL) ; si rien ne le précise, Varnish gardera un objet « cachable » en mémoire pendant 120 secondes.

2.2. VCL

L'autre grande force de Varnish est son extrême flexibilité : vous pouvez intégralement personnaliser son comportement et adapter les règles énoncées ci-dessus selon vos envies ou vos contraintes. Pour ce faire, les développeurs du logiciel ont mis au point un langage de programmation dédié -- le VCL, pour Varnish Configuration Language -- qui a la particularité d'être traduit en langage C puis compilé et chargé dynamiquement par le binaire varnishd telle une bibliothèque de fonctions classique !

Afin de commencer en douceur et ne pas se faire (trop) peur, je suggère de ne pas se baser sur le fichier VCL fourni dans le paquet Debian (/etc/varnish/default.vcl) : déplacez-le en sûreté afin de pouvoir y revenir plus tard et éditez un nouveau fichier, par exemple /etc/varnish/conf.vcl.

Donnons un peu de contexte à nos exemples : imaginons un site web dont la partie web dynamique (PHP) tourne sur deux frontaux Apache et dont le contenu statique (images, scripts JavaScript, CSS...) est servi par un seul frontal Nginx.

La première étape dans la configuration de Varnish consiste à déclarer le(s) serveur(s) source(s) -- appelés « backends » dans la terminologie du VCL. L'étape suivante de la configuration concerne la modulation du comportement du reverse-proxy pour la mise en cache des objets HTTP. En interne, Varnish appelle des sous-routines qui sont exécutées à des moments-clés du traitement des requêtes : ce sont elles que vous allez altérer à l'aide d'actions pour influer sur les réactions du logiciel ; si vous n'appelez pas d'action au sein d'une sous-routine, Varnish exécutera le code VCL prévu dans le fichier de configuration par défaut.

Il existe actuellement dix sous-routines, toutefois seulement deux sont réellement utiles et concentrent 99% des modifications habituelles :

vcl_recv : cette sous-routine est exécutée lorsqu'une requête HTTP envoyée par le navigateur d'un client a été reçue et analysée par varnishd : c'est à ce moment que vous décidez si cette requête doit être servie ou non, si des propriétés doivent être modifiées, voire supprimées et enfin, à quel backend la transmettre. Typiquement, c'est à cette étape que vous pouvez décider d'ajouter ou de supprimer des en-têtes HTTP ou encore supprimer des cookies présentés par un navigateur.

vcl_fetch : cette sous-routine est exécutée lorsque Varnish a fini de réceptionner la réponse du backend à la requête précédemment transmise : vous pouvez ici retenter la même requête avec un backend différent si la réponse obtenue ne vous satisfait pas (erreur 404 ou 500), ajouter ou supprimer certaines en-têtes HTTP renvoyées par le serveur source (X-Powered-By, au hasard) ou encore supprimer des cookies renvoyés à tort et à travers par un certain applicatif web dont je tairai le nom -- qui commence par « Word » et finit par « Press ».

Voici les différentes actions qu'il est possible d'invoquer à l'issue d'une condition vérifiée ou non dans une sous-routine :

pass : invoquer cette action aura pour effet de « passer » la requête originale à un backend (si appelée depuis vcl_recv) ou la réponse d'un backend au client (si appelée depuis vcl_fetch) sans la cacher.

lookup : dans la sous-routine vcl_recv, ordonne à Varnish de servir le contenu caché correspondant à la requête du client (s'il en existe) même si tout indique qu'elle devrait être passée directement à un serveur source ; il n'est pas possible de l'invoquer depuis la sous-routine vcl_fetch.

deliver : indique à Varnish de servir les données cachées au client.

pipe : d'usage plus rare, cette action indique à Varnish de se court-circuiter et de se limiter à transférer les octets entre le client et le backend et vice versa sans même analyser le contenu des données qui lui passeront au travers.

esi : indique à Varnish qu'il doit interpréter les balises ESI[VARNESI] trouvées dans la réponse du backend avant de la retourner au client.

Voici maintenant les structures internes que vous allez pouvoir manipuler dans les sous-routines vues à l'instant :

req ("request") : représente la requête provenant du client une fois que Varnish l'a intégralement reçue : l'essentiel des modifications que vous voudriez y apporter s'effectue dans la sous-routine vcl_recv.

beresp ("backend response") : cet objet représente la réponse du backend. En manipulant cet objet, vous pourrez altérer les en-têtes HTTP du serveur source consulté par Varnish dans la sous-routine vcl_fetch.

obj ("object") : cet objet représente l'objet HTTP stocké en cache. Seule sa propriété obj.ttl est modifiable, les autres ne sont accessibles qu'en lecture seule.

Outre ces sous-routines, actions et objets, le VCL vous propose des opérateurs tels qu'on en trouve dans tous les langages de programmation afin de définir la logique de votre politique de mise en cache :

= opérateur d'assignation

== opérateur de comparaison

~ opérateur de correspondance (« match »)

! opérateur de négation

&& "ET" logique

|| "OU" logique

Assez de théorie, passons à la pratique avec une configuration VCL simple à renseigner dans le fichier /etc/varnish/conf.vcl :

backend static1 {

        .host = "10.0.0.1";

        .port = "8080";

}

backend php1 {

        .host = "10.0.1.1";

        .port = "8080";

}

backend php2 {

        .host = "10.0.1.2";

        .port = "8080";

}

director php round-robin {

        { .backend = php1; }

        { .backend = php2; }

}

sub vcl_recv {

        if(req.http.host == "static.monsite.fr"){

                   set req.backend = static1;

        }

        else if(req.http.host ~ "(www.)?monsite.fr"){

                   set req.http.host = "www.monsite.fr";

                   set req.backend = php;

        }

        if(req.http.authorization || req.request == "POST" || req.http.cookie){

                   return(pass);

        }

        return(lookup);

}

sub vcl_fetch {

        if(req.http.host == "www.monsite.fr" && !req.url ~ "^/admin"){

                       unset beresp.http.set-cookie;

        }

}

Disséquons à présent cette configuration. Nous avons déclaré trois backends à qui Varnish s'adressera pour récupérer le contenu à cacher. Notez que j'ai regroupé les deux backends PHP dans une section nommée « director » : à l'instar de Nginx, Varnish est capable de faire office de répartiteur de charge entre plusieurs serveurs sources. Ainsi configuré, en indiquant le director au lieu du backend directement, il interrogera tour à tour (« round-robin ») l'un des deux backends PHP.

À l'étape vcl_recv
 -- exécutée lors de la réception de la requête du client --, nous commençons par aiguiller le trafic en fonction du FQDN spécifié dans l'en-tête HTTP Host : à destination du backend « static1 » pour le domaine « static.monsite.fr » et au backend director « php » pour « www.monsite.fr » et « monsite.fr » ; si vous vous demandez pourquoi j'ai pris la peine de redéfinir l'en-tête HTTP Host à « www.monsite.fr », c'est dans un souci de normalisation : en effet, la documentation de Varnish recommande de procéder ainsi pour éviter les doublons d'objets cachés dans le cas où les visiteurs accéderaient à votre site web via des URL différentes.
Nous nous assurons ensuite que la requête n'implique pas d'authentification HTTP ou soit une requête de type POST, auquel cas nous demandons à Varnish de la passer au backend sans servir de contenu caché.

La dernière partie de notre fichier VCL se déroule dans la sous-routine vcl_fetch une fois que nous avons obtenu la réponse du backend : nous exigeons de Varnish qu'il supprime les cookies éventuellement retournés par les serveurs sources sauf si l'URL des requêtes contient la chaine « admin » et ce, uniquement pour le contenu dynamique.

Une fois votre configuration prête à être testée, reste à modifier légèrement les options de la ligne de commandes du daemon varnishd pour lui indiquer qu'il doit désormais baser son comportement sur cette dernière plutôt que selon ses réglages par défaut :

DAEMON_OPTS="-a :80 \

-f /etc/varnish/conf.vcl \

-u varnish \

-g varnish \

-s file,/var/lib/varnish/$INSTANCE/varnish_storage.bin,1G"

Pour le reste, vous trouverez une référence complète du langage VCL dans la traditionnelle page de man (vcl(7)).

Après l'effort, le réconfort : il est temps de démarrer Varnish avec sa configuration personnalisée. Si tout se passe bien, le daemon devrait se lancer sans autre message que celui-ci :

# /etc/init.d/varnish start

Starting HTTP accelerator: varnishd.

3. Essais

Voici venue l'heure de vérité. Pour savoir si Varnish cache comme souhaité les requêtes qui transitent par lui, il vous faudra analyser les en-têtes HTTP retournées par ce dernier. En effet, le daemon ajoute dans la réponse les en-têtes X-Varnish et Age qui donnent des indications sur la manière dont a été traitée la requête par Varnish, notamment si la réponse qu'il retourne au navigateur client est une réponse cachée ou non.

Simulons un site web dynamique afin de vérifier si notre reverse-proxy est opérationnel et bien configuré. La page d'accueil du site définit un cookie « dispensable » -- qui selon nos réglages de Varnish devrait donc être supprimé de la réponse --, tandis que le cookie défini par la page permettant l'administration du contenu du site doit être relayé entre le client et le serveur source.

marc@oxide:~% curl -s -D - http://www.monsite.fr/

HTTP/1.1 200 OK

Server: Apache/2.2.9 (Debian) PHP/5.2.6-1+lenny9 with Suhosin-Patch

X-Powered-By: PHP/5.2.6-1+lenny9

Cache-Control: max-age=30

Expires: Tue, 08 Feb 2011 14:22:36 GMT

Vary: Accept-Encoding

Content-Type: text/html

Content-Length: 248

Date: Tue, 08 Feb 2011 14:22:06 GMT

X-Varnish: 1067808251

Age: 0

Via: 1.1 varnish

Connection: keep-alive

<html><body>

<p>

monsite.fr<br />

cookie:

</p>

</body></html>

Lors de la première requête -- pour laquelle Varnish n'a pas encore de réponse en cache --, la réponse comporte un en-tête X-Varnish dont la valeur est le « XID » (l'identifiant interne d'une requête dans Varnish) : lorsque cet en-tête ne contient qu'un seul XID, cela signifie que la réponse servie par Varnish n'était pas cachée. Si on refait exactement la même requête deux secondes plus tard, on constate cette fois que le reverse-proxy, ayant gardé la précédente en mémoire, nous la ressort depuis son cache :

marc@oxide:~% curl -s -D - http://www.monsite.fr/

HTTP/1.1 200 OK

Server: Apache/2.2.9 (Debian) PHP/5.2.6-1+lenny9 with Suhosin-Patch

X-Powered-By: PHP/5.2.6-1+lenny9

Cache-Control: max-age=30

Expires: Tue, 08 Feb 2011 14:22:36 GMT

Vary: Accept-Encoding

Content-Type: text/html

Content-Length: 248

Date: Tue, 08 Feb 2011 14:22:08 GMT

X-Varnish: 1067808252 1067808251

Age: 2

Via: 1.1 varnish

Connection: keep-alive

<html><body>

<p>

monsite.fr<br />

cookie:

</p>

</body></html>

Remarquez que l'en-tête X-Varnish contient désormais deux XID : celui correspondant à la présente requête et celui correspondant à la requête dont le résultat en cache est servi pendant la durée indiquée par le serveur source dans l'en-tête Cache-Control ; en outre, l'en-tête HTTP Age nous informe que le TTL de l'objet HTTP qui nous a été retourné était de 2 secondes (sur 30, dans cet exemple).

marc@oxide:~% curl -s -D - http://www.monsite.fr/admin/

HTTP/1.1 200 OK

Server: Apache/2.2.9 (Debian) PHP/5.2.6-1+lenny9 with Suhosin-Patch

X-Powered-By: PHP/5.2.6-1+lenny9

Set-Cookie: var=blah; expires=Sun, 20-Feb-2011 04:00:24 GMT; path=/; domain=.monsite.fr

Cache-Control: max-age=30

Expires: Tue, 08 Feb 2011 15:01:29 GMT

Vary: Accept-Encoding

Content-Type: text/html

Content-Length: 182

Date: Tue, 08 Feb 2011 15:00:59 GMT

X-Varnish: 1067808253

Age: 0

Via: 1.1 varnish

Connection: keep-alive

<html><body>

<p>

[ADMIN] monsite.fr<br />

cookie: </p>

</body></html>

Lorsque l'on demande la page d'administration du site, celle-ci nous retourne un cookie que Varnish a laissé passer, comme prévu dans la configuration. De plus, si nous re-demandons la même page en présentant ce cookie, Varnish « passera » la requête au serveur source :

marc@oxide:~% curl -s -b "var=blah; expires=Sun, 20-Feb-2011 04:00:24 GMT; path=/; domain=.monsite.fr" -D - http://www.monsite.fr/admin/

HTTP/1.1 200 OK

Server: Apache/2.2.9 (Debian) PHP/5.2.6-1+lenny9 with Suhosin-Patch

X-Powered-By: PHP/5.2.6-1+lenny9

Set-Cookie: var=blah; expires=Sun, 20-Feb-2011 04:00:24 GMT; path=/; domain=.monsite.fr

Cache-Control: max-age=30

Expires: Tue, 08 Feb 2011 15:11:51 GMT

Vary: Accept-Encoding

Content-Type: text/html

Content-Length: 188

Date: Tue, 08 Feb 2011 15:11:21 GMT

X-Varnish: 1067808260

Age: 0

Via: 1.1 varnish

Connection: keep-alive

<html><body>

<p>

[ADMIN] monsite.fr<br />

cookie: "blah"</p>

</body>

</html>

On constate cette fois que l'applicatif web a bien reçu le cookie que nous avons présenté dans notre requête HTTP. Enfin, si nous demandons maintenant la page d'accueil en présentant le cookie obtenu dans l'interface d'administration :

marc@oxide:~% curl -s -b "var=blah; expires=Sun, 20-Feb-2011 04:00:24 GMT; path=/; domain=.monsite.fr" -D - http://www.monsite.fr/

HTTP/1.1 200 OK

Server: Apache/2.2.9 (Debian) PHP/5.2.6-1+lenny9 with Suhosin-Patch

X-Powered-By: PHP/5.2.6-1+lenny9

Cache-Control: max-age=30

Expires: Tue, 08 Feb 2011 15:24:02 GMT

Vary: Accept-Encoding

Content-Type: text/html

Content-Length: 68

Date: Tue, 08 Feb 2011 15:23:32 GMT

X-Varnish: 1067808266

Age: 0

Via: 1.1 varnish

Connection: keep-alive

<html><body>

<p>

monsite.fr<br />

cookie: "blah"</p>

</body></html>

Varnish a encore une fois passé la requête directement au serveur source tel qu'exigé dans la sous-routine vcl_recv et la page PHP a pu recevoir le cookie présenté ; vous pouvez exécuter cette requête plusieurs fois de suite, vous obtiendrez toujours une réponse non cachée.

Conclusion

Cet article n'est qu'un lointain survol de ce qu'il est possible d'accomplir avec Varnish. Outre ses excellentes performances « out of the box », il existe beaucoup de façons différentes de tirer partie de la puissance de ce reverse-proxy qui est utilisé par nombre de sites web à forte audience -- Facebook, Twitter, Slashdot pour ne citer qu'eux. J'espère vous avoir mis le pied à l'étrier pour l'essayer facilement, voire l'utiliser sur votre propre site web.

Références

[VARNISH] http://www.varnish-cache.org/

[ARNOT] http://www.varnish-cache.org/trac/wiki/ArchitectNotes

[PHK] http://en.wikipedia.org/wiki/Poul-Henning_Kamp

[VARNDOC] http://www.varnish-cache.org/docs/2.1/

[VARNPLF] http://www.varnish-cache.org/docs/2.1/phk/platforms.html

[VARNESI] http://www.varnish-cache.org/trac/wiki/ESIfeatures