Serveur minecraft

Java dans des conteneurs – Deuxième partie – Monter un serveur MineCraft

Le 20 mai 2020 - 37 minutes de lecture

Transcription

Delabassee: Cette session a été annoncée comme Java dans des conteneurs, mais j'ai changé le titre en JVM dans des conteneurs, car à la fin de la journée, ce qui compte vraiment, c'est la machine virtuelle Java qui s'exécute dans le conteneur. La JVM ne comprend que le bytecode. La plupart du temps, ce bytecode provient du compilateur javac. Si cela vient d'autre chose, cela n'a pas d'importance.

Je m'appelle David Delabassee. Je travaille pour Oracle dans le groupe de plate-forme Java. Je suis éloigné. Vous pouvez me trouver sur Twitter @delabassee. Le groupe de plate-forme Java est essentiellement l'organisation au sein d'Oracle qui est responsable de la plate-forme Java et d'OpenJDK. Ceci est une piste Java. Il y a un schéma. Vous avez besoin de deux choses. Le premier est un duc. Le deuxième, n'importe qui peut deviner quelle est la deuxième chose dont vous avez besoin dans la piste Java, une diapositive de déclaration de refuge. C'est celui d'Oracle.

Java – 25 ans

Parlons de JVM et Java. Cette année est une année importante pour Java car nous sommes sur le point de célébrer le 25e anniversaire de Java. Depuis 25 ans, Java a été développé et a évolué. Cette évolution repose essentiellement sur deux principes fondamentaux. Le premier est la productivité des développeurs. Il doit être facile d'écrire du code. L'écriture de code n'est qu'une partie de l'histoire. La maintenabilité est également très importante. C'est pourquoi il est également très important de pouvoir facilement écrire et lire le code qui a été produit. C'est l'un des piliers de l'évolution de Java.

Le second est la performance des applications. Au fil des ans, nous avons amélioré la plateforme pour nous assurer que votre code s'exécute de manière très efficace. Nous avons amélioré le GC qui était présent dans les premiers jours de la plateforme. Nous en avons ajouté de nouveaux comme G1 est un GC. Nous avons un compilateur JIT qui permet à la machine virtuelle Java de faire l'optimisation à la volée. Cette évolution au cours des 25 dernières années s'est faite sur la base de ces deux piliers. Gardant à l'esprit que le monde évolue également. Il y a de nouveaux paradigmes de programmation qui apparaissent ou qui deviennent plus populaires, donc Java doit s'assurer de faire face aux bons. L'expression lambda introduite dans 8, en est un bon exemple.

Style d'application, il y a 20 ans, nous écrivions principalement des monolithes, à l'époque, tout tourne autour des microservices et fonctionne comme un service. De toute évidence, la plate-forme est également en mesure de faire face à cela. Le matériel sur lequel nous exécutons notre code évolue également beaucoup. Nous avons plus de CPU, plus de cœurs. Chaque CPU dispose d'un cache à plusieurs niveaux. La plate-forme, la JVM doit pouvoir y faire face. Enfin, les styles de déploiement évoluent également. Il y a 20 ou 15 ans, nous déployions des monolithes. Si nous sommes agiles, nous déployions une ou deux fois par an dans nos propres centres de données. Ces jours-ci, avec CI / CD, nous faisons essentiellement plusieurs déploiements par jour dans des conteneurs qui sont déployés dans un cluster Kubernetes dans le cloud.

Combien d'entre vous utilisent des conteneurs aujourd'hui? Fondamentalement, un conteneur est un moyen standard d'empaqueter un logiciel avec sa dépendance. Il existe plusieurs exécutions: Docker, CRI-O, LXC. À la fin de la journée, ce que je vais discuter aujourd'hui s'applique à tous les conteneurs conformes OCI basés sur des espaces de noms et des groupes de contrôle. L'espace de noms et les cgroups sont des technologies de noyaux Linux qui nous permettent essentiellement de créer des conteneurs. Je vais utiliser Docker, mais simplement parce que j'y suis habitué. Cela fonctionnerait également sur Podman ou tout autre runtime.

Conteneur vs VM

Les conteneurs ne doivent pas être confondus avec la machine virtuelle. Nous avons des conteneurs super légers. Le démarrage d'un conteneur prend quelques millisecondes, selon le cas, mais le temps de démarrage des conteneurs peut être assez rapide. Le problème avec le conteneur est qu'ils fonctionnent sur certaines fonctionnalités du noyau Linux sous-jacent, de l'espace de noms, des groupes de contrôle, des systèmes unifiés, pour nous donner essentiellement cette idée que notre code s'exécute de manière isolée. Ce n'est pas vraiment isolé car à la fin de la journée, nos conteneurs s'appuient sur l'hôte du noyau sous-jacent pour exécuter le code. Cela signifie essentiellement que nous fonctionnons sur le même système d'exploitation que nous avons sur l'hôte.

Nous avons une machine virtuelle qui nous fournit une véritable isolation. Le truc avec VM, c'est qu'ils sont très chers. Le démarrage d'une machine virtuelle est une opération coûteuse, vous ne devez donc pas le faire trop souvent. Démarrer un conteneur et le tuer est presque instantanément. Vous pouvez le faire autant de fois que vous le souhaitez. Le conteneur peut devenir éphémère si vous le souhaitez. Avec VM, nous avons une forte isolation, mais cela a un coût. Avec le conteneur, nous n'avons pas ce coût, mais nous n'avons pas un très bon isolement. Entre les deux, nous avons des conteneurs Kata, qui reposent essentiellement sur cette machine virtuelle légère pour fournir essentiellement les deux mondes. Nous avons l'isolement de VM avec essentiellement le coût et le temps de démarrage que nous pouvons atteindre du côté du conteneur.

Paysage de conteneur JVM

Java et la JVM. Si nous regardons le paysage des conteneurs JVM, je pense que nous sommes assez chanceux car il est difficile de nos jours de trouver des cadres modernes qui ne prennent pas en charge les conteneurs prêts à l'emploi. Si vous utilisez Helidon, Micronaut, JHipster, vous pouvez créer directement une version conteneurisée de votre application. Côté outillage, nous avons un plugin Maven. Nous avons la possibilité d'utiliser Jib, donc de créer des conteneurs en utilisant Jib directement depuis Maven. L'IDE, si vous prenez IntelliJ, par exemple, il prend en charge immédiatement la possibilité de créer directement une image de conteneur.

Si nous regardons le paysage des conteneurs JVM, c'est assez solide. La première chose dont je voudrais discuter est la sensibilisation aux conteneurs JVM. Depuis Java 8, nous avons investi dans la JVM pour nous assurer qu'elle fonctionne correctement chaque fois que cette JVM s'exécute dans les conteneurs. Il y a plusieurs raisons à cela, comme une bonne utilisation des ressources. Une raison importante est ce que nous appelons l'ergonomie JVM. L'ergonomie JVM est fondamentalement quelque chose qui a été ajouté dans Java 8, je crois. C'est la capacité de la JVM à fournir un comportement par défaut qui fonctionne. La JVM, si vous n'accordez pas la JVM, vous fournira un comportement décent. La JVM examinera quelques mesures, comme le nombre de CPU, combien de mémoire possède-t-elle? Sur cette base, il déduira une configuration telle que la taille de segment de mémoire à utiliser.

Pour illustrer cela, j'ai une démo. Tout d'abord, je vais SSH sur une machine virtuelle dans le cloud. Ce que j'ai ici, j'ai une simple application HelloWorld qui regarde essentiellement le nombre de processeurs qu'elle possède. Quelle est la mémoire? Quelques métriques de base. Quels sont les fournisseurs JVM? Nous pouvons l'exécuter, java HelloWorld.java. Je ne compile pas la classe. Je l'exécute directement contre Java. C'est JDK 13.0.2, le fournisseur est Oracle. Il dispose de quatre processeurs. Les trois que vous voyez ici sont le thread ForkJoinPool commun, qui sont configurés en fonction du nombre de processeurs. Étant donné que nous avons quatre processeurs, nous avons trois threads pour le ForkJoinPool commun. Ensuite, je vais Dockerize ce code. Ceci est mon fichier Docker. C'est assez basique. Je prends une image basée sur Java, je crée un répertoire, je copie la source, je javac, donc je compile cette source. Je lance ça. Docker build -t qcon, et c'est le contexte. Lançons cela. L'image est QCon. Ce code s'exécute dans mon conteneur. La version Java est différente, c'est une ancienne. Il est exécuté à l'intérieur du conteneur. Il a quatre cœurs. Le ForkJoinPool commun est défini sur 3. Si je regarde le nombre de processeurs que ma machine virtuelle a, il a quatre processeurs, donc cela correspond.

Ce que je vais faire ensuite, je vais exécuter le même code, mais je vais limiter les ressources que mon conteneur peut utiliser. Cette fois, au lieu de quatre CPU, mon conteneur ne pourra en utiliser que deux. Cette fois, la JVM à l'intérieur voit toujours quatre cœurs. Toute l'ergonomie sera basée sur cette fausse hypothèse. Vous voyez qu'en termes de mémoire, il dispose de 3 Go. Ce que je peux faire, je peux limiter cette mémoire à 256 Mo. Vous voyez que ma JVM voit toujours plus que cela, et elle voit toujours les 4 CPU. Quelque chose ne va pas. Pourquoi? Tout simplement à cause de cela. Si je regarde le fichier Docker, j'utilise cette dernière image Java, qui est complètement obsolète. J'ai besoin de réparer ça. Je vais simplement passer à OpenJDK, le plus récent. Reconstruisons l'image. Exécutons à nouveau l'image avec la limitation que nous avons définie, 3 processeurs et 256 Mo. Cette fois, la JVM voit 3 CPU, donc le ForkJoinPool commun sera configuré avec 2 threads, ce qui est logique. Le temps d'exécution et la mémoire sont inférieurs à ce que la JVM voit. Il existe une formule qui calcule cela. Cette fois, nous pouvons être sûrs que notre code exécuté dans ce conteneur ne sera pas tué par le moteur de conteneur.

Dans le cas précédent, la JVM voyait 4 CPU, et voyait également plus de mémoire à laquelle elle a accès, que la JVM sera rapidement tuée par le moteur de conteneur car elle essaiera essentiellement de surconsommer les ressources disponibles . C'est pourquoi l'utilisation d'une JVM compatible avec les conteneurs est importante.

Parlons de performance. Quelque chose que je ne vous ai pas montré dans l'exemple précédent est que l'image Docker générée était assez lourde, de l'ordre de plusieurs centaines de mégaoctets, pour un simple Hello World. C'est un problème car chaque fois que vous devez exécuter ce conteneur, le moteur de conteneur doit récupérer cette image de conteneur dans un registre qui, nous l'espérons, est situé sur le même réseau. Pourtant, pour exécuter une application très basique, vous devez toujours récupérer plusieurs centaines de mégaoctets. Voilà un problème. Nous devons y travailler.

En ce qui concerne les conteneurs, nous avons tendance à parler de latence. La latence est essentiellement le temps nécessaire pour envoyer le résultat aux utilisateurs. Nous pouvons diviser cela en deux. Il y a le temps qu'il faut pour démarrer les conteneurs. Ensuite, il y a le temps qu'il faut pour démarrer votre application, dans ce cas, la machine virtuelle Java dans les conteneurs. Si nous regardons la première couche, le démarrage du conteneur, nous devons garder à l'esprit que le conteneur est essentiellement composé de couches. Nous avons plusieurs couches les unes sur les autres qui composent notre image de conteneur. L'idée ici est d'avoir des couches aussi petites que possible. En ce qui concerne l'image du conteneur JVM, nous avons trois types de couches, de haut en bas. En haut, nous avons le code Java ou le bytecode avec toutes ses dépendances. Ensuite, nous avons sous cela, la couche d'exécution Java, c'est la machine virtuelle Java et autre chose. Ensuite, nous avons la couche du système d'exploitation. L'idée de base est essentiellement de réduire autant que possible ces couches.

Couche d'application Java

Sur la couche d'application Java, nous ne pouvons pas faire grand-chose. Il n'y a pas grand-chose que la JVM puisse faire. Nous ne pouvons que vous donner des conseils, comme faire attention à la dépendance que vous utilisez. Ne vous assurez pas que vous n'incorporez pas le monde entier avec une dépendance transitive dans votre code. Ce sont vraiment les meilleures pratiques. La chose que vous devez faire est d'essayer de tirer parti du mécanisme de cache que nous avons avec les conteneurs. C'est pourquoi c'est une bonne idée de garder tout ce qui est relativement statique dans différentes couches. C'est pourquoi peut-être que Fat JAR n'est pas une bonne idée en ce qui concerne les conteneurs. La chose que la JVM fournit est CDS. C'est quelque chose que vous souhaitez exploiter.

Couche d'exécution Java

À la couche Java Runtime, il y a quelque chose que nous pouvons utiliser pour réduire la taille de cette couche, et c'est JLink. JLink est un outil qui a été ajouté dans Java 9. Fondamentalement, JLink vous donne la possibilité de créer votre propre runtime personnalisé. Votre code n'a pas besoin d'être modulaire pour cela. Vous pouvez prendre une application Java existante et l'exécuter au-dessus d'un runtime personnalisé créé par JLink. Pour vous donner une idée des avantages que nous pouvons obtenir en utilisant JLink, j'ai pris JDK 13, OpenJDK. J'ai commencé avec un JDK complet qui pesait plus de 300 Mo. Ensuite, vous voulez créer juste un runtime, c'est aussi une bonne idée dans l'espace conteneur. Du point de vue de la sécurité, c'est une bonne idée de réduire l'attaque potentielle en surface de vos conteneurs. C'est pourquoi lorsque vous exécutez votre code, vous n'avez pas besoin par exemple d'avoir javac, ou jmap, tous ces outils dans vos conteneurs. Ils ne sont pas nécessaires à l'exécution. Débarrasse-toi d'eux.

Nous voulons utiliser un Java Runtime au lieu d'un JDK complet. Avec JLink, nous avons la possibilité de créer ce runtime personnalisé, donc tous les modules. Il s'agit essentiellement d'un Java Runtime qui comprend tous les modules, 168 Mo. C'est très stupide de créer un tel runtime car aucune application sensée ne peut utiliser tous les modules de la plateforme. C'est ce que je fais ensuite. Je crée un Java Runtime avec juste les modules dont mon code a besoin. Dans ce cas, il s'agit d'une fonction Java sans serveur. De 168 Mo, je descends à 50 Mo. Ensuite, JLink est livré avec quelques indicateurs supplémentaires que nous pouvons utiliser. Nous avons la possibilité de supprimer le fichier d'en-tête, la page de manuel. Si nous le faisons, nous passons de 50 Mo à 44 Mo. JLink fournit également deux niveaux de compression, la compression 2 étant Zip Deflate. Si nous le faisons, nous passons de 44 Mo à 34 Mo, essentiellement 34 Mo. J'ai un Java Runtime personnalisé qui comprend tous les modules et uniquement les modules dont mon code a besoin. Cela réduira considérablement la taille de ces couches.

La chose que nous devons garder à l'esprit est que si vous regardez en bas, j'ai utilisé la compression juste pour réduire la taille de ces couches. La compression signifie la décompression, donc ce n'est peut-être pas une bonne idée d'utiliser la compression si nous voulons gagner du temps, car au moment de l'exécution, cela impliquerait un certain coût de décompression. Peut-être que j'aurais dû arrêter à 44 Mo. C'est quelque chose que vous devriez idéalement mesurer.

Couche du système d'exploitation

Au niveau du système d'exploitation, la JVM ne peut pas faire grand-chose. Vous devez utiliser une distribution optimisée, donc une distribution slim, par exemple. Ce n'est pas parce qu'ils amincissent le titre de la distribution qu'il amincit vraiment, vérifiez cela. Il y a la distribution sans distraction de Google. Il pesait près de 200 Mo. C'est un peu lourd, mais il a Java. Le fait est que c'est uniquement Java 11. Il a une certaine limitation. Ensuite, il existe des outils tels que Docker-slim qui, dans mon cas, ne fonctionnent pas.

Nous pouvons aller plus loin. Nous pouvons utiliser des distributions Linux super optimisées telles que Alpine. Alpine est une distribution optimisée qui pèse entre 5 Mo et 6 Mo, selon la version. Il s'agit essentiellement d'une distribution Linux complète pour exécuter votre code. De toute évidence, avec cette taille, vous n'avez pas toutes les cloches et les sifflets que vous avez dans une distribution Linux typique. Du point de vue des conteneurs, nous voulons réduire autant que possible l'attaque potentielle en surface. Il est bon d'enlever toutes ces cloches et sifflets qui ne sont d'aucune façon nécessaires. Le fait est qu'Alpine s'appuie sur musl, musl étant la bibliothèque C que le code C, C ++ utilise pour parler au noyau Linux. OpenJDK utilise libc. OpenJDK utilise une libc différente pour parler au noyau sous-jacent. Vous avez deux options pour exécuter OpenJDK sur Alpine. Soit vous comptez sur le package Alpine glibc, qui est essentiellement une couche d'intermédiation qui, chaque fois qu'il y a un appel libc, le transformera en un appel musl. Je ne suis pas sûr que ce soit la bonne approche. Ou, vous pouvez utiliser Project Portola. Le projet Portola compile essentiellement OpenJDK au-dessus de musl. Vous pouvez utiliser OpenJDK sur Alpine. Aujourd'hui, la version actuelle de Java est Java 13, donc nous ne publions pas vraiment les versions de Portola pour 14. Pourquoi? Parce que Portola ne passe pas par tous les tests que nous faisons habituellement sur OpenJDK. Vous pouvez l'utiliser mais en vous basant sur une certaine attention. Cela dit, nous recherchons toujours de l'aide pour nous aider à maintenir cette version. Si vous êtes prêt à nous aider, vous êtes les bienvenus.

JLink Alpine

Je vais faire une démonstration de ça. ITS'DEMO est essentiellement une chaîne de vente au détail au Japon. Ce que je vais vous montrer maintenant, c'est essentiellement JLink avec Alpine. Pour cela, j'utilise un serveur. J'utilise le serveur Minecraft, car il était open-source. Ce que j'ai ici, sever.jar est le serveur Minecraft de Microsoft, et certains des fichiers dont ce serveur a besoin, comme eula.txt s'il n'est pas là, il ne démarrera pas les propriétés du serveur. J'ai un fichier Docker pour conteneuriser ce type. C'est une construction en plusieurs étapes. La première version, en gros je prends 14-Alpine. Ensuite, j'utilise JLink ici pour créer un runtime personnalisé, compresser 2 pour tout compresser autant que possible. Ensuite, j'ai juste besoin de spécifier quels modules sont nécessaires au serveur Minecraft. Il s'est avéré que le serveur Minecraft avait besoin de ces 12 modules. Il existe un outil spécifique que vous pouvez utiliser pour déterminer quels modules sont nécessaires à votre code. Ensuite, la deuxième étape est essentiellement d'une distribution alpine pure. Je copie tous les fichiers. Ce sont les fichiers du serveur Minecraft. Je copie également depuis l'étape de prévisualisation, le runtime personnalisé. Ensuite, je lance juste ce type.

Docker build -t mine. En utilisant OpenJDK, donc la distribution OpenJDK 13 de la communauté OpenJDK, donc celle qui utilise Oracle Linux. Les 12 modules pèsent 88 Mo. Si je supprime le débogage, essentiellement si je supprime les informations de débogage de ce runtime personnalisé, j'économiserais 14 Mo. Si je compresse 1, à partir de 88 Mo, j'économiserais encore 18 Mo. Compressez 2, j'économiserais 31 Mo sur ces 88 Mo. Si je supprime le fichier d'en-tête sur la page de manuel, je n'enregistrerai rien car dans cette distribution particulière, ils ne sont pas inclus par défaut.

Quelque chose d'important, en plus vous avez un OpenJDK 13 personnalisé avec un module. OpenJDK 13 à partir de la build effectuée par Oracle, il pèse 50 Mo. Ensuite, en bas, la même image d'exécution personnalisée, un module, utilisant la version Debian OpenJDK 13, il pèse près de 500 Mo. Environ 10 fois la taille pour le même runtime personnalisé exact. Pourquoi? Parce que Debian incorpore les symboles de débogage. Si vous êtes préoccupé par la taille de votre couche Java Runtime, vous devez supprimer clairement ces symboles. Dans Java 13, nous avons ajouté ces nouvelles fonctionnalités aux symboles de débogage natif de bande JLink que vous pouvez utiliser pour supprimer ces symboles. Si vous faites cela, vous passerez de 499 Mo à 51 Mo. C'est quelque chose que vous devez clairement examiner.

Je construis mon serveur Minecraft. Regardons la taille de cette image. Ce serveur Minecraft, le conteneur pèse 100 Mo. On peut regarder ce qu'il y a dedans, plonger le mien. Ce sont les différentes couches. La première couche correspond aux couches du système d'exploitation. Ensuite, celui-ci consiste simplement à créer un répertoire. Les couches simples avaient juste ce petit fichier texte. Celui-ci avait le dossier de propriété. Peut-être que cela aurait pu être une bonne idée de combiner tous ces éléments ensemble, mais ce n'est pas le but ici. Ensuite, nous ajoutons le server.jar, qui est le serveur Minecraft. Ce type pèse 36 Mo. Le serveur Minecraft lui-même fait déjà 36 Mo. Ensuite, la couche suivante est essentiellement le Java Runtime personnalisé. Si nous regardons la taille de cela, 58 Mo dans ce cas, c'est essentiellement ce que j'avais avec la compression 2 auparavant. En clair, 100 Mo, nous avons le système d'exploitation Alpine. Nous avons le Java Runtime personnalisé. Nous avons notre application qui, dans ce cas particulier, pèse déjà 36 Mo. C'est quelque chose que vous devez clairement examiner.

Nous avons expliqué comment, du côté de Java, vous pouvez améliorer le temps de démarrage du conteneur. L'étape suivante consiste essentiellement à regarder l'heure de démarrage de Java elle-même. Cette diapositive montre le temps de démarrage entre Java 8 et 9. De toute évidence, ce n'est pas sur ma diapositive, mais plus petit est mieux. Nous avons eu une grosse régression entre 8 et 9. C'est la mauvaise nouvelle. La bonne nouvelle est qu'elle a été corrigée. Il s'agit d'une simple application HelloWorld en JDK 14 qui sortira dans 2 semaines, a été améliorée, contre 13. Je peux vous dire qu'en 15, nous pourrons raser encore quelques millisecondes supplémentaires. C'est quelque chose qui est très important dans l'espace conteneur lorsque vous utilisez des conteneurs éphémères. Fondamentalement, un conteneur qui sera appelé effectuera cette tâche et mourra sous peu. Ensuite, le temps de démarrage de l'application devient important. Il s'agit de la même diapositive, mais avec des données supplémentaires, comme Hello World utilisant Lambdas, Hello World utilisant des chaînes de concaténation. Fondamentalement, la conclusion est que, si possible, vous devriez regarder la version récente de la JVM si l'heure de démarrage est quelque chose qui est important pour vous.

Nous pouvons également utiliser CDS. L'exemple précédent était assez visible car nous venions d'avoir le temps de démarrage de l'application et l'application était très triviale. CDS est quelque chose que nous pouvons utiliser pour des applications non triviales. Quelqu'un sait ce qu'est le CDS, le partage de données de classe? Ce qui est très faible, car CDS n'est pas une nouvelle fonctionnalité, CDS est dans la plate-forme depuis Java 5. L'idée de base avec CDS est la suivante. Lorsque vous exécutez une application Java, la JVM charge le bytecode à partir du disque. Je dois effectuer un tas d'opérations avant d'avoir quelque chose que la JVM puisse utiliser, à partir de la mémoire. Avec CDS, la JVM le fera. Une fois qu'il a la représentation en mémoire de votre bytecode, il le videra sur le disque, de sorte que la prochaine fois que vous utiliserez votre application, la JVM le mettra directement en mémoire. Il n'a pas à passer par toutes les opérations, qui sont chères. Le truc, c'est qu'évidemment vous avez beaucoup de cours. C'est l'idée de base avec CDS. Au début, le CDS était assez limité. CDS pour Java SE 5 était juste pour les classes d'exécution, donc le rt.jar, à l'époque. Il était limité, je pense, au Serial GC. Fondamentalement, toutes ces limitations ont été levées depuis lors.

Démo CDS

Ayons une rapide démo CDS. Pour cela, je vais retourner dans ma box Linux. Je vais compiler cette application HelloWorld, Java HelloWorld. Je vais chronométrer cette invocation, l'heure HelloWorld, donc 115 millisecondes. Pour obtenir un nombre plus précis, je vais invoquer cette application plusieurs fois sans et avec CDS. Pour cela, j'ai un bel outil, qui invoquera l'application 42 fois, perf42. Pour le premier tour, je vais désactiver CDS. Pour désactiver CDS, je dois utiliser cet indicateur. L'application est invoquée 42 fois et, en moyenne, cela prend 220 millisecondes. Maintenant, je vais utiliser CDS. Je vais faire 42 invocations mais avec CDS. Pour utiliser CDS, c'est assez simple. Supprimez ce type, car maintenant CDS est activé par défaut, donc 222 contre 157. Cette fois, nous sommes à 71% du temps de démarrage que le temps de démarrage sans CDS. C'est quelque chose que vous obtenez gratuitement. C'est dans la plateforme. Vous pourriez dire: "C'est bien. C'est une énorme victoire, mais c'était une application banale".

Ce tweet provient du responsable du projet JRuby. Il travaille chez Red Hat. Fondamentalement, le nombre qu'ils ont maintenant avec JDK 14 en utilisant App CDS, jdk-08.202, 853 millisecondes, maintenant avec jdk-13, 717 millisecondes. L'écart est assez important. JRuby n'est pas une application triviale. C'est vraiment une application complexe. Dans les conteneurs, lorsque le temps de démarrage est important, c'est quelque chose que vous devez considérer, en utilisant CDS. CDS d'application est essentiellement la capacité que CDS vous donne de vider toutes vos classes de votre propre code. Votre propre application, vous pouvez créer une archive pour CDS.

CDS introduit en Java 5. Il a été open-source, je pense, en 10, avec l'application CDS. Dans Java 9, c'était une fonctionnalité commerciale. Il a été open-source en 10. Nous continuons d'améliorer CDS. Par exemple, dans 13, nous avons ajouté Dynamic CDS, qui donne la possibilité de créer l'archive de vos classes lorsque votre application se termine. Fondamentalement, vous exécutez votre application une fois et à la fin lorsqu'elle se termine, elle créera une archive avec toutes les classes.

GraalVM

Vous pouvez aller plus loin, si le temps de démarrage et l'encombrement sont importants. Vous pouvez regarder GraalVM. GraalVM est un projet géré par Oracle Labs. Il s'agit d'une machine virtuelle polyglotte hautes performances qui offre de nombreuses fonctionnalités. Côté Java, il y a cette API polyglotte que vous pouvez utiliser pour interagir avec différents langages. Ils ont un compilateur JIT, qui est écrit en Java. Il s'agit d'un compilateur JIT que vous pouvez utiliser pour brancher et remplacer C2 dans HotSpot. La bonne chose à ce sujet est que le compilateur JIT est écrit en Java. C'est très performant. Ensuite, ils ont ces capacités d'image native, qui vous permettent essentiellement de prendre une application Java, donc un bytecode, et de la transformer en un exécutable natif pour une plate-forme donnée.

Jetons un œil à GraalVM. Java HelloWorld. Tout d'abord, je compile l'application, puis j'utilise cet outil d'image native de Graal pour le compiler AOT. Fondamentalement, ce que je donne à native-image est une classe simple pour mon application. Je vais lui donner une application. Il passera par cet outil d'image native qui analysera le code pour trouver tous les chemins d'exécution potentiels. Ensuite, il supprimera toutes les branches mortes. Une fois qu'il a cela, il transformera ce code en code natif. Code machine qui s'exécutera sur une plateforme donnée, dans ce cas, c'est une plateforme Mach. J'aurai en terme de sortie, un exécutable Mach. C'est fait. Cela a pris 42 secondes. Si nous regardons ici, nous avons ce fichier HelloWorld. Si nous regardons le type de ce fichier, c'est l'exécutable Mach OS 64 bits que je peux évidemment invoquer. Vous voyez que le temps de démarrage est assez rapide, 13 millisecondes. C'est assez rapide. Si nous regardons la taille de cet exécutable, 8 Mo. C'est la seule chose dont j'ai besoin. Aucun Java Runtime externe n'est nécessaire.

Si j'ai comparé cela à du Java pur, pour cela, je vais passer à une autre version de Java. Commençons par compiler la classe avec cette version, java HelloWorld. Disons le temps, 143 minutes. Vous voyez que la version Java est différente. C'est Java 14. Pour être honnête avec vous, j'ai un tas de JDK installé sur ma machine, donc cela a un peu de temps. Si je veux obtenir un résultat plus précis, je dois appeler directement le bon JDK, JDK-14 RC1, home / bin / java HelloWorld. Je dois chronométrer ça. J'étais à 145 millisecondes et cette fois je suis à 93, 93, 83. C'est un chiffre plus précis. Pourtant, nous n'avons pas la performance en termes de temps que nous avons avec Graal, essentiellement. Pourquoi? Parce qu'ici nous avons une JVM à part entière, nous avons HotSpot avec toutes les capacités de HotSpot.

Voilà les capacités de l'image native GraalVM. Il y a quelques limitations. Il ne prend en charge que Java 8 et 11. Il existe également des limitations, la plupart du temps prises en charge, cela signifie qu'il fonctionne, mais vous devez le configurer, l'utilisation d'un JNI, par exemple. Ensuite, il y a quelques éléments qui ne sont pas pris en charge. Le fait est que ce que vous obtiendrez en termes de sortie est un exécutable natif. Vous perdez la portabilité de Java, vous n'avez pas de JAR. Vous avez un exécutable natif. C'est quelque chose que vous devez garder à l'esprit. Vous perdrez également la possibilité d'utiliser certains des outils que vous pourriez avoir du côté Java, pour gérer ou observer votre application, JMX, ces choses pourraient ne pas être disponibles. C'est quelque chose que vous devez garder à l'esprit. Pourtant, si le temps de démarrage est quelque chose d'important, vous devez regarder l'image native GraalVM. La chose aussi avec GraalVM native-image est que l'empreinte mémoire sera plus faible. Quelque chose que Graal n'a pas, c'est par exemple le GC et le G1. Ils ont un GC basé sur G1 à faible latence qui a été introduit. Je ne suis pas sûr que ce soit encore toutes les fonctionnalités. De toute évidence, ils n'ont pas toutes les capacités que nous avons avec les HotSpots en termes de GC, par exemple.

G1 GC

En parlant de GC, en 14, nous avons amélioré le GC G1 avec les capacités NUMA. NUMA signifie accès mémoire non uniforme. Cela signifie essentiellement que votre mémoire n'est pas toujours à égale distance du processeur. C'est une bonne idée lorsque vous accédez à quelque chose en mémoire que votre code en est conscient. Le GC est désormais capable de gérer l'architecture NUMA.

Une autre chose qui n'est pas clairement annoncée est l'ancienne amélioration que nous apportons à la plate-forme. Entre Java 8 et 14, plus de 700 améliorations ont été apportées uniquement pour G1. Tous ensemble, ils apportent de nouvelles capacités supplémentaires. L'une que je montre sur cette diapositive est l'empreinte mémoire de G1 pour le tas natif, dans ce cas de 16 Mo. La surcharge supplémentaire dans JDK 8 pour 16 Go, c'était 4 Go de mémoire supplémentaire. En 11, nous étions juste en dessous de 3. En 14, nous sommes en dessous de 2. Fondamentalement, entre 8 et 14, nous avons baissé de 2, l'empreinte supplémentaire nécessaire pour faire face à un tas de 16 Go. Non seulement cela, nous avons également augmenté les performances de G1. Ce sont des fonctionnalités qui ne sont pas annoncées, mais que vous devez garder à l'esprit lorsque vous regardez une nouvelle version de Java. C'est quelque chose qui est également important lorsque votre JVM s'exécute dans des conteneurs. L'empreinte est évidemment très importante.

Sécurité

Parlons de sécurité. Vous avez peut-être vu ce tweet qui dit que les 10 conteneurs Docker les plus populaires contiennent chacun au moins 30 vulnérabilités. La bonne nouvelle, Java n'est pas là. Voilà la bonne nouvelle. La mauvaise nouvelle, c'est que nous ne sommes pas à l'abri. C'est quelque chose qui est apparu sur la liste de diffusion OpenJDK. Dans ce cas, c'était Debian, ils ont publié une version GA de quelque chose, avant même qu'elle ne soit officiellement GA. Le code source est là pour que tout le monde puisse construire à partir du code source et prétendre que c'est GA. Fais attention.

Il y a deux semaines, je l'ai vu. Quelqu'un cherchait une version Linux openjdk-11-GA pour musl. Le fait est qu'il n'y a pas une telle construction GA. Néanmoins, quelqu'un a pensé que si vous prenez cette version particulière, vous pouvez la considérer comme la version OpenJDK-11 Alpine General Availability, même si cette version n'existe pas. Ensuite, vous regardez le fichier Docker. C'est très petit, mais en gros, le fichier Docker fait un wget à partir d'un fichier provenant d'une université en Autriche via HTTP. La bonne nouvelle est qu'ils vérifient la somme de contrôle SHA-256, mais à partir de la même source via HTTP. C'est sur Internet, donc ça devrait être vrai. Ensuite, d'autres projets utilisaient cela comme Linux 11-GA. Mon conseil ici, en ce qui concerne Java, mais aussi le conteneur en général, vous devez choisir judicieusement votre image de base. Non seulement cela, vous devez le sécuriser. Vous ne devriez pas faire confiance à quelqu'un d'autre pour le faire. Ne croyez pas ce que vous lisez sur Internet.

Conteneur sans racines

Il y a ces nouvelles tendances, où c'est une bonne idée d'exécuter des conteneurs sans racines. Fondamentalement, nous voulons donner le moins de privilèges possible au conteneur. C'est un peu délicat à faire. Docker, ils ont un mode sans racine. La dernière fois que j'ai vérifié, c'était toujours dans des fonctionnalités expérimentales. Si vous voulez faire des conteneurs purs sans racines, vous devez utiliser Podman. Pour ce Podman, utilisez cgroups v2, ce qui n'est pas nouveau. Cgroups v2 a été ajouté au noyau Linux il y a six ans. Il n'était pas activé par défaut. Il est désormais activé par défaut sur Fedora. Fedora est la première distribution majeure qui permet la prise en charge de cgroups v2 par défaut. Avec cela, vous pouvez facilement faire un conteneur sans racine. Côté JDK, vous avez besoin de JDK 15. JDK 15, nous avions un support pour cgroups v2. Je mets un petit astérisque, souvenez-vous de la première diapositive, la diapositive d'avertissement qui dit essentiellement que c'est là, le jour est là, mais c'est vraiment JID. JDK 15 prendrait en charge cgroups v2. Si cgroups v2 n'est pas pris en charge sur la plateforme, il reviendra automatiquement à cgroups v1 comme aujourd'hui.

Bon sens

Ensuite, en termes de sécurité, c'est juste du bon sens. Choisissez judicieusement votre image de base. Le reste n'est qu'un pur récipient. Attention aux certificats que vous avez dans votre image de conteneur, il existe un tas d'offres de sécurité d'outils que vous pouvez utiliser pour analyser votre image afin de détecter les vulnérabilités. C'est aussi un conteneur. L'idée de base, vous devriez essayer de réduire autant que possible les attaques potentielles en surface. Du côté de Java, nous avons JLink qui est utile pour y parvenir.

Le JDK est livré avec un tas d'outils, jcmd, jinfo, jps, jmap. Ce que nous avons tendance à oublier, c'est que nous pouvons également les utiliser sur l'hôte. Pour ce faire, évidemment, l'hôte doit avoir un accès privilégié. Ce n'est pas nécessairement une bonne idée. You can also run them within the containers, doing a Docker exec, and then the name of the container, and then for example jinfo, doing that you can get useful information about the JVM that is running inside your container without having to do anything else. That's something that you should try to leverage.

Something else that is coming in the JDK since 11 is JDK Flight Recorder. JFR is basically a black box that is built into the JVM. The JVM is emitting event. It's very low overhead. That's something that you can use in production. Then you have the ability to analyze those events. Until now, those events, the analysis of those events were basically done postmortem, or after the fact. You start the recording, you stop the recording, you don't do recording, and then you do the analysis. In JDK 14, we have the ability to stream the event as they happen. You can analyze the event in process, doesn't really make sense in this case, but you can also analyze the event outside of the process. What you can have is you have your container that is using JFR. It's low overhead. That's something that you can have on all the time. For example, the repository is basically where the events are being put, can be in the volumes that you can access from different containers or from the host. That means that you can now start to think about scenarios that are using JFR to basically emit event from the JVM that is running within the containers and consume them outside. That's something which is new in JDK 14.

Emballer

JVM in containers, the JVM needs to behave as a good container citizen. The advice here is, you need to use a recent version of the JVM. Don't rely on an old version of the JVM like I did at the beginning. Then I'll discuss quickly some tricks and tools that we can use to reduce latency. That's something which is important in the container space, keeping in mind that there are two types of latency. First, container latency, so the time it takes to start the container, then the Java application latency. Then something which is important is that all the other investment that we're doing into OpenJDK are also leaking into containers. If I take for example, Loom, Panama, ZGC, all those features which are not by design, conceived for containers, will also work nicely when they are being deployed into containers. That's also something that you need to look at over time.

JVM in containers, choose your base image wisely, secure it. Use the latest Java version, and never use Java latest, that base image. We're trying to get rid of that one, but it's very difficult. That's only something that you can use on stage when you're doing demo and you want to clearly make a point to not use it. Only rely on actively supported version. They are all container aware. JVM ergonomics will kick in. There is this flag that I often see, use container support. Those there, you don't need to use that flag anymore because the JVM is by default using the container support. You should only use that flag if you explicitly want to disable the container support, assuming you have a valid reason. Then, don't use a full JDK, but try to use a JRE, or a custom Java Runtime for your code that is running into containers.

Questions et réponses

Participant 1: You mentioned that one of GraalVM's limitations is invokedynamic. All of invokedynamic?

Delabassee: No. It's just a copy and paste from their site, invokedynamic works, but something that I didn't really check. That's a copy and paste from the GitHub page from Graal, but no. I want to make sure that I use the right limitation that they're saying.

Participant 1: Lambdas work?

Delabassee: Yes.

Participant 2: I just wanted to ask you about the thing that you mentioned with all the official supported JVMs being container aware. Is that valid from Java 8 onwards for all of them?

Delabassee: Is Java 8 supported into containers?

Participant 2: As in, container aware.

Delabassee: I believe it's starting from 8, 199. My advice if you're using Java 8, use the latest version of Java 8. The container support has been added in, I think 199. Yes. If you use a recent version of 8, you're on the good side. Having said that, it's a good idea also to look at 11 and more for just the extra features that are provided.

Participant 2: I was just wondering if you still need to use the two flags, ExperimentalVMOptions in the cgroup, or something.

Delabassee: No. Those have been removed. You need to look if it's in 199 or in 210. Those flags are removed. The container support is by default.

See more presentations with transcripts

Commentaires

Laisser un commentaire

Votre commentaire sera révisé par les administrateurs si besoin.