Deblan blog

Symfony2, Varnish et l'ESI

Les développements d'applications web à moyens ou forts traffics admet nécessairement des problèmatiques de cache.

Il existe deux types de cache : ceux coté application avec la génération finie de données rendues, et puis ceux coté HTTP où le client et/ou un reverse proxy interviennent dans le méchanisme.

Symfony2 met en cache des choses mais prend le parti de laisser la couche HTTP faire le plus gros travail. En effet, même si une page peut-être générée dans un fichier et rendu tel quel au client sans calcul préalable, le gain de performance n'est pas toujours évident. Comme le protocole HTTP permet de faire énormément de choses alors pourquoi s'en passer ?

La documentation concernant les ESI et Symfony2 est assez ample à ce sujet, cependant j'ai rencontré deux difficultés.
De base, lorsque vous coller Varnish devant le serveur web, les logs du serveur web indiquent l'adresse IP du serveur Varnish. Ce premier problème résolu, je me suis rendu compte que l'ESI tombait car Symfony2 test si l'IP associée à la requête ESI est de confiance : dans la mesure où dans l'entête HTTP Varnish va donner l'IP du client, ça pose évidement des problèmes.

Je vais la faire en étape et traiter la problèmatique de log. Personnellement je travail avec Apache donc je vous laisse traduire la solution si vous avez un autre type serveur web.

Dans un premier temps, on va demander à Varnish de placer l'IP du client dans l'entête HTTP de la requête faite à Apache :

sub vcl_recv {
    [...]

    remove req.http.X-Forwarded-For;
    set req.http.X-Forwarded-For = client.ip;
        
    [...]
}

Dans /etc/apache2/conf.d/varnish.conf, on va placer ce format de log :

LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" varnishcombined

Dans les vhost, il faudra modifier la conf pour la génération des logs comme suit :

<VirtualHost *:unport>
    [...]
    CustomLog /chemin/vers/fichier.log varnishcombined
</VirtualHost>

À partir ce ce moment, il ne reste qu'à reload Apache et Varnish pour que les IP des clients apparaissent dans les logs.

Seulement, le second problème arrive : si vous utilisez les ESI alors ils ne seront plus fonctionnels. Si vous analysez les logs, vous verrez des lignes semblables à ça :

xx.xx.xx.xx - - [30/Oct/2013:00:04:00 +0100] "GET /_fragment?_path=foobar HTTP/1.1" 500 20 "-" "Server Density External Llama v1.0"
[Wed Oct 30 00:04:00 2013] [warn] [client xx.xx.xx.xx] mod_fcgid: stderr: PHP Fatal error:  Uncaught exception 'Symfony\\Component\\HttpKernel\\Exception\\AccessDeniedHttpException' in /var/www/service-web/www/foo.fr/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php:93

On a donc une erreur 500 sur l'adresse ESI (le fragment) due à une exception levée pour un accès refusé.

En regardant le code source de FragmentListener.php, dans la méthode validateRequest, 3 tests sont faits. En collant manuellement du log, vous verrez que le premier qui test si la méthode est safe est OK, que le second testant la signature de la requête est bon aussi. Le dernier traitant de l'IP pose problème (if (IpUtils::checkIp($remoteAddress, $trustedIps)) ? FAUX).

Il s'avère que Symfony2 récupère l'IP du client (dans la requête ESI) et évidement, elle n'est pas considérée de confiance. Dans le cas ou Varnish est sur la même machine, l'IP récupérée avant notre modification était 127.0.0.1 et la conf de base la considère correct.

Pour illustrer un peu ce qu'il se passe avec ESI, une sous requête HTTP est balancée à Apache pour récupérer chaque fragment (bloc ESI) : Symfony2 génère une URL dédiée pour chacun d'eux. Ces URL sont préfixée d'une chaine de caractère définie dans le fichier app/config/config.yml. Voici un exemple :

framework:
    esi:       { enabled: true }
    fragments: { path: /_fragment }

Ici la chaine "/_fragment" est notre préfixe. On va donc dire à Varnish de ne pas changer l'entête HTTP de la requête quand c'est une requête ESI (identifiable pour ce morceau de chaine).

sub vcl_recv {
    [...]

    if (!req.url ~ "^/_fragment") 
        remove req.http.X-Forwarded-For;
        set req.http.X-Forwarded-For = client.ip;
    }
        
    [...]
}

Si Varnish est sur une autre machine, vous pouvez ajouter les IP de ces dernières dans le firewall de Symfony.

Quoiqu'il en soit, après avoir reload Varnish, tout devrait fonctionner à merveille :)


  • Pierre
    • ,
    • Salut.
      Article intéressant, mais quid de mod_rpaf ? ou de mod_remote-ip pour apache 2.4 ?
      Avec l'un ou l'autre des modules, $_SERVER[REMOTE_ADDR] présente bien l'IP réelle du client (pour peu que la chaine rproxy et module soient bien configurés). Du coup, inutile de retirer le X-Forwarded-For pour les ESI... Mais plutôt utiliser la docu Varnish pour bien les implémenter : https://www.varnish-cache.org/docs/3.0/tutorial/esi.html
      Non ?
  • Simon
    • ,
    • Je n'ai plus en tête le pourquoi du comment, mais il est possible que le X-Forwarded-For ne soit pas alimenté par l'IP du client mais celle de la machine avec Varnish. Ce serait une raison pour laquelle j'éditerais son contenu.

      D'ailleurs, en lisant rapidement quelques articles, il s'avère que je ne suis pas le seul à opérer de cette façon.

      mod_rpaf ou son compère mod_remote-ip sont bien évidements essentiels, ils auraient du apparaître dans cet article d'ailleurs.

      je vais songer à une mise à jour de l'article :)

Ajouter un commentaire

Vous pouvez utiliser du markdown.Afficher l'aide.