Mise en cache intelligente avec Django

Pour un site plus rapide et sans désagréments !

Ce contenu provient initialement de Progdupeupl et a été écrit par MicroJoe sous la licence CC BY. Il a été mis à jour et importé par mes soins avec l’accord de l’auteur. Vous pouvez retrouver la version originale sur Progdupeupl1 et la source de cette version ici.

Vous êtes en train de construire votre site internet avec Python et Django, tout va pour le mieux du monde ; mais plus votre site grossit et plus des fonctionnalités gourmandes apparaissent, effectuant de plus en plus de requêtes SQL même si vous les optimisez afin d’éviter la casse ?

Que faire quand on a optimisé la génération de ses pages mais que le rendu est encore lent ? Utiliser la mise en cache !

Ce tutoriel est écrit pour Django 1.11 LTS, 2.0, 2.1, 2.2 LTS et 3.0 mais les différences avec les autres versions restent minimes.


  1. le site est désormais hors ligne.

Présentation

La mise en cache est une technique courante qui consiste à stocker pendant un temps défini un résultat qui provient d’une opération lourde. Lors de la demande d’accès à ce résultat, on retourne la valeur que l’on a gardée en mémoire au lieu de relancer l’opération lourde, il en résulte un gain de réactivité (très) important.

Voici un exemple du fonctionnement du cache sur un site web :

Fonctionnement du cache sur une page web
Fonctionnement du cache sur une page web

Les différentes implémentation du cache

Django nous propose nativement un système de cache assez poussé. Celui-ci vous permet d’utiliser différentes plate-formes (aussi appelées backend) afin de se souvenir des valeurs que vous voulez réutiliser. On compte notamment :

  • Solutions très rapides : via la RAM

    • memcached, la référence en la matière. L’outil va stocker directement dans la mémoire vive les valeurs afin de garantir un temps d’accès optimal. De plus, l’architecture distribuée1 de memcached permet d’utiliser des machines distantes afin de répartir la mise en cache, tout est géré automatiquement.
    • Si vous n’êtes pas en mesure d’installer, de configurer et d’utiliser memcached (par exemple si vous n’avez pas les droits root sur le serveur) mais que vous voulez tout de même avoir un stockage en RAM pour un accès plus rapide, Django possède un backend prévu à cet effet.
  • Solutions un peu moins rapides : via le disque

  • Autres solutions :

C’est à vous de faire le choix concernant le backend que vous voulez utiliser en fonction de vos besoins, mais sachez qu’il est possible d’utiliser plusieurs backends sur un même site ! Par exemple, les éléments peu nombreux et utilisés très souvent pourront être stockés dans la mémoire vive alors que les éléments plus volumineux, nombreux et moins souvent accédés pourront se trouver sur le disque (sauf si vous avez à votre disposition 256Go de RAM).

Différents niveaux de mise en cache

De plus, Django nous permet différent seuils de granularité :

  • Une mise en cache globale du site : chaque page sera cachée pendant un certain temps.
  • Une mise en cache par vue : seulement certaines vues seront cachées par l’utilisation d’un décorateur Python.
  • Une mise en cache au niveau des templates : c’est la méthode la plus fine, qui permet de cacher uniquement certains bouts dans certains modèles HTML.

C’est en se fondant sur cette dernière technique que nous allons réaliser une mise en cache intelligente.


  1. Vous pourrez trouver une description de l’architecture distribuée ici.

Exemple : système de notifications

Situation initiale

Prenons un cas concret : vous êtes en train de réaliser une application pour votre site qui s‘occupe de gérer l’envoi de messages entre les membres de votre site. Vous avez intégré la notification de nouveaux messages via une icône présente sur tout le reste de votre site.

<div class="message-box-icon">
    Vous avez {{ messages_count }} message{{ messages_count|pluralize }}.
</div>

Cependant, la requête pour récupérer le nombre de nouveaux messages est lourde et prend 3 secondes. Sachant que cette requête est effectuée sur chaque page puisque vous vous servez le l’icône pour afficher le nombre de nouveaux messages, toutes les pages de votre site mettent 3 secondes de plus à charger à cause de cette requête.

Mettre du cache pour combler certaines lenteurs peut être une mauvaise idée : si votre site est lent c’est peut-être lié un souci de conception. En effet, un site doit pouvoir fonctionner normalement sans cache, ce dernier étant là pour des questions d'optimisations. Avant la mise en place d’un cache il est nécessaire vérifier qu’il est impossible d’améliorer les performances sans cache et que ce cache va vraiment apporter un gain, ce qui ne serait pas le cas sur une page lente que personne ne va voir par exemple.

Heureusement, comme on l’a vu précédemment, Django nous propose un système de mise en cache au niveau des templates HTML. Et si nous utilisions cet outil afin d’accélérer la génération de nos pages ?

Mise en place du cache

Rendons nous dans notre template où le code gourmand est appelé, et mettons ce résultat en cache :

{% load cache %}

<div class="message-box-icon">
    {# On cache le bloc suivant sous le nom de 'messages-count' (qui n'est pas une variable mais une "clef" comme dans un dictionnaire) pendant 120 secondes #}
    {% cache 120 message-count %}
        Vous avez {{ messages_count }} message{{ messages_count|pluralize }}.
    {% endcache %}
</div>

Après avoir configuré correctement le système de cache (n’utilisez pas le cache qui ne cache pas sinon ça risque de ne pas fonctionner), on remarque que la première génération de cette page prend toujours 3 secondes mais que les suivantes sont rendues quasiment instantanément !

Cependant il y a un problème. En effet, si vous vous connectez sous le nom d’un autre utilisateur, alors vous aller voir le bloc qui a été caché pour le premier utilisateur à avoir entraîné la mise en cache. Ainsi il se peut très bien que vous ayez une notification de nouveau message alors que c’est en fait le nombre de messages pour le premier utilisateur qui avait été mis en cache !

Indépendance du cache

Django nous permet de palier à ce problème : il nous suffit de trouver un élément unique qui diffère et de le passer à la balise de mise en cache pour que celle-ci stocke les différentes valeurs en fonction de cet élément unique. Dans le cadre de nos utilisateurs, on peut dire que le pseudonyme est un bon élément unique :

{% load cache %}

<div class="message-box-icon">
    {# On cache le bloc suivant sous le nom de 'messages-count' et avec pour clé unique 'user.username' pendant 120 secondes #}
    {% cache 120 message-count user.username %}
        Vous avez {{ messages_count }} message{{ messages_count|pluralize }}.
    {% endcache %}
</div>

Ainsi, chaque utilisateur aura sa propre mise en cache de son nombre de nouveaux messages. Ouf !

Astuce : vous pouvez stocker un bloc unique pour chaque utilisateur et un autre dans le cas d’un utilisateur non connecté avec {% cache 1800 menu user.pk|default_if_none:"0" %}. Ici on met en cache pendant 1800 secondes un bloc qui s’appelle « menu » et dont la clé unique est le pk de l’utilisateur. Si l’utilisateur n’est pas connecté alors ce pk vaudra None et et on le remplace par 0 (les pk commencent à 1) avec le filtre default_if_none. Ainsi tous les visiteurs non connectés verront le bloc avec "0" alors que les utilisateurs authentifiés auront leur propre bloc. Cela peut être utile sur un menu où tous les utilisateurs n’ont pas accès aux mêmes parties du site.

Mise à jour intelligente du cache

Persistance indésirable

Il nous reste cependant un dernier petit problème à régler :

Considérons que je viens de recevoir un nouveau message ; la valeur mise en cache m’indique bien que j’ai un nouveau message puisque c’est la première page que je génère, tout fonctionne comme prévu.

Je lis mon message et éventuellement répond à son auteur avant de continuer paisiblement ma navigation sur le site. Cependant, je remarque que le nombre de nouveaux messages reste bloqué à 1 ! En effet, la valeur a été mise en cache pendant un certain temps (120 secondes dans notre exemple précédent) et ne sera pas actualisée avant que ces 120 secondes se soient écoulées !

Cet effet est gênant pour tous les utilisateurs de votre site, mais comment faire pour éviter ce cas de figure tout en gardant les bénéfices de la mise en cache ?

Un exemple de cache non actualisé : le compteur (en cache) affiche 11 notifications alors qu'il n'y a qu'une seule
Un exemple de cache non actualisé : le compteur (en cache) affiche 11 notifications alors qu'il n'y a qu'une seule

Péremption automatique

Afin de solliciter un appel frais à la fonction de génération de notifications, il nous faudrait invalider le contenu qui a été précédemment mis en cache. Pour cela, nous allons devoir utiliser l’accès « bas niveau » au cache de Django.

Le cache fonctionne comme un dictionnaire avec un système de clés/valeurs ; pour supprimer un élément il nous faut donc retrouver sa clé. Les clés utilisées au niveau du cache sont assez bizarres, considérons le bloc suivant :

{% cache 120 message-count user.username %}
    ...
{% endcache %}

La clé associée sera la suivante :

template.cache.message-count.f71dbe52628a3f83a77ab494817525c6

On y retrouve le nom du bloc ainsi qu’un hash MD5 créé à partir des arguments passés au bloc. Pour retrouver facilement cette clé, on peut utiliser la fonction make_template_fragment_key :

>>> from django.core.cache.utils import make_template_fragment_key
>>> make_template_fragment_key('messages-count', ['toto'])
'template.cache.message-count.f71dbe52628a3f83a77ab494817525c6'

Il nous reste maintenant à supprimer cette clé du cache lors des actions qui pourraient potentiellement mettre à jour ce cache. Par exemple, si l’on reprend notre cas, on aimerai bien que le cache soit mis à jour lors de la consultation du nouveau message :

from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key


def view_message(request):
    # Si on consulte un message qui n’était pas lu jusqu’à présent,
    # on peut considérer qu’il est lu et donc mettre à jour l’icône
    # du nombre de messages non-lus.
    if not message_read(msg):
        cache.delete(make_template_fragment_key('message-count', request.user.username))

    # Rendu du template, etc.

Il vous reste à mettre en place cette demande explicite de suppression de contenu mis en cache à chaque endroit où ce contenu aurait pu en effet être modifié. Ainsi, les utilisateurs sont toujours prévenus en temps et en heure, en plus si aucun changement n’a été détecté la valeur peut directement être lue dans le cache !


Vous êtes désormais en mesure d’utiliser la mise en cache de Django sur votre site web sans y retrouver les effets néfastes : la mise à jour différée des données.

Sources :

Je rappelle que ce contenu provient initialement de Progdupeupl et a été écrit par MicroJoe sous la licence CC BY. Je le remercie de m’avoir laisser le reprendre et le mettre à jour.

Merci à artragis et spacefox pour les remarques toujours pertinentes qu’ils ont eues.

5 commentaires

Merci pour ce tuto.

Petite remarque : je n'ai pas compris tout de suite que message-count est le nom du fragment que l'on veut cacher, j'ai cru que c’était une variable (ce qui m'a fait tilter sur le -), il serait utile de l'expliciter, comme dans la doc :

The name will be taken as is, do not use a variable.

Du coup, si je comprend bien, le second argument du tag cache est toujours interprété littéralement, les suivants pouvant eux être des variables (ce qui semble être le cas).

+3 -0

Super boulot d'avoir repris ce tutoriel ici et de l'avoir remis au gout du jour avec des illustrations. C'est une bonne initiative et j'espère qu'il y en aura d'autres pour ramener le contenu intéressant de PDP tout en respectant les licences libres comme ici.

J'ai remarqué quelques coquilles qui proviennent peut-être de la version originale (flemme de vérifier) que j'ai signalé sur Twitter.

Sinon contenu très intéressant, on gagne en temps machine sans perdre en fonctionnalités, vive le cache ! :P (avis complètement biaisé pour ceux qui ont lu ce document en diagonale)

+2 -0

Moi qui suis développeur php j'ai beaucoup aimé les concepts présenté et je me demandais si Symfony proposais ce même genre de comportement.

Il existe une extension Twig proposant un mode de fonctionnement identique dans les template Twig, je tenais à le signaler pour ceux qui développerai avec et qui, comme moi serai tombé sur ce tuto et ont été très intéressé.

Ps: J'ai pas compris comment installer cette extension, mais une très rapide recherche m'a donner un Bundle basé sur cette extension tout à fait standard quant à son installation.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte