Optimiser une application JEE : mesurer l’impact sur un cas pratique (4ème et dernière partie)

Dernière partie : l'analyse mémoire.

Nous sommes désormais proches de notre objectif d'optimiser notre application pour la rendre robuste à 1000 utilisateurs connectés simultanément.

Notre précédent article faisait état d'une montée linéaire de l'allocation de la Heap malgré un nombre important de Minor GC ne libérant pas suffisamment de mémoire.

A ce stade de l'analyse, ce type de symptômes nous fait penser à une fuite mémoire. Nous allons nous assurer dans cet article du bon fonctionnement de notre mémoire Glassfish.

Pour cela, nous allons démarrer un test à 500 utilisateurs, tout en réduisant la taille de la Heap (-Xms256m -Xmx384m)

Dans l'hypothèse où le manque de libération mémoire  serait dû à une fuite mémoire , on devrait arriver à faire crasher la JVM dans un temps réduit.

10 minutes d'injection, et le graphe ci-dessous nous montre bien l'espace alloué à la Heap qui ne se libère plus jusqu'à la taille maximum "allouable" par le serveur. Le graphe de gauche quant à lui nous montre bien l'activité élevée du Garbage Collector lorsque l'espace mémoire libre se fait de plus en plus petit (ce qui l'oblige à se déclencher très souvent).

 

La fuite mémoire est bien confirmée dans les logs serveur java.lang.OutOfMemoryError

 

A ce stade, il est nécessaire de passer par la génération d'un HeapDump pour analyser le contenu de la Heap et de découvrir les Leak Suspect. Pour cela, rien de mieux qu’Éclipse MAT (Memory Analyser).

Pour une explication sur la génération d'un HeapDump, l'utilisation de l'outil MAT et plus globalement sur le principe d'une fuite mémoire, prenez le temps de lire cette démonstration (en 2 parties) avant de continuer.

Plusieurs moyens s'offrent à nous pour générer le fameux fichier dump (.hprof), et par esprit de simplicité nous utilisons l'option HeapDump de VisualVM pour le générer à la volée durant le tir et avant le crash.

Eclipse.org

 

Lecture du fichier heapDump.hprof avec Eclipse MAT.

 

 Grâce à l'option LeakSuspect, MAT parcourt l'intégralité de l'arbre mémoire et nous identifie de suite un Leak Suspect (objet(s) Java anormalement gros ou nombreux) situé dans org.springframework.samples.petclinic.Owner

En déroulant l'arbre d'objets Java :

 La cause : un objet java.util.Arraylist de plus de 150 Mo de taille, et contenant 376 737 objets <Pets> !!

Allons faire un tour dans la classe Java <Owner> qui récupère les <Pets> de chaque propriétaire d'animaux  :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Owner extends Person {
       ....................
       private static final List CACHE_OWNER = new ArrayList();
       ....................
       ....................
       public List<Pet> getPets() {
            List<Pet> sortedPets = new ArrayList<Pet>(getPetsInternal());
            PropertyComparator.sort(sortedPets, new MutableSortDefinition("name", true, true));
 
            // Report all concerned pets issued from every "day searches" in order to get BO statistics
 
            if (sortedPets != null) {
               for (int i =0 ; i< sortedPets.size(); i++) {
                    addToCache(sortedPets.get(i));
               }
            }
 
             return Collections.unmodifiableList(sortedPets);
       }
 
       public static void addToCache(Object o) { Owner.CACHE_OWNER.add(o); }
 
       ....................
       ....................
}

On découvre l'implémentation d'un cache utilisant une ArrayList plutôt qu'une HashMap !!!

Une fois le code corrigé (et petclinic.war redéployé sur notre serveur Glassfish), nous avons lancé cette foi-ci l'ultime test à 1000 users simultanés.

Nous tenons désormais la pleine charge ! 😎 😎 😎 😎

Cependant, cette réussite n'est pas faite sans contre-partie : nous constatons une charge CPU proche des 100% (35% en moyenne côté Glassfish, et prés de 40% pour MySQL). Une activité réseau et IO Disk importante (débit à 4Mo/sec et logs  MySql)

 

Ce qui rend les temps de réponse utilisateurs très variables et dépendant de l'état de la Runqueue au moment de chaque requête (valeurs qui varient de 13 à 23 sec sur les 90 percentile.)

Conclusion finale

Nous aurons donc fini cette petite aventure par la découverte d'une fuite mémoire, dont l'origine était une unique liste Java gérant un cache d'objets.

Si vous venez à vous frotter à un comportement qui ressemble à une fuite mémoire, vous tomberez 9 fois sur 10 dans une situation d'Accumulation Point (accumulation de plusieurs objets de même nature). Eclipse MAT sait facilement identifier ce type de fuite mémoire, et pourra vous le signaler dès son 1er parcours d'arbre.

Notre audit s'arrête là,  l'objectif partiellement atteint (1000vu, avec une augmentation des temps de réponse au delà de 600vu) , et des limites atteintes par les ressources systèmes de la machine (cpu).

Il existe encore plusieurs pistes d'optimisation pour améliorer nos temps de réponse et la stabilité de notre architecture : optimisation de la base de données, cluster d'instances Glassfish en failover, mise en place d'un cache distribué type EhCache ...
Dans l'objectif de rendre notre plateforme scalable, nous vous montrerons dans nos prochains posts comment mettre en application ces axes d'amélioration.

<< Optimiser une application JEE : mesurer l'impact sur un cas pratique : partie 1
<< Optimiser une application JEE : mesurer l'impact sur un cas pratique : partie 2
<< Optimiser une application JEE : mesurer l'impact sur un cas pratique : partie 3

 

Une réponse à to “Optimiser une application JEE : mesurer l’impact sur un cas pratique (4ème et dernière partie)”

  • Jean-Christophe:

    Bonjour,

    Merci pour se partage de connaissance. Vos articles sont passionnants, j’espère que vous continuerez longtemps à nous en faire profiter.

    Cordialement,

Laisser un commentaire

Merci d'effectuer cette opération simple pour valider le commentaire *

Mots-clés
RSS Feed