Utilité d'un cache. Caractéristiques. Difficultés. Solutions Rails. Cas du projet A : problème et solution.

Article en cours d'écriture

Après quelques rappels sur le fonctionnement d'un cache et les possibilités qu'offre Rails pour en mettre un en œuvre, cet article décrira comment cette technique est utilisée dans le cadre de l'application projet A qui génère plusieurs sites indépendants au moyen de générateurs et de composants adaptables.

1. Généralités sur le cache

1.1. Pourquoi un cache ?

Les pages des sites web dynamiques sont produites par interrogation d'une base de données, puis par l'intégration des résultats dans un ou plusieurs modèles (des vues) de code HTML, eux-mêmes ensuite intégrés dans un gabarit de pages. Ce processus est relativement long et il nécessite des ressources du serveur alors que, la plupart du temps, une page doit être servie à de multiples utilisateurs sans que la moindre information qu'elle contient n'ait été modifiée.

Une manière de réduire cette surcharge inutile consiste à enregistrer spécifiquement dans une cache des résultats intermédiaires (retours des requêtes SQL, blocs HTML), voire l'intégralité des pages à servir, puis d'utiliser tels quels ces résultats lors d'une nouvelle requête similaire.

1.2. Caractéristiques d'un cache

Toutes les techniques de cache ont un certain nombre de propriétés en commun, même si leurs réalisations peuvent être très variées : l'accès simple aux données ; le stockage rapide ; la taille limitée ; la durée de validité des données ; la permanence des données ; un partage des données ; la charge de gestion.

L'accès simple aux données est généralement fourni par l'association d'une clé à chaque bloc de données.

La gestion du cache doit être plus rapide que la production des données qu'il doit enregistrer. Une solution simple est l'enregistrement de chaque donnée dans un fichier dont le nom est la clé, dans une arborescence de dossiers optimisée pour un accès rapide même avec de très nombreux fichiers. L'utilisation d'un bloc de mémoire pour stocker la totalité du cache est encore plus rapide, mais volatile.

Sauf dans le cas d'espaces disque de grande capacité, la taille du stockage consacré au cache est limitée. Un algorithme d'effacement automatique des données les plus vieilles ou les moins utilisées est alors mis en œuvre, ce qui est le plus souvent peu gênant quand les données supprimées ont peu ou pas de chances d'être rappelées.

L'application utilisatrice du cache peut connaître à priori la durée de validité des données qu'elle met en cache et la lui communiquer. Certains caches ne savent pas gérer cette information.

Dans le cas où la production des pages HTML est longue est complexe, la permanence des données dans le cache peut garantir un délai rapide de délivrance des pages, car, dans tous les cas, elles proviendront du cache avec un temps de réponse rapide. Cette propriété n'est possible que si la taille du cache est plus grande que celle de la totalité des pages à servir ; elle s'accompagne logiquement d'un processus de production des pages au démarrage du serveur ou de l'application.

Quand le service d'une application est réparti entre plusieurs processus, sur le même serveur ou sur plusieurs, Par exemple au moyen de logiciels tels qu'Unicorn ou Passenger, le partage du cache entre tous les processus mutualise la charge de production des pages.

Enfin, suivant les techniques utilisées, la charge de gestion du cache sera plus ou moins importante et elle peut parfois entrainer des temps de latences très important, notamment au démarrage de l'application ou lors de la suppression d'éléments dans le cache.

1.3. Où se cache les difficultés ?

Les plus grandes difficultés dans la mise en œuvre d'une technique de cache proviennent de l'utilisation de données pourries, c'est-à-dire d'informations qui ont été modifiées depuis leur mise en cache. Si elles ne sont pas effacées du cache, ou tout au moins rendues inaccessibles, des pages non actualisées peuvent être envoyées aux internautes.

Une page contient toujours des données très diverses, en plus de celles objets de la page : le nom de la ou des rubriques englobantes, le menu de navigation, des actualités, un bas de page personnalisé ou non, avec des commentaires associés, etc. Et si, par construction, le programmeur connait de quoi est composé une page, l'inverse est très souvent loin d'être le cas : dans quelles pages chaque information de base est-elle reprise, directement ou indirectement ? Le problème est d'autant plus complexe que le site est lui-même complexe, composé de différents modules indépendants dont la logique échappe parfois au concepteur du site.

2. Gestion du cache avec Rails

On se réfèrera avantageusement au guide Rails Caching with Rails: An overview pour la description détaillée des solutions qu'offrent Rails concernant la mise en œuvre d'un cache, description dont trois volets sont brièvement repris ci-dessous : modes de stockage, portée et calcul des clés.

2.1. Modes de stockage

En résumé, Rails propose trois modes de stockage du cache :

  • FileStore : enregistrement en fichiers, stockage permanent donc, mais qui n'autorise pas la définition d'une durée de validité des éléments enregistrés ;
  • MemoryStore : enregistrement dans un espace mémoire dépendant du processus, donc pas de partage si la charge de l'application est répartie sur plusieurs ;
  • MemCacheStore : enregistrement en mémoire via le service memcached installé indépen­damment de l'application, donc partage possible entre plusieurs instances de l'application, même sur des serveurs différents ; par contre, comme ce mode privilégie les temps de réponse, il n'implémente pas l'effacement d'enregistrements autre que la réinitialisation complète du cache.

 2.2. Techniques de cache

Rails propose trois mécanismes de cache : par page, par action et par fragment. Les deux premiers nécessite l'ajout d'une gem depuis la version 4 de Rails.

2.3. Cache par page

La méthode de cache par page est la plus efficace, car elle enregistre la page HTML générée intégralement et la sert à nouveau sans passer par toute la pile des traitements d'une requête Rails.

Par contre, son activation s'effectue par action d'un contrôleur et, comme elle ne passe pas par le contrôleur quand la page est dans le cache, elle n'est utilisable que pour des pages quasi-statiques, ne dépendant pas du contexte. Les pages ne seront en effet modifiées que sur effacement explicite ou sur limite du délai de validité.

2.4. Cache par action

Comme la précédente, la méthode de cache par action enregistre la page HTML générée, mais elle exécute les filtres (before_filter) du contrôleur avant de chercher à réutiliser le cache. De plus, elle permet une particularisation de la clé de cache, permettant d'y inclure des éléments du contexte. Elle permet d'exclure du cache le gabarit (layout) associée à la vue si celui-ci contient des éléments dynamiques.

2.5. Cache par fragment

La méthode de cache par fragment permet de cacher une ou plusieurs parties de la page à générer. Celle-ci est alors composée à partir des fragments réutilisables du cache et des parties non cachées de la vue .

La clé de chaque fragment peut être adaptée à chaque contexte en fonction de son contenu. Ainsi, si seul un fragment d'une page est devenu caduque, seul celui-ci est régénéré.

La méthode de cache par fragment peut bien sûr être utilisée avec les autres méthodes quand certains fragments sont communs à plusieurs pages.

2.6. Calcul des clés

 La demande de mise en cache d'un fragment s'effectue dans une vue en appelant la méthode cache avec, comme bloc associé, les lignes de code de génération du fragment.

Exemple en ERB :

<% cache [paramètres] do %>
   lignes de code
   de génération
<% end %>

Exemple en HAML :

- cache [paramètres] do
  lignes de code
  de génération
 

La méthode cache admet un paramètre simple ou un tableau de paramètres à partir desquels sera construite la clé d'enregistrement dans le cache. Pour cela, la méthode cache_key, sinon la méthode to_param, est appelée sur chaque objet ; pour un objet du modèle de donnée, cache_key retourne par défaut une chaine contenant le nom de la classe de l'objet, son identifiant et son heure de dernière mise à jour. Exemple : works/2423-20140123173257.

Cette structure de clé facilite une gestion automatique du cache appelée « poupées russes » (Russians dolls) : quand l'objet de la page (works dans l'exemple) est modifié, sa date change et avec elle la clé du cache ; l'ancien enregistrement en cache n'est plus accessible ; il sera automatiquement effacé quand le cache aura besoin d'espace. Si la page inclut des informations de plusieurs objets, la clé de la page est composée à partir de leurs identifiants et de leurs dates de dernière actualisation ou, à minima, de la date la plus récente.

Pour le cache par action, la clé par défaut est construite à partir de la route d'accès de l'action du contrôleur concernés, mais il est possible de la personnaliser de la même façon.

3. Cas du projet A

Examinons maintenant le cas du logiciel projet_a : ses contraintes et les solutions apportées pour l'utilisation d'un cache.

3.1. Caractéristiques de l'application

Le but de l'application est de produire des sites. Ceux-ci sont quasi-indépendants les uns des autres et, pour des questions de cohérence, chaque objet est rattaché à un site.

Une fois construits, les sites sont peu modifiés ; leur optimisation en consultation est donc prioritaire.

La structure des sites est basée sur l'arbre des espaces (spaces), puis sur les œuvres (works) associées. Sauf quelques exceptions, les pages générées sont donc des pages associées chacune à un espace décrivant des œuvres ou à une seule œuvre.

La forte capacité d'adaptation aux besoins du logiciel conduit toutefois à de notables variations et exceptions :

Quelques générateurs de pages prennent en compte les paramètres HTML figurant en fin d'URL.

Toutes les pages comportent un menu de navigation (deux ou trois niveaux d'espaces) ou les niveaux précédents. Quelques sites (AIO ; CG) et certaines pages de gestion des espaces affichent l'arbre global sur chaque page. Un webmestre peut demander à voir toutes les pages qu'il gère à tout niveau de visibilité et avec quelques options de construction (grilles de calage...).

Une très forte majorité de pages sont en accès public. Les autres ont un contenu privé fonction des quatre niveaux de visibilité correspondants.

Presque toutes les pages ont un contenu stables, sauf quelques unes avec effets spéciaux (par exemple, l'accueil des sites CM et MAD ont une image ou une couleur aléatoire).

Les informations des pages d'actualités sont reprises au niveau de la page d'accueil du site et sur la page portail AIO. Il existe quelques autres dépendantes inter-espaces, enregistrées dans leurs paramètres de génération.

Quelques collages (stickers) ont des dates de début et de fin de publication, en particulier ceux des actualités.

Quelques pages affiche des données d'un groupe d'utilisateurs, voire de plusieurs. Sinon, il n'y a pas d'informations utilisateur autre que sur le bas de page en mode privé.

Les modifications des composants ou des générateurs et celles des paramètres de site ou de branches d'espaces sont peu fréquentes, mais peuvent se produire à tout moment.

3.2. Choix du stockage du cache

Le choix d'un stockage de cache conditionne la programmation, car, comme nous l'avons vu ci-dessus, chaque type de stockage n'implémente que certaines fonctionnalités.

3.2.1. FileStore ?

La première idée qui paraît la plus simple est l'utilisation d'un enregistrement sous forme de fichiers : solution incluse dans Rails ; espace disque disponible ; permanence du cache, compatible avec le taux très faible des mises à jour.

Une première tentative avec FileStore a vite montré des limites quasi-rédhibitoires :

Dès que le cache s'est un peu rempli (quelques centaines d'éléments), la méthode d'effacement par expression régulière devient insupportablement longue. Elle doit en effet lire la totalité des répertoires et sous-répertoires du cache pour retrouver les noms de fichiers (égaux aux clés), puis les filtrer en leur appliquant l'expression. Force est donc de se passer de cette méthode et d'utiliser des clés horodatées de la stratégie de poupée russes précédemment décrite.

On peut aussi gérer soi-même l'indexation du cache... en stockant les tables d'index dans le cache par exemple. La structure du contenu de l'application projet_a étant assez simple, essentiellement basée sur les espaces (objet Space), une table d'index à accès aléatoire vers une table d'enregistrements chainés en cas de collision donnait des résultats satisfaisants. Un peu usine à gaz, mais ça tournait bien.

Hélas ! Il y a toujours des cas qui ne rentrent pas bien dans votre système d'indexation ou globalement dans votre gestion du cache. Il vous reste alors quelques pages – pas beaucoup, très peu même – qui s'obstinent à s'afficher à partir d'un élément du cache contenant des données caduques.

Et comme le stockage FileStore du cache ne permet pas de définir de délais de validité, même par défaut, il ne vous reste plus qu'à purger régulièrement la totalité du cache, perdant ensuite pendant de longues dizaines de minutes le bénéfice de cette technique.

3.2.2. MemCacheStore ! (avec memcached)

Le stockage CacheStore ne pouvant être partagé entre plusieurs processus, cette solution n'est pas envisageable, car l'application projet_a est placée derrière un serveur Nginx qui permet de paralléliser des traitements concurrents.

Il ne reste donc plus que MemCacheStore qui enregistre les données au travers d'un service Linux (ou Windows si vous y tenez) memcached.

Les avantages de cette solution est qu'elle est simple à mettre en œuvre, qu'elle est très rapide et peu gourmande en ressources CPU, qu'elle est partageable entre plusieurs processus ou applications, Rails ou non, sur le même serveur physique ou non, qu'elle accepte un délai de validité – par défaut ou au coup par coup – pour chaque élément mis en cache.

Ses inconvénients sont qu'elle ne permet pas l'effacement ciblé d'éléments dans le cache et que, ce dernier résidant en mémoire vive, il est complètement réinitialisé lors d'un redémarrage du serveur.

3.3. Installation et configuration du cache

Sous Linux Debian, pour installer memcached, tapez sous le compte root :

apt-get memcached

Le service correspondant est installé et démarré avec le fichier de configuration sur /etc/memcached.conf. Les valeurs par défaut sont opérationnelles.

La communication avec une application Ruby passe par la gem dalli. Il faut donc ajouter au fichier Gemfile de l'application :

gem dalli

puis lancer un : bundle update

Dans le fichier production.rb, modifier ou ajouter la ligne, pour une durée de validité par défaut de 24 heures :

config.cache_store = :dalli_store, { expires_in: 86400 }

Quand vous voudrez tester le cache en développement, ajouter cette même ligne dans le fichier development.rb et activez temporairement le cache par :

config.action_controller.perform_caching = true

À suivre...

version 0.9.5-0442-150126

↑ Haut