Source code of my website
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

✏️ : correct typos

+139 -128
+139 -128
content/posts/2024-02-05-tomcat-11-virtual-threads/index.md
··· 1 1 --- 2 2 created: "2024-02-05" 3 + modified: "2024-02-15" 3 4 date: "2024-02-05" 4 5 language: fr 5 - draft: true 6 6 tags: 7 7 - Java 8 - title: Tomcat 11 & Virtual Threads 8 + - Tomcat 9 + title: Tomcat 11 & Virtual Threads 🧵 9 10 --- 10 11 11 12 ![](tomcat.jpg) 12 13 13 - _Apache Tomcat_ est le plus célèbre des conteneurs de _Servlets_ Java. 14 - Les versions se succèdent au fil des années. Avec _Spring Boot_, et son utilisation de la version "embedded", son usage en tant que serveur "installé" a diminué, mais il reste encore au coeur de la majorité de nos micro-services, parfois sans que les développeurs ne s'en rendent compte. 14 + Apache Tomcat est le plus célèbre des conteneurs de Servlets Java. 15 + Les versions se succèdent au fil des années. Avec Spring Boot, et son utilisation de la version &laquo;embedded&raquo;, son usage en tant que serveur &laquo;installé&raquo; a diminué, mais il reste encore au cœur de la majorité de nos micro-services, parfois sans que les développeurs s'en rendent compte. 15 16 16 - Chaque version majeure de _Tomcat_ apporte le support des nouvelles versions des API 'Java EE' ou 'JEE'. 17 + Chaque version majeure de Tomcat apporte le support des nouvelles versions des API `Java EE` ou `JEE`. 17 18 18 - La version ayant eu le plus d'impact sur les développeurs est la version 10, qui a intégré le support des api `jakarta`, en remplacement des anciennes API `javax`, Cette version 10 de _Tomcat_ était liée à _Java&nbsp;11_, dans laquelle suppression des packages `javax` liés à _Java EE_ a eu lieu. Les modules supprimés sont documentés dans la [JEP&nbsp;320](https://openjdk.org/jeps/320). on y retrouve les tristement célèbres `java.xml.bind`, `javax.transcation` et `javax.activation`, qui ont donné du fil à retordre aux développeurs souhaitant migrer leurs applications. 19 + La version ayant eu le plus d'impact sur les développeurs est la version 10, qui a intégré le support des API `jakarta`, en remplacement des anciennes API `javax`. Cette version 10 de Tomcat était liée à Java&nbsp;11, dans laquelle la suppression des packages `javax` liés à Java&nbsp;EE a eu lieu. Les modules supprimés sont documentés dans la [JEP&nbsp;320](https://openjdk.org/jeps/320). On y retrouve les tristement célèbres `java.xml.bind`, `javax.transaction` et `javax.activation`, qui ont donné du fil à retordre aux développeurs souhaitant migrer leurs applications. 19 20 20 - Les versions de _Tomcat_ sont donc à chaque fois compatibles avec une version minimale de Java, et des API `jakarta`. 21 + Les versions de Tomcat sont donc à chaque fois compatibles avec une version minimale de Java, et des API `jakarta`. 21 22 Le tableau ci-dessous reprend la liste des versions compatibles&nbsp;: 22 23 23 24 | **Servlet Spec** | **Apache Tomcat Version** | **Supported Java Versions** | **Release date** | ··· 28 29 | 3.1 | 8.5.x | 7 and later | jan. 2014 | 29 30 | 3.0 | 7.0.x (archived) | 6 and later | jan. 2011 | 30 31 31 - La version 11 de _Tomcat_ est donc dédiée à la version 21 de Java. 32 - Cette stratégie n'est pas surprenante en soit, la version 21 étant la dernière version LTS en date. 32 + La version 11 de Tomcat est donc destinée à la version 21 de Java. 33 + Cette stratégie n'est pas surprenante en soi, la version 21 étant la dernière version LTS à date. 33 34 34 - Même si _Tomcat&nbsp;11_ n'est pas encore en version finale, les travaux pour son développement durent depuis déjà plus d'un an à l'écriture de ces lignes. La première version _milestone_ de _Tomcat&nbsp;11_ a été publiée en décembre 2022&nbsp;! La première version était prévue pour supporter Java 11 (cf la [release note Tomcat 11.0.0-M1](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M1/RELEASE-NOTES)). La version Java 17 a ensuite été choisie à partir de la _milestone_ 3 de _Tomcat 11_ (cf la [release note Tomcat 11.0.0-M3](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M3/RELEASE-NOTES)). 35 - La version 21 a été choisie à partir de la _milestone_ 7 de _Tomcat 11_, publiée en Juin 2023, soit 3 mois avant la sortie de Java 21 (cf la [release note Tomcat 11.0.0-M7](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M7/RELEASE-NOTES)). 35 + Même si Tomcat&nbsp;11 n'est pas encore en version finale, les travaux pour son développement durent depuis déjà plus d'un an à l'écriture de ces lignes. La première version _milestone_ de Tomcat&nbsp;11 a été publiée en décembre 2022&nbsp;! 36 + La première version était prévue pour supporter Java&nbsp;11 (cf. la [_release note_ Tomcat 11.0.0-M1](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M1/RELEASE-NOTES)). 37 + La version Java&nbsp;17 a ensuite été choisie à partir de la _milestone_&nbsp;3 de Tomcat&nbsp;11 (cf. la [_release note_ Tomcat 11.0.0-M3](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M3/RELEASE-NOTES)). 38 + La version 21 a été choisie à partir de la _milestone_&nbsp;7 de Tomcat&nbsp;11, publiée en juin&nbsp;2023, soit 3&nbsp;mois avant la sortie de Java&nbsp;21 (cf. la [_release note_ Tomcat 11.0.0-M7](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M7/RELEASE-NOTES)). 36 39 37 - La version actuelle est la _milestone_ 16, publiée le 9 janvier 2024. C'est cette version qui sera testée dans cet article. 40 + La version actuelle est la _milestone_&nbsp;16, publiée le 9&nbsp;janvier&nbsp;2024. C'est cette version qui sera testée dans cet article. 38 41 39 - Un des principaux avantages de cette version 11, liée à Java 21 est le support des _Virtual Threads_. Bien que le code nécessaire a été ajouté à Tomcat en version 10.1, on peut considérer que le support n'était qu'expérimental, puisque les _Virtual Threads_ n'ont été intégrés qu'en version _preview_ à partir de Java 19, et en version finale en Java 21. 42 + Un des principaux avantages de cette version 11, avec le support de Java&nbsp;21, est le support des _Virtual Threads_. Bien que le code nécessaire ait été ajouté à Tomcat en version&nbsp;10.1, on peut considérer que le support n'était qu'expérimental, puisque les _Virtual Threads_ n'ont été intégrés qu'en version _preview_ à partir de Java&nbsp;19, et en version finale en Java&nbsp;21. 40 43 41 44 ## C'est quoi les _Virtual Threads_&nbsp;? 42 45 43 - Avant d'explorer l'implémentation de _Tomcat_ et son usage des _Virtual Threads_, un rapide rappel de ce qu'ils sont et de comment ils fonctionnent. 46 + Avant d'explorer l'implémentation de Tomcat et son usage des _Virtual Threads_, un rapide rappel de ce qu'ils sont et de la manière dont ils fonctionnent. 44 47 45 - Les _Virtual Threads_ sont des _Thread_ dits "légers", parfois appelés "Green Threads" ou "Routines/Coroutines" dans d'autres langages. Ils sont mis en opposition aux _Threads_ dits "Plateforme". Les _Threads_ plateforme sont des _Threads_ gérés directement par le système d'exploitation. 48 + Les _Virtual Threads_ sont des _Thread_ dits &laquo;légers&raquo;, parfois appelés &laquo;Green Threads&raquo; ou &laquo;Routines/Coroutines&raquo; dans d'autres langages. Ils sont mis en opposition aux _Threads_ dits &laquo;Plateforme&raquo;. Les _Threads_ plateforme sont des _Threads_ gérés directement par le système d'exploitation. 46 49 47 - > Une excellente conférence de José Paumard sur le projet Loom, qui introduit les _Threads Virtuels_ en Java est visible sur [Youtube](https://www.youtube.com/watch?v=v7DzKOniNh0). Cette vidéo est une très bonne introduction à ce sujet. 50 + > Une excellente conférence de José Paumard sur le projet Loom, qui introduit les _Virtual Threads_ en Java, est visible sur [Youtube](https://www.youtube.com/watch?v=v7DzKOniNh0). Cette vidéo est une très bonne introduction à ce sujet. 48 51 49 - ### Les Threads _Plateforme_ 52 + ### Les _Threads_ Plateforme 50 53 51 - Lorsqu'un programme demande la création d'un _Thread_, le système d'exploitation stoppe l'exécution du code et crée le _Thread_, avec sa mémoire dédiée, appellée la _Stack_. Il redonne ensuite la main au programme pourqu'il continue son exécution. 54 + Lorsqu'un programme demande la création d'un _Thread_, le système d'exploitation stoppe l'exécution du code et crée le _Thread_, avec sa mémoire attribuée, appelée la _Stack_. Il redonne ensuite la main au programme pour qu'il continue son exécution. 52 55 Ces deux étapes impliquent, à chaque fois, que le CPU sauvegarde l'état courant de l'exécution du programme, et le restaure ensuite. C'est ce qu'on appelle un _context switch_, un changement de contexte d'exécution. 53 56 54 - Lors de la création d'un _Thread_, le système d'exploitation doit donc effectuer plusieurs _context switchs_, et allouer un peu de mémoire au _Thread_. Ces étapes ont donc un coût, en temps et en mémoire. 57 + Lors de la création d'un _Thread_, le système d'exploitation doit donc effectuer plusieurs _context switches_, et allouer un peu de mémoire au _Thread_. Ces étapes ont donc un coût, en temps et en mémoire. 55 58 56 59 #### Le coût en temps 57 60 58 61 Le temps de création d'un _Thread_ dépend principalement du système d'exploitation et de sa charge actuelle. 59 - Pour mesurer ce temps, un benchmark écrit avec l'outil [JMH](https://github.com/openjdk/jmh) (dont l'utilisation vaudrait un article à elle seule) permet d'estimer le temps de démarrage d'un _Thread_ Java sur une machine&nbsp;: 62 + Pour mesurer ce temps, un _benchmark_ écrit avec l'outil [JMH](https://github.com/openjdk/jmh) (dont l'utilisation vaudrait un article à elle seule) permet d'estimer le temps de démarrage d'un _Thread_ Java sur une machine&nbsp;: 60 63 61 64 ```java 62 65 @BenchmarkMode(Mode.AverageTime) ··· 71 74 72 75 @Benchmark 73 76 public void computeInPlatformThread() throws InterruptedException { 74 - // exécution dans un thread plateforme dédié 77 + // exécution dans un thread plateforme 75 78 var thread = Thread.ofPlatform().start(() -> { 76 79 Blackhole.consumeCPU(1024); 77 80 }); ··· 81 84 public static void main(String[] args) throws RunnerException { 82 85 Options opt = new OptionsBuilder() 83 86 .include(ThreadsBenchmark.class.getSimpleName()) 84 - .warmupIterations(1) // une itération de pré-chauffage de la JVM 87 + .warmupIterations(1) // une itération de préchauffage de la JVM 85 88 .measurementIterations(3) // 3 itérations de mesure 86 89 .forks(1) 87 90 .build(); ··· 92 95 } 93 96 ``` 94 97 95 - Les deux méthodes annotées `@Benchmark` sont exécutées en boucle pendant 10 secondes pour mesurer le temps moyen de leur exécution, et cela 4 fois en tout, une première fois pour pré-chauffer la JVM (warmup), et 3 fois pour mesurer les performances réelles. Le `fork(1)` permet de créer une JVM dédiée à l'exécution des tests. 98 + Les deux méthodes annotées `@Benchmark` sont exécutées en boucle pendant 10&nbsp;secondes pour mesurer le temps moyen de leur exécution, et cela 4&nbsp;fois en tout&nbsp;: une première fois pour préchauffer la JVM (_warm-up_), et 3&nbsp;fois pour mesurer les performances réelles. La ligne `forks(1)` permet de préciser de créer une JVM destinée à l'exécution des tests. 96 99 97 - La première méthode effectue un calcul au combien inutile, à travers la classe `Blackhole` fournie par JMH. La seconde méthode effectue ce même calcul, mais dans un _Thread_ plateforme dédié et attend la fin son exécution. De cette manière, on peut extrapoler le surcoût de l'exécution de la tâche dans un _Thread_, surcoût qui comprend donc la création du _Thread_, et sa suppression. 100 + La première méthode effectue un calcul ô combien inutile, à travers la classe `Blackhole` fournie par JMH. La seconde méthode effectue ce même calcul, mais dans un _Thread_ plateforme et attend la fin de son exécution. De cette manière, on peut extrapoler le surcoût de l'exécution de la tâche dans un _Thread_, surcoût qui comprend donc la création du _Thread_, et sa suppression. 98 101 99 - Le résultat de l'exécution du benchmark est le suivant&nbsp;: 102 + Le résultat de l'exécution du _benchmark_ est le suivant&nbsp;: 100 103 101 104 ```bash 102 105 Benchmark Mode Cnt Score Error Units ··· 104 107 ThreadsBenchmark.computeInPlatformThread avgt 3 0.038 ± 0.015 ms/op 105 108 ``` 106 109 107 - On observe que le `Blackhole.consumeCPU(1024)` s'exécute en moyenne en 0.002 millisecondes. L'exécution de la même instruction dans un _Thread_ dédié se fait en 0.038 millisecondes. Le surcoût lié à la création et destruction du _Thread_ est donc de 0.036 millisecondes. 110 + On observe que le `Blackhole.consumeCPU(1024)` du premier _benchmark_ s'exécute en moyenne en 0,002&nbsp;millisecondes. L'exécution de la même instruction dans un _Thread_ plateforme se fait en 0,038&nbsp;millisecondes. Le surcoût lié à la création et destruction du _Thread_ est donc de 0,036&nbsp;millisecondes. 108 111 109 - > Créer un Thread pour effectuer un calcul peut donc être contre-productif&nbsp;! 112 + > Créer un _Thread_ pour effectuer un calcul peut donc être contre-productif&nbsp;! 😱 110 113 111 114 #### Le coût en mémoire 112 115 113 - Le coût en mémoire d'un _Thread_ est connu à l'avance et contrôlé par les paramètres `-Xss` ou `-XX:ThreadStackSize` de la JVM. Cependant, attention aux confusions. On parle bien ici de mémoire réservée, et non pas de mémoire effectivement utilisée. Pour un Thread qui ne remplit pas sa _stack_, sa consommation réelle sera bien moindre. 116 + Le coût en mémoire d'un _Thread_ est connu à l'avance et contrôlé par les paramètres `-Xss` ou `-XX:ThreadStackSize` de la JVM. Cependant, attention aux confusions. On parle bien ici de mémoire réservée, et non pas de mémoire effectivement utilisée. Pour un_Thread_qui ne remplit pas sa _Stack_, sa consommation réelle sera bien moindre. 114 117 115 118 La commande suivante permet de constater les valeurs par défaut de la mémoire d'un _Thread_ Java&nbsp;: 116 119 ··· 122 125 intx VMThreadStackSize = 1024 {pd product} {default} 123 126 ``` 124 127 125 - La valeur est exprimé en kilo-octets. Un _Thread_ réservera donc 1024&nbsp;ko de RAM, soit 1&nbsp;Mo. 200&nbsp;_Threads_ réserveront donc 200&nbsp;Mo de RAM native, en plus de la RAM allouée à la _heap_ Java. 128 + La valeur est exprimée en kilo-octets. Un _Thread_ réservera donc 1&nbsp;024&nbsp;ko de RAM, soit 1&nbsp;Mo. 200&nbsp;_Threads_ réserveront donc 200&nbsp;Mo de RAM native, en plus de la RAM allouée à la _heap_ Java. 126 129 127 - ### Les Threads _Virtuels_ 130 + ### Les _Virtual Threads_ 128 131 129 - Les _Threads Virtuels_ sont créés, orchestrés et exécutés directement par la JVM, qui se charge de gérer leur stack et leur exécution de manière interne. La création d'un _Thread Virtuel_ n'implique donc pas forcément la création d'un _Thread_ plateforme. 130 - Le coût de création d'un _Thread Virtuel_ est donc bien inférieur à un _Thread_ plateforme, puisqu'il ne nécessite pas de _context switch_, ni d'allocation d'un bloc de mémoire dédié. 132 + Les _Virtual Threads_ sont créés, orchestrés et exécutés directement par la JVM, qui se charge de gérer leur _Stack_ et leur exécution de manière interne. La création d'un _Virtual Thread_ n'implique donc pas forcément la création d'un _Thread_ plateforme. 133 + Le coût de création d'un _Virtual Thread_ est donc bien inférieur au coût d'un _Thread_ plateforme, puisqu'il ne nécessite pas de _context switch_, ni d'allocation d'un bloc de mémoire. 131 134 132 - On peut mesurer le coût temporel de la création d'un _Thread Virtuel_ en ajoutant cette méthode à notre benchmark précédent&nbsp;: 135 + On peut mesurer le coût temporel de la création d'un _Virtual Thread_ en ajoutant cette méthode à notre _benchmark_ précédent&nbsp;: 133 136 134 137 ```java 135 138 @Benchmark ··· 141 144 } 142 145 ``` 143 146 144 - Notez l'usage de `Thread.ofVirtual()` pour créer un _Thread Virtuel_ en lieu et place du `Thread.ofPlatform()`. 147 + Notez l'usage de `Thread.ofVirtual()` pour créer un _Virtual Thread_ en lieu et place du `Thread.ofPlatform()`. 145 148 146 149 Les durées d'exécution observées sont les suivantes&nbsp;: 147 150 ··· 152 155 ThreadsBenchmark.computeInVirtualThread avgt 3 0.005 ± 0.002 ms/op 153 156 ``` 154 157 155 - Le benchmark utilisant les _Threads Virtuels_ présente un surcoût d'exécution de 0.002 millisecondes par rapport à l'exécution dans le _Thread_ principal, mais est largement inférieur au surcoût lié à l'exécution dans un _Thread Plateforme_. 158 + Le _benchmark_ utilisant les _Virtual Threads_ présente un surcoût d'exécution de 0,003&nbsp;millisecondes par rapport à l'exécution dans le _Thread_ principal, mais est largement inférieur au surcoût lié à l'exécution dans un _Thread_ plateforme. 156 159 157 - > Le coût d'exécution en temps d'un _Thread Virtuel_ est donc 15 fois inférieur à un Thread plateforme. 160 + > Le coût de création en temps d'un _Virtual Thread_ est donc 15&nbsp;fois inférieur à un _Thread_ plateforme. 158 161 159 - Notes qu'avant l'avènement des _Threads Virtuels_, le problème du coût de création des _Threads Plateforme_ était souvent adressé par l'utilisation de pools de _Threads_, qui permettent de ré-utiliser des _Threads_ existants (vive le recyclage ♻️),plutôt que de les re-créer. 162 + Notez qu'avant l'avènement des _Virtual Threads_, le problème du coût de création des _Threads_ plateforme était souvent adressé par l'utilisation de _pools_ de _Threads_, qui permettent de réutiliser des _Threads_ existants (vive le recyclage ♻️), plutôt que de les recréer. 160 163 161 164 ## L'implémentation de Tomcat 162 165 163 166 ![](tomcat-executors.png) 164 167 165 - Dans le code de _Tomcat_, l'interface `Executor` décrit les objets qui ont pour responsabilité d'exécuter les requêtes entrantes. Depuis la version 10.1 de _Tomcat_, cette interface a deux implémentations. L'implémentation historique `StandardThreadExecutor`, qui s'appuie sur un pool de _Threads_ _workers_ et une `BlockingQueue` de taille fixe pour les requêtes entrantes, et la nouvelle implémentation `StandardVirtualThreadExecutor` qui utilise un _Thread Virtuel_ pour exécuter chaque requête entrante. 168 + Dans le code de Tomcat, l'interface `Executor` décrit les objets qui ont pour responsabilité d'exécuter les requêtes entrantes. Depuis la version 10.1 de Tomcat, cette interface a deux implémentations. L'implémentation historique `StandardThreadExecutor`, qui s'appuie sur un _pool_ de _Threads_ _workers_ et une `BlockingQueue` de taille fixe pour les requêtes entrantes, et la nouvelle implémentation `StandardVirtualThreadExecutor` qui utilise un _Virtual Thread_ pour exécuter chaque requête entrante. 166 169 167 - En farfouillant dans le code de Tomcat, on peut observer cette implémentation dans la classe `VirtualThreadExecutor`, qui est utilisée par le `StandardVirtualThreadExecutor`&nbsp;: 170 + En fouillant dans le code de Tomcat, on peut observer cette implémentation dans la classe `VirtualThreadExecutor`, qui est utilisée par le `StandardVirtualThreadExecutor`&nbsp;: 168 171 169 172 ```java 170 173 public class VirtualThreadExecutor extends AbstractExecutorService { ··· 185 188 } 186 189 ``` 187 190 188 - > Il est par ailleurs surprenant que Tomcat aie choisi de développer son propre `ExecutorService`, au lieu d'utiliser celui construit par `Executors.newVirtualThreadPerTaskExecutor()`. Il semble que ce choix soit lié à la gestion de l'arrêt de l'`ExecutorService` qui est implémentée du côté du `ThreadPoolExecutor`. 191 + > Il est par ailleurs surprenant que Tomcat ait choisi de développer son propre `ExecutorService`, au lieu d'utiliser celui construit par `Executors.newVirtualThreadPerTaskExecutor()`. Il semble que ce choix soit lié à la gestion de l'arrêt de l'`ExecutorService` qui est implémentée du côté du `ThreadPoolExecutor`. 189 192 190 - ## Le benchmark 193 + ## Le _benchmark_ 191 194 192 195 Dans cette section, nous allons tester les performances de deux versions de Tomcat&nbsp;: 193 196 194 - - la version 10.1, sans support des _Threads Virtuels_ 195 - - la version 11.0.0-M16, avec support des _Threads Virtuels_ activés 197 + - la version&nbsp;10.1, sans support des _Virtual Threads_&nbsp;; 198 + - la version&nbsp;11.0.0-M16, avec support des _Virtual Threads_ activés. 196 199 197 - Pour monter l'environnement de test, j'ai installé une version 21 de Java, en particulier le build _eclipse-temurin_ disponible chez [adoptium.net](https://adoptium.net/temurin/releases/?version=21)&nbsp;: 200 + Pour monter l'environnement de test, j'ai installé une version 21 de Java, en particulier le _build_ _eclipse-temurin_ disponible chez [adoptium.net](https://adoptium.net/temurin/releases/?version=21)&nbsp;: 198 201 199 202 ```bash 200 203 java --version ··· 203 206 OpenJDK 64-Bit Server VM Temurin-21.0.1+12 (build 21.0.1+12-LTS, mixed mode, sharing) 204 207 ``` 205 208 206 - J'ai aussi installé les version 10 et 11 de Tomcat&nbsp;: 209 + J'ai aussi installé les versions 10 et 11 de Tomcat&nbsp;: 207 210 208 - - la dernière version disponible de [Tomcat 10](https://tomcat.apache.org/download-10.cgi), la 10.1.18 209 - - la dernière version disponible de [Tomcat 11](https://tomcat.apache.org/download-11.cgi), la 11.0.0-M16 211 + - la dernière version disponible de [Tomcat&nbsp;10](https://tomcat.apache.org/download-10.cgi), la 10.1.18&nbsp;; 212 + - la dernière version disponible de [Tomcat&nbsp;11](https://tomcat.apache.org/download-11.cgi), la 11.0.0-M16. 210 213 211 - Ma machine de test est équipée d'un CPU _11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz_ et de 64Go de RAM (!). 214 + Ma machine de test est équipée d'un CPU _11<sup>th</sup> Gen Intel(R) Core(TM) i7-1165G7 @ 2.80&nbsp;GHz_ et de 64&nbsp;Go de RAM (!). 212 215 213 - Les JVM sont démarrées avec l'otion suivantes `-Xms512m -Xmx512m` pour positionner une taille de la heap à 512 Mo directement consommée. 214 - L'option `-XX:NativeMemoryTracking=summary` permet d'observer la consommation mémoire de la JVM. 216 + Les JVM sont démarrées avec les options `-Xms512m -Xmx512m` pour positionner une taille de la _heap_ à 512&nbsp;Mo directement consommée. 217 + L'option `-XX:NativeMemoryTracking=summary` permet d'observer la consommation mémoire de la JVM, pour analyser plus finement les tailles de mémoire réservées et consommées auprès du système d'exploitation. 215 218 216 219 ```bash 217 220 export CATALINA_OPTS='-Xms512m -Xmx512m -XX:NativeMemoryTracking=summary' ··· 219 222 220 223 > Je n'ai pas positionné de paramétrage propre au GC ou d'autres options, ce qui m'intéresse ce sont uniquement la consommation de RAM liée aux _Threads_ et les performances liées à des temps de réponse aux requêtes. 221 224 222 - ### La configuration de Tomcat 11 225 + ### La configuration de Tomcat&nbsp;11 223 226 224 - _Pour_ utiliser les _Threads Virtuels_ dans Tomcat 11, il faut paramétrer l'_Executor_ de Tomcat pour utiliser la classe qui instancie de _Threads Virtuels_, en lieu et place de l'implémentation standard qui utilise un pool de _Threads Plateforme_, et assigner l'exécutor au _Connector_. Ce paramétrage n'est pas actif par défaut. Il se fait dans le fichier `settings.xml`, dans la balise `<Service>`, comme indiqué dans [la documentation](https://tomcat.apache.org/tomcat-11.0-doc/config/executor.html#Virtual_Thread_Implementation)&nbsp;: 227 + Pour utiliser les _Virtual Threads_ dans Tomcat&nbsp;11, il faut paramétrer l'_Executor_ de Tomcat pour activer la classe qui instancie les _Virtual Threads_, en lieu et place de l'implémentation standard qui utilise un _pool_ de _Threads_ plateforme, et assigner l'_Executor_ au _Connector_ en charge d'écouter sur le port HTTP. Ce paramétrage n'est pas actif par défaut. Il se fait dans le fichier `settings.xml`, dans la balise `<Service>`, comme indiqué dans [la documentation](https://tomcat.apache.org/tomcat-11.0-doc/config/executor.html#Virtual_Thread_Implementation)&nbsp;: 225 228 226 229 ```xml 227 230 <Service name="Catalina"> ··· 241 244 </Service> 242 245 ``` 243 246 244 - On paramètre donc le `StandardVirtualThreadExecutor` comme devant traiter les requêtes allouées au _Connector_ écoutant sur le port `8080`. 247 + On paramètre donc le `StandardVirtualThreadExecutor` comme devant traiter les requêtes allouées au _Connector_ écoutant sur le port&nbsp;`8080`. 245 248 246 - Aucune autre configuration n'est nécessaire. Aucune configuration particulière n'est fait sur le Tomcat 10.1. 249 + Aucune autre configuration n'est nécessaire sur le Tomcat&nbsp;11. Aucune configuration particulière n'est faite sur le Tomcat&nbsp;10.1 pour les tests. 247 250 248 - ### Les perfs attendues 251 + ### Les performances attendues 249 252 250 - On s'attend, entre Tomcat&nbsp;10.1 et Tomcat&nbsp;11, avec l'utilisation des _Threads Virtuels_, d'avoir une empreinte mémoire réservée inférieure, ainsi que de meilleures performances à l'exécution des requêtes. 251 - En principe, les _Threads Virtuels_ ne devraient utiliser que quelques _Threads Plateforme_, et donc limiter les _context switch_ en cas de charge importante. 253 + On s'attend, entre Tomcat&nbsp;10.1 et Tomcat&nbsp;11, avec l'utilisation des _Virtual Threads_, à avoir une empreinte mémoire réservée inférieure, ainsi que de meilleures performances à l'exécution des requêtes. 254 + En principe, les _Virtual Threads_ utilisés par Tomcat&nbsp;11 ne devraient utiliser que quelques _Threads_ plateforme hôtes pour leur exécution, et donc limiter les _context switches_ en cas de charge importante. 252 255 253 256 ### Démarrage et empreinte mémoire à vide 254 257 255 - #### Tomcat 10.1 258 + #### Tomcat&nbsp;10.1 256 259 257 - Le Tomcat 10.1 est démarré avec la commande `startup.sh`&nbsp;: 260 + Tomcat&nbsp;10.1 est démarré avec la commande `startup.sh`&nbsp;: 258 261 259 262 ```bash 260 263 ./startup.sh ··· 267 270 Tomcat started. 268 271 ``` 269 272 270 - L'empreinte mémoire de notre _Tomcat_ se fait avec la séquence de commandes&nbsp;: 273 + La récupération de l'empreinte mémoire de notre Tomcat se fait à l'aide des commandes `jps` et `jcmd`&nbsp;: 271 274 272 275 ```bash 273 276 # listing des JVM en cours d'exécution ··· 297 300 298 301 ``` 299 302 300 - On observe que notre Heap est bien réservée à 512&nbsp;Mo (524288KB), et que 41&nbsp;_Threads_ ont été démarrés (dont les 25&nbsp;_Threads_ liés à notre `Executor`), pour une consommation de 41&nbsp;Mo supplémentaires. Nous avons un total de mémoire consommée de près de 630&nbsp;Mo, car d'autres espaces sont réservés par la JVM (espaces de code, etc...). 303 + On observe que notre _Heap_ est bien réservée à 512&nbsp;Mo (524&nbsp;288&nbsp;KB), et que 41&nbsp;_Threads_ ont été démarrés (dont les 25&nbsp;_Threads_ liés à notre `Executor`), pour une consommation de 41&nbsp;Mo supplémentaires. Nous avons un total de mémoire consommée de près de 630&nbsp;Mo, car d'autres espaces sont réservés par la JVM (espaces de code, etc.). 301 304 302 - En générant un peu de charge sur les applis exemples par défaut, on force _Tomcat_ à instancier les _Threads_ supplémentaires pour atteindre les 200&nbsp;_Threads_. 305 + En générant un peu de charge sur les applications exemples par défaut, on force Tomcat à instancier les _Threads_ supplémentaires pour atteindre les 200&nbsp;_Threads_. 306 + 307 + La charge est générée avec la commande [`hey`](https://github.com/rakyll/hey), en utilisant 400 workers pour envoyer un million de requêtes à la _Servlet_ d'exemple. 303 308 304 309 ```bash 305 310 $ hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/HelloWorldExample 311 + ``` 306 312 313 + On récupère ensuite l'empreinte mémoire de notre Tomcat pour observer les nouvelles valeurs&nbsp;: 314 + 315 + ```bash 307 316 $ jcmd $(jps -l | grep -v 'jps' | cut -d ' ' -f 1) VM.native_memory 308 317 309 318 Native Memory Tracking: ··· 324 333 (arena=269KB #460) (peak=317KB #52) 325 334 ``` 326 335 327 - On observe que le nombre de _Threads_ est passé à 231, et qu'on a maintenant plus de 230 Mo réservés pour les _Threads_. 336 + On observe que le nombre de _Threads_ est passé à 231, et qu'on a maintenant plus de 230&nbsp;Mo réservés pour les _Threads_. 328 337 329 - #### Tomcat 11 338 + #### Tomcat&nbsp;11 330 339 331 - Comme pour le Tomcat 10.1, le Tomcat 11 est démarré&nbsp;: 340 + Comme pour Tomcat&nbsp;10.1, Tomcat&nbsp;11 est démarré&nbsp;: 332 341 333 342 ```bash 334 343 $ ./bin/startup.sh ··· 364 373 (arena=34KB #60) (peak=317KB #52) 365 374 ``` 366 375 367 - On observe qu'à froid, moins de _Threads_ sont alloués au démarrage, seulement 31 au lieu des 41 _Threads_ démarrés par Tomcat 10.1. 376 + On observe qu'à froid, moins de _Threads_ sont alloués au démarrage, seulement 31 au lieu des 41 _Threads_ démarrés par Tomcat&nbsp;10.1. 368 377 369 - Après avoir passé une charge identique au test du Tomcat 10.1&nbsp;: 378 + Après avoir passé une charge identique au test du Tomcat&nbsp;10.1, toujours avec la commande`hey`&nbsp;: 370 379 371 380 ```bash 372 381 $ hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/HelloWorldExample 382 + ``` 373 383 384 + On récupère à nouveau l'empreinte mémoire de Tomcat&nbsp;11&nbsp;: 385 + 386 + ```bash 374 387 $ jcmd $(jps -l | grep -v 'jps' | cut -d ' ' -f 1) VM.native_memory 375 388 376 389 Native Memory Tracking: ··· 392 405 393 406 ``` 394 407 395 - On observe que Tomcat a instancié quelques _Threads_ en plus, pour passer à 39 et on atteint donc les 39&nbsp;Mo de stack allouée. 396 - On économise donc pas loin de 200 Mo comme attendu. 408 + On observe que Tomcat a instancié quelques _Threads_ en plus, pour passer à 39 et on atteint donc les 39&nbsp;Mo de _stack_ alloués. 409 + On économise donc pas loin de 200&nbsp;Mo comme attendu. 397 410 398 - > Attention, cette mémoire est bien de la mémoire réservée, et non pas l'empreinte de la mémoire réelle consommée (dénommée `committed`). Les OS utilisent des mécanismes de mémoire virtuelle qui permettent de promettre de la mémoire à un process qui la demande. Cette mémoire n'est pas écrite sur la RAM tant qu'elle n'est pas réellement consommée. 411 + > Attention, cette mémoire est bien de la mémoire réservée, et non pas l'empreinte de la mémoire réelle consommée (dénommée _committed_). Les OS utilisent des mécanismes de mémoire virtuelle qui permettent de promettre de la mémoire à un _process_ qui la demande, même si la mémoire n'est pas disponible physiquement. Cette mémoire n'est pas écrite sur la RAM tant qu'elle n'est pas réellement consommée. 399 412 400 - Comme on pouvait s'y attendre, l'empreinte de la mémoire réservée par Tomcat pour les _Threads_ est plus faible. Cependant, comme cette mémoire n'est pas utilisée, l'impact sur les performances est faible. L'intérêt des _Threads Virtuels_ ne réside pas principalement dans cette éventuelle économie. 413 + Comme on pouvait s'y attendre, l'empreinte de la mémoire réservée par Tomcat pour les _Threads_ est plus faible. Cependant, comme cette mémoire n'est pas utilisée, l'impact sur les performances est faible. L'intérêt des _Virtual Threads_ ne réside pas principalement dans cette éventuelle économie. 401 414 402 415 ### Performances avec une Servlet simple 403 416 404 - Pour mesurer les performances de Tomcat 10 et 11, j'utilise la commande `hey`, pour exécuter 1 million de requêtes, dans 400 workers différents. 417 + Pour mesurer les performances de Tomcat&nbsp;10 et 11, j'utilise la commande `hey`, pour exécuter 1&nbsp;million de requêtes, dans 400&nbsp;_workers_ différents. 405 418 406 419 > Notez que je lance cette commande sur la même machine que ma machine de test, ce qui n'est clairement pas idéal, mais c'est suffisant pour ces tests. 407 420 408 - Je requête la servlet `HelloWorldExample`, qui est fournie avec Tomcat. Cette servlet affiche simplement une page web contenant le message _Hello World_. 421 + Je requête la Servlet `HelloWorldExample`, qui est fournie avec Tomcat. Cette Servlet affiche simplement une page web contenant le message _Hello World_. 409 422 410 - #### Tomcat 10.1 - Threads Plateforme 423 + #### Tomcat&nbsp;10.1 - Threads Plateforme 411 424 412 425 ``` 413 426 hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/HelloWorldExample ··· 447 460 448 461 ``` 449 462 450 - Sur ce premier tir avec Tomcat 10.1, le temps moyen d'exécution est de 3.4 millisecondes, et 99% des requêtes ont reçu une réponse en moins de 9.7 millisecondes. 463 + Sur ce premier tir avec Tomcat&nbsp;10.1, le temps moyen d'exécution est de 3,4&nbsp;millisecondes, et 99&nbsp;% des requêtes ont reçu une réponse en moins de 9,7&nbsp;millisecondes. 451 464 452 - #### Tomcat 11 - Threads Virtuels 465 + #### Tomcat&nbsp;11 - _Virtual Threads_ 453 466 454 - Le même test a été lancé sur Tomcat 11 configuré avec des _Threads Virtuels_&nbsp;: 467 + Le même test a été lancé sur Tomcat&nbsp;11 configuré avec des _Virtual Threads_&nbsp;: 455 468 456 469 ``` 457 470 hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/HelloWorldExample ··· 490 503 99% in 0.0080 secs 491 504 ``` 492 505 493 - Le temps moyen d'exécution est de 3,1&nbsp;millisecondes, et 99% des réponses ont été données en moins de 9&nbsp;millisecondes. 494 - On a une amélioration de performances de près de 10% pour une simple servlet&nbsp;! 506 + Le temps moyen d'exécution est de 3,1&nbsp;millisecondes, et 99&nbsp;% des réponses ont été données en moins de 9&nbsp;millisecondes. 507 + On a une amélioration des performances de près de 10&nbsp;% pour une simple Servlet&nbsp;! 495 508 496 - On peut facilement interpréter cette amélioration. Les performances accrues sont probablement liées au fait que le système d'exploitation ne doit pas switcher entre l'exécution de 200&nbsp;Threads en paralèlle dans le cas de Tomcat&nbsp;11. Ce qui occasionne donc plus de temps disponible, et donc des meilleurs temps de réponse. 509 + On peut facilement interpréter cette amélioration. Les performances accrues sont probablement liées au fait que le système d'exploitation ne doit pas _switcher_ entre l'exécution de 200&nbsp;_Threads_ en paralèlle dans le cas de Tomcat&nbsp;11, ce qui occasionne donc plus de temps disponible, et donc des meilleurs temps de réponse. 497 510 498 511 ### Performances avec une Servlet effectuant un appel bloquant 499 512 500 - Pour aller un peu plus loin, nous allons exécuter un tir de performances similaire, avec une Servlet effectuant un appel bloquant de 50 millisecondes&nbsp;: `Thread.sleep(50)`&nbsp;: 513 + Pour aller un peu plus loin, nous allons exécuter un tir de performances similaire, avec une Servlet effectuant un appel bloquant de 50&nbsp;millisecondes avec `Thread.sleep(50)`&nbsp;: 501 514 502 515 ```java 503 516 public class ThreadInfo extends HttpServlet { 504 517 505 518 @Override 506 519 public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { 507 - response.setContentType("text/html"); 508 - response.setCharacterEncoding("UTF-8"); 509 - 510 - PrintWriter out = response.getWriter(); 511 - out.println("<!DOCTYPE html><html>"); 512 - out.println("<head>"); 513 - out.println("<meta charset=\"UTF-8\" />"); 514 - 515 - out.println("<title>Thread info</title>"); 516 - out.println("</head>"); 517 - out.println("<body><h1>" + Thread.currentThread().getName() + "</h1></body>"); 518 - 519 520 try { 520 521 Thread.sleep(50L); // fais dodo 521 522 } catch (InterruptedException ex) { ··· 531 532 ``` 532 533 533 534 Quel est l'impact attendu&nbsp;? 534 - Pour Tomcat 10.1, qui dispose de 200&nbsp;_Threads_ maximum, on s'attend à obtenir un débit de 4000&nbsp;requêtes par secondes maximum (200 threads \* 1000 ms / 50 ms ), donc un temps d'exécution total de 250 secondes (1 million de requêtes / 4000 req / seconde ). 535 - Pour Tomcat 11, non limité par des _Threads_, on s'attend à obtenir un débit similaire au test précédent. 535 + Pour Tomcat&nbsp;10.1, qui dispose de 200&nbsp;_Threads_ maximum, on s'attend à obtenir un débit de 4&nbsp;000&nbsp;requêtes par seconde maximum (200 _Threads_ \* 1&nbsp;000&nbsp;ms&nbsp;/&nbsp;50&nbsp;ms), donc un temps d'exécution total de 250&nbsp;secondes (1&nbsp;million de requêtes / 4&nbsp;000&nbsp;req / s). 536 + 537 + Pour Tomcat&nbsp;11, non limité par des _Threads_, on s'attend à obtenir un débit similaire au test de la Servlet précédente sans les appels bloquants. 536 538 537 - #### Tomcat 10.1 - Threads Plateforme - Appels Bloquants 539 + #### Tomcat&nbsp;10.1 - _Threads_ plateforme - appels bloquants 538 540 539 - Le tir de performances sur le Tomcat 10.1 donne le résultat suivant&nbsp;: 541 + Le tir de performances sur Tomcat&nbsp;10.1 donne le résultat suivant&nbsp;: 540 542 541 543 ``` 542 544 $ hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/ThreadInfo ··· 575 577 99% in 0.1451 secs 576 578 ``` 577 579 578 - Les 250 secondes attendues sont bien réelles et on observe un débit à 3967 requêtes par secondes. 99% des requêtes ont une réponse en moins de 145 millisecondes. Cette performance n'est pas terrible, quand on met en lumière le fait que l'opération bloquante n'est que de 50 millisecondes. La requête la plus rapide a bien été exécutée en 50 millisecondes, mais en moyenne, l'exécution est de 100 millisecondes. 580 + Les 250&nbsp;secondes attendues pour le temps d'exécution sont bien réelles et on observe un débit à 3&nbsp;967&nbsp;requêtes par seconde. 99&nbsp;% des requêtes ont une réponse en moins de 145&nbsp;millisecondes. Cette performance n'est pas terrible, quand on met en lumière le fait que l'opération bloquante n'est que de 50&nbsp;millisecondes. La requête la plus rapide a bien été exécutée en 50&nbsp;millisecondes, mais en moyenne, l'exécution est de 100&nbsp;millisecondes. 579 581 580 - Cette lenteur supplémentaire est probablement liée au _context switch_ que doit exécuter le système d'exploitation. Le débit observé de moins de 4000 requêtes par seconde est bien lié à la contrainte des 200&nbsps;threads bloqués et occupés pendant 50&nbsp;millisecondes chacun. 582 + Cette lenteur supplémentaire est liée au temps d'attente des requêtes pour obtenir un _Thread_ disponible. Passé le premier lot de 200&nbsp;requêtes, les autres attendent 50&nbsp;millisecondes avant d'obtenir un _Thread_, qui lui même bloque pendant 50&nbsp;millisecondes le traitement d'autres requêtes. Le débit observé de moins de 4&nbsp;000&nbsp;requêtes par seconde est bien lié à la contrainte des 200&nbsp;_Threads_ bloqués et occupés pendant 50&nbsp;millisecondes chacun. 581 583 582 - #### Tomcat 11 584 + #### Tomcat&nbsp;11 - _Virtual Threads_ - appels bloquants 583 585 584 - Le même tir de performances sur Tomcat&nbsp;11 configuré avec les _Threads Virtuels_ donne un résultat complètement différent&nbsp;: 586 + Le même tir de performances sur Tomcat&nbsp;11 configuré avec les _Virtual Threads_ donne un résultat complètement différent&nbsp;: 585 587 586 588 ``` 587 589 $ hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/ThreadInfo ··· 620 622 99% in 0.0544 secs 621 623 ``` 622 624 623 - On observe que le temps moyen de réponse à une requête est bien de 50&nbsp;millisecondes. Aucune surcharge liée à du _context switch_ n'est observée ici. 99% des requêtes sont répondues en 54 millisecondes. 625 + On observe que le temps moyen de réponse à une requête est bien de 50&nbsp;millisecondes. Aucune surcharge liée à du _context switch_ n'est observée ici. 99&nbsp;% des requêtes obtiennent une réponse en 54&nbsp;millisecondes. 624 626 625 - Attention cependant, on observe que le débit est de seulement 7900 requêtes par seconde. La limitation ici est liée au nombre de workers de ma commande `hey` positionné à 400. 627 + Attention cependant, on observe que le débit est de seulement 7&nbsp;900&nbsp;requêtes par seconde. La limitation ici est liée au nombre de _workers_ positionné à 400 sur la ma commande `hey`. La commande n'envoie pas suffisamment de requêtes pour atteindre le débit théorique maximum. 626 628 627 - Un test rapide permet d'augmenter le nombre de workers à 1000 pour observer la différence&nbsp;: 629 + Un second test avec le nombre de _workers_ à 1&nbsp;000 permet d'observer la différence de débit&nbsp;: 628 630 629 631 ``` 630 632 $ hey -c 1000 -n 1000000 http://localhost:8080/examples/servlets/servlet/ThreadInfo ··· 663 665 99% in 0.0568 secs 664 666 ``` 665 667 666 - Avec 1000 workers, le temps moyen de réponse reste autour de 50&nbsp;millisecondes. 99% des requêtes reçoivent une réponse en moins de 58 millisecondes. Le débit passe à 1900 requêtes par seconde&nbsp;! 668 + Avec 1&nbsp;000&nbsp;_workers_, le temps moyen de réponse reste autour de 50&nbsp;millisecondes. 99&nbsp;% des requêtes reçoivent une réponse en moins de 58&nbsp;millisecondes. Le débit passe à 19&nbsp;000&nbsp;requêtes par seconde&nbsp;! 667 669 668 670 On atteint malheureusement ici les limites de ma machine, puisque à ce stade quelques erreurs sont observées&nbsp;: `dial tcp 127.0.0.1:8080: socket: too many open files`. 669 671 670 672 Cependant, ces performances laissent deviner qu'il serait possible d'aller encore plus loin. 671 673 672 - ## Bonus, avec Spring Boot 3 674 + ## Bonus, avec Spring Boot&nbsp;3 673 675 674 - > "Julien, tu es bien gentil avec tes Servlets, mais plus personne n'en développe." 676 + > 🤓 &laquo; Julien, tu es bien gentil avec tes Servlets, mais plus personne n'en développe.&nbsp;&raquo; 675 677 676 - Cette partie "bonus" teste le même comportement, mais avec Spring Boot 3&nbsp;! 678 + Cette partie &laquo;bonus&raquo; teste le même comportement, mais avec Spring Boot&nbsp;3&nbsp;! 677 679 678 - ### Configurer Spring Boot 3 680 + ### Configurer Spring Boot&nbsp;3 679 681 680 - Malheureusement, il n'est pas possible pour le moment d'utiliser Tomcat 11 avec Spring Boot 3. 681 - Néanmoins, Spring Boot 3 a intégré le support des _Threads Virtuels_ et de l'Exécutor Tomcat à Tomcat 10&nbsp;! 682 + Malheureusement, il n'est pas possible pour le moment d'utiliser Tomcat&nbsp;11 avec Spring Boot&nbsp;3. 683 + Néanmoins, Spring Boot&nbsp;3 a intégré le support des _Virtual Threads_ et de l'_Executor_ _VirtualThreadExecutor_ à Tomcat&nbsp;10&nbsp;! 682 684 683 - Pour utiliser les Virtual Threads dans Spring Boot 3, il faut positionner la properties suivante&nbsp;: 685 + Pour utiliser les _Virtual Threads_ dans Spring Boot&nbsp;3, il faut positionner la _properties_ suivante&nbsp;: 684 686 685 687 ```properties 686 688 spring.threads.virtual.enabled=true 687 689 ``` 688 690 689 - Côté Spring Boot, cette properties est interprétée par l'annotation `@ConditionalOnThreading` et configure un `TomcatVirtualThreadsWebServerFactoryCustomizer`&nbsp;: 691 + Aucune autre modification n'est nécessaire&nbsp;! 692 + 693 + Pour comprendre comment cette _properties_ opère sa magie, il faut parcourir le code de Spring Boot. 694 + Cette _properties_ est interprétée par l'annotation `@ConditionalOnThreading` et configure un `TomcatVirtualThreadsWebServerFactoryCustomizer`&nbsp;: 690 695 691 696 ```java 692 697 @Configuration(proxyBeanMethods = false) ··· 702 707 } 703 708 ``` 704 709 705 - Le `TomcatVirtualThreadsWebServerFactoryCustomizer` configure le Tomcat embedded pour utiliser l'executor `VirtualThreadExecutor`&nbsp;: 710 + Le `TomcatVirtualThreadsWebServerFactoryCustomizer` configure le Tomcat _embedded_ pour utiliser l'_Executor_ `VirtualThreadExecutor`&nbsp;: 706 711 707 712 ```java 708 713 public class TomcatVirtualThreadsWebServerFactoryCustomizer ··· 716 721 } 717 722 ``` 718 723 719 - Dans notre code, un simple `@Controller` Spring permet de re-créer le comportement équivalent à la servlet utilisée pour le benchmark précédent&nbsp;: 724 + Dans notre code, un simple `@Controller` Spring permet de recréer le comportement équivalent à la Servlet utilisée pour le _benchmark_ précédent&nbsp;: 720 725 721 726 ```java 722 727 @RestController ··· 730 735 } 731 736 ``` 732 737 733 - Avec la properties `spring.threads.virtual.enabled=false`, on obtient les performances suivantes, similaires à ce qu'on avait en utilisant Tomcat 10.1, sans support des Threads Virtuels&nbsp;: 738 + Avec la _properties_ `spring.threads.virtual.enabled=false`, on obtient les performances suivantes, similaires à ce qu'on avait en utilisant Tomcat&nbsp;10.1, sans support des _Virtual Threads_&nbsp;: 734 739 735 740 ``` 736 - hey -c 400 -n 1000000 http://localhost:8080 741 + $ hey -c 400 -n 1000000 http://localhost:8080 737 742 738 743 Summary: 739 744 Total: 253.9172 secs ··· 769 774 99% in 0.1352 secs 770 775 ``` 771 776 772 - Les temps de réponse sont autour de 100 millisecondes, pour un débit de moins de 4000 requêtes par secondes, et 99% des réponses en moins de 135 millisecondes. 777 + Les temps de réponse sont autour de 100&nbsp;millisecondes, pour un débit de moins de 4&nbsp;000&nbsp;requêtes par seconde, et 99&nbsp;% des requêtes reçoivent une réponse en moins de 135&nbsp;millisecondes. 773 778 774 - Avec la properties `spring.threads.virtual.enabled=true`, on obtient les performances suivantes, qui sont similaires aux performances de Tomcat 11 avec les Threads Virtuels&nbsp;: 779 + Avec la _properties_ `spring.threads.virtual.enabled=true`, on obtient les performances suivantes, qui sont similaires aux performances de Tomcat&nbsp;11 avec les _Virtual Threads_&nbsp;: 775 780 776 781 ``` 782 + $ hey -c 400 -n 1000000 http://localhost:8080 783 + 777 784 Summary: 778 785 Total: 126.7462 secs 779 786 Slowest: 0.1738 secs ··· 808 815 99% in 0.0539 secs 809 816 ``` 810 817 811 - Les temps de réponse sont autour de 50 millisecondes, pour un débit d'un peu moins de 8000 requêtes par seconde, et 99% des requêtes ont une réponse en moins de 53 millisecondes&nbsp;! 818 + Les temps de réponse sont autour de 50&nbsp;millisecondes, pour un débit d'un peu moins de 8&nbsp;000&nbsp;requêtes par seconde, et 99&nbsp;% des requêtes obtiennent une réponse en moins de 53&nbsp;millisecondes&nbsp;! 812 819 813 820 ## Conclusion 814 821 815 - Les résultats sont impressionnants. En utilisant le `VirtualThreadExecutor`, dans Tomcat 10.1 ou 11, on observe 10% de gains de performances sans rien faire de particulier, pour des Servlets n'effectuant aucun appel bloquant. Mais c'est vraiment à partir du moment où des appels bloquants sont effectués que les gains de performances sont les plus importants. Un Tomcat avec 200 _Threads Plateforme_ prend du temps à exécuter des _context switch_ qui ont un impact réel sur les temps de réponse. Ces impacts semblent purement annulés avec l'utilisation des _Threads Virtuels_. 822 + Les résultats sont impressionnants. En utilisant le `VirtualThreadExecutor`, dans Tomcat&nbsp;11, on observe déjà 10&nbsp;% de gains de performances sans rien faire de particulier, pour des Servlets n'effectuant pas d'appel bloquant. 823 + 824 + Mais c'est vraiment à partir du moment où des appels bloquants sont effectués que les gains de performances sont les plus importants. Sur un Tomcat avec 200&nbsp;_Threads_ plateforme, une fois les 200 _Threads_ bloqués, les autres requêtes sont mises en attente, ce qui occasionne des temps de réponse moyens plus longs. Ces impacts semblent purement annulés avec l'utilisation des _Virtual Threads_, puisque le nombre de _Threads_ n'est plus limité. 825 + Le débit théorique d'une application n'est maintenant plus limité par son nombre de _Threads_. 816 826 817 - Pour aller plus loin, l'utilisation des _Threads Virtuels_ dans Tomcat rendrait presque inutile l'utilisation des approches de programmation réactive. Le fait de rendre les Threads peu-coûteux à instancier, lié à leur mode d'exécution sur un Thread hôte, limite la charge déportée sur le système d'exploitation en _context switch_. 827 + Pour aller plus loin, l'utilisation des _Virtual Threads_ dans Tomcat rendrait presque inutile l'utilisation des approches de programmation réactive. Le fait de rendre les _Threads_ peu coûteux à instancier, lié à leur mode d'exécution sur un _Thread_hôte, limite la charge déportée sur le système d'exploitation en _context switches_, et maximise l'utilisation du CPU. 818 828 819 829 Il n'est maintenant plus problématique de bloquer un _Thread_. 820 830 821 - On peut déjà bénéficier de ces améliorations de performances avec Spring Boot 3, et Tomcat 10.1, à condition de bien utiliser une JVM 21. Donc pourquoi se priver&nbsp;? 831 + On peut déjà bénéficier de ces améliorations de performances avec Spring Boot&nbsp;3 et Tomcat&nbsp;10.1, à condition de bien utiliser une JVM&nbsp;21. Donc pourquoi se priver&nbsp;? 822 832 823 - À suivre lors de la sortie future de Tomcat 11, quelle en sera l'intégration dans Spring Boot. Spring Boot ayant annoncé supporter Java 17 en version de base, la properties restera probablement toujours disponible, avec une valeur `false` par défaut. 833 + À suivre lors de la sortie future de Tomcat&nbsp;11, quelle en sera l'intégration dans Spring Boot. Spring Boot ayant annoncé supporter Java&nbsp;17 en version de base, la _properties_ `spring.threads.virtual.enabled` restera toujours disponible, avec probablement une valeur `false` par défaut. 824 834 825 835 ## Liens et références 826 836 827 837 - [JEP&nbsp;320](https://openjdk.org/jeps/320) - Suppression des modules Java EE et CORBA 828 838 - [JEP&nbsp;444](https://openjdk.org/jeps/444) - _Virtual Threads_ 829 839 - Documentation de [Tomcat](https://tomcat.apache.org/) 830 - - [RELEASE-NOTES Tomcat 11.0.0-M16](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M16/RELEASE-NOTES) 831 - - [Tomcat 11 Virtual Thread Implementation](https://tomcat.apache.org/tomcat-11.0-doc/config/executor.html#Virtual_Thread_Implementation) - Configuration des _Threads Virtuels_ dans Tomcat 832 - - [Virtual Threads](https://docs.spring.io/spring-boot/docs/3.2.2/reference/htmlsingle/#features.spring-application.virtual-threads) - Configuration des _Threads Virtuels_ dans Spring Boot 833 - - [Programmation Concurrente et Asynchrone&nbsp;: Loom en Java 20 et 21](https://www.youtube.com/watch?v=v7DzKOniNh0) - José Paumard 840 + - [RELEASE-NOTES Tomcat&nbsp;11.0.0-M16](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M16/RELEASE-NOTES) 841 + - [Tomcat&nbsp;11 Virtual Thread Implementation](https://tomcat.apache.org/tomcat-11.0-doc/config/executor.html#Virtual_Thread_Implementation) - Configuration des _Virtual Threads_ dans Tomcat 842 + - [Virtual Threads](https://docs.spring.io/spring-boot/docs/3.2.2/reference/htmlsingle/#features.spring-application.virtual-threads) - Configuration des _Virtual Threads_ dans Spring Boot 843 + - [Programmation Concurrente et Asynchrone&nbsp;: Loom en Java&nbsp;20 et 21](https://www.youtube.com/watch?v=v7DzKOniNh0) - José Paumard 834 844 - [JMH](https://github.com/openjdk/jmh)&nbsp;: Java Microbenchmark Harness 845 + - [hey](https://github.com/rakyll/hey)&nbsp;: HTTP load generator, ApacheBench (ab) replacement