Optimale Java-Laufzeitumgebungen in der Cloud

Die Mehrheit der Cloud-Anbieter stellen ihre Dienste nach verschiedenen Cloud-Modellen bereit. IaaS, Paas, CaaS und FaaS sind die im Moment zu verzeichnenden Hauptmodelle. Sie alle bestehen unter der Haube nur aus Tausenden von Servern, Festplatten, Routern und Kabeln. Sie fügen lediglich Abstraktionsebenen hinzu, um die Verwaltung zu vereinfachen und die Entwicklungsgeschwindigkeit zu erhöhen.

Das FaaS-Modell (Function-as-a-service) ermöglicht es Entwicklern, Code (sogenannte Funktionen) auszuführen, ohne eine komplexe Infrastruktur bereitstellen oder warten zu müssen. Cloud-Provider stellen Kundencode in vollständig verwalteten, zeitlich in ihrer Lebensdauer begrenzten Containern bereit, die nur während des Aufrufs der Funktionen aktiv sind. Unternehmen können sich dadurch nun auf die Entwicklung neuer Funktionen und Innovationen konzentrieren und zahlen nur für die Rechenzeit, die sie verbrauchen. Deswegen eignet sich dieser auch Serverless Computing genannte Ansatz sehr gut für eventbasierte Architekturen und kann Unternehmen zu niedrigeren Betriebskosten in der Cloud verhelfen.

Wie bei anderen Cloud-Lösungen stehen den großen Vorteilen auch Nachteile gegenüber. Die Vorteile, die sich durch die sofortige, unbegrenzte Skalierbarkeit von Serverless ergeben, bringen Einschränkungen und einzigartige Überlegungen mit sich, die wir berücksichtigen müssen. Eines der größten Probleme, mit denen wir konfrontiert werden, sind Kaltstarts.

Das Kaltstartproblem in Serverless-Architekturen

Ein Kaltstart ist die zusätzliche Zeit, die eine Funktion zur Ausführung benötigt, wenn sie nicht erst kürzlich aufgerufen wurde. Dieser tritt typischerweise auf, wenn eine Anfrage beim Router oder API Gateway ankommt, ohne dass eine Funktionsinstanz zur Bearbeitung der Anfrage verfügbar ist. Wenn die Plattform eine erste Instanz einer Funktion hochfahren muss, benötigen die zugrundeliegenden Ressourcen noch Zeit für die Initialisierung. Dieses Bootstrapping findet auch statt, wenn der Autoscaler zusätzliche Instanzen bereitstellt, um den Datenverkehr zu verarbeiten. Hier entstehen die wunderbaren Funktionen der automatischen Skalierbarkeit und der Pay-as-You-Go-Cloud-Computing (PAYG), da Anbieter wie AWS die Ressourcen so verwalten können, dass sie genau den Anforderungen der ausgeführten Anwendung entsprechen. Sowohl Amazon Lambda als auch Microsoft Azure haben ein ähnliches Abrechnungsmodell für serverlose Funktionen. Beide berechnen den Preis nach einer Kombination aus Ausführungszeit und Speichernutzung. Lambda-Funktionen laufen in ihrem eigenen Container. Bevor sie zur Ausführung kommen, wird jedem Container die erforderliche CPU- und RAM-Kapazität zugewiesen. Nach der Ausführung der Funktion wird der zugewiesene Arbeitsspeicher mit der Zeit multipliziert, die die Funktion in Anspruch genommen hat. AWS stellt dann dem Kunden den zugewiesenen Speicher und die Laufzeit der Funktion in Rechnung.

Die Kaltstart-Phase ist für jede Funktionsinstanz unvermeidlich und führt zu einer Verzögerung, bis die Instanz auf die Anfrage(n) reagieren kann. Kaltstarts sind gewissermaßen ein inhärentes Problem des Serverless-Modells.

Die Initialisierung kann mehrere Sekunden dauern, was eine sehr lange Zeit für einen Dienst ist, der eine Reaktionszeit im einstelligen Millisekunden-Bereich aufweisen soll. Schlimmer kann es werden, wenn man es mit einer Architektur zu tun hat, in der viele serverlose Dienste verknüpft sind und alle einen Kaltstart durchmachen müssen. Obwohl FaaS-Angebote typischerweise unter Kaltstarts leiden, variiert der Overhead, der jede Plattform verursacht, je nach Größe und Laufzeitumgebung der zugrunde liegenden Implementierung der Funktionen.

Java und das Kaltstartproblem

Wie oben beschrieben wurde, ist es heutzutage aufgrund architektonischer Veränderungen immer wichtiger, sich um die Startzeit und den Ressourcenverbrauch einer Anwendung zu kümmern. Dies ist etwas, das bis vor ein paar Jahren noch kein allzu großes Thema war. Der Kaltstart von Java-Anwendungen wird hauptsächlich durch die Java Virtual Machine (JVM) verursacht. Die JVM gibt es schon seit über 25 Jahren. Sie wurde entwickelt, um vor allem eine objektorientierte Programmiersprache plattformunabhängig laufen lassen zu können. Bevor Java-Code ausgeführt wird, lädt die JVM Klassendateien in den Speicher und verifiziert den Bytecode. Beim Einsatz von modernen Java Frameworks (z. B. EJB 3, JPA, Spring) müssen sehr viel Klassen geladen und verifiziert werden, bevor die Hauptanwendung ausgeführt wird. Dies verbraucht Zeit und Speicher und hat somit direkt Einfluss auf die Startzeit und den Ressourcenverbrauch einer Anwendung. Der Vorgang wiederholt sich jedes Mal, wenn eine neue Instanz zur Ausführung kommen muss. Im Cloud-Umfeld ist dies ein Problem. Kubernetes zum Beispiel zerstört und erzeugt in regelmäßigen Abständen Pods. Je länger die Startzeit ist, desto langsamer reagiert die Anwendung auf eine Anfrage. Was kann also getan werden? Glücklicherweise gibt es seit Java 9 eine Reihe von Entwicklungen, die Entwicklern Hoffnung machen.

Warum serverlose Anwendungen mit Java?

Java ist alt, aber nicht veraltet! Die Sprache bleibt dank der vielfältigen Bemühungen einer Reihe von Unternehmen auch in der Cloud-Ära lebendig. Wenn es um gut getestete, bewährte Open-Source-Bibliotheken und -Frameworks geht, hat Java den klaren Vorteil. AWS Toolkit for Eclipse und AWS Toolkit for IntelliJ unterstützen die serverlose Anwendungsentwicklung mit Java, indem sie die Erstellung und Bereitstellung Ihrer Funktionen erheblich vereinfachen. Bei aufeinanderfolgenden Ausführungen ist Java schneller als Python und Node.js. In dieser Studie werden repräsentative Benchmark-Tests und Ergebnisse gezeigt, die die Sprachen beim Betrieb von AWS Lambda im Warmstart vergleichen. Es ist klar, dass Java in diesem Fall gewinnt, auch wenn es vielleicht nicht immer so schnell ist, wenn die Funktion kalt ist. Was nicht überraschend ist. Denn die Just-In-Time-Kompilierung ermöglicht es, eine Anwendung im Laufe der Zeit zu verstehen und den endgültigen Code je nach Beschaffenheit zu optimieren. Dies ist der Grund, warum Java für langlaufende Prozesse performanter als viele andere Sprachen ist.

Kaltstart und Speicherverbrauch mit AppCDS minimieren

Für die Entwicklung und den Betrieb von serverlosen Anwendungen waren Verbesserung an der JVM mehr als notwendig, damit Java und Java Frameworks auch weiterhin benutzt werden können. Seit dem Erscheinen von Java 10 steht ein nichtkommerzielles Tools namens Application Class-Data Sharing (AppCDS) für OpenJDK bereit. AppCDS ermöglicht es Entwicklern, Anwendungsklassen zusammen mit den JDK-Bibliotheken in einer Archivdatei zu archivieren. Eine ähnliche Möglichkeit, genannt CDS (Class-Data Sharing), gab es schon im JDK 1.5. Das CDS-Feature beschränkte sich aber nur auf die Archivierung der Bootstrap ClassLoader-Ebene, was nur sehr begrenzte Leistungsverbesserungen mit sich brachte. Wie der Name schon sagt, kann AppCDS nicht nur mit dem Bootstrap ClassLoader sondern auch mit dem Application ClassLoader und benutzerdefinierten ClassLoadern arbeiten, was den Anwendungsbereich von CDS erheblich erweitert. Das AppCDS-Archiv wird einmal erzeugt und beim Starten einer anderen Instanz derselben Java-Anwendung wiederverwendet. Die erzeugte Archivdatei kann deswegen über verschiedene JVMs hinweg geteilt werden. Die JVM kann die vorausberechneten Daten aus dem Archiv wiederverwenden. Dies nimmt die Arbeit des Ladens, Linkens und der Bytecode-Verifizierung von Klassen ab. Dadurch reduziert sich auch der Gesamtspeicherbedarf. In der Cloud ist es üblich, dass eine Java-Anwendung nach Bedarf skaliert wird, wobei mehrere JVMs die gleiche Anwendung ausführen. Dies ist ein hervorragender Anwendungsfall, der von AppCDS profitieren würde.

Die Generierung und Verwendung des AppCDS-Archivs ist ziemlich einfach. Ausreichend sind die folgenden Schritte:

  1. java -XX:DumpLoadedClassList=classes.lst -jar app-cds.jar
  2. java -Xshare:dump -XX:SharedClassListFile=classes.lst -XX:SharedArchiveFile=app-cds.jsa –class-path app-cds.jar
  3. java -Xshare:on -XX:SharedArchiveFile=appcds.jsa -jar app-cds.jar

Der erste Befehl veranlasst die Untersuchung der Klassen in app-cds.jar. Die gewonnenen Informationen werden dann in classes.lst abgelegt. Der zweite Befehl generiert aus app-cds.jar mithilfe von classes.lst das AppCDS-Archiv und legt das Ergebnis in app-cds.jsa ab.  Der dritte Befehl führt die Anwendung mit dem generierten Archiv.

Ab Java 13 sind nur noch zwei Schritte nötig:

  1. java -XX:ArchiveClassesAtExit=app-cds.jsa -jar app-cds.jar
  2. java -XX:SharedArchiveFile=app-cds.jsa  -Xshare:on -jar app-cds.jar

In allen Fällen soll die Anwendung nicht als Fat Jar verpackt werden. AppCDS kann damit nicht umgehen. Das AppCDS-Archiv lässt sich sowohl mit Maven als auch mit Docker automatisch erstellen. Es müssen einfach die obenstehenden Befehle in den jeweiligen Build-Prozess integriert werden.   

In Fall einer Spring-Anwendung sollte man auf ein paar Best Practices achten. Spring macht intensiv gebraucht von Reflection, um dynamisch Attribute von Klassen zu ermitteln. Zudem sucht das Framework den Klassenpfad zur Laufzeit nach annotierten Klassen durch. Dieses Verhalten kann den Start einer Anwendung stark verzögern. Deswegen sind die folgenden Punkte von großer Bedeutung:

  1. @Configuration(proxyBeanMethods=false) deklarieren.
  2. Spring-context-indexer einsetzen.
  3. Reaktiv mit Spring WebFlux vorgehen: Reduziert die Anzahl der benötigenden Threads.
  4. spring.main.lazy-initialization=true deklarieren.
  5. spring.data.jpa.repositories.bootstrap-mode=lazy deklarieren.
  6. Jar-Dateien aus dem Klassenpfad entfernen, die nicht benutzt werden.

Obwohl AppCDS den Betrieb von Java-Anwendungen im Cloud-Umfeld optimiert, können weitere Tools aus dem JDK verwendet werden, um die Größe der eingesetzten JVM so klein wie möglich zu halten.

Schlanke Java-Laufzeitumgebung mit jlink und JDeps

Vor Java 9 war die Java-Laufzeitumgebung (JRE) ein monolithisches Programm, das in ihrer Gesamtheit mit jeder Java-Anwendung bereitgestellt werden musste. In Java 9 wurde das JDK-Softwaresystem in ein modulares System umstrukturiert. Die allgemeine Idee mit Java-Modulen ist, dass es möglich ist, Teile eines Programms zu entfernen, die eine Anwendung nicht benötigt. Das gilt auch für die JRE. Jetzt ist es möglich, ein eigenes JRE zu erstellen, das nur aus den für eine Anwendung erforderlichen Modulen besteht. Dafür wurden jlink und JDeps geschaffen.

Das Java Dependency Analysis Tool (JDeps) ist ein Werkzeug, das dafür verwendet wird, alle statischen Abhängigkeiten einer Anwendung zu ermitteln, die für eine Anwendung nötigen JDK-APIs zu entdecken und automatisch einen Moduldeskriptor für eine JAR-Datei zu generieren. JDeps ist dann sehr nützlich, wenn man die modulare Abhängigkeiten einer Anwendung nicht explizit angeben kann.

Jlink kann die Ausgabe von JDeps benutzen, um eine optimale benutzerdefinierte Laufzeitumgebung zu erzeugen.

jlink \
    --module-path /opt/jdk/jdk-11/jmods \
    --verbose \
    --add-modules $(jdeps --ignore-missing-deps --print-module-deps spring-petclinic-2.4.5.jar),java.xml,jdk.unsupported,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \
    --compress 2 \
    --no-header-files \
    --output /jdk-11-minimal

Wenn es darum geht, ein optimales Docker-Image zu erzeugen, sollte man unbedingt auf die beiden Tools zugreifen. Angewendet auf die Spring-PetClinic-Anwendung bekommt man folgendes:

ARG BUILD_IMAGE=maven:3.5-jdk-11
ARG RUNTIME_IMAGE=openjdk:11-slim

FROM ${BUILD_IMAGE} as dependencies
COPY pom.xml .
RUN mvn -B dependency:go-offline

FROM dependencies as BUILD
COPY /src /src/
RUN mvn -B clean package

FROM ${RUNTIME_IMAGE} as CUSTOM-JRE
COPY --from=BUILD /target/spring-petclinic-2.4.5.jar .
RUN jlink \
    --module-path /opt/jdk/jdk-11/jmods \
    --verbose \
    --add-modules $(jdeps --ignore-missing-deps --print-module-deps spring-petclinic-2.4.5.jar),java.xml,jdk.unsupported,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \
    --compress 2 \
    --no-header-files \
    --output /jdk-11-minimal

FROM debian:buster-slim
EXPOSE 8080
COPY --from=BUILD /target/spring-petclinic-2.4.5.jar .
COPY --from=CUSTOM-JRE /jdk-11-minimal /opt/jdk/
ENV PATH=$PATH:/opt/jdk/bin
CMD ["java", "-jar", "spring-petclinic-2.4.5.jar"]

Es ist deutlich zu sehen, dass die Verwendung von jlink und JDeps eine signifikante Verringerung der Größe des Docker-Images um ~34 % ermöglicht. Dies wird definitiv dazu beitragen, das Starten einer neuen Instanz zu beschleunigen.

All-In-One mit GraalVM

 GraalVM ist eine Hochleistungslaufzeitumgebung, die erhebliche Verbesserungen der Anwendungsleistung bietet und laut Webseite des Herstellers Oracle erheblich effizientere Microservices ermöglicht. Der Graal-Compiler ist das Herzstück der GraalVM. Eines der Hauptziele des Graal-Compilers ist die Fähigkeit, aus Java-Code native Binaries zu erzeugen. Dies geschieht durch eine Komponente namens „SubstrateVM“. Es handelt sich um eine leichtgewichtige, in Java geschriebene VM, die alle Komponenten sowohl für die Erzeugung des nativen Binarys als auch für deren Ausführung bietet. Während des Erstellens des Binarys werden alle Klassenabhängigkeiten und statischen Initialisierungsblöcke der Klassen zusammen mit dem entstehenden Heap und die SubstrateVM selbst in dem Binary gebündelt. Alle Optimierungen werden zur Compilezeit gemacht. Dabei analysiert der Graal-Compiler alle möglichen Ausführungspfade vollständig, um einige brillante Optimierungen anwenden zu können. Zusätzlich entfernt er alle Symbole, von denen er nachweisen kann, dass sie zur Laufzeit eigentlich nie benötigt werden. Dadurch wird die Größe der Anwendung erheblich reduziert, was oft auch zu einer verbesserten Laufzeitleistung führt.

Die SubstrateVM hat interessante Vorteile:

  1. Viel kürzere Anlaufzeit: Das Laden, die Verifizierung und das Linken zur Laufzeit entfallen. Alle diese Vorgänge fanden schon zur Kompilierzeit statt. Im Allgemeinen benötigt eine auf diese Weise kompilierte Anwendung nur einige zehn Millisekunden zum Starten.
  2. Geringerer Speicherverbrauch: Da das native Image nur wenige wesentliche Komponenten der JVM enthält und wenig Verarbeitung zur Laufzeit durchführt (keine Bytecode-Interpretation, kein JIT, keine Statistik), ist sein Speicherverbrauch tatsächlich geringer.

Die oben beschriebenen Vorteile machen Java optimal für die Entwicklung im Cloud-Umfeld. Neue Frameworks wie Quarkus und Micronaut haben das Potenzial der GraalVM schnell erkannt. Sie bieten uns alle notwendigen Werkzeuge, um komplette Anwendungen zu erstellen, die mit GraalVM kompatibel sind. Graalvm-native-image-in-aws zeigt, wie gut die GraalVM im AWS-Umfeld abschneidet. Das Spring-Framework rüstet nach. Mittlerweile lassen sich Spring-Anwendungen mit der GraalVM betreiben.

Ein Nachteil sollt jedoch auch erwähnt werden: In der Community-Edition ist nur der SerialGC verfügbar. Der ist nur für kleine Heaps effizient. Die Latenz einer Anwendung kann darunter leiden.

Fazit

Java ist eine fortschrittliche Sprache, die ständig mächtiger wird. Obwohl Go in der ersten Welle von Cloud-Infrastrukturen sehr stark vertreten war, entwickelt sich Java zu einer starken Alternative, insbesondere für Unternehmen mit etablierter Java-Erfahrung. Im Laufe der Jahre hat sie eine Vielzahl an Funktionen akkumuliert, die sie speicherschonender und in jeder Hinsicht schneller macht. Mit der neuen Release-Politik wird Java noch schneller wachsen. Der Hersteller Oracle und die riesige Community rund um die Sprache setzen alles daran, dass Java-Benutzer nahtlos in die Cloud migrieren und von erhöhten Leistungen profitieren können.

Sowohl die Java HotSpot VM kombiniert mit AppCDS und einer benutzerdefinierten Laufzeitumgebung, als auch die GraalVM machen Java optional für Cloud-Anwendungen. Sie verschaffen der JVM eine respektable Position in Cloud Native und Serverless Computing, wo ihr Platz vor ein paar Jahren nicht vorstellbar war. Sie sind auf jeden Fall eine Bereicherung für das JVM-Ökosystem.

Views All Time
468
Views Today
9
Short URL for this post: https://blog.oio.de/nrD1o
This entry was posted in Java Runtimes - VM, Appserver & Cloud and tagged , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *