10 mai 2023 17:03:28 seb leridon avatar

Gestion du cache

Introduction

La gestion du cache est un élément essentiel pour garantir des performances optimales de Lutece.

Nombre d'opérations de construction des pages sont coûteuses en CPU. On peut citer notamment toutes les opération mettant en oeuvre de la transformation XML/XSL ou du parsing ou des accès à la base données. Sans cache, Lutece ne pourrait supporter que quelques utilisateurs avec des temps de réponse très médiocres. Faire appel au cache est donc absolument nécessaire !

Les données stockées dans le cache occupent de l'espace mémoire donc leur volume est un paramètre a garder toujours à l'esprit.

Par ailleurs, les données du cache ne sont qu'un reflet des données réelles. Dans certains cas on tiendra à ce qu'elle soient toujours en phase avec les données réelles : le cache devra être "invalidé" à chaque modification d'une données ou celles-ci ne sont pas "cachées". Dans d'autres cas, c'est la durée de vie des objets en cache qui donnera la limite acceptable pour le délai de mise à jour des données.

Ce sont toutes ces considérations qu'il s'agit de discuter dans cet article pour configurer Lutece dans une optique de performances maximales.

Principes de base des caches

L'algorithme de base d'une gestion de cache est relativement trivial. Le cache est un grand réservoir en mémoire dans lequel on stocke des objets dont chacun est identifié par une clé unique calculée. Le modèle de collections utilisé en général pour stocker un cache est une Map.

L'algorithme de récupération d'un objet est donc le suivant :

  • on calcule sa clé de cache (hash, identifiant, ou combinaison de clés)
  • on recherche l'objet dans le cache
  • si l'objet est trouvé
    • on le renvoie
  • sinon
    • on récupère ou on recalcule l'objet réel,
    • on met l'objet dans le cache associé à sa clé
    • on renvoie l'objet

La construction de la clé de cache est donc un aspect important de la gestion.

Par ailleurs, les services de Lutece s'appuient sur le produit Ehcache qui permet une gestion plus fine des caches en introduisant des notions telles que :

nombre maximum d'objets durée de vie des objets stockage sur disque ...

Une interface centralisée de gestion des caches

Lutece offre une interface centralisée, accessible à l'administrateur du site, de gestion de tous les services de cache. Interface de gestion des caches - Lutece v4

Cette interface liste les services de cache installés, leur statut, les principales options de configuration et des éléments dynamiques d'utilisation (nombre d'objets, mémoire occupée, ...).

Les différents niveaux de cache

Les dispositifs de cache sont présents à plusieurs niveaux et dès qu'ils peuvent apporter des gains pour accéder à des ressources lourdes à calculer ou à charger.

On distingue deux principaux niveaux.

Les services de cache de premier niveau

Ces services sont implémentés sous forme de filtre de servlet. C'est à dire qu'ils interceptent tout appel HTTP à une ressource (jsp, js, images, ...).

Ces services sont basés sur la brique Ehcache-Web. Les clés de cache sont construites à partir des éléments de la requête HTTP : method, uri path, querystring.

Ces services fournissent d'autres optimisations :

  • compression GZIP des réponses
  • headers HTTP adaptés à la durée de vie des objets pour profiter au mieux des caches des navigateurs et des proxys notamment pour les ressources statiques (images, css, scripts, ...).

Les services de cache des ressources

Ces services assurent le cache des différents objets Lutece : page, portlet, menus, arborescence du site, document.

Configuration des caches

Deux fichiers situés dans le répertoire WEB-INF/conf/ permettent de gérer la configuration des caches :

  • caches.properties
  • caches.dat

caches.properties

Ce fichier contient le paramétrage par défaut des caches.

# Default cache configuration
   lutece.cache.default.maxElementsInMemory=10000
   lutece.cache.default.eternal=false
   lutece.cache.default.timeToIdleSeconds=10000
   lutece.cache.default.timeToLiveSeconds=10000
   lutece.cache.default.overflowToDisk=true
   lutece.cache.default.diskPersistent=true
   lutece.cache.default.diskExpiryThreadIntervalSeconds=120
   lutece.cache.default.maxElementsOnDisk=10000

Il contient également les options pour le monitoring des caches par JMX.

# JMX monitoring properties
   lutece.cache.jmx.monitoring.enabled=false
   lutece.cache.jmx.monitorCacheManager=false
   lutece.cache.jmx.monitorCaches=false
   lutece.cache.jmx.monitorCacheConfiguration=false
   lutece.cache.jmx.monitorCacheStatistics=false

caches.dat

Ce fichier contient le statut et le paramétrage spécifique des caches au démarrage de la Webapp.

#Caches status file
   #Sun Mar 27 03:06:48 CEST 2011
   SiteMapService.enabled=1
   MyPortalWidgetService.enabled=1
   PortalMenuService.enabled=1
   DocumentResourceServletCache.enabled=1
   PageCacheService.enabled=1
   PortletCacheService.enabled=1
   MyPortalWidgetContentService.enabled=1
   PageCachingFilter.enabled=1
   StaticFilesCachingFilter.enabled=1
   StaticFilesCachingFilter.timeToLiveSeconds=1000000

La racine des propriétés est le nom des caches (avec les espaces supprimés si le nom original en contient). La propriété enabled indique si le cache est activé (valeur=1) ou désactivé (valeur=0). Si cette propriété est absente, le cache est activé par défaut.

Les autres propriétés sont les mêmes paramètres de cache que ceux du fichier caches.properties. La valeur indiqué dans ce fichier surchargera la valeur par défaut définie dans caches.properties.

A partir de la version 4.1 ces infos sont sauvegardées en base dans le Datastore. Les fichiers caches.properties et caches.dat ne servent que pour l'initialisation des valeurs.

Stratégies de configuration


Comme indiqué en introduction, il n'y a pas de configuration universelle. Il s'agit de trouver les meilleurs compromis entre fraicheur des données, performances et consommation de ressources mémoire.

Voici les principales configurations.

Configuration de développement

En développement, il n'y a pas d'enjeux de performances et il est préférable de suivre la mise à jour des données sans effet de cache. La configuration à retenir est donc de désactiver tous les caches.

StaticFilesCachingFilter.enabled=0
   PageCachingFilter.enabled=0
   PageCacheService.enabled=0
   PortletCacheService.enabled=0
   PortalMenuService.enabled=0
   SiteMapService.enabled=0

Il est possible également de désactiver l'ensemble des caches en modifiant une propriété system au niveau du lancement de la VM Java.

java -Dnet.sf.ehcache.disabled=true

Configuration de production pour site sans personnalisation

Le cache de premier niveau peut être activé à la fois sur les ressources statiques (images, css, scripts) mais aussi sur les pages du portail dont notamment Portal.jsp.

La durée de vie du cache des ressources statiques peut être configuré à une semaine soit 604800 secondes Celui des pages JSP peut être configuré à une heure soit 3600 secondes

StaticFilesCachingFilter.enabled=1
    StaticFilesCachingFilter.timeToLiveSeconds=604800
    PageCachingFilter.enabled=1
    PageCachingFilter.timeToLiveSeconds=3600

Les caches de deuxième niveau (pages, portlets, menu,...) ne sont pas obligatoire dans ce cas. Si ils sont activés, la durée de vie des objets doit être cohérente avec celle du cache de premier niveau, soit dans le cas présent la valeur à retenir serait également une heure.

PageCacheService.enabled=0
    PortletCacheService.enabled=0
    PortalMenuService.enabled=0
    SiteMapService.enabled=0
    DocumentResourceServletCache.enabled=1

Configuration de production pour site avec personnalisation

La personnalisation empêche d'utiliser le cache de premier niveau pour les pages JSP. En effet la même page ne s'affichant pas de la même manière en fonction de l'utilisateur connecté, il n'est pas possible de servir une page à partir sa seule adresse.

Pour assurer un cache efficace des pages, il faut alors nécessaire de s'appuyer sur les caches de second niveau qui peuvent gérer dans leur clés l'identifiant de l'utilisateur.

StaticFilesCachingFilter.enabled=1
    StaticFilesCachingFilter.timeToLiveSeconds=604800
    PageCachingFilter.enabled=0
    PageCacheService.enabled=1
    PortletCacheService.enabled=1
    PortalMenuService.enabled=1
    SiteMapService.enabled=1
    DocumentResourceServletCache.enabled=1

Dans ce type de configuration, les identifiants des utilisateurs étant dans la clé de cache, il faut impérativement tenir compte du nombre d'utilisateurs pour tous les aspects de dimensionnement. Il peut être envisagé de créer des clés de cache spécifiques en fonction du mode de personnalisation pour réduire le nombre d'objets et les performances. Configuration de production pour environnement limité en mémoire

Dans un contexte où la mémoire disponible est faible et/ou la taille des ressources est importante, il convient de dimensionner correctement la paramètre maxElementsInMemory. Si le nombre d'objets en cache dépasse cette valeur, les objets les moins récemment utilisés (eviction policy LRU Least Recently Used) seront déplacés vers un cache disque, limitant ainsi les risques de dépassement mémoire.

Développement d'un service de cache

Pour développer un service de cache intégré à Lutece il suffit d'étendre la classe abstraite AbstractCacheableService.

public class MyCacheService extends AbstractCacheableService
{
   private static final String SERVICE_NAME = "My Cache Service";


   public MyCacheService()
   {
       initCache();
   }

   public String getName(  )
   {
       return SERVICE_NAME;
   }

   public MyResource getResource( String strId )
   {
        MyResource r = getFromCache( strId );
        if( r == null )
        { 
            r = getResourceFromSource( strId );
            putInCache( strId , r );
        }
        return r;
    }
}

Construction des clés de cache

Pour rendre les clés faciles à lire, la norme retenue est de concaténer les éléments sous la forme [ key1:value1][key2:value2]...[user:id].

Par ailleurs, ces constructions étant présumées être fortement sollicitées on utilisera une concaténation avec un StringBuilder.

Voici une implémentation typique respectant ces normes :

private String getCacheKey( String strId , LuteceUser user )
{
     StringBuilder sbKey = new StringBuilder();
     sbKey.append( "[res:" ).append( strId ).append( "][user:").append( user.getName() ).append("]");
     return sbKey.toString();
}

L'interface ICacheKeyService et l'implementation DefaultCacheKeyService

Certains services tels que PageService accepte l'injection par le biais du contexte Spring d'une classe implementant l'interface ICacheKeyService permettant de générer des clés de cache à partir d'une map de paramètres et l'objet user.

La classe DefaultCacheKeyService propose une implémentation par défaut de cette interface. L'interface prévoit de définir pour la génération de la clé :

une liste de paramètres à utiliser pour générer la clé. Il s'agit d'une mesure de sécurité pour empêcher qu'un générateur utilisant des paramètres fictifs dans les urls ne génère autant de clés de cache qui viendrait à saturer la mémoire. une liste de paramètres à ignorer. Certains paramètres peuvent ne pas être pertinents dans la construction et peuvent générer des doublons dans le cache. Il convient donc de les éliminer en les déclarant dans cette liste.

Voici l'exemple d'injection de CacheKeyService dans un contexte Spring pour PageService :

<bean id="pageCacheKeyService" class="fr.paris.lutece.portal.service.cache.DefaultCacheKeyService" >
       <property name="allowedParametersList" >
           <list>
               <value>page_id</value>
           </list>
       </property>
   </bean>

   <bean id="portletCacheKeyService" class="fr.paris.lutece.portal.service.cache.DefaultCacheKeyService" >
       <property name="ignoredParametersList" >
           <list>
               <value>page-id</value>
               <value>site-path</value>
           </list>
       </property>
   </bean>

   <bean id="pageService" class="fr.paris.lutece.portal.service.page.PageService">
       <property name="pageCacheKeyService" ref="pageCacheKeyService" />
       <property name="portletCacheKeyService" ref="portletCacheKeyService" />
   </bean>