Cocktailand - Ajoute du cache HTTP dans mon symfony

Mise en place des ESIs sur un site symfony. Le cas pratique de cocktailand.

Publié le 01/06/2018

C'est quoi un ESI ?

Les ESI ou Edge Side Includes est un balisage supporté par Varnish qui permet de

gérer des temps de cache différent pour des blocs de la même page.

Dans le cadre de Cocktailand certains blocs sont actualisés régulièrement comme le "Cocktail du jour" mais d'autres ne changent quasiment jamais comme la liste des catégories.

ESI homepage cocktailand

Voici donc le découpage que j'ai fait sur la page principale. Pour la barre de menu c'est bien évidemment le contenu du mega menu que j'ai voulu mettre en évidence.

Il est donc intéressant de ne pas avoir à invalider toute la page lorsque le cocktail du jour est changé. Le second avantage en terme de performance est que les blocs peuvent être utilisés sur différentes pages. Cela signifie qu'un ESI présent sur toutes les pages du site qu'ils ne seront générés qu'une seule fois. Lors des autres appels, Varnish utilisera son cache.

Configuration de varnish

Pour Cocktailand la configuration de varnish est assez simple car je ne fais pas de purge et parce qu'il n'y a pas d'espace connecté sur le site.

Voici la configuration que j'ai:

vcl 4.0;

import std;

backend default {
      // le hostname du nginx dans ma stack
      .host = "front";
      .port = "80";
}

sub vcl_recv {
    set req.http.Surrogate-Capability = "abc=ESI/1.0";
    unset req.http.Cookie;
}

//Ensuite, ce block est appelé après la réception des headers de réponse.
//Nous supprimons le header et activons les ESI
sub vcl_backend_response {
      if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
          unset beresp.http.Surrogate-Control;
          set beresp.do_esi = true;
      }

      if (bereq.url ~ "\.(jpe?g|png|gif|pdf|tiff?|css|js|ttf|woff2?|otf|eot|svg)$") {
          set beresp.ttl = std.duration(beresp.http.age+"s",0s) + 24h;
      }
}

sub vcl_deliver {
    if (obj.hits > 0) {
            set resp.http.X-Cache = "HIT";
    } else {
            set resp.http.X-Cache = "MISS";
    }
}

La mise en place dans Symfony 4

ESI

Le support des ESI dans Symfony est intégré nativement dans le framework.

Dans le fichier config/packages/framework.yaml il faut activer les ESIs.

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

Toutes vos routes qui servent des ESI devront commencer par _fragment.

Dans vos vues twig vous avez des helpers à disposition pour poser vos tags.

{{ render_esi(url('popular_cocktails')) }}

Cache http

En utilisant le package sensio/framework-extra-bundle on peux gérer le cache sur les controller avec des annotations.

/**
* @Route("/cocktail/recette/{name}-{id}", name="cocktail_detail", requirements={"id"="\d+", "name"="[0-9a-z-]+"})
* @Cache(public=true, maxage=86400, mustRevalidate=false, maxStale=86400)
**/
public function index($name, $id)
{
  // ...
}

Temps de réponse

Une fois les ESIs et le cache HTTP mis en place on peut analyser les performances avec Webpagetest.

Webpagetest

L'outil va nous donner des informations sur le temps de réponse de l'application et surtout des indications sur ce qu'il faudrait améliorer

Webpagetest

Par exemple sur ce test il me dit que je peux potentiellement améliorer la gestion des fonts et des images.

// @todo activer gzip sur les fonts

Ce que l'on peut remarquer c'est que le site commence à envoyer le HTML après 231ms. Dans ce temps il y a en moyenne 40ms de DNS Lookup, 30ms de connexion et 80ms de négociation SSL. Malheureusement sur cette partie je n'ai pas la main c'est donc l'overhead de base pour toute page du site. Mais comme j'utilise HTTP2 je vais mutualiser toute cette partie pour les images assets servi sur le même domaine.

Webpagetest

Les nouvelles problèmatiques

Tout mettre en cache c'est bien pour les performances mais malheureusement certaines informations ont besoin de "temps réel".

Quand un visiteur ajoute une note sur une recette il est nécessaire que cette note mise à jour si le visiteur rafraichi la page.

Il y a plusieurs solutions:

  • Faire des bans de cache lors de l'ajout d'une note

  • Afficher les données du cache et rafraichir les données en AJAX

Bien évidemment la première solution est celle qui devrait être implémentée. Maintenant c'est pas mal de code et de configuration sur varnish. C'est d'autant plus compliqué que dans sa version gratuite Varnish ne permet pas de faire un ban sur plusieurs instances.

Faire les bans impliquerais de maintenir une liste des varnishs qui tournent (si jamais je fais scaller cette brique) et de faire n appels curl pour faire un ban partout.

Pour le moment j'ai fait le choix de la requête AJAX non cachée. Si jamais le site gagne en popularité il faudra retravailler sur ce point.

Conclusion

Sur un blog ou un site dans lequel le visiteur ne peut quasiment pas interagir avec vos données c'est très simple de mettre en place du cache varnish et le gain de performance est énorme. Maintenant si vous avez des besoins plus complexes (site transactionnel, forum,...) vous allez devoir mettre en place une mécanique d'invalidation de cache.