Gestion de cache et péremption des données

Je suis actuellement en train de travailler sur une couche d’abstraction de données (pour améliorer celle de mon framework afin de la rendre plus transparente).
J’ai buté sur un problème assez classique, que je vais partager avec vous car cela pourrait vous intéresser.

Ma couche d’accès aux données sert habituellement à faire des requêtes sur une base SQL. Afin de réduire le nombre de requêtes réellement effectuées, et d’améliorer les performances, cette couche a la capacité de mettre les résultats des requêtes en cache. Ce cache est basé sur Memcache, qui garde les données en RAM pendant une durée maximale définie, et la bibliothèque PHP qui y accède sérialise les données sans qu’on ait à s’en préoccuper.

Mais comment stocker ces données en cache ? Memcache permet d’enregistrer une variable en la nommant par une chaîne de caractère.
Ainsi, si je vais récupérer le contenu d’un enregistrement d’une table à partir de sa clé primaire, je commence par créer une chaîne constituée de la sorte :

__dao:nom_de_la_base:nom_de_la_table:get:clé_primaire

Ensuite, je vais regarder si j’ai une donnée en cache disponible avec ce nom. Si ce n’est pas le cas, je fais la requête, stocke son résultat en cache puis le retourne.

(bon, vous aurez compris que lorsque je parle à la première personne, c’est pour me mettre à la place du code ; c’est bizarre comme habitude, mais on fait tous ça, hein)

Mais dans le cas où on fait une modification sur les données de la table, il est impossible de savoir à l’avance si les données préalablement récupérées sont toujours valables ou non. La démarche la plus logique est donc de vouloir tout invalider d’un coup. On se heurte alors à une limitation de Memcache ; il est impossible de récupérer, modifier ou effacer plusieurs variables de manière groupée. C’est dommage, parce qu’il serait très pratique de pouvoir écrire quelque chose du genre :

$cache->deleteFromPrefix('__dao:nom_de_la_base:nom_de_table');

Si on prend le temps de réfléchir, il n’existe pas des milliards de manières de solutionner ça. En fait, je vois trois grandes possibilités.

Tricher pour contourner le problème

Le plus simple, c’est déjà de faire ce qu’il faut pour ne pas avoir à gérer l’expiration des données. La plupart du temps, on peut se permettre d’afficher des données qui ne soient pas complètement à jour. Par contre, sur un site à très fort trafic, il reste important de ne pas frapper la base de données à chaque affichage de page.

On peut alors diminuer la durée pendant laquelle les données sont mises en cache, à seulement quelques minutes. C’est suffisant pour alléger considérablement le travail de la base. Et une fois que le délai est écoulé, les données se mettront automatiquement à jour.

C’est idiot, mais c’est souvent très efficace. Toutefois, je privilégie ce type de fonctionnement pour du cache applicatif, quand on peut choisir d’appliquer ce genre de stratégie. Pour du cache géré au niveau d’un couche d’accès au données, c’est déjà un peu plus scabreux, car certaines applications pourraient avoir besoin d’avoir des données systématiquement à jour.

Tricher pour contourner le problème (bis)

Extension directe de la méthode précédente, on pourrait simplement se dire que la couche d’accès aux données offrirait la possibilité de désactiver l’utilisation du cache. Ainsi, une application qui aurait des besoins « temps réel » pourrait imposer des requêtes systématiques, quitte à mettre en place du cache au niveau applicatif.

Là encore, c’est simple, c’est con, c’est bourrin, mais qu’est-ce que ça marche bien !

Bon, il n’empêche que ce serait quand même bien de trouver une « vraie » solution.

Lister les variables

Une solution évidente consiste à ajouter une variable de cache supplémentaire, qui contienne simplement la liste des noms des autres variables de cache. Quand on se retrouve à modifier des données, on commence par récupérer cette liste, puis on la parcourt pour effacer les variables une à une (puis on efface la variable de cache qui contient la liste). C’est une approche valable si on compte que la plupart des applications comptent bien plus de lectures que d’écritures.

Avantage : C’est simple à gérer dans le code, il n’y a pas de mauvaise surprise.

Inconvénient : Si on fait beaucoup de requêtes différentes les unes des autres, par exemple des recherches avec des critères très variés, on risque d’avoir beaucoup de variables en cache. Et donc la liste pourrait être très longue ; la parcourir pourrait devenir problématique. En plus de cela, il y a un risque d’incohérence des données au moment où la liste est mise à jour, entre sa lecture et son écriture, ou durant l’effacement de toutes le variables listées.

Versionner les variables

L’autre solution envisageable est de stocker, dans une variable de cache, un « numéro de version ». Memcache permet d’incrémenter la valeur stockée dans une variable en un seul appel, évitant ainsi les problèmes d’incohérence d’un cycle lecture-incrémentation-écriture.

Chaque donnée placée en cache voit alors son nom préfixé ou suffixé par ce numéro de version. À chaque fois qu’une requête est susceptible de modifier les données (et donc qu’il faudra vider les variables en cache), il suffit d’incrémenter le numéro de version. À la lecture suivante, les anciennes variables ne seront plus accédées.

Avantage : Pas de ralentissement lors de l’effacement des variables obsolètes.

Inconvénient : Chaque lecture de donnée impose deux accès au cache, l’un pour récupérer le numéro de version, l’autre pour lire la variable de données, ce qui réduit d’autant le gain en performance. Suivant le nombre d’écritures, le cache peut vite se remplir de variables inutiles.

Au final

La dernière solution reste celle qui paraît la plus intéressante. Dans le cadre d’une couche d’abstraction de données qui est destinée à être étendue intelligemment (comprendre par un développeur) dès qu’on en fait une utilisation qui ne soit pas minimale, c’est l’idée qui m’a semblé la meilleure, et que j’ai implémenté.

Est-ce que vous avez déjà été confronté à ce genre de réflexion ? Avez-vous d’autres idées ?

Quelques lectures intéressantes

Voici, en vrac, quelques liens qui ont alimenté ma réflexion :

12 commentaires pour “Gestion de cache et péremption des données

  1. Une autre idée est d’utiliser un outil un peu plus complexe que Memcache, mais beaucoup plus flexible.

    Redis est un bon exemple. C’est ce qui est utilisé pour cacher les données utilisateur de la nouvelle version de linuxfr.

    Il doit exister d’autres exemples…

  2. Je n’ai jamais été confronté à ce genre de problème.

    Et une solution qui viserait à détecter les modification de la BD et donc de modifier le contenu de memcache ?
    Ça impose que la BD n’est pas modifier en dehors de l’application, ou déporter l’utilisation de la bd dans une autre application (un genre un proxy)

  3. @Bob : Redis est une base noSQL. L’utiliser simplement comme un cache me semble dommage. Je prône le mélange SQL/noSQL, mais le mélange SQL/memcache est plus courant (dans l’idée d’un framework utile au plus grand nombre).

    @Eric : On utilisait APC au début, comme système de cache. Malheureusement, on avait souvent des incohérences, ce qui nous avait amené à mettre des « magic cookie » dans chaque donnée stockée en cache.
    Au final, Memcache est plus fiable, presque aussi performant, et bien plus « scalable » (plusieurs serveurs de cache se partagent les données).
    J’ai jeté un coup d’œil à ta fonction. C’est intéressant, et ça utilise bien les capacités d’APC. Mais il reste tout de même un risque quand tu boucles sur les variables pour les effacer une à une. Il se peut qu’une variable soit mise à jour d’un côté pendant que de l’autre tu es en plein milieu de la boucle.

    @Paul : Oui, à partir du moment où il y a du cache de bas niveau (a contrario du cache applicatif), il faut que tous les accès se fassent à travers la même couche d’abstraction. Sinon il y aura forcément des comportement imprévisibles.

  4. Oui, je suis d’accord. Enfin, c’est la couche d’abstraction de données qui doit pouvoir gérer les deux.
    Là, je veux surtout avoir un système qui permette de gérer le plus simplement possible la combinaison technologique la plus répandue (SQL + memcache). Tout en permettant de gérer n’importe quelle autre techno en écrivant un peu de code.

  5. J’utilise Pear Cache lite pour ce genre de choses. La notion de groupes dont il dispose permet de supprimer des parties du cache sans en toucher d’autre. Par exemple la mise à jour ou l’ajout d’une actualité va supprimer tout le cache qui concerne l’actualité (les requêtes liées à la table actualite et categorie_actualite).
    Le nom des fichiers de cache est crée à partir d’un MD5 de la requête (auquel j’adjoint le contenu de la clause limit (et start)).

    Par contre, pour des parties plus légères comme certains paramètres du site, Memcache me semble très bien adapté.

  6. Ce qui me dérange avec Cache Lite, c’est qu’il écrit ses données de cache sur disque.

    1. Théoriquement, c’est moins rapide que Memcache, qui utilise la RAM (mais il faudrait faire des benchmarks).
    2. Ça augmente le nombre d’I/O. Sur certaines applications qui ont une utilisation intensive du cache ou des disques durs, ça peut avoir un impact négatif.
    3. C’est gênant quand on veut avoir un cache partagé entre plusieurs machines. Le seul moyen est de passer par un filesystem réseau (NFS), ce qui n’est franchement pas ce qu’il y a de plus rapide.

  7. je suis entièrement d’accord sur le 3ieme point par contre pour le reste ça peut se résoudre avec un disque en RAM
    ce qui me gêne dans l’utilisation de Memcache pour stocker du cache Mysql c’est la taille que cela pourrait atteindre

  8. Un disque en RAM, pourquoi pas. Mais si tu veux avoir du cache partagé qui tourne sur plusieurs serveurs, le problème reste entier.
    La taille du cache est indépendante de la techno utilisée. Qu’il s’agisse d’un cache de bas niveau ou d’un cache applicatif, l’idée reste d’y stocker tout ce qu’on peut pour gagner en performance. Si on sature le cache, le données les plus anciennes sont effacées.

  9. Bonjour,
    tout d’abord je tiens à dire que j’aprécie énormément la qualité des articles et des liens proposés !

    J’aimerais me permettre de poser une question qui m’interesse beaucoup :
    Est-ce qu’il existe des solutions open-source qui répondent à votre besoin ?
    Je pense à des ‘abstractions de base de donnéee’ comme ADObd (je ne connais pas cette librarie).

  10. @Hadrien : Il existe tout un tas de solutions open-source pour gérer les connexions aux bases de données. Ça peut aller des surcouches assez légères (ADODB, DB2, PDO, …) jusqu’aux ORM très complets (Propel, Doctrine, …).
    Mais, au milieu de tout ça, je n’ai rien trouvé qui soit léger (pas besoin ni envie d’un ORM complet) tout en étant capable de gérer un système de cache pour améliorer les performances.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Notifiez-moi des commentaires à venir via email. Vous pouvez aussi vous abonner sans commenter.