8 Minutes read

Implémentation de la gestion du cache dans Drupal : une approche terre-à-terre (1/4)

Introduction au cache et aux stratégies de gestion de cache dans Drupal.

ekino fait partie des entreprises qui proposent à ses employés de participer à plusieurs sessions de conférences dans l’année mais aussi d’y postuler en tant qu’orateur. Grace à cela j’ai eu l’opportunité d’assister aux Drupal Dev Days 2024 à Bourgas en Bulgarie du 26 au 28 juin 2024, et d’accompagner ma collègue Nerea Enrique qui y a présenté une éclairante conférence sur le fonctionnement des indexes dans Elasticsearch.

Lors de ces journées, j’ai aussi pu assister à une conférence ayant pour thème une introduction au cache dans Drupal. Cette conférence m’a donné envie de creuser les notions évoquées car, pour moi, le fonctionnement du cache Drupal est une de ses fonctionnalités “magiques”. Mais dans ma conception des choses, l’impression de magie derrière une API est plutôt le symptôme d’un manque de compréhension des mécanismes en jeu.

Je me suis donc demandé “mais concrètement, comment Drupal va servir la réponse en cache ?”.

Une définition de la mise en cache de données

Avant d’entrer dans le vif du sujet, le cache Drupal, il peut être intéressant de clarifier la notion de cache.

De façon synthétique, le cache est généralement un espace mémoire qui offre un meilleur temps d’accès aux données par rapport aux systèmes de stockage traditionnel. Ceci grâce à sa proximité physique au centre de traitement de la donnée et/ou grâce à ses qualités techniques qui permettent d’accéder rapidement à l’information. On y copie donc les données dont les temps d’accès sont critiques pour le fonctionnement du système. Toutefois, de manière générale, plus la mémoire cache est rapide plus en contrepartie l’espace de stockage devient “coûteux”.

De cette notion dérive celle de “la mise en cache” pour le web. L’objectif est le même mais avec une petite nuance, nous cherchons à fournir le plus rapidement possible une réponse HTTP. Pour réduire le temps d’accès à cette réponse nous enregistrons les résultats des opérations qui servent à générer cette réponse. Bien sûr, nous cherchons parallèlement à stocker ces résultats dans un espace de stockage offrant un accès le plus rapide possible à ces données.

Pour une introduction aux concepts et aux enjeux du cache HTTP, je vous propose de voir la présentation de mon collègue Robin Colombier qu’il a donnée lors des conférences BDX I/O 2024.

https://medium.com/media/42e1d5dbdc3cb3b53b530369f6bd102f/href

Mécanisme de réponse du cache des pages dans Drupal.

Pour essayer d’avoir une réponse à ma question, j’ai commencé par remonter le fil de l’exécution de Drupal. Le CMS étant basé sur le patron de conception Front Controller, le meilleur point de départ pour cela était le fichier “index.php”.

Lors de l’initialisation de Drupal, la requête HTTP est passée sous la forme d’un objet Request au Kernel Drupal, basé sur l’interface HttpKernelInterface de Symfony. C’est à ce stade que sera compilé le container. Jusque-là rien de surprenant pour qui travaille avec un framework.

La requête est ensuite passée dans un “sous-kernel” Drupal, le StackedHttpKernel. Cette classe réalise une exécution en cascade des middlewares enregistrés pour le kernel Drupal. Ces middlewares implémentent eux aussi HttpKernelInterface. La cascade d’exécution se terminera avec l’exécution de la méthode handle par le HttpKernel Symfony, sauf si un middleware retourne une réponse avant.

C’est à ce niveau d’exécution que nous allons rencontrer une première notion de cache Drupal. Ce que j’appellerai les “stratégies de rendu de cache”.

Drupal prévoit trois stratégies qui peuvent être indépendantes mais qui sont en réalité plutôt complémentaires et qui correspondent à trois modules du core Drupal. Le “Internal Page Cache” qui sert des pages statiques, le “Dynamic Page Cache” qui peut gérer différents niveaux de cache dans une même page et “BigPipe” qui permet de servir la page en cache en plusieurs paquets dans la même requête en maintenant la connexion ouverte.

On pourrait voir ces trois couches comme différents niveaux de sophistication de la gestion du cache. Voyons ce que chacune propose.

Internal Page Cache

Ce système de cache fait partie des bibliothèques du cœur de Drupal. Il est très rapide car il ne prend pas en compte le contexte de la requête si ce n’est l’URL et la méthode de la requête. La réponse générée est toujours la même quel que soit l’utilisateur et son rôle.

La réponse en cache est directement servie par un middleware, appelé dans la pile du StackedHttpKernel que nous avons évoqué. Le middleware responsable de l’Internal Cache Page est la classe PageCache. Elle a la responsabilité de fournir une réponse si sa politique de cache est validée. Dans ce cas, si elle récupère des données en cache, la classe PageCache retournera alors un objet de type HtmlResponse (pour une page HTML) qui était préalablement sérialisé dans le stockage du cache.

Schéma du comportement du cache avec le module Internal Page Cache

Outre le fait de générer la réponse plus en amont dans l’exécution, sa rapidité est aussi due au fait qu’il n’y a pas de sollicitation supplémentaire de la logique applicative : ce sont des pages statiques qui sont directement servies. Par contre, si on a besoin d’un site un minimum dynamique, le module PageCache ne sera plus le bon outil.

Dynamic Page Cache

Le cache dynamique est un cache qui ne sera pas toujours le même pour une URL donnée si le contexte de la requête change. C’est la stratégie de cache la plus pertinente pour une grande partie des sites proposant des interactions avec l’utilisateur (sessions, compte utilisateur, commentaires, contenu personnalisé, etc.).

Le cache est servi par l’intermédiaire du DynamicPageCacheSubscriber. C’est un event-subscriber accroché à l’événement kernel “kernel.request. Celui-ci est déclenché au plus tard (si aucun middleware ne le déclenche avant) avec le dernier HttpKernel dans la pile du HttpStackedKernel.

Si un cache peut être servi, il sera alors affecté à la propriété response du RequestEvent dans le DynamicPageCacheSubscriber. Le cache sera ensuite retournée sans poursuivre plus loin l’exécution du kernel, comme on peut le comprendre de l’extrait de code ci dessous.

// from SymfonyComponentHttpKernelHttpKernel component used in Drupal 10

private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Response
{
// request
$event = new RequestEvent($this, $request, $type);
$this->dispatcher->dispatch($event, KernelEvents::REQUEST);

if ($event->hasResponse()) {
return $this->filterResponse($event->getResponse(), $request, $type);
}

// continue to handle request ...
Schéma du comportement du cache pour le dynamic page cache

La spécificité du “Dynamic Cache Page” est de permettre le cache partiel de page ou de composant. Cela signifie que les données mises en cache se feront selon leur niveau de “cachabilité”. Si un composant de l’élément doit être rendu à un niveau de cache différent, il sera remplacé par un placeholder. Cela permet ainsi de récupérer la partie non dynamique de la page (le layout principal par exemple) puis les autres parties plus dynamiques de la page. Avant de servir le cache complet l’application va combler ces trous, soit avec les composants en cache, soit en calculant le rendu manquant. Cette étape est réalisée sur un autre événement de type Response (plus de détails dans la partie 4).

Le rendu des placeholders a quand même un coût en termes de ressources et de temps d’exécution. Pour atténuer et rendre plus rapide le premier rendu, Drupal est l’un des rares CMS à implémenter une technologie connue pour sa mise en œuvre par Facebook, le BigPipe.

BigPipe

BigPipe est un système de rendu de cache qu’on peut qualifier d’hybride. Ce système n’améliore pas réellement le temps d’exécution total, coté serveur, par rapport au Dynamic Cache Page mais il affiche un premier rendu presque aussi rapidement que l’Internal Cache Page. Ce premier rendu contiendra une partie de la réponse, celle avec la plus grande capacité à être mise en cache. Il va ensuite servir à la chaine les autres éléments, depuis le cache ou après un calcul de rendu. BigPipe ne traite que les réponses au format HTML.

Il en est capable en s’appuyant sur le Dynamic Cache Page pour générer le premier rendu partiel. Il récupèrera ensuite dans la même requête, en plusieurs morceaux, les éléments pour compléter les placeholders. Ceux-ci seront envoyés dans le flux de la connexion au fur et à mesure qu’ils sont calculés.

Pour cela, BigPipe s’interface avec la réponse générée dans le flux traditionnel de Drupal via l’événement “kernel.response”. Cet événement est alors écouté par le HtmlResponseBigPipeSubscriber. C’est ensuite à l’exécution de celui-ci que se déclenchent deux méthodes : la première se déclenche tôt dans la pile d’exécution des subscribers et prepare la réponse pour être traitée par BigPipe. La seconde méthode (onRespond) se déclenchera à la fin de la pile pour exposer la réponse via BigPipe.

Schématiquement la réponse BigPipe, qui est envoyée depuis la classe BigPipe, se compose de trois grandes parties :

  • D’abord il va envoyer le contenu principal, tout ce qu’il y a avant la balise </body>, avec les placeholders qui ne sont pas encore traités.
  • Puis il va envoyer les contenus qui remplaceront les placeholders au fur et à mesure qu’ils sont récupérés (donc potentiellement plusieurs envois). La substitution se fera alors côté client.
  • Enfin BigPipe envoie la balise </body> et tout ce qui se trouve après pour fermer le document HTML.
Schéma du détail de la réponse avec la fonctionnalité BigPipe

Pour insérer dans la page les éléments qui combleront les placeholders, il exploite un script JS embarqué côté client. Le résultat est un premier rendu aussi véloce que le cache statique. Puis une complétion des éléments dynamiques de la page donnant un effet proche du chargement de contenus via Javascript et XHR mais sans faire de nouvelles requêtes côté client. Une image valant parfois mille mots, je vous invite à visionner cette vidéo de Dries Buytaert (ci-dessous).

https://medium.com/media/742f6063f5181ade76b179c5017f2687/href

Cache Policies

Chaque stratégie de cache utilise des politiques de requête (Request Policy) pour vérifier si les conditions sont réunies pour servir son cache.

Le fonctionnement est assez simple. Le service de cache (les classes PageCache ou DynamicPageCacheSubscriber par exemple) reçoivent par injection de dépendance la classe ChainRequestPolicy. Cette classe empile des objets supportant l’interface RequestPolicyInterface, qui sont autant de contraintes validant, ou non, si le service doit traiter la requête.

Pour le PageCache, les RequestPolicy dans la pile sont :

  • la classe CommandLineOrUnsafeMethod, qui vérifie que la requête n’est pas exécutée dans un contexte CLI (donc dans un contexte web).
  • la classe NoSessionOpen qui vérifie qu’une requête n’a pas de cookie de session.

Pour le Dynamic Cache Page il n’y a qu’une seule politique de requête dans la pile : CommandLineOrUnsafeMethod.

Quel module activer ou désactiver ?

Tous ces modules sont complémentaires et font partie du cœur de Drupal. Internal et Dynamic Cache Page sont activés par défaut et il n’y a aucune raison en principe pour les désinstaller.

Toutefois, si notre site web est très dynamique, même pour les utilisateurs anonymes, il est conseillé de désinstaller le module de l’Internal Cache Page.

Par exemple, s’il y a un panier d’achat pour les utilisateurs non connectés, ce composant doit être être spécifique pour chaque utilisateur et ne peut pas être mis en cache de façon globale avec le module Internal Cache Page.

BigPipe n’est pas activé par défaut. Pour qu’il puisse fonctionner, il faut vérifier que l’infrastructure serveur puisse le prendre en charge (voir les prérequis).

Il faudra aussi préparer l’affichage pour éviter le “Layout Shift ”, ces déplacements de contenus dans la page lorsqu’on récupère du nouveau contenu pour l’ajouter dans la page servie, ce qui peut rapidement devenir très désagréable pour les utilisateurs.

Vérifier rapidement quel module de cache a généré la réponse.

Il n’est pas toujours évident de savoir quel module s’est déclenché ou pas lorsqu’on consulte une page. Pour cela, on peut contrôler le fonctionnement des modules de caches par la présence des headers :

  • X-Drupal-Cache avec la valeur HIT signale un cache provenant du module Internal Cache Page.
  • X-Drupal-Dynamic-Cache avec la valeur HIT indique que c’est le Dynamic Cache Page qui est servi.
  • Surrogate-Control avec la valeur no-store, content=”BigPipe/1.0" identifiera un cache rendu par BigPipe.

Pour ne pas rendre indigeste la lecture de ma découverte de l’implémentation de la gestion du cache Drupal, j’ai préféré la découper en quatre parties. Dans cette première partie, j’ai commencé à appréhender le fonctionnement des modules de cache Drupal qui correspondent, selon ma façon de voir, à des niveaux de sophistication de la gestion du cache.

Si jamais vous souhaitez continuer à creuser le sujet, ou si certaines de mes explications ne sont pas suffisamment claires, je vous invite à visionner la conférence de Wim Leer lors de la DrupalCon 2017 à Viennes.

Dans la partie suivante de cette série, où je fouille dans l’implémentation du cache dans Drupal, nous allons nous intéresser à l’implémentation du stockage du cache.


Implémentation de la gestion du cache dans Drupal : une approche terre-à-terre (1/4) was originally published in ekino-france on Medium, where people are continuing the conversation by highlighting and responding to this story.