Drupal et le cache

DrupalCamp 1917
Lannion

Old Briat

Drupal et le cache

DrupalCamp 2017
Lannion

Olivier Briat

![me-myself-and-I](img/me-myself-and-I.png) ## Olivier Briat - Capgemini Nantes - Lead Dev Drupal - sncf.com - 5 ans de Drupal (6, 7 et 8) - Contributeur au module Redis (D8)

Inspirations

Proudly found elsewhere

  • Drupal 8 Caching overview< / Berdir / Drupal Dev Days Seville 2017
  • Render API & Cache API / Artusamak / Drupal Camp Nantes 2016
  • Drupal 8 Caching - A Developer’s Guide / Peter Sawczynec / Pacific Northwest Drupal Submit 2016

Visualiser en ligne (sources)

Au sommaire

  • Rappel des basiques
  • Caches en amont de Drupal
  • Cache anonyme de page
  • Cache dynamique de page
  • Cache de "rendu"
  • Utilisation du cache dans son code
  • Backends
  • Des Questions ?

Rappel des basiques

Pourquoi ?

Pour améliorer les performances

Comment ?

En stockant le résultat de processus coûteux pour ne pas avoir à les calculer encore et encore...

Contrairement à Drupal 7 le cache est activé par défaut dans Drupal8.

Il est vivement conseiller de le désactiver en mode développement

Il suffit de décommenter les lignes suivantes du settings.php

if (file_exists(__DIR__ . '/settings.local.php')) {
  include __DIR__ . '/settings.local.php';
}
On reviendra plus tard sur ce fichier.

Caches en amont de Drupal

Le premier cache est celui du navigateur

Une url de page web : http://www.monsitedrupal.org/fr?arg=1#section1
Voici les header HTTP liés (méta-données)
Réglage du max-age dans le BO

Pour les ressources (img, css, ...) le réglage s'effectue dans le .htaccess

# Requires mod_expires to be enabled.

  # Enable expirations.
  ExpiresActive On

  # Cache all files for 2 weeks after access (A).
  ExpiresDefault A1209600

  
    # Do not allow PHP scripts to be cached unless they explicitly
    # send cache headers themselves(...)
    ExpiresActive Off
  

Les caches suivants sont :

Proxy serveurs mandataire

CDN Content Delivery Network (Akamaï, Level5, ...)

Varnish :

  • Choix classique avec Drupal
  • Couteau suisse des headers http
  • Peut cacher un site indisponible
  • Peut être piloté par Drupal (module purge)
  • Peut-être remplacé par le micro caching de votre serveur Nginx
  • Il peut remplacer le module Internal Page Cache

Mais avant d'aller plus loin

Désactiver le cache et activer les options de débogage.

settings.local.php
$settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';

$config['system.logging']['error_level'] = 'verbose';

$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;

# $settings['cache']['bins']['render'] = 'cache.backend.null';

# $settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
development.services.yml
parameters:
  http.response.debug_cacheability_headers: true
services:
  cache.backend.null:
    class: Drupal\Core\Cache\NullBackendFactory

http.response.debug_cacheability_headers permet de faire apparaître les headers HTTP X-Drupal- très utiles pour le débogage du cache Drupal.

Avec drupal console : drupal site:mode dev (https://www.drupal.org/node/2598914)


Mais surtout :

N'oubliez de réactiver les caches avant de tester votre dev...

Cache anonyme de page

  • Géré par le module Internal Page Cache
  • Cache l'intégralité du rendu html d'une page anonyme dans le cache
  • Très rapide
  • La clé est l'URL complète
  • Utilise les "cache tags", mais pas les "cache contexts" ou le "max age"
  • Respect le header "Expires" de la response)
  • Comme on l'a vu, il peut être désactivé si on utilise un type de cache similaire (Varnish)
  • Header HTTP correspondant : X-Drupal-Cache (HIT/MISS)

Cache dynamique de page

  • Module du coeur : "Dynamic Page Cache"
  • Ce cache est plus fin (mais plus lent) que le cache anonyme.
  • Il utilise le cache de chaque élément* qui compose une page.
    * On reviendra plus tard sur ce qui compose ces éléments.
  • Chaque élément à ses propres méta-données de cache, celles-ci permettent d'en déterminer la validité.
  • Elles bouillonnent" et sont agrégées aux éléments parents jusqu'à remonter au niveau de la page.
  • Donc si le cache d'un élément n'est plus valide, par "bouillonnement", il invalidera donc celui de tous ses parents.
  • Voici un exemple :

Voici une page composé de deux blocs avec chacun un liste de noeuds.


Schémas @berdir

Modification du node 5 et création du node 7 (en rouge les méta-données invalidées)

En rouge les contenus recalculés :

#lazy_builder
et
Auto-placeholdering

Pour empêcher les éléments très dynamiques d'invalider systématiquement le cache de page, ceux-ci sont remplacés par des placeholders, en toute fin de rendu ils sont substitués par le contenu du callback.

https://www.drupal.org/docs/8/api/render-api/auto-placeholdering

Il peut être défini avec le paramètre #lazy_builder

// Callback : class:method ou (mieux) service:method
return [
  '#lazy_builder' => ['hello_world.lazy_builder:renderSalutation', []],
  '#create_placeholder' => TRUE,
];
  

ou être détecté automatique en fonction des condition suivantes (par defaut) :

# Conditions pour être "autoplaceholderer"
  renderer.config:
    auto_placeholder_conditions:
      max-age: 0
      contexts: ['session', 'user']
      tags: []

Cache de "rendu"

Les éléments utilisés par le "cache dynamique de page" sont bien entendu les "render array" Drupal.

La plupart ont des méta-données de cache (clé #cache) dont les parents héritent :

Fils
Parent

Ces métas-données s'agrègent donc jusqu'à la page et ses headers

Module renderviz : pour le débogage en profondeur des méta-données de cache.

Les méta-données de cache en détails

  • Keys : Optionel, permet de nommer cette partie de la render array et de le mettre en cache.
  • Cache Contexts : Permet d'avoir des variantes de cache en fonction du... contexte (theme, language, user roles, permissions, URL, QS, timezone,... )
  • Cache Tags : Permet d'invalider des types de caches (node:x, config:, user:x, library_info, route_match, node_list , ...)
  • Max Age : Permanent (-1), age en seconds (3600), ne pas cacher (0)

Gérer les méta-données d'une Render Array

$config = \Drupal::config('system.site');
$current_user = \Drupal::currentUser();

$build = [
  '#markup' => t('Salut, %name, bienvenu sur @site!', [
    '%name' => $current_user->getUsername(),
    '@site' => $config->get('name'),
  ]),
  '#cache' => [
    'contexts' => [
      'user', // Sera traiter en #lazybuilding
    ],
    'tags' => $config->getCacheTags(), // ou addCacheableDependency()
  ],
];

// Autre moyen d'ajouter une dépendance de cache.
$renderer = \Drupal::service('renderer');
$renderer->addCacheableDependency(
  $build,
  \Drupal\user\Entity\User::load($current_user->id())
);
// Fusionner des cache tags
$tags = Cache::mergeTags($conf_one->getCacheTags(),$conf_two->getCacheTags());

Plugin contexts

Fonctionne avec les annotation context (typiquement dans la définition d'un block)

*   context = {
*     "node" = @ContextDefinition("entity:node", label = @Translation("Node"))
*   }
https://api.drupal.org/api/drupal/core!core.api.php/group/annotation/8.5.x
https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Annotation!ContextDefinition.php/group/plugin_context/8.5.x

Les plugin (et donc les blocs) implémentent CacheableDependencyInterface et donc des méthodes pour initialiser les méta-données de cache.

\Drupal\Core\Plugin\ContextAwarePluginBase::getCacheContexts()

  public function getCacheContexts() {
    return Cache::mergeContexts(parent::getCacheContexts(), ['user.node_grants:view']);
  }

  public function getCacheTags() {
    return Cache::mergeTags(parent::getCacheTags(), ['node_list']);
  }

    public function getCacheMaxAge() {
    return 0;
  }
A propos des block, attention à bien rendre content, dans le fichier Twig du contenu, car c'est lui qui porte les méta-données de cache, voir :
https://www.previousnext.com.au/blog/ensuring-drupal-8-block-cache-tags-bubble-up-page

Utilisation du cache dans son code

Utilisateurs, sites-builders, pas de panique :

Drupal gère le cache tout seul !

Développeurs de tous horizons, voici comment l'utiliser :

Static

  • "cache" utilisant le fait qu'une variable static n'est pas détruite à la fin de l'exécution d'une fonction.
  • On peut continuer à utiliser drupal_static() pour le code procédural.
  • En mode OO, n'oubliez pas que les "services" sont des "singletons" et donc leurs propriétés sont également persistantes

Get / Set

function mes_donnees_metier();
  // Ma clé
  $key = 'mon_module' . ':' . __FUNCTION__ . ':' .
  \Drupal::languageManager()->getCurrentLanguage()->getId();
  // Est-ce que le cache existe ?
  if ($cache = \Drupal::cache()->get($key)) {
    $data = $cache->data;
  }
  // Pas de cache : on traite les données et
  // on les stocke dans le cache.
  else {
    $data = mon_traitement_metier_super_lent();
    \Drupal::cache()->set($key, $data);
  }
  return $data;
}

Duration

// Possible de préciser une durée de validité (timestamp)
\Drupal::cache()->set($key, $data, REQUEST_TIME + 600);

Tags

// Associer des tags
\Drupal::cache()->set(
  $key,
  $data,
  Cache::PERMANENT,
  [
   'tag1',
    'node:1',
    'config:system.menu',
    'config:mon_module',
  ]
);
// Découvrir les tags des entités :
$node->getCacheTags();
$entity_type->getListCacheTags(); //EntityTypeInterface
\Drupal\views\Entity\View::load('front')->getCacheTags();
;

Suppression / Invalidation

// Suppression du cache (rapide)
\Drupal::cache()->delete('mon_module:donnees_caches');
\Drupal::cache()->deleteMultiple([
  'mon_module:cle1',
  'mon_module:cle2',
  ...
]);
\Drupal::cache()->deleteAll();

// Invalidation : non conseillé (Berdir)

// Invalidation de tag (incrémentation du compteur d'invalidation)
$cache_tag_invalidator->invalidateTags(['my_tag']);
Cache::invalidateTags(['my_tag']);
            

À l'enregistrement d'un cache on stocke de la somme des compteurs d'invalidation des tags concernés.

À l'appel du cache on recalcule ce checksum, s'il diffère le cache est régénéré.

Multiples

\Drupal::cache()->getMultiple($keys);
\Drupal::cache()->setMultiple($items);

Utiliser du cache périmé (pour faire patienter)

$cache = \Drupal::cache()->get('my-key', TRUE);
if ($cache && $cache->valid) {
  return $cache->data;
}
elseif (\Drupal::lock()->acquire('my-key')) {
  // Rebuild and set new data.
}
elseif ($cache) {
  // Someone else is rebuilding, work with stale data.
  return $cache->data;
}
else {
  // Wait or rebuild.
}

Bins

Il y a différents "bacs" (bin) de cache :

  • bootstrap : pour le démarrage de Drupal.
  • render : cache des rendu HTML
  • default : cache par défaut, pour de petits contenu avec peu de clés
  • data : pour les gros caches avec de nombreuses clés
  • discovery : Petit, utilisé fréquemment, principalement par les plugins et les processus de découvertes

On peut créer son propre "bin". On peut associer un backend différent à chaque "bin".

Backends

Méthode de stockage d'un bin.

  • Dans le core core/core.services.yml :
    • Memory : En mémoire, donc non persistant
    • Database : Base de donnée, le stockage par défaut *
    • APCu : Partage mémoire au travers du processus PHP (donc pas de partage avec drush/CLI ou avec d'autres frontaux)
    • Null : pour désactiver le cache (dev)
  • Module contribués :
    • Slushi Cache : Idem database, mais avec une durée de vie paramétrable
    • Memcache : Base clé/valeur stocké en RAM cache.backend.memcache_storage
    • Redis: Base clé/valeur stocké en mémoire cache.backend.redis

Le ChainedFast Backend (cache.backend.chainedfast) permet de chaîner un backend rapide au-dessus d'un backend lent.

* Depuis la 8.4 la taille des tables de cache n'est plus infinie.

Merci

à toute l'équipe du DrupalCamp Lannion 2017

aux sponsors

Des questions ?

Références

## Conférences - https://md-systems.github.io/drupal-8-caching/ - https://nantes2016.drupalcamp.fr/programme/sessions/render-api-cache-api - https://happyculture.coop/blog/drupal-8-le-cache-nouveautes-mecanismes - https://pnwdrupalsummit.org/sites/default/files/slides/Drupal%208%20Caching.pdf - Drupal 8 cache for developers - José Jiménez Carrión #DrupalCampES @jjca : https://youtu.be/kfy_JAKudnw - Drupal 8 Caching: A Developer’s Guide : https://youtu.be/eB4NWo5XwMY - BigPipe : https://youtu.be/JwzX0Qv6u3A
## APIs et docs - https://api.drupal.org/api/drupal/core!core.api.php/group/cache/8.5.x - https://www.drupal.org/docs/8/administering-drupal-8-site/internal-page-cache - https://www.drupal.org/docs/8/core/modules/dynamic-page-cache/overview - https://www.drupal.org/docs/8/api/render-api/auto-placeholdering - https://www.drupal.org/docs/8/api/render-api/cacheability-of-render-arrays - https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching - https://www.drupal.org/project/renderviz

Livre

  • Drupal 8 Module development de Daniel Sipos (upchuk)
    seulement 5€ chez Packt : https://www.packtpub.com/web-development/drupal-8-module-development
## Serveurs - https://varnish-cache.org - https://redis.io/documentation