Comment rendre les processus Java exécutés sur Linux / Docker simples et directs

En tant qu'ingénieur DevOps, je travaille souvent sur l'automatisation de l'installation et de la configuration d'une variété de systèmes informatiques dans différents environnements: des conteneurs au cloud. J'ai dû travailler avec de nombreux systèmes basés sur la pile Java: des petits (comme Tomcat) aux grands (Hadoop, Cassandra, etc.).


De plus, presque tous ces systèmes, même les plus simples, pour une raison quelconque, avaient un système de lancement unique et complexe. Au minimum, il s'agissait de scripts shell multi-lignes, comme dans Tomcat , et même des frameworks entiers, comme dans Hadoop . Mon «patient» actuel de cette série, qui m'a inspiré pour écrire cet article, est le référentiel d'artefacts Nexus OSS 3 , dont le script de lancement prend environ 400 lignes de code.


L'opacité, la redondance et la complexité des scripts de démarrage créent des problèmes même lors de l'installation manuelle d'un composant sur le système local. Imaginez maintenant que vous devez emballer un ensemble de ces composants et services dans un conteneur Docker, écrire une autre couche d'abstraction dans le sens d'une orchestration adéquate, le déployer dans un cluster Kubernetes et implémenter ce processus en tant que pipeline CI / CD ...


En bref, regardons l'exemple du Nexus 3 mentionné, comment revenir du labyrinthe de scripts shell à quelque chose de plus similaire à java -jar <program.jar> , compte tenu de la disponibilité d'outils DevOps modernes et pratiques.


D'où vient cette complexité?


En bref, dans les temps anciens, quand UNIX n'était pas mentionné lors de la demande: «dans le sens de Linux?», Il n'y avait pas Systemd et Docker, etc., des scripts shell portables (scripts init) et PID - étaient utilisés pour contrôler les processus fichiers. Les scripts d'initialisation définissent les paramètres d'environnement nécessaires, qui étaient différents dans les différents UNIX, et, selon les arguments, ont démarré le processus ou l'ont redémarré / arrêté à l'aide de l'ID du fichier PID. L'approche est simple et claire, mais ces scripts ont cessé de fonctionner dans toutes les situations inhabituelles, nécessitant une intervention manuelle, ne vous ont pas permis d'exécuter plusieurs copies du processus ... mais pas le point.


Donc, si vous regardez attentivement les scripts de démarrage mentionnés ci-dessus dans les projets Java, vous pouvez voir les signes évidents de cette approche préhistorique, y compris même la mention de SunOS, HP-UX et d'autres UNIX. En règle générale, ces scripts font quelque chose comme ceci:


  • utiliser la syntaxe shell POSIX avec toutes ses béquilles pour la portabilité UNIX / Linux
  • déterminer la version et la version du système d'exploitation via uname , /etc/*release , etc.
  • ils recherchent JRE / JDK dans les recoins du système de fichiers et sélectionnent la version la plus «appropriée» selon des règles astucieuses, parfois aussi spécifiques à chaque OS
  • Les paramètres numériques JVM sont calculés, par exemple, la taille de la mémoire ( -Xms , -Xmx ), le nombre de threads GC, etc.
  • optimiser les paramètres JVM via -XX en tenant compte des spécificités de la version sélectionnée de JRE / JDK
  • recherchez leurs composants, leurs bibliothèques, leurs chemins d'accès dans les répertoires environnants, les fichiers de configuration, etc.
  • personnaliser l'environnement: ulimits, variables d'environnement, etc.
  • générer CLASSPATH avec une boucle comme: for f in $path/*.jar; do CLASSPATH="${CLASSPATH}:$f"; done for f in $path/*.jar; do CLASSPATH="${CLASSPATH}:$f"; done
  • arguments de ligne de commande analysés: start|stop|restart|reload|status|...
  • compiler la commande Java que vous devez finalement exécuter à partir de ce qui précède
  • et enfin exécuter cette commande java . Souvent, les mêmes fichiers PID notoires, & , nohup , ports TCP spéciaux et autres astuces du siècle dernier sont utilisés explicitement ou implicitement (voir l' exemple de Karaf )

Le script de lancement de Nexus 3 mentionné est un exemple approprié d'un tel script.


En fait, toute la logique de script répertoriée ci-dessus, pour ainsi dire, tente de remplacer l'administrateur système, qui installerait et configurerait tout manuellement pour un système spécifique du début à la fin. Mais en général, il est impossible de prendre en compte les exigences des systèmes les plus divers. Par conséquent, il s'avère, au contraire, un casse-tête, à la fois pour les développeurs qui doivent prendre en charge ces scripts et pour les ingénieurs système qui doivent comprendre ces scripts plus tard. De mon point de vue, il est beaucoup plus facile pour un ingénieur système de comprendre les paramètres JVM une fois et de les configurer comme il se doit, que de comprendre les subtilités de ses scripts de démarrage chaque fois que vous installez un nouveau système.


Que faire?


Pardonne! KISS et YAGNI sont entre nos mains. De plus, l'année 2018 est dans la cour, ce qui signifie que:


  • à quelques exceptions près, UNIX == Linux
  • le problème de contrôle de processus est résolu à la fois pour un serveur distinct ( Systemd , Docker ) et pour les clusters ( Kubernetes , etc.)
  • Il existe de nombreux outils de gestion de configuration pratiques ( Ansible , etc.)
  • l'automatisation totale est arrivée à l'administration et s'est déjà solidifiée à fond: au lieu de configurer manuellement de fragiles "serveurs flocons de neige" uniques , il est maintenant possible d'assembler automatiquement des machines et des conteneurs virtuels reproductibles unifiés à l'aide d'un certain nombre d'outils pratiques, y compris Ansible et Docker susmentionnés
  • les outils de collecte de statistiques d'exécution sont largement utilisés, à la fois pour la JVM elle-même ( exemple ) et pour une application Java ( exemple )
  • et, plus important encore, des experts sont apparus: des ingénieurs système et DevOps qui peuvent utiliser les technologies répertoriées ci-dessus et comprendre comment installer correctement la JVM sur un système spécifique et l'ajuster ensuite en fonction des statistiques d'exécution collectées

Reprenons donc la fonctionnalité des scripts de démarrage, en tenant compte des points énumérés ci-dessus, sans essayer de faire le travail pour l'ingénieur système, et supprimons tous ceux "inutiles" à partir de là.


  • Syntaxe du shell POSIX/bin/bash
  • Détection de version de système d'exploitation ⇒ UNIX == Linux, s'il existe des paramètres spécifiques au système d'exploitation, vous pouvez les décrire dans la documentation
  • Recherche JRE / JDK ⇒ nous avons la seule version, et c'est OpenJDK (enfin, ou Oracle JDK, si vous en avez vraiment besoin), java et l'entreprise sont dans le chemin système standard
  • calcul des paramètres numériques JVM, tuning JVM ⇒ cela peut être décrit dans la documentation de mise à l'échelle de l'application
  • recherchez vos composants et bibliothèques ⇒ décrire la structure de l'application et comment la configurer dans la documentation
  • réglage de l'environnement ⇒ décrire les exigences et les fonctionnalités de la documentation
  • Génération CLASSPATH-cp path/to/my/jars/* ou même, en général, Uber-JAR
  • analyse des arguments de ligne de commande ⇒ il n'y aura pas d'arguments, car le gestionnaire de processus s'occupe de tout sauf du lancement
  • Assemblage de commande Java
  • exécution de commande java

Par conséquent, nous avons juste besoin d'assembler et d'exécuter une commande Java de la forme java <opts> -jar <program.jar> utilisant le gestionnaire de processus sélectionné (Systemd, Docker, etc.). Tous les paramètres et options ( <opts> ) sont laissés à la discrétion de l'ingénieur système, qui les ajustera à un environnement spécifique. Si la liste des options <opts> assez longue, vous pouvez à nouveau revenir à l'idée d'un script de démarrage, mais, dans ce cas, aussi compact et déclaratif que possible , c'est-à-dire ne contenant aucune logique logicielle.


Exemple


À titre d'exemple, voyons comment vous pouvez simplifier le script de démarrage du Nexus 3 .


L'option la plus simple, pour ne pas entrer dans la jungle de ce script - il suffit de l'exécuter dans des conditions réelles ( ./nexus start ) et de regarder le résultat. Par exemple, vous pouvez trouver la liste complète des arguments de l'application en cours d'exécution dans la table de processus (via ps -ef ), ou exécuter le script en mode débogage ( bash -x ./nexus start ) pour observer tout le processus de son exécution et, à la toute fin, la commande de lancement.


Je me suis retrouvé avec la commande Java suivante
 /usr/java/jdk1.8.0_171-amd64/bin/java -server -Dinstall4j.jvmDir=/usr/java/jdk1.8.0_171-amd64 -Dexe4j.moduleName=/home/nexus/nexus-3.12.1-01/bin/nexus -XX:+UnlockDiagnosticVMOptions -Dinstall4j.launcherId=245 -Dinstall4j.swt=false -Di4jv=0 -Di4jv=0 -Di4jv=0 -Di4jv=0 -Di4jv=0 -Xms1200M -Xmx1200M -XX:MaxDirectMemorySize=2G -XX:+UnlockDiagnosticVMOptions -XX:+UnsyncloadClass -XX:+LogVMOutput -XX:LogFile=../sonatype-work/nexus3/log/jvm.log -XX:-OmitStackTraceInFastThrow -Djava.net.preferIPv4Stack=true -Dkaraf.home=. -Dkaraf.base=. -Dkaraf.etc=etc/karaf -Djava.util.logging.config.file=etc/karaf/java.util.logging.properties -Dkaraf.data=../sonatype-work/nexus3 -Djava.io.tmpdir=../sonatype-work/nexus3/tmp -Dkaraf.startLocalConsole=false -Di4j.vpt=true -classpath /home/nexus/nexus-3.12.1-01/.install4j/i4jruntime.jar:/home/nexus/nexus-3.12.1-01/lib/boot/nexus-main.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.apache.karaf.main-4.0.9.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.osgi.core-6.0.0.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.apache.karaf.diagnostic.boot-4.0.9.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.apache.karaf.jaas.boot-4.0.9.jar com.install4j.runtime.launcher.UnixLauncher start 9d17dc87 '' '' org.sonatype.nexus.karaf.NexusMain 

Tout d'abord, appliquez-lui quelques astuces simples:


  • changer /the/long/and/winding/road/to/my/java en java , car il se trouve dans le chemin du système
  • mettre la liste des paramètres Java dans un tableau séparé, le trier et supprimer les doublons

Nous obtenons déjà quelque chose de plus digeste
 JAVA_OPTS = ( '-server' '-Dexe4j.moduleName=/home/nexus/nexus-3.12.1-01/bin/nexus' '-Di4j.vpt=true' '-Di4jv=0' '-Dinstall4j.jvmDir=/usr/java/jdk1.8.0_171-amd64' '-Dinstall4j.launcherId=245' '-Dinstall4j.swt=false' '-Djava.io.tmpdir=../sonatype-work/nexus3/tmp' '-Djava.net.preferIPv4Stack=true' '-Djava.util.logging.config.file=etc/karaf/java.util.logging.properties' '-Dkaraf.base=.' '-Dkaraf.data=../sonatype-work/nexus3' '-Dkaraf.etc=etc/karaf' '-Dkaraf.home=.' '-Dkaraf.startLocalConsole=false' '-XX:+LogVMOutput' '-XX:+UnlockDiagnosticVMOptions' '-XX:+UnlockDiagnosticVMOptions' '-XX:+UnsyncloadClass' '-XX:-OmitStackTraceInFastThrow' '-XX:LogFile=../sonatype-work/nexus3/log/jvm.log' '-XX:MaxDirectMemorySize=2G' '-Xms1200M' '-Xmx1200M' '-classpath /home/nexus/nexus-3.12.1-01/.install4j/i4jruntime.jar:/home/nexus/nexus-3.12.1-01/lib/boot/nexus-main.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.apache.karaf.main-4.0.9.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.osgi.core-6.0.0.jar:/home/nexus/nexus-3.12.1-01/lib/boot/org.apache.karaf.diagnostic.boot-4.0.9.jar:/home/nexus/nexus-3.12.1-01/lib/boot/' ) java ${JAVA_OPTS[*]} com.install4j.runtime.launcher.UnixLauncher start 9d17dc87 '' '' org.sonatype.nexus.karaf.NexusMain 

Maintenant, vous pouvez aller en profondeur.


Install4j est un tel programme d'installation graphique Java. Il semble être utilisé pour l'installation initiale du système. Nous n'en avons pas besoin sur le serveur, nous le supprimons.


Nous convenons du placement des composants et des données Nexus dans le système de fichiers:


  • mettre l'application elle-même dans /opt/nexus-<version>
  • pour plus de commodité, créez un lien symbolique /opt/nexus -> /opt/nexus-<version>
  • placez le script lui-même au lieu de l'original sous /opt/nexus/bin/nexus
  • toutes les données de notre Nexus se trouveront sur un système de fichiers séparé monté sous /data/nexus

La création même de répertoires et de liens est le destin des systèmes de gestion de configuration (pour tout ce qui concerne les 5 à 10 lignes dans Ansible), alors laissons cette tâche aux ingénieurs système.


Laissez notre script au démarrage changer le répertoire de travail en /opt/nexus - alors nous pouvons changer les chemins d'accès aux composants Nexus en chemins relatifs.


Options du formulaire -Dkaraf.* les paramètres d' Apache Karaf , le conteneur OSGi dans lequel notre Nexus est évidemment «emballé». Modifiez karaf.home , karaf.base , karaf.etc et karaf.data fonction de l'emplacement des composants, en utilisant si possible des chemins relatifs.


Voyant que CLASSPATH consiste en une liste de fichiers jar qui se trouvent dans le même répertoire lib/ , remplacez cette liste entière par lib/* (vous devrez également désactiver l'extension générique avec set -o noglob ).


Remplacez java par exec java afin que notre script ne démarre pas java tant que processus enfant (le gestionnaire de processus ne verra pas ce processus enfant), mais qu'il se "remplace" lui-même par java ( description de exec ).


Voyons ce qui s'est passé:


 #!/bin/bash JAVA_OPTS=( '-Xms1200M' '-Xmx1200M' '-XX:+UnlockDiagnosticVMOptions' '-XX:+LogVMOutput' '-XX:+UnsyncloadClass' '-XX:LogFile=/data/nexus/log/jvm.log' '-XX:MaxDirectMemorySize=2G' '-XX:-OmitStackTraceInFastThrow' '-Djava.io.tmpdir=/data/nexus/tmp' '-Djava.net.preferIPv4Stack=true' '-Djava.util.logging.config.file=etc/karaf/java.util.logging.properties' '-Dkaraf.home=.' '-Dkaraf.base=.' '-Dkaraf.etc=etc/karaf' '-Dkaraf.data=/data/nexus/data' '-Dkaraf.startLocalConsole=false' '-server' '-cp lib/boot/*' ) set -o noglob cd /opt/nexus \ && exec java ${JAVA_OPTS[*]} org.sonatype.nexus.karaf.NexusMain 

Un total de 27 lignes au lieu de> 400, transparent, clair, déclaratif, pas de logique inutile. Si nécessaire, ce script peut facilement être transformé en un modèle pour Ansible / Puppet / Chef et n'ajouter que la logique nécessaire à une situation spécifique.


Ce script peut être utilisé comme ENTRYPOINT dans un Dockerfile ou appelé dans le fichier-unité Systemd, en même temps qu'il ajuste les ulimits et d'autres paramètres système, par exemple:


 [Unit] Description=Nexus After=network.target [Service] Type=simple LimitNOFILE=1048576 ExecStart=/opt/nexus/bin/nexus User=nexus Restart=on-abort [Install] WantedBy=multi-user.target 

Conclusion


Quelles conclusions peut-on tirer de cet article? En principe, cela se résume à quelques points:


  1. Chaque système a son propre but, c'est-à-dire qu'il n'est pas nécessaire de marteler les ongles avec un microscope.
  2. Règles de simplicité (KISS, YAGNI) - pour mettre en œuvre uniquement ce qui est nécessaire pour une situation spécifique donnée.
  3. Et le plus important: c'est cool qu'il y ait des informaticiens de différents profils. Interagissons et rendons nos systèmes informatiques plus simples, plus clairs et meilleurs! :)

Merci de votre attention! Je serai heureux de recevoir des commentaires et une discussion constructive dans les commentaires.

Source: https://habr.com/ru/post/fr415893/


All Articles