Drupal 8 : Le cache, nouveautés, mécanismes

Cet article est extrait de notre formation drupal 8 "de Drupal 7 à Drupal 8" à destination des développeurs. N'hésitez pas à nous contacter pour en savoir plus !

Les applications et sites web modernes soulèvent tous des problèmes liés aux performances. Plus les systèmes s’assouplissent et se complexifient et plus les ressources nécessaires à leur exécution augmentent. Cependant, les utilisateurs recherchent de plus en plus de contenus personnalisés tout en conservant un temps de réponse optimal. Ce dernier critère étant devenu récemment l’un des plus importants pour le référencement via Google, nous ne pouvons plus nous permettre de ne pas faire attention aux performances de nos réalisations.

Afin d’éviter au maximum les calculs coûteux et répétitif, nous disposons de plusieurs niveaux de cache qui couvrent la totalité des besoins d’un développeur.

Le cache applicatif

Déjà présent dans Drupal 7 et dans la plupart des Frameworks modernes, cette couche de cache est la plus profonde et correspond à ce que l’on peut faire de plus unitaire possible. On distingue deux variantes en fonction du besoin : le cache statique et le cache d’exécution. La différence majeure entre ces deux types de cache concerne la durée pendant laquelle sont conservées les données. Le cache d’exécution n’est valable que pendant la durée de vie d’une requête HTTP alors que le cache statique lui peut persister plus longtemps.

Cache d’exécution

Dans Drupal 7 comme dans Drupal 8, celui-ci n’a pas changé. Il s’agit donc de faire appel à la fonction drupal_static() et de récupérer son retour comme une référence.

# anywhere.php
function my_overused_function($id) {
  $cache = &drupal_static(__FUNCTION__, []);
  if (empty($cache[$id])) {
    $cache[$id] = do_something_long($id);
  }
  return $cache[id];
}

De cette manière, même si la fonction est appelée de très nombreuses fois lors de la même requête HTTP, le calcul ne sera fait qu’une fois et le résultat sera conservé en mémoire. Attention à ne pas abuser de cette fonction sous peine de saturer la mémoire de votre serveur.

Dans Drupal 8, ce cache aura tendance à être moins utilisé que dans Drupal 7 car la plupart des objets PHP sont toujours passés comme référence et peuvent donc faire office de conteneurs de stockage pour le temps de l’exécution.

Cache statique

Parfois, un calcul est si coûteux qu’il est préférable de le faire une seule fois ou de temps en temps mais pas à chaque requête. Dans Drupal 7 il était possible de stocker des données en cache de manière temporaire ou permanente à l’aide de la fonction cache_set() et de les récupérer à l’aide de cache_get().

Le principe de base n’a pas changé dans Drupal 8. Seule la syntaxe a évolué, introduisant l’utilisation d’un service de gestion du cache.

# anywhere.php
function do_something_long($id) {
  $cid = 'something_long:' . $id;

  $data = NULL;
  if ($cache = \Drupal::cache()->get($cid)) {
    $data = $cache->data;
  }
  else {
    $data = do_the_really_long_thing($id);
    \Drupal::cache()->set($cid, $data);
  }

  return $data;
}

Par défaut, les caches sont stockés en base de données, dans un ensemble de tables cache_* qui peuvent être déportées en mémoire grâce à un système de type Memcache ou Redis.

Si vous avez un peu suivi vous aurez constaté que l’on peut combiner ensemble les deux exemples précédents afin de ne solliciter le cache statique qu’une seule fois par requête. Cette combinaison n’est ni nécessaire ni obligatoire car il faut toujours garder à l’esprit que tout a un coût. Le cache d’exécution a un coût en mémoire et le stockage statique un coût d’accès. Votre travail est de faire un choix en adéquation avec la situation.

Les caches d’objets

Depuis Drupal 6 et l’avènement de CCK puis Drupal 7 et l’arrivée des entités, les objets de données et la structure de la base se sont grandement complexifiés. Reconstituer une entité à partir de ses données en tenant compte des révisions, de la langue, des gestions de droits et des nombreux hooks qui peuvent intervenir au milieu de tout ça est devenu une opération coûteuse. Sont donc apparus des modules tels que Entity Cache ou Views Content Cache par exemple.

Dans Drupal 8, toutes les entités de contenu et de configuration sont mises en cache par défaut.

Le cache de rendu

Voilà enfin la grosse nouveauté de Drupal 8 sur laquelle repose une grande partie des performances. Introduit en tant que concept dans Drupal 7 à l’aide du module Render Cache, le cache de rendu s’est imposé dans le cœur petit à petit jusqu’à la beta 16 et l’apparition du module Internal Dynamic Page Cache.

Le principe de base est assez simple. Chaque élément d’une page que l’on veut construire est un render array qui dispose de méta-données spécifiant les conditions dans lesquelles il doit être mis en cache et qui peut contenir des sous-éléments disposant de leurs propres méta-données. Lors de la première construction de la page, chacun de ces éléments va être rendu et ses méta-données ainsi que celles de ses enfants vont être additionnées. Au second chargement de la page, seuls les éléments qui doivent varier seront compilés, les autres seront récupérés depuis le cache. On dit que ces méta-données se “propagent” (bubble up).

Ce module fonctionne donc sur le même modèle qu’un Reverse Proxy qui irait consommer les ressources associées à des Edge Sides Includes uniquement lorsque nécessaire. Ce modèle est tellement efficace qu’il ne serait pas étonnant de le voir apparaître sur les systèmes concurrents de Drupal à court terme.

Les méta-données de cache

Au nombre de quatre, celles-ci permettent de s’assurer que le cache est justement dosé. Un cache trop important permettrait potentiellement à des utilisateurs de voir le contenu destiné à d’autres alors qu’un cache pas assez important solliciterait trop les ressources du serveur. Afin de trouver un juste milieu, les méta-données de cache vont permettre de définir de façon explicite les variations possibles et les dépendances du contenu mis en cache.

Les clefs de cache (cache keys) permettent d’identifier l’objet rendu. Elles correspondent à l’identifiant utilisé par le service de cache pour stocker ou récupérer des données. Elles servent de préfixe à toutes les variantes du même contenu afin de pouvoir invalider un ensemble de variantes plus facilement. Les clefs de cache ne sont pas obligatoires et devraient être utilisées uniquement lorsque l’on souhaite procéder à un cache intermédiaire. Par exemple, la plupart des champs d’une entité n’auront pas de clé de cache car ils sont faciles à reconstruire. En revanche, certains champs plus complexes comme par exemple une référence d’entité en auront une pour pouvoir limiter l’invalidation et éviter d’avoir à tout reconstruire si un seul champ venait à changer.

Les contextes de cache (cache contexts) permettent de définir les diverses variantes du contenu. Ainsi, chaque variante est mise en cache séparément et servie aux utilisateurs qui correspondent aux critères. Par exemple, il est possible de définir un contenu qui variera en fonction de la langue et des permissions de l’utilisateur. Cela permettra de garantir que tous les utilisateurs disposant d’une combinaison de permissions équivalente verront le même contenu pour peu qu’ils accèdent au site dans la même langue.

Les contextes de cache sont résolus au moment de la génération du contenu et ajoutés aux clefs de cache afin de générer l’identifiant de la variation.

Les tags de cache (cache tags) sont utilisés pour définir les dépendances entre le cache et des objets arbitraires. Toutes les entités de contenu et de configuration sont des dépendances valides mais il est également possible de créer ses propres dépendances en cas de besoin. Si jamais l’un de ces objets est modifié, tous les caches qui lui sont rattachés sont immédiatement invalidés.

Les tags de cache s’expriment, par convention, sous la forme objet:identifiant. Par exemple, node:2 est un tag valable. Afin de simplifier la création de ces tags pour déclarer des entités en tant que dépendance, il est possible d’utiliser la méthode EntityInterface::getCacheTags() qui retourne les tags correspondant à l’entité (rappelez vous que ces derniers se propagent).

L’âge d’expiration (cache max-age) sert quant à lui dans les rares cas où l’on souhaite forcer le cache à expirer au bout d’un certain temps même si ses composants n’ont pas changé. C’est souvent utilisé lorsque l’on souhaite faire une remontée d’éléments aléatoire rafraîchie régulièrement. Cette valeur est exprimée en secondes. La valeur 0 indique que l’élément ne doit jamais être mis en cache et la valeur Cache::PERMANENT qu’il ne doit jamais expirer sur la base de son âge.

Voici un exemple reprenant tous ces éléments :

# anywhere.php

use Drupal\Core\Cache\Cache;

function link_my_nodes($ids) {
  $nodes = \Drupal::entityManager()->getStorage('node')->loadMultiple($ids);
  $build = [
    '#theme' => 'links',
    '#links' => [],
    '#cache' => [
      'keys' => ['link_my_nodes'],
      'contexts' => ['languages', 'user.permissions'],
      'tags' => [],
      'max-age' => Cache::PERMANENT,
    ],
  ];

  foreach ($nodes as $node) {
    $build['#links'][] = [
      'title' => $node->label(),
      'url' => $node->urlInfo(),
    ];

    $build['#cache']['contexts'] = Cache::mergeContexts($build['#cache']['contexts'], $node->getCacheContexts());
    $build['#cache']['tags'] = Cache::mergeTags($build['#cache']['tags'], $node->getCacheTags());
    $build['#cache']['max-age'] = Cache::mergeMaxAges($build['#cache']['max-age'], $node->getCacheMaxAge());
  }

  return $build;
}

Le cache HTTP

Le dernier niveau de cache est le plus simple en apparence. Il concerne le cache du navigateur (ou du reverse proxy si vous en avez un). Ce dernier s’appuie sur les entêtes HTTP renvoyés par l’application comme Cache-Control, Expires, Etag, Vary, Pragma, etc. La bonne nouvelle c’est que Drupal 8 s’en sort beaucoup mieux que son prédécesseur dans la création de ces entêtes. En effet, celui-ci s’appuie sur les méta-données de cache de la page, et donc de tous les éléments qui la constituent, pour générer ces entêtes. La configuration pour faire fonctionner Drupal 8 derrière un reverse proxy ou un CDN est donc très simple voir même pas nécessaire du tout !

Commentaires

Christophe Caron (non vérifié) Lundi 12 février 2018 - 12:05
Attention l'article indique : 'contexts' => ['language', 'user.permissions'], Le context est "languages" avec un s sinon on aboutit à une erreur de type context indéfini.

Votre commentaire

À propos de Julien

Gérant - Co-fondateur - Scrum master & Expert technique

Utilisateur de Drupal depuis 2008, j’ai fait mes armes comme développeur chez Commerce Guys puis me suis mis à encadrer les nouveaux arrivants avant de donner des formations, participer aux avant ventes et accompagner les équipes au passage à Scrum.

Je suis impliqué dans la communauté française de Drupal depuis 2009, j’ai été tour à tour président puis vice-président de l’association Drupal France et francophonie entre 2011 et 2013.

Nous rejoindre ?

Vous avez envie de rejoindre une équipe éthique ?
Vous avez envie de faire un partenariat ?