25
Une architecture SOA a pour principe d’exposer un ensemble de services à destination des utilisateurs. Ces services peuvent éventuellement être relayés par ces mêmes utilisateurs qui peuvent les décorer de nouvelles fonctionnalités. Un des facteurs critique de ce genre d’architecture est la «scalabilité». C’est à dire que le système doit pouvoir répondre linéairement à une charge de requêtes croissante.
Pour garantir une bonne scalabilité, l’enjeu est d’optimiser les ressources matérielles (calcul, mémoire, i/o) de façon à servir toutes les requêtes.
Conséquence d’une mauvaise gestion des ressources:
Cet article a pour objectif de montrer comment optimiser les performances de notre service en donnant quelques pistes pour une bonne gestion des ressources au sein de notre architecture.
Rendez-vous aux pages suivantes pour un rappel sur les ressources matérielles: http://www.opensides.fr/?p=293
Et sur le fonctionnement d’une JVM (hotspot): http://www.opensides.fr/?p=271
Maintenant que nous maîtrisons ces bases, voyons comment ces ressources sont sollicitées aux travers d’un cas pratique.
Pour illustrer la gestion des ressources, nous allons prendre l’exemple d’un musée. Ce musée représentera notre application. Des visiteurs formeront une file d’attente à l’entrée pour acheter leur ticket. Il représenteront les requêtes utilisateurs qui attendent d’acquérir une connexion au système. Lorsqu’un visiteur achètera son ticket, un agent lui préparera un casque d’écoute. Ce casque représentera l’accès à un service externe et la préparation par l’agent, l’acquisition de cette ressource. Le visiteur pourra alors faire sa visite avec son casque.
Lançons l’application pour en observer le fonctionnement:
[0 ms] Le visiteur #1 achète un ticket [3 ms] Le guichetier récupère le casque #1 ... [1000 ms] Le guichetier initialise le casque #1 ... [2001 ms] Le visiteur #1 prend le casque le casque #1 [0 ms] Le visiteur #1 entre dans le musée [1 ms] Le visiteur #2 achète un ticket [0 ms] Le visiteur #1 commence sa visite ... [3 ms] Le guichetier récupère le casque #2 ... [1000 ms] Le guichetier initialise le casque #2 ... [2000 ms] Le visiteur #2 prend le casque le casque #2 [0 ms] Le visiteur #2 entre dans le musée [0 ms] Le visiteur #3 achète un ticket [1 ms] Le visiteur #2 commence sa visite ... [1 ms] Le guichetier récupère le casque #3 ... [1000 ms] Le guichetier initialise le casque #3 ... [2000 ms] Le visiteur #3 prend le casque le casque #3 [1 ms] Le visiteur #3 entre dans le musée [0 ms] Le visiteur #3 commence sa visite ... [0 ms] Le visiteur #4 achète un ticket [707 ms] Le visiteur #2 arrête sa viste ... [1001 ms] Le guichetier initialise le casque #79 ... [1000 ms] Le visiteur #79 prend le casque le casque #79 [0 ms] Le visiteur #79 entre dans le musée [0 ms] Le visiteur #79 commence sa visite ... [0 ms] Le visiteur #80 achète un ticket Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at fr.opensides.lab.musee1.Guichetier.accueilVisiteur(Guichetier.java:16) at fr.opensides.lab.musee1.Main.main(Main.java:11) [322 ms] Le visiteur #75 arrête sa viste [3248 ms] Le visiteur #79 arrête sa viste [3 ms] Le visiteur #77 arrête sa viste [1527 ms] Le visiteur #78 arrête sa viste
On remarque sans surprise que l’initialisation des casques est très coûteuse ( 1 seconde pour la récupération / 2 secondes pour l’initialisation) et surtout systématique. Elle pénalise donc l’ensemble de la file d’attente.
Penchons nous maintenant sur la mémoire. Pour cela, ouvrons un VisualVM et regardons la gestion de la heap. (cf. ci-dessous)

On observe que les Garbage Collector (GC) représenté sur le graph par des creux sont réguliers, ce qui est normal mais ne sont pas profond, ce qui l’est moins… Cela indique que le GC est inefficace car il ne parvient pas à récupérer de la mémoire.
Conséquences
>Le musée est saturé à 80 visiteurs et nous renvoie l'erreur ci-dessous: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at fr.opensides.lab.musee1.Guichetier.accueilVisiteur(Guichetier.java:16) at fr.opensides.lab.musee1.Main.main(Main.java:11)
Explication: qu’est ce qu’une fuite mémoire ?
C’est le pattern typique d’une fuite mémoire, la JVM ne parvient pas à relâcher les objets car ils sont référencés (en référence forte) par les visiteurs. La mémoire va donc se remplir jusqu’à saturation( palier en haut de la courbe), ce qui se traduit par le message ci-dessus.
Dans notre exemple, le gardien du musée (la JVM) interrompt le travail du guichetier pour aller chercher les casques dans le musée (Garbage Collector).
Le problème c’est que lorsqu’il trouve un casque il n’a aucun moyen de savoir si le casque est en cours d’utilisation par un visiteur. Dans le doute, il ne peut pas le récupérer car le casque appartient au visiteur (référence forte), il le laisse donc à sa place et les casques s’accumulent dans le musée.
Quels sont les objet qui encombrent le musée ? Les casques et les visiteurs. Il faut donc les faire sortir pour corriger notre fuite mémoire.
Cela nous semble logique dans un cas de la vie réel mais étrangement cela parait beaucoup moins intuitif en programmation..
Comment faire ?
A la fin de la visite, nous allons simplement demander au visiteur de :
1/ rendre leur casque:
–> en pratique on remet le casque à null
2/ de sortir du musée:
-> en pratique ils se supprime de la map « Musée » au niveau de la méthode « interrupt » du thread visteur:
getMusee().remove(this.getId());//le visiteur sort du musée
Conséquence:
La fuite mémoire est corrigée, le musée ne sature plus.
Il reste un problème de lenteur au niveau de la récupération l’initialisation des casque. Pour réduire ce temps d’attente des visiteurs, l’agent peut sortir à l’avance un ensemble de casques de son sac, les initialiser et les disposer sur un présentoir de façon à les distribuer plus rapidement.
Conséquence:
L’utilisation du présentoir permet de réduire considérablement les temps d’attentes des visteurs. Malgré le fait que les visteurs ne renden pas relement leur casque car ils sont référencés dans le cache, le cache en SoftRéférence permet de relâcher les objets lorsque la mémoire sature.
Le guichetier prend un casque sur son présentoir [0 ms] [0 ms] Le visiteur #244 prend le casque le casque #244 [0 ms] Le visiteur #243 commence sa visite ... [0 ms] Le visiteur #244 entre dans le musée [0 ms] Le visiteur #245 achète un ticket Le guichetier prend un casque sur son présentoir [0 ms] [1 ms] Le visiteur #245 prend le casque le casque #245 [0 ms] Le visiteur #244 commence sa visite ... [1 ms] Le visiteur #245 entre dans le musée [0 ms] Le visiteur #246 achète un ticket Le guichetier prend un casque sur son présentoir [0 ms] [1 ms] Le visiteur #246 prend le casque le casque #246 [0 ms] Le visiteur #245 commence sa visite ... [1 ms] Le visiteur #246 entre dans le musée [0 ms] Le visiteur #247 achète un ticket Le guichetier prend un casque sur son présentoir [0 ms] [2 ms] Le visiteur #247 prend le casque le casque #247 [1 ms] Le visiteur #246 commence sa visite ... [3 ms] Le visiteur #247 entre dans le musée [0 ms] Le visiteur #248 achète un ticket [0 ms] Le visiteur #247 commence sa visite ... [0 ms] Le visiteur #240 commence sa visite ... [51 ms] Le visiteur #238 rend dépose le casque sur le présentoir [64 ms] Le visiteur #198 rend dépose le casque sur le présentoir
>L’application tourne en continue sans saturation
Mémoire
On observe que la heap se remplie jusqu’à atteindre son seuil max. Proche du max, les SoftReference joue leur rôle en permettant à la JVM de libérer les objets au lieu d’atteindre le max (générant une OOME).
cpu
On voit que ce mécanisme à un coût en terme de GC (en bleu l’activité cpu liée au GC).
thread
Au bout de 14 minutes, 10377 threads on pu être traités par l’application. Soit autant de visiteurs on pu visiter le musée.
Remarques:
On remarque une augmentation de la heap jusqu’à se rapprocher du max. C’est la SoftReference qui fait bien son job en permettant de relacher les objets quand la mémoire sature.
Il est important de noter que ce fonctionnement a un cout. Cette fois c’est la processeur
qui travaille en déplaçant les objets de la heap vers la perm et en relaçhant les objets (cf. cpu)
Explication
Cette opération consiste à utiliser un pool (ou cache) représenté par le comptoir qui permet d’éviter de répéter les opérations coûteuses de connexion à la base de donnée. On parle dans ce cas de « cacher » les informations pour les rendre accessible plus rapidement lors des prochaines utilisations. Dans notre exemple, le présentoir aura une capacité limité à 100 casques. Ce qui implique que le guichetier devra nettoyer régulièrement son présentoir.
Il devra donc gérer les problématiques suivantes:
La technique que nous utiliserons pour implémenter cet exemple est un pool qui aura pour caractéristique de libérer les ressources les moins récemment utilisées. Ce type d’algorithme de relâchement des ressources est appellé LRU pour Last Recently Used (cf. http://fr.wikipedia.org/wiki/Algorithmes_de_remplacement_des_lignes_de_cache#LRU_.28Least_Recently_Used.29).
Il existes différents types de références selon leur degrés d’attachement (ici classés du plus fort au moins fort) (cf. http://blog.developpez.com/adiguba/p2107/java/comprendre-les-references-en-java/):
Nous avons vu que le cache en SoftReference permettait de libérer les objets lorsque la mémoire sature. Cette solution est pratique car elle évite les OOME mais n’est pas optimale puisqu’elle génére une forte activité de Garbage Collection pour libérer les ressources.
Nous avons pu constater plus haut une ofrte activité du GC pour libérer les ressources du pool. Pour éviter ce problème il suffit de retourner les objets dans le pool en demandant aux visteurs de reposer leur casque sur le présentoir à la fin de la visite.
//On remet le casque dans le pool pour éviter un overhead cpu lors de la libération des SoftReference au moment du garbage
presentoire.returnObject(this.get(key).getCasque());
Conséquence
Le résultat est assez spectaculaire
On remarque que l’activité du garbage a considérablement baissé. L’activité CPU est maintenant mobilisée sur l’application.
Niveau mémoire, on remarque des GC réguliers sans saturation.
Au bout de 10 minutes, l’application a pu gérer près de 100 000 threads (soit autant de visteurs) contre 10 000 en utilisant uniquement les SoftReference.
Ajoutons quelques options à la JVM:
-Xms1024m -Xmx1024m -Xmn512m -XX:+AggressiveOpts -XX:+UseParallelGC -XX:ParallelGCThreads=16
Et observons le résultat:
Le GC occupe à peine 1% de l’activité CPU.
Les minor GC sont très réguliers jusqu’à atteindre le seuil (ce qui declenchera un full GC)
Le nombre de threads traité passe de 100 000 à 211 124. Nous avons doublé le nombre de visiteurs dans le musée (throughput).
L’objectif de l’article était de donner quelques pistes pour améliorer les performances d’une application. Dans un premier temps, nous avons vu qu’il était important de libérer les ressources pour éviter, au mieux, les garbages coûteux, au pire, les fuites mémoires. Un mécanisme de cache s’est révélé utile pour éviter la (re)création d’objets coûteux mais pouvait potentiellement engendrer une fuite mémoire en marquant une référence à un objet. Cela peut être évité avec l’utilisation d’un cache en SoftReference qui garantit la libération des objets en cas de saturation mémoire. Nous avons vu ensuite que ce seul mécanisme n’était pas suffisant pour garantir de bonnes performance et qu’il fallait nécessairement relâcher les objets afin d’éviter un overhead cpu lié au Garbage collector. Enfin un petit tunning de la JVM permet d’améliorer nos performances (elle a permis de doublé le nombre de visiteurs dans le musée). Nous verrons dans un prochain article comment améliorer encore les performances de notre application en recrutant de nouveaux guichetiers et en ouvrant de nouveaux musées…
Bibliographie