lundi 3 juin 2013

Mise en place de Varnish avec Symfony2

Après une mise en place un peu laborieuse, voici un petit récapitulatif de ce que j'ai fait pour faire fonctionner Varnish avec Symfony 2.

La configuration utilisée : Ubuntu 11.10, Varnish 3.0.0-4, Symfony 2.0

Installation


Ouvrir le terminal et taper :
sudo apt-get install varnish

Configuration de Varnish


Ouvrir et modifier le fichier de configuration de Varnish, par exemple :
sudo vi /etc/varnish/default.vcl

Surcharger les méthodes vcl_recv (réception des données), vcl_hash (construction de la clé unique qui correspondra à une entrée de cache donnée) et vcl_fetch :
sub vcl_recv {
    set req.http.Surrogate-Capability = "abc=ESI/1.0";

    if (req.restarts == 0) {
        if (req.http.x-forwarded-for) {
            set req.http.X-Forwarded-For =
            req.http.X-Forwarded-For + ", " + client.ip;
        } else {
            set req.http.X-Forwarded-For = client.ip;
        }
    }

    if (req.request != "GET" &&
        req.request != "HEAD" &&
        req.request != "PUT" &&
        req.request != "POST" &&
        req.request != "TRACE" &&
        req.request != "OPTIONS" &&
        req.request != "DELETE") {
            /* Non-RFC2616 or CONNECT which is weird. */
            return (pipe);
        }

    if (req.request != "GET" && req.request != "HEAD") {
        /* We only deal with GET and HEAD by default */
        return (pass);
    }

    if (req.http.Authorization || req.http.Cookie) {
        /* Not cacheable by default */
        #return (pass);
    }

    return (lookup);
}
set req.http.Surrogate-Capability = "abc=ESI/1.0"; permet de dire à Varnish de prendre en charge le langage ESI, qui est un langage de balisage qui s'insère dans le code HTML, et déclenche des traitement de construction de la page.

sub vcl_hash {
    ### these 2 entries are the default ones used for vcl. Below we add our own.
    hash_data(req.url);
    hash_data(req.http.host);

    if( req.http.Cookie ~ "locale" ) {
        hash_data(regsub( req.http.Cookie, "^.*?locale=([^;]*);*.*$", "\1" ));
    }


    return (hash);
}
if( req.http.Cookie ~ "locale" ) {
        hash_data(regsub( req.http.Cookie, "^.*?locale=([^;]*);*.*$", "\1" ));
}

permet de gérer un cache par langue, dans le cas où la langue n'est pas spécifiée dans l'URL. Par défaut, Varnish prend pour hash l'URL de la page, donc si votre langue est en session, vous obtiendrez le même contenu pour toutes vos langues. Mettre la locale en cookie, puis ici ajouter sa valeur au hash règle ce problème.

Nous verrons plus bas comment mettre votre locale en cookie.

sub vcl_fetch {
    if (beresp.http.surrogate-control ~ "ESI/1.0") {
        unset beresp.http.surrogate-control;
        set beresp.do_esi = true;
    }

    if (beresp.ttl <= 0s ||
        beresp.http.Set-Cookie ||
        beresp.http.Vary == "*") {
        /*
        * Mark as "Hit-For-Pass" for the next 2 minutes
        */
        set beresp.ttl = 120 s;
        return (hit_for_pass);
    }

    return (deliver);
}


Après avoir modifié ce fichier, redémarrez Varnish :
sudo service varnish restart


Configuration de Symfony2


app/config/config.yml

framework:
  templating: { engines: ['twig'] }
  session:
    default_locale: fr
    auto_start: false # Pour ne pas générer le PHPSESSID à chaque page (cache multi user)
  esi: true # Pour utiliser les tags ESI dans les templates Twig

app/config/routing.yml

_internal:
  resource: "@FrameworkBundle/Resources/config/routing/internal.xml"
  prefix: /_internal
-> pour la prise en charge des routes internes de Symfony2 dans les tags ESI

web/app.php

//$kernel = new AppCache($kernel);
-> On commente cette ligne pour passer par Varnish et non plus par le système de cache de Symfony2. (voir s’il y a une possibilité de gérer ça autrement car ce n'est pas très propre !)

Mise en place des blocs et pages cachées dans Symfony2


Pour cacher un bloc, il faut que celui-ci soit rendu par une action :
{% render "NomDeBundle:Test:monAction" with {'locale': app.session.locale}, {'standalone': true} %}

La locale sert à gérer le cache par langue, du fait que ce paramètre se retrouve dans l’url interne et donc dans le hash utilisé par Varnish. Si la langue n’est pas encore dans le cookie, le le hash ne contiendra pas la locale ni fr, ni en, mais sera vide. Il est donc mieux de faire :
{% if app.session.locale %}
    {% render "NomDeBundle:Test:monAction" with {'locale': app.session.locale}, {'standalone': true} %}
{% else %}
    {% render "NomDeBundle:Test:monAction" with {'locale': app.session.defaultLocale}, {'standalone': true} %}
{% endif %}

L’option 'standalone': true va faire que ce bloc deviendra un tag ESI.

Le tag ESI ressemblera à :
<esi:include src="http://example.com/1.html" onerror="continue"/>

Dans l’action qui rend le bloc :
/**
 * Renders the user cart block on the top right of the page
 * ESI block
 *
 * @Extra\Template("NomDeBundle:Test:esi_header_top.html.twig")
 */
public function cartBlockAction() {
    $response = new Response();

    $response->setContent($this->renderView('NomDeBundle:Test:esi_header_top.html.twig', array()));
    $response->setSharedMaxAge(600); # en secondes, donc environ 10 minutes
    $response->setPublic();


    return $response;
}
-> On ajoute les bons headers à la réponse, où setSharedMaxAge est le temps en secondes de la durée de vie du cache.
-> Même fonctionnement pour les pages complètes.

Pour ne pas mettre en cache les blocs utilisant par exemple la session, mettre les headers adéquats dans la réponse :
$response->setMaxAge(0);
$response->setPrivate();

Cookie de langue


Comme on l'a vu plus haut, si votre locale est dans votre session et non dans l'URL, il faut créer un cookie contenant la locale, au moment où vous changez votre locale ; dans une action changeLocaleAction par exemple.

Lorsque vous vous apprêtez à retourner l'objet Response :
$response->headers->setCookie(
    new Cookie(
        'locale',
        $votreLocale,
        time() + 600));

Pour faire vos tests


Créer un fichier de test qui fait un CURL sur une URL passée en paramètre GET avec le header “Surrogate-Capability:abc=ESI/1.0”
-> celà permet de voir les balises ESI dans le code source (par exemple en inspectant avec Firebug).

Par exemple web/testvarnish.php
<?php

$url = $_GET["url"];
$ret = curl_init($url);
curl_setopt($ret, CURLOPT_HTTPHEADER, array(
'Surrogate-Capability:abc=ESI/1.0'
));
echo curl_exec($ret);

?>

Enfin, vous pouvez tester les performances avec un outil tel que JMeter.

6 commentaires:

  1. Salut,
    Merci pour ce tuto.
    Est-ce qu'avec cette configuration, par défaut toutes les pages sont mises en cache par Varnish ? J'ai l'impression que oui et c'est pas ce que je recherche. Je voudrais que par défaut, sans rien spécifier dans la Response, Varnish n'intervienne pas...
    Merci

    RépondreSupprimer
  2. Non ici on ne met en cache qu'un "bout" de page. C'est l'option standalone qui va faire de ce bloc un bloc ESI.

    RépondreSupprimer
  3. Pour les blocs ESI c'est OK. Je parlais des pages "normales", qui sont renvoyées par les controllers via une Response. Dans mes essais, Varnish les stocke en cache alors que je ne le veux pas. Faut-il ajouter un setPrivate() ou setMaxAge(0) sur toutes les Response retournées ? Ca me paraît bizarre, car j'avais lu dans la doc S2 que par défaut les réponses sont private pour justement éviter une mise en cache non souhaitée. J'ai du rater quelque chose! Mais quoi?

    RépondreSupprimer
  4. J'ai trouvé: il fallait que j'enlève "set beresp.ttl = 120 s;" car sinon, par défaut, Varnish met en cache toutes les pages pour 2 minutes !

    RépondreSupprimer
  5. Salut,

    La config "auto_start" pour la session ne fonctionne pas en symfony2.4 et superieur.
    J'ai donc forcement un Token Anonymous dans toutes mes pages. Cela marche tres bien avec le AppCache, mais des que je passe a Varnish cela ne marche pas plus ...

    As-tu essaye de faire fonctionner ta conf varnish avec la nouvelle version de symfo ??

    RépondreSupprimer
    Réponses
    1. Salut, non désolé je n'ai pas retouché à Varnish depuis.

      Supprimer