Java et les microservices
Cet article est le premier d’une série abordant les concepts de la nouvelle machine virtuelle GraalVM.
Nous allons aborder ici des notions de bases sur les microservices, sur Java et sa JVM HotSpot. Cela nous donnera une première idée sur l’adéquation de Java au monde des conteneurs et notammement des microservices.
Qu’est-ce qu’un microservice ?
On peut, pour les plus vieux d’entre nous, considérer les microservices comme le prolongement du concept de SOA
(Architecture Orientée Service) avec l’avènement du cloud.
Un microservice
peut se caractériser comme étant :
- Un service conçu pour gérer une seule fonctionnalité (par exemple, la gestion des utilisateurs)
- Elastique, c’est à dire facilement scalable. Ceci implique un déploiement rapide, un service léger, tout cela, dans un conteneur, voire dans le Cloud.
- Automatisé, du processus de build jusqu’au déploiement. Généralement, il est maintenu par une équipe dédiée et réduite, avec des tests automatisés pour déployer rapidement une nouvelle version.
Microservice vs FaaS
FaaS
, pour Functions-as-a-Service, va un peu plus loin dans ce concept, en ajoutant les notions de « serverless » (l’infrastructure nécessaire au service est de la responsabilité du fournisseur) et « sans état » (cher à la programmation fonctionnelle, x -> f(x), toujours !)
Qu’est-ce que Java ?
Tout le monde connait Java ! Un langage interprété par une JVM et portable sur différents systèmes. Révisons tout de même quelques notions sur son fonctionnement interne pour bien en saisir la portée.
Le fonctionnement de la JVM
L’interprétation
- La JVM est un exécutable qui lit du bytecode puis l’interprète.
- Le bytecode est dans des packages .jar sous forme de fichiers .class
- La JVM recherche le fichier .class dans les packages .jar, vérifie le fichier .class puis le charge
- Une fois que le bytecode est chargé, la JVM peut l’exécuter (le semi-interpréter)
Exécuter du bytecode a donc un coût :
- Le bytecode est recherché, vérifié puis interprété par la JVM qui elle-même s’exécute sur le processeur.
Le compilateur Just-In-Time (JIT)
1.  Lors de l’exécution d’une méthode Java, le compilateur C1
de JIT (just-in-time, à la volée) va la compiler en code natif et le Profiler
va commencer Ă recueillir des informations sur son utilisation.
C1 est un compilateur léger et rapide mais il ne produit pas du code natif optimisé.
2.  Losrque le profiler détecte une méthode très utilisée, « Hot », le compilateur C2
va se servir des informations du Profiler pour produire un code natif, agressif, optimisé et très bien adapté au contexte d’utilisation.
C2 est un compilateur lourd et lent mais il produit un code natif très bien optimisé et très rapide.
Il y a en réalité un cycle entre la compilation C1 et C2. Le compilateur C2 va souvent recompiler des morceaux de bytecode avec de nouvelles informations provenant du profiler pour produire un binaire toujours plus optimal.
3.  Au bout d’un certain temps, lorsque de nombreux morceaux de bytecode auront été compilés par le compilateur C2, l’application Java fonctionnera très rapidement.
- Il faut donc un temps de chauffe, « warm-up », à une application Java pour être pleinement réactive.
- C’est un réel problème pour un microservice qui doit pouvoir être déployé et opérationnel très rapidement.
L’empreinte mémoire
Architecture générale de la JVM
Lorsque l’on regarde l’architecture générale d’une JVM, on ne peut que constater qu’il y a beaucoup de composants. On voit aussi que son espace mémoire est compartimenté.
Concentrons-nous sur 2 d’entre eux.
Détail de 2 espaces mémoires
La JVM alloue de la mémoire pour l’application mais aussi pour ses propres métadonnées et son fonctionnement :
Le « Java Heap » stocke les instances des objets Java. Il est divisé en 2 parties : le « Young Generation » qui contient les objets récemment créés et le « Tenured Generation » qui contient lui des objets qui ont résisté au ramasse-miettes (« Garbage Collector »).
Le « Metaspace » (anciennement «PermGen») contient les métadonnées des classes (le bytecode des méthodes, les symboles, les «constant pools», les annotations…).
- Pour une application de 10Mo, la JVM occupe souvent une taille de 100Mo.
- Là encore, c’est un problème pour un microservice qui doit avoir une empreinte mémoire la plus petite possible.
Le fonctionnement des Frameworks Java
Il est, à present, usuel d’embarquer plusieurs frameworks dans une application Java afin de simplifier certains aspects techniques ou bien d’organiser ses couches applicatives.
Prenons le cas de 2 frameworks, sans doute, les plus utilisés dans le monde Java : Spring et Hibernate.
Voici ce qu’une application Java qui utilise ces frameworks, va exécuter à son démarrage :
- Lecture et parsing des fichiers de configuration,
- Scanne complet des classes pour récupérer les métadonnées (annotations, accesseurs,…),
- Création d’un métamodèle,
- Préparation de la réflexion,
- Création des proxies (beaucoup de proxies !),…
Ce sont pourtant des frameworks très utilisés par les développeurs et, en réalité, très bien adaptés aux applications monolithiques.
- Les frameworks Java amplifient les problèmes de temps de démarrage et de consommation mémoire de la JVM.
On fait comment à présent ?
Pour résumer
Nous avons vu les problèmes de Java :
- Consommation importante de la mémoire
- Nécessité d’un temps de chauffe au démarrage
- Optimisation du code natif au fil de l’eau
A priori, tout ce qu’il ne faut pas pour un microservice.
Alors, que fait-on à présent ? On oublie Java et on se met tous au C++ ??
Rien de tout cela bien sûr. La réponse dans l’article suivant présentant GraalVM. Et vous allez voir que ça déménage !
Cheers…