Accélérer le temps de démarrage des applications Spring Boot

Introduction

Dans ce tutoriel, nous allons couvrir les différentes configurations qui peuvent aider à réduire le temps de démarrage des applications.
Tout d’abord, nous allons passer en revue les configurations propres à Spring. Ensuite, nous couvrirons les options de la machine virtuelle Java. Enfin, nous verrons comment tirer parti de GraalVM et de la compilation d’images natives pour réduire davantage le temps de démarrage.

Ajustements de Spring:

Avant de commencer, mettons en place une application de test. Nous allons utiliser Spring Boot version 2.5.4 avec Spring Web, Spring Actuator, et Spring Security comme dépendances. Dans le fichier pom.xml, nous ajouterons spring-boot-maven-plugin avec la configuration pour empaqueter notre application dans un fichier jar :

<plugin> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-maven-plugin</artifactId> 
    <version>${spring-boot.version}</version> 
    <configuration> 
        <finalName>springStartupApp</finalName> 
        <mainClass>com.yagnilabs.springStart.SpringStartApplication</mainClass> 
    </configuration> 
    <executions> 
        <execution> 
            <goals> 
                <goal>repackage</goal> 
            </goals> 
        </execution> 
    </executions> 
</plugin>

Nous exécutons notre fichier jar avec la commande standard java -jar et surveillons l’heure de démarrage de notre application :

c.b.springStart.SpringStartApplication   : Started SpringStartApplication in 3.403 seconds (JVM running for 3.961)

Comme nous pouvons le constater, notre application démarre en 3. 4 secondes environ. Nous utiliserons ce temps comme référence pour les ajustements futurs.

2.1. Lazy Initialization

Spring Framework supporte l’initialisation paresseuse. L’initialisation paresseuse signifie que Spring ne créera pas tous les beans au démarrage. De même, Spring n’injectera aucune dépendance jusqu’à ce que le bean soit nécessaire. Depuis la version 2.2 de Spring Boot, il est possible d’activer l’initialisation paresseuse en utilisant le fichier application.properties :

spring.main.lazy-initialization=true

Après avoir construit un nouveau fichier jar et l’avoir démarré comme dans l’exemple précédent, le nouveau temps de démarrage est légèrement meilleur :

 c.b.springStart.SpringStartApplication   : Started SpringStartApplication in 2.95 seconds (JVM running for 3.497)

Selon la taille de notre code de base, l’initialisation paresseuse peut entraîner une réduction significative du temps de démarrage. Cette réduction dépend du graphe de dépendances de notre application.

De plus, l’initialisation paresseuse présente des avantages pendant le développement en utilisant la fonctionnalité de redémarrage à chaud de DevTools. Un nombre accru de redémarrages avec l’initialisation paresseuse permettra à la JVM de mieux optimiser le code.

Cependant, l’initialisation paresseuse présente quelques inconvénients. L’inconvénient le plus important est que l’application servira la première requête plus lentement. Comme Spring a besoin de temps pour initialiser les beans nécessaires, un autre inconvénient est que nous pouvons manquer certaines erreurs au démarrage. Cela peut entraîner une ClassNotFoundException pendant l’exécution.

2.2. Exclusion de l’autoconfiguration inutile

Spring Boot a toujours privilégié la convention plutôt que la configuration. Spring peut initialiser des beans non nécessaires à notre application. Nous pouvons vérifier tous les beans auto configurés en utilisant les journaux de démarrage. Il suffit de définir le niveau de journalisation à DEBUG sur org.springframework.boot.autoconfigure dans le fichier application.properties :

logging.level.org.springframework.boot.autoconfigure=DEBUG

Dans les journaux, nous verrons de nouvelles lignes dédiées à l’auto configuration, commençant par :

============================
CONDITIONS EVALUATION REPORT
============================

En utilisant ce rapport, nous pouvons exclure des parties de la configuration de l’application. Pour exclure une partie de la configuration, nous utilisons l’annotation @EnableAutoConfiguration :

@EnableAutoConfiguration(exclude = {JacksonAutoConfiguration.class, JvmMetricsAutoConfiguration.class, 
  LogbackMetricsAutoConfiguration.class, MetricsAutoConfiguration.class})

Si nous excluions la bibliothèque JSON de Jackson et certaines configurations de métriques que nous n’utilisons pas, nous pourrions gagner du temps au démarrage :

c.b.springStart.SpringStartApplication   : Started SpringStartApplication in 3.183 seconds (JVM running for 3.732)

2.3. Autres petites améliorations

Spring Boot est livré avec un conteneur de servlets intégré. Par défaut, nous avons Tomcat. Même si Tomcat est tout à fait suffisant dans la plupart des cas, d’autres conteneurs de servlets peuvent être plus compétitifs. Dans les tests, Undertow de JBoss est plus performant que Tomcat ou Jetty. Il nécessite moins de mémoire et a un meilleur temps de réponse moyen. Pour passer à Undertow, nous devons modifier pom.xml :

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

L’amélioration mineure suivante peut se faire dans l’analyse du classpath. L’analyse du classpath de Spring est une action rapide. Nous pouvons améliorer le temps de démarrage en créant un index statique lorsque nous avons un codebase volumineux. Nous devons ajouter une dépendance à spring-context-indexer pour générer l’index. Spring ne nécessite aucune configuration supplémentaire. Lors de la compilation, Spring va créer un fichier supplémentaire dans META-INF\spring.components. Spring l’utilisera automatiquement lors du démarrage :

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-indexer</artifactId>
    <version>${spring.version}</version>
    <optional>true</optional>
</dependency>

Comme nous n’avons qu’un seul composant Spring, cette modification n’a pas donné de résultats significatifs dans nos tests.

Ensuite, il existe plusieurs endroits valables pour les fichiers application.properties (ou .yml). Les plus habituels sont à la racine du classpath ou dans le même dossier que le fichier jar. Nous pouvons éviter de rechercher plusieurs emplacements en définissant un chemin explicite avec le paramètre spring.config.location et gagner quelques millisecondes sur la recherche :

java -jar .\target\springStartupApp.jar --spring.config.location=classpath:/application.properties

Enfin, Spring Boot propose quelques MBeans pour surveiller notre application à l’aide de JMX. Désactivez entièrement JMX et évitez le coût de création de ces beans :

spring.jmx.enabled=false

Ajustements de la JVM

3.1. Le paramètre Verify

Ce paramètre définit le mode de vérification du bytecode. La vérification du bytecode permet de savoir si les classes sont correctement formatées et si elles respectent les contraintes des spécifications de la JVM. Nous mettons cet indicateur sur la JVM lors du démarrage.

Il existe plusieurs options pour cet indicateur :

  • -Xverify est la valeur par défaut et active la vérification sur toutes les classes non-bootloader. 
  • -Xverify:all permet la vérification de toutes les classes. Cette configuration aura un impact négatif significatif sur les performances des démarrages.
  • -Xverify:none (or -Xnoverify). Cette option désactive complètement la vérification et réduit considérablement le temps de démarrage.

Nous pouvons passer ce paramètre au démarrage :

java -jar -noverify .\target\springStartupApp.jar 

Nous recevrons un avertissement de la JVM indiquant que cette option est obsolète. De plus, le temps de démarrage diminuera :

 c.b.springStart.SpringStartApplication   : Started SpringStartApplication in 3.193 seconds (JVM running for 3.686)

Ce paramètre présente un inconvénient de taille. Notre application peut s’arrêter durant l’exécution avec une erreur que nous aurions pu attraper plus tôt. C’est l’une des raisons pour lesquelles cette option est considérée comme obsolète dans Java 13, et disparaîtra dans les prochaines versions.

3.2. Le paramètre TieredCompilation

Java 7 a introduit la compilation en plusieurs niveaux. Le compilateur HotSpot utilisera plusieurs niveaux de compilation pour le code.

Comme nous le savons, le code Java est d’abord interprété en bytecode. Ensuite, le bytecode est compilé en code machine. Cette traduction se fait au niveau de la méthode. Le compilateur C1 compile une méthode après un certain nombre d’appels. Après un nombre encore plus important d’exécutions, le compilateur C2 la compile, ce qui augmente encore les performances.

En utilisant l’indicateur -XX:-TieredCompilation, nous pouvons désactiver les niveaux de compilation intermédiaires. Cela signifie que nos méthodes seront interprétées ou compilées avec le compilateur C2 pour une optimisation maximale. Cela n’entraînera pas une diminution de la vitesse de démarrage. Ce dont nous avons besoin, c’est de désactiver la compilation C2. Nous pouvons le faire avec l’option -XX:TieredStopAtLevel=1. En conjonction avec le drapeau -noverify, cela peut réduire le temps de démarrage. Malheureusement, cela ralentira le compilateur JIT lors des étapes ultérieures.

Le drapeau TieredCompilation seul apporte une solide amélioration :

 c.b.springStart.SpringStartApplication   : Started SpringStartApplication in 2.754 seconds (JVM running for 3.172)

Pour un coup de pouce supplémentaire, l’utilisation conjointe des deux drapeaux de cette section réduit encore davantage le temps de démarrage :

 java -jar -XX:TieredStopAtLevel=1 -noverify .\target\springStartupApp.jar
c.b.springStart.SpringStartApplication : Started SpringStartApplication in 2.537 seconds (JVM running for 2.912)

Spring Native

Une image native est un code Java compilé à l’aide d’un compilateur et packagé dans un fichier exécutable. Il n’a pas besoin de Java pour s’exécuter. Le programme créé est plus rapide et moins gourmand en termes de mémoire car il ne nécessite pas de JVM. Le projet GraalVM a introduit des images natives et des outils de construction appropriés.

Conclusion

Dans cet article, nous avons exploré différentes façons d’améliorer le temps de démarrage des applications Spring Boot. Tout d’abord, nous avons couvert diverses fonctionnalités liées à Spring qui peuvent aider à réduire le temps de démarrage. Ensuite, nous avons présenté les options spécifiques à la JVM. Enfin, nous avons présenté Spring Native et la création d’images natives.

Laisser un commentaire