So machen Sie Java-Prozesse unter Linux / Docker einfach und unkompliziert

Als DevOps-Ingenieur arbeite ich häufig an der Automatisierung der Installation und Konfiguration einer Vielzahl von IT-Systemen in verschiedenen Umgebungen: von Containern bis zur Cloud. Ich musste mit vielen Systemen arbeiten, die auf dem Java-Stack basierten: von klein (wie Tomcat) bis groß (Hadoop, Cassandra usw.).


Darüber hinaus verfügte fast jedes dieser Systeme, selbst das einfachste, aus irgendeinem Grund über ein komplexes, einzigartiges Startsystem. Zumindest waren dies mehrzeilige Shell-Skripte wie in Tomcat und sogar ganze Frameworks wie in Hadoop . Mein aktueller „Patient“ in dieser Serie, der mich zum Schreiben dieses Artikels inspiriert hat, ist das Nexus OSS 3- Artefakt-Repository, dessen Startskript ca. 400 Codezeilen umfasst.


Die Deckkraft, Redundanz und Komplexität von Startskripten verursacht Probleme, selbst wenn eine Komponente manuell auf dem lokalen System installiert wird. Stellen Sie sich nun vor, Sie müssen eine Reihe solcher Komponenten und Dienste in einen Docker-Container packen, eine weitere Abstraktionsebene für eine angemessene Orchestrierung schreiben, diese in einem Kubernetes-Cluster bereitstellen und diesen Prozess in Form einer CI / CD-Pipeline implementieren ...


Kurz gesagt, schauen wir uns das Beispiel des erwähnten Nexus 3 an, wie man aus dem Labyrinth der Shell-Skripte zu etwas zurückkehrt, das java -jar <program.jar> ähnlicher ist, da praktische moderne DevOps-Tools verfügbar sind.


Woher kommt diese Komplexität?


Kurz gesagt, in der Antike, als UNIX nicht erwähnt wurde, als gefragt wurde: "Im Sinne von Linux?", Gab es kein Systemd und Docker usw., wurden tragbare Shell-Skripte (Init-Skripte) und PID- verwendet, um die Prozesse zu steuern Dateien. Init-Skripte legen die erforderlichen Umgebungseinstellungen fest, die in verschiedenen UNIX-Einstellungen unterschiedlich waren, und haben den Prozess abhängig von den Argumenten mithilfe der ID aus der PID-Datei gestartet oder neu gestartet / gestoppt. Der Ansatz ist einfach und klar, aber diese Skripte funktionierten nicht mehr in jeder ungewöhnlichen Situation und erforderten manuelle Eingriffe. Sie konnten nicht mehrere Kopien des Prozesses ausführen ... aber nicht den Punkt.


Wenn Sie sich also die oben in Java-Projekten erwähnten Startskripte genau ansehen, können Sie die offensichtlichen Anzeichen dieses prähistorischen Ansatzes erkennen, einschließlich der Erwähnung von SunOS, HP-UX und anderen UNIX-Systemen. Normalerweise machen solche Skripte ungefähr so:


  • Verwenden Sie die POSIX-Shell-Syntax mit all ihren Krücken für die UNIX / Linux-Portabilität
  • Bestimmen Sie die Betriebssystemversion und geben Sie sie durch uname , /etc/*release usw. frei.
  • Sie suchen in den Ecken des Dateisystems nach JRE / JDK und wählen die am besten geeignete Version nach cleveren Regeln aus, die manchmal auch für jedes Betriebssystem spezifisch sind
  • Die numerischen JVM-Parameter werden berechnet, z. B. die Speichergröße ( -Xms , -Xmx ), die Anzahl der GC-Threads usw.
  • Optimieren Sie die JVM-Parameter durch -XX unter Berücksichtigung der Besonderheiten der ausgewählten Version von JRE / JDK
  • Suchen Sie in den umliegenden Verzeichnissen, Konfigurationsdateien usw. nach ihren Komponenten, Bibliotheken, Pfaden zu ihnen.
  • Passen Sie die Umgebung an: ulimits, Umgebungsvariablen usw.
  • generiere CLASSPATH mit einer Schleife wie: for f in $path/*.jar; do CLASSPATH="${CLASSPATH}:$f"; done for f in $path/*.jar; do CLASSPATH="${CLASSPATH}:$f"; done
  • analysierte Befehlszeilenargumente: start|stop|restart|reload|status|...
  • Kompilieren Sie den Java-Befehl, den Sie letztendlich ausführen müssen, von oben
  • und führen Sie schließlich diesen Java-Befehl aus . Oft werden dieselben berüchtigten PID-Dateien & , nohup , spezielle TCP-Ports und andere Tricks aus dem letzten Jahrhundert explizit oder implizit verwendet (siehe das Beispiel von Karaf ).

Das erwähnte Nexus 3-Startskript ist ein geeignetes Beispiel für ein solches Skript.


Tatsächlich versucht die oben aufgeführte Skriptlogik sozusagen, den Systemadministrator zu ersetzen, der von Anfang bis Ende alles manuell für ein bestimmtes System installiert und konfiguriert. Generell ist es jedoch unmöglich, die Anforderungen unterschiedlichster Systeme zu berücksichtigen. Daher stellt sich im Gegenteil ein Kopfschmerz heraus, sowohl für Entwickler, die diese Skripte unterstützen müssen, als auch für Systemingenieure, die diese Skripte später verstehen müssen. Aus meiner Sicht ist es für einen Systemingenieur viel einfacher, die JVM-Parameter einmal zu verstehen und so zu konfigurieren, wie es sollte, als bei jeder Installation eines neuen Systems die Feinheiten seiner Startskripte zu verstehen.


Was tun?


Vergib! KISS und YAGNI sind in unseren Händen. Darüber hinaus ist das Jahr 2018 in der Werft, was bedeutet, dass:


  • mit sehr wenigen Ausnahmen UNIX == Linux
  • Das Prozesssteuerungsproblem wird sowohl für einen separaten Server ( Systemd , Docker ) als auch für Cluster ( Kubernetes usw.) gelöst.
  • Es gibt eine Reihe praktischer Konfigurationsverwaltungstools ( Ansible usw.)
  • Die vollständige Automatisierung ist in der Verwaltung angekommen und hat sich bereits gründlich verfestigt: Anstatt fragile, einzigartige "Schneeflockenserver" manuell einzurichten , ist es jetzt möglich, einheitliche reproduzierbare virtuelle Maschinen und Container mithilfe einer Reihe praktischer Tools, einschließlich der oben genannten Ansible und Docker, automatisch zusammenzustellen
  • Tools zum Sammeln von Laufzeitstatistiken werden häufig sowohl für die JVM selbst ( Beispiel ) als auch für eine Java-Anwendung ( Beispiel ) verwendet.
  • und vor allem erschienen Experten: System- und DevOps-Ingenieure, die die oben aufgeführten Technologien verwenden und verstehen, wie die JVM auf einem bestimmten System ordnungsgemäß installiert und anschließend basierend auf den gesammelten Laufzeitstatistiken angepasst wird

Lassen Sie uns also die Funktionalität von Startskripten unter Berücksichtigung der oben aufgeführten Punkte noch einmal durchgehen, ohne zu versuchen, die Arbeit für den Systemingenieur zu erledigen, und alle "unnötigen" von dort entfernen.


  • POSIX-Shell-Syntax/bin/bash
  • Erkennung der Betriebssystemversion ⇒ UNIX == Linux, wenn es betriebssystemspezifische Parameter gibt, können Sie diese in der Dokumentation beschreiben
  • JRE / JDK-Suche ⇒ Wir haben die einzige Version, und dies ist OpenJDK (naja, oder Oracle JDK, wenn Sie es wirklich brauchen), java und das Unternehmen befinden sich im Standardsystempfad
  • Berechnung der numerischen Parameter JVM, Abstimmung der JVM ⇒ Dies kann in der Dokumentation zur Anwendungsskalierung beschrieben werden
  • Suchen Sie nach Ihren Komponenten und Bibliotheken ⇒ Beschreiben Sie die Struktur der Anwendung und deren Konfiguration in der Dokumentation
  • Umgebungseinstellung ⇒ Beschreiben Sie die Anforderungen und Funktionen in der Dokumentation
  • CLASSPATH-Generation-cp path/to/my/jars/* oder allgemein Uber-JAR
  • Analysieren von Befehlszeilenargumenten ⇒ es wird keine Argumente geben, weil Der Prozessmanager kümmert sich um alles außer dem Start
  • Java-Befehlsassembly
  • Ausführung des Java-Befehls

Daher müssen wir nur einen Java-Befehl der Form java <opts> -jar <program.jar> mit dem ausgewählten Prozessmanager (Systemd, Docker usw.) zusammenstellen und ausführen. Alle Parameter und Optionen ( <opts> ) liegen im Ermessen des Systemingenieurs, der sie an eine bestimmte Umgebung anpasst. Wenn die Liste der Optionen <opts> ziemlich lang ist, können Sie wieder zur Idee eines Startskripts zurückkehren, in diesem Fall jedoch so kompakt und deklarativ wie möglich , d. H. keine Softwarelogik enthalten.


Beispiel


Lassen Sie uns als Beispiel sehen, wie Sie das Nexus 3-Startskript vereinfachen können.


Die einfachste Option, um nicht in den Dschungel dieses Skripts zu gelangen - führen Sie es einfach unter realen Bedingungen aus ( ./nexus start ) und sehen Sie sich das Ergebnis an. Sie können beispielsweise die vollständige Liste der Argumente der ausgeführten Anwendung in der Prozesstabelle finden (über ps -ef ) oder das Skript im Debug-Modus bash -x ./nexus start ( bash -x ./nexus start ), um den gesamten Prozess seiner Ausführung und ganz am Ende den bash -x ./nexus start zu beobachten.


Am Ende hatte ich den folgenden Java-Befehl
 /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 

Wenden Sie zunächst ein paar einfache Tricks an:


  • ändere /the/long/and/winding/road/to/my/java zu java , weil sie sich im systempfad befindet
  • Fügen Sie die Liste der Java-Parameter in ein separates Array ein , sortieren Sie sie und entfernen Sie Duplikate

Wir bekommen schon etwas verdaulicheres
 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 

Jetzt kannst du tief gehen.


Install4j ist ein solches grafisches Java-Installationsprogramm. Es scheint für die Erstinstallation des Systems verwendet zu werden. Wir brauchen es nicht auf dem Server, wir entfernen es.


Wir sind uns einig über die Platzierung von Nexus-Komponenten und -Daten im Dateisystem:


  • /opt/nexus-<version> die Anwendung selbst in /opt/nexus-<version>
  • Erstellen Sie der /opt/nexus -> /opt/nexus-<version> einen symbolischen Link /opt/nexus -> /opt/nexus-<version>
  • Platzieren Sie das Skript selbst anstelle des Originals als /opt/nexus/bin/nexus
  • Alle Daten unseres Nexus werden in einem separaten Dateisystem gespeichert, das als /data/nexus

Die Erstellung von Verzeichnissen und Verknüpfungen ist das Schicksal von Konfigurationsmanagementsystemen (für alle 5-10 Zeilen in Ansible). Überlassen wir diese Aufgabe also den Systemingenieuren.


Lassen Sie unser Skript beim Start das Arbeitsverzeichnis in /opt/nexus ändern - dann können wir die Pfade zu Nexus-Komponenten in relative ändern.


Optionen des Formulars -Dkaraf.* die Einstellungen für Apache Karaf , den OSGi-Container, in den unser Nexus offensichtlich „gepackt“ ist. Ändern Sie karaf.home , karaf.base , karaf.etc und karaf.data entsprechend der Platzierung der Komponenten und verwenden karaf.data nach Möglichkeit relative Pfade.


Da CLASSPATH aus einer Liste von JAR-Dateien besteht, die sich im selben lib/ -Verzeichnis befinden, ersetzen Sie diese gesamte Liste durch lib/* (Sie müssen auch die Platzhaltererweiterung mit set -o noglob ).


Ändern Sie java in exec java damit unser Skript java als java Prozess startet (der Prozessmanager sieht diesen untergeordneten Prozess nicht), sondern sich selbst durch java ( Beschreibung von Exec ).


Mal sehen, was passiert ist:


 #!/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 

Insgesamt 27 Zeilen statt> 400, transparent, klar, deklarativ, keine unnötige Logik. Bei Bedarf kann dieses Skript einfach in eine Vorlage für Ansible / Puppet / Chef umgewandelt werden und nur die Logik hinzufügen, die für eine bestimmte Situation erforderlich ist.


Dieses Skript kann als ENTRYPOINT in einer Docker-Datei verwendet oder in der Systemd-Unit-Datei aufgerufen werden, wobei gleichzeitig ulimits und andere Systemparameter dort optimiert werden, zum Beispiel:


 [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 

Fazit


Welche Schlussfolgerungen können aus diesem Artikel gezogen werden? Im Prinzip kommt es auf ein paar Punkte an:


  1. Jedes System hat seinen eigenen Zweck, d. H. Es ist nicht notwendig, Nägel mit einem Mikroskop einzuschlagen.
  2. Einfachheitsregeln (KISS, YAGNI) - nur das implementieren, was für eine bestimmte Situation erforderlich ist.
  3. Und vor allem: Es ist cool, dass es IT-Spezialisten mit unterschiedlichen Profilen gibt. Lassen Sie uns interagieren und unsere IT-Systeme einfacher, klarer und besser machen! :) :)

Vielen Dank für Ihre Aufmerksamkeit! Ich freue mich über Feedback und konstruktive Diskussion in den Kommentaren.

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


All Articles