Pratiques de caching avançées

Cet article résume les différents points que nous avons abordés (ou pas par manque de temps) lors du hands-on avec Mathilde Lemee (@MathildeLemee).

1) Le mot ‘cache’ est tiré du Québecquois, cache, qui dénomme originellement l’endroit ou on entrepose des réserves. Il vient du français ‘cache’ qui désignait l’endroit où on cachait ses réserves (comme un grenier ou une cave) pour les coup durs.

Bref, le cache c’est la zone mémoire dans laquelle on stocke des données. Et en particulier un ‘hotset’ de données, c’est à dire le sous-ensemble de données qui est utilisé le plus souvent. Ce sous-ensemble est généralement beaucoup lu et peu écrit/mis à jour (10% écriture et 90% de lecture).

La raison primordiale d’utiliser un cache est la vitesse. On utilise un cache lorsque l’on veut augmenter les performances de son application.

L’avantage du cache est sa proximité. Par rapport à la source de donnée, le cache se situe souvent plus proche de l’application. Ehcache en l’occurence est un cache applicatif, qui, parcequ’il se situe au sein de l’application Java, et donc dans la même JVM, permet d’accéder aux données de manière extrêmement rapide.

Deux termes sont importants dans la notion de cache: ‘Hit’ et ‘Miss’. Lorsque l’application va chercher la donnée dans le cache et la trouve, c’est un ‘Hit’, lorsqu’elle ne la trouve pas, c’est un ‘Miss’, et il faudra aller chercher la donnée dans la base de donnée.

Le cache est donc la zone de mémoire qui contient les données les plus utilisées par l’application. Il y à ainsi des problématiques de concurrence : Plusieurs opérations peuvent s’effectuer sur la même donnée simultanément. C’est au cache de gérer les problèmes de concurrences, mais ça peut aussi être au développeur, notamment avec l’utilisation de locks.

On peut encore augmenter les performances pour certains cas d’utilisations, avec les caches distribués entre différentes instances d’une application. Chaque instance aura accès aux mêmes données grâce au cache clusteré (clustered cache).

EXERCICE 1 : Cache Aside

On a introduit ici le concept de cache aside. L’algorithme est simple: on va chercher la données, dans le cache et si elle n’y est pas, on va la chercher dans la base de données et en passant on la met dans le cache.

Ainsi la prochaine fois, elle sera disponible dans le cache.

A ne pas oublier qu’un cache a un algorithme d’éviction pour éliminer les données qui ne sont pas utilisées donc on peut y mettre les données et le cache se chargera d’y enlever les données superflues.

EXERCICE 2 : Read through

Ici la pattern introduite est d’utiliser le cache as a System Of Record. C’est à dire qu’on prend la logique pour aller chercher les données dans la base de données (Master System of Record), et on déplace cette logique dans une classe implémentant l’interface CacheEntryFactory, puis on englobe l’instance du Cache dans une instance de type SelfPopulatingCache.

Le code métier peut ainsi appeler cette nouvelle instance avec les méthodes habituelles du cache (get(), put(), remove()). Si la donnée est dans le cache, elle sera renvoyée, mais si elle n’y est pas, le cache ira charger automatiquement la donnée.

EXERCICE 3 : Write through

La pattern de l’exercice 2 permet aux couches métier de l’application d’avoir le cache comme system of record et de lui déléguer la lecture vers la base de données. Pour gérer l’écriture, on va passer par la pattern Write Through.

Pour cela, on implémente l’interface CacheWriter, et on configure le cache pour utiliser ce CacheWriter lors des opérations d’écritures. Dans l’exercice précédent on avait déplacé la logique de lecture, ici nous déplaçcons la logique d’écriture dans le cachewriter.

EXERCICE 4 : Write behind

La pattern write behind est similaire à la pattern write-through : L’application insére des données dans le cache, et le cache écrit dans la base de données. Il existe des cas où pour des raisons de performances, on veut que l’écriture vers la base de données se fasse de manière asynchrone. En effet le cache est beaucoup plus rapide que la base de données, et si il n’est pas primordial d’avoir une cohérence parfaite entre données dans la base et dans le cache, on peut utiliser cette pattern.

On peut ainsi même faire du coalescing, c’est à dire laisser la cache regrouper les opérations de manières intelligente, par exemple la séquence:

cache.put(“key1”, myObject1);

myObject1.setSomething(“something different”);
cache.put(“key1”, myObject1);

insére l’objet 2 fois dans le cache, et seule la dernière valeur est intéressante puisque c’est celle mise a jour, le cache va donc regrouper ces 2 opération en une seule et ne faire qu’une seule insertion dans la base de données, avec la dernière valeur.

EXERCICE 5/6 : Refresh Ahead

Cette pattern continue sur le principe du cache as System Of Record, on va déléguer le chargement depuis la base de données vers le cache via une classe implémentant  l’interface  net.sf.ehcache.loader.CacheLoader

La particularité ici est de définir un interval de temps au delà duquel le cache va lui même charger la donnée, avant même qu’une requête en cache n’est été faite.

Cette pattern est très utile pour parer au problème du ‘Thundering herd’. Ceci apparait lorsque un grand nombre de lecture sont faite dans le cache, simultanément pour la même donnée, pour la première fois, et que puisque la donnée n’est pas encore présente, un grand nombre d’appels simultanés vers la base de données s’exécute, et ralentit l’ensemble du système puisque la base de données doit faire face à un grand nombre d’appels concurrent, et que l’application doit attendre que la base de données réagsse afin de récupérer la donnée.

En définissant le chargement proactivement, on évite ce problème.

Cette pattern est aussi utile dans le cas où on veut éviter d’avoir des données dormantes dans le cache, et qu’on veut un rafraichissement régulier (ScheduleRefresh dans l’exercice 6).

EXERCICE 7 : Search

Le cache est un <Key, Value> Store, pour accéder aux données, on doit utiliser la clef.

Cependant, il existe des fonctionnalités avançées, comme la recherche. Vous pouvez créer des requêtes en java en utilisant un langage dédié (DSL), pour faire des recherches à la manière des requêtes SQL.

Puisque la fonctionnalité Search n’est pas nécessaire pour tout le monde, et afin d’optimiser, par défaut un cache n’est pas Searchable, il faut donc le configurer afin qu’il le soit.

Ensuite il faudra créer la requête pour récupérer les resultats, par exemple,
Query query = cache.createQuery().addCriteria(new EqualTo(“name”, name)).includeValues().includeKeys();
final Results results = query.execute();

final List<Result> all = results.all();
List<Wine> wineList = new ArrayList<Wine>();
for (Result result : all) {
wineList.add((Wine)result.getValue());
}

A noter qu’on peut faire de nombreuses différentes types de Requêtes, à voir la documentation officielle pour approfondir:

http://ehcache.org/documentation/apis/search

EXERCICE 8 : Fast Restartable Store

Les données du cache sont en mémoire, mais il est possible d’activer la persistence (sur le disque) pour qu’en cas de redémarrage, ou de crash de l’application, on puisse la redémarrer et récuperer le cache avec les mêmes données avant le redémarrage.

A ce niveau, le cache se rapproche d’un rôle de In Memory Data Store: On peut y stocker des données, redémarrer le cache en gardant ces mêmes données, et y faire des requêtes.

EXERCICE 9 : Automatic Resource Control

Parmi toutes les solutions de caches existantes, il existe deux manières de configurer la taille d’un cache:

– Par unité (count or unit based) : On définit le nombre d’élements qu’un cache peut contenir.

C’est une manière simple qui peut être utile dans certains cas. Par exemple si on veut mettre en cache la liste des 196 pays dans le monde, on peut configurer un cache ‘Pays’ pour contenir 196 élements.

Cependant, il y a une limitation: On ne sait pas combien de mémoire le cache va prendre, et ceci pourrait entrainer des ralentissements si on commence à utiliser trop de mémoire. C’est le point faible de la JVM, en utilisant trop de mémoire, le Garbage Collector peut créer de longues pauses, et bloquer totalement l’application.

– Par espace mémoire : On définit la taille en mémoire que le cache va allouer.

Par rapport à la configuration par unité, ici on sait combien de place le cache va prendre.

Le seul souci de cette configuration est que dans l’exemple précédent sur les pays, on ne peut pas être certain que tous les élements vont entrer dans la taille du cache. Aussi, on risque d’allouer trop de mémoire au cache, et de la gaspiller.

– La solution Ehcache : Allocation automatique

Comme expliqué dans mon article

https://jsoftbiz.wordpress.com/2011/08/01/ehcache-2-5-goes-beta-explanation-included/

Il est possible d’allouer un espace mémoire global à un ensemble de caches, et laisser Ehcache gérer la taille des caches suivant leur utilisation. Ceci est un bon moyen d’avoir les avantages des deux configurations précédentes… On est toujours sûr d’avoir les caches fortement sollicités utilisant un maximum de mémoire.

EXERCICE 10 : BigMemory

La mémoire Heap de la JVM est celle utilisée pour stocker les objets. Lorsqu’une application utilise plus que  quelques GO de heap, le Garbage collector ralentit celle-ci.

Pour parer à cette limitation, BigMemory utilise la mémoire qui n’est pas la heap, celle-ci est nommé off-heap (=hors heap). Comme indiqué dans cette étude, il est possible d’avoir un cache de 200Mo à 1,8To sans perte de performance

http://terracotta.org/resources/whitepapers/bigmemory-performance-results

EXERCICE 11/12 : Clustering

Augementer la taille du cache, tout comme augmenter la taille d’une application dépend des ressources de la machine. Rajouter de la mémoire, des processeurs ou de l’espace disque pour la rendre plus puissante es tle principe de scalabilité verticale : On fait ‘grandir’ la machine pour avoir une application plus ‘grosse’.

Cependant, il y a une limite du hardware, et il est nécessaire à partir d’une certaine taille de rajouter des machines dans une topologie cluster, c’est ce qu’on appelle la scalabilité horizontale : Plusieurs instances d’une même application tournent sur plusieurs machines.

Configurer un cache en mode clusteré permet de partager ce cache parmi les différentes instances de l’application.

La clustering avec Ehcache se fait en conjonction avec le serveur Terracotta. L’architecture est du type client-serveur : Chaque instance de l’application contient le cache qui est un client et se connecte sur le serveur Terracotta. Ce dernier gère la réplication des données entres les multiples instances du même cache.

Rajouter des serveurs Terracotta permet de scaler de manière horizontale grâce au support d’un nombre important de clients (généralement 1 par JVMs)

EXERCICE 13 : HA (Haute disponibilité)

EXERCICE 14 : Cache partitionné

EXERCICE 15 : Replication WAN

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s