Ein recht gängiger Ansatz bei der Arbeit mit serverlosem Code besteht darin, ihn als Python-, Node- oder Go-Anwendung zu schreiben, da diese für ihre sehr schnellen Kaltstarts bekannt sind.
Was aber, wenn wir mit bereits vorhandenen Java-Apps konfrontiert werden, die auf serverlose Umgebungen wie AWS Lambda abzielen? Möglicherweise hostet ein großer Teil unserer Codebasis Java, und wir haben ein umfangreiches Ökosystem an Tools und Bibliotheken entwickelt, die wir gerne wiederverwenden würden. Das Umschreiben der gesamten Flotte solcher Anwendungen in eine andere Sprache ist teuer, ganz zu schweigen davon, dass wir auf Funktionen wie statische Typsicherheit und Optimierungen der Kompilierzeit verzichten.
Vor einiger Zeit war ich mit genau diesem Szenario konfrontiert: 9 in Java geschriebene AWS Lambda-Apps, die bei Kaltstarts so langsam waren, dass es bei einigen von ihnen gelegentlich zu Timeouts kam.
Die betreffenden Lambdas wurden hinter API Gateway platziert und für Admin-Aufgaben durch Aufruf der entsprechenden REST-APIs verwendet. Diese Funktionalität wurde nicht sehr stark genutzt und so waren Kaltstarts unvermeidlich; Da dies jedoch kein kritischer Dienst war, war es eine perfekte Gelegenheit zum Experimentieren: um herauszufinden, ob diese Lambdas gerettet werden könnten.
Es dauerte nicht lange, bis ich auf mehrere andere Blogbeiträge über Entwickler stieß, die GraalVM und Frameworks wie Quarkus erfolgreich nutzten, um genau dieses Problem anzugehen. Und so habe ich beschlossen, es selbst auszuprobieren.
Aber was sind das überhaupt für Tools?
Kurz gesagt ist GraalVM eine Java Virtual Machine, die über ein Toolset verfügt, mit dem Java zu einem nativen Image kompiliert und mithilfe der Graal-JVM ausgeführt werden kann.
Normalerweise verwendet Java den „Just In Time“ (JIT)-Compiler, der, wie der Name schon sagt, während der Ausführung unseres Codes Optimierungen und Kompilierungen durchführt. Die lang laufenden Anwendungen profitieren davon, da die JVM-Optimierer die Programmausführung ständig überwachen und Feinabstimmungen vornehmen, die im Laufe der Zeit zu einer besseren Leistung führen.
Das ist großartig, wenn eine Anwendung einmal instanziiert wird und voraussichtlich mehrere Stunden oder länger ausgeführt wird, aber nicht so toll, wenn wir es mit Kubernetes, AWS Lambdas und Batch-Jobs zu tun haben, die Java-Apps schnell starten und ausführen sollen zeitkritische Vorgänge und Skalierung je nach Bedarf – wo wir gerade von einem Turboloch für die Autoenthusiasten da draußen sprechen.
Und hier kommt die Native Image-Funktion von GraalVM ins Spiel. Anstatt den JIT-Compiler zu verwenden, wählt es einen ganz anderen Ansatz, unseren Code im Voraus zu kompilieren (AOT). Es backt unseren Kuchen mithilfe statischer Codeanalyse vor und initialisiert sogar bestimmte Klassen während der Erstellungszeit vor, sodass sie jederzeit einsatzbereit sind, wenn unser Anwendungscode ausgeführt wird.
Das Ergebnis? Sehr schnelle Kaltstarts, wodurch native Images in serverlosen Domänen, in denen Apps nur eine kurze Lebensdauer haben und schnell gestartet werden müssen, sehr leistungsfähig sind.
Es ist zu beachten, dass GraalVM zwar AOT-fähig ist, aber auch als Ersatz für die vorhandene JVM dienen kann und angesichts des neuen in Java geschriebenen JIT-Compilers von GraalVM eine bessere Leistung bietet.
Aber Moment, es gibt noch mehr! Da Native Image nur den Code enthält, der sich auf dem bekannten Ausführungspfad befindet, reduzieren wir den Fettgehalt und alle Java-Klassen, deren Beibehaltung nicht explizit deklariert wurde, sind nicht verfügbar. Da wir nur die Bits behalten, deren Ausführung erwartet wird, erhöhen wir die Sicherheit unserer Anwendung.
Nehmen Sie zum Beispiel die berüchtigte Log4J-Sicherheitslücke, die Remotecodeausführung als Mittel zur Gefährdung des Hosts nutzte. Bei Native Images ist die Gadget-Verkettung höchstwahrscheinlich nicht erfolgreich, da die Teile des Bibliothekscodes, die zur Übermittlung des Angriffs erforderlich sind, nicht einmal erreichbar sind.
Quarkus hingegen ist ein für serverlose Anwendungen optimiertes Java-Framework, das über eine Toolbox verfügt, die die Erstellung nativer Images erleichtert, indem sie eine Erweiterung für die spezifische Konfiguration und Erstellung von AWS Lambdas als native ausführbare Dateien bietet.
Während meiner Lambda-Optimierungsreise bin ich auch auf alternative Optimierungstechniken gestoßen. Eine dieser Optimierungen war die vorgeschlagene ausschließliche Verwendung eines C1-Compilers während der Ausführung von Lambda, was schnellere Kaltstarts versprach. Normalerweise verwenden Java-Anwendungen, die in einer JVM ausgeführt werden, eine mehrstufige Kompilierung, die aus einem schnelleren, aber weniger optimalen C1 besteht, gefolgt von C2, das langsamer ist, aber eine optimalere Leistung für Java-Apps bietet, die über einen längeren Zeitraum ausgeführt werden. Da Lambdas nur von kurzer Dauer sind, sind die Vorteile der C2-Kompilierung vernachlässigbar.
Eine Anleitung, die durch den Prozess der Konfiguration der C1-Kompilierung für AWS Lambdas führt, ist hier verfügbar.
Natürlich wollte ich wissen, welche Verbesserung diese Technik im Vergleich zu meinem bestehenden GraalVM-Masterplan bieten kann, und habe sie daher auch in meine nachstehenden Ergebnisse aufgenommen.
Weitere Details zur mehrstufigen Kompilierung von JVM sowie zum brandneuen JIT-Compiler von GraalVM finden Sie in diesem Baeldung-Artikel.
Ironischerweise brachte AWS ein paar Monate, nachdem ich meine Änderungen in die Produktion gebracht hatte, seine neueste SnapStart-Funktion auf den Markt, die einen Snapshot eines laufenden Lambda erstellt und, anstatt ihn noch einmal neu zu initialisieren, Snapshot-Bilder als verwendet ein Wiederherstellungspunkt, der schnellere Kaltstarts verspricht. Ich musste es ausprobieren, um herauszufinden, ob der Einsatz von GraalVM vergebliche Mühe war, und habe es auch in meine Erkenntnisse einbezogen.
Es ist erwähnenswert, dass für die optimale Nutzung von SnapStart eine Code-Umgestaltung erforderlich gewesen wäre, um die Hooks beforeCheckpoint und afterRestore zu nutzen (weitere Details hier). Da ich größere Codeänderungen möglichst vermeiden wollte, habe ich diese Funktion „wie sie ist“ verwendet, ohne diese Methoden zu implementieren und Code neu anzuordnen.
Jetzt zurück zu GraalVM! Zu meiner Überraschung waren nach der Integration dieser Lösung, abgesehen vom Hinzufügen und Anpassen von Build-Konfigurationsdateien und einigen erforderlichen Metadaten, absolut keine Änderungen am Java-Code erforderlich.
Klingt zu schön, um wahr zu sein?
Vielleicht ein bisschen. Angesichts der Tatsache, dass wir in der Java-Welt die AOT-Kompilierung verwenden, stellt dies eine gewisse Herausforderung dar, wenn es um die Verwendung von Sprachfunktionen wie Reflexionen, Proxys, Schnittstellen und Dienstregistern geht, auf die viele Bibliotheken angewiesen sind. Aus diesem Grund erfordert der GraalVM-Compiler die Deklaration zusätzlicher Konfigurationsmetadaten, die bestimmte Klassen und Dienste explizit registrieren, damit sie in das endgültige Artefakt aufgenommen werden können. GraalVM bietet einen sogenannten Agenten, der neben Ihrer ausführbaren Datei ausgeführt werden kann, um automatisch die erforderliche Konfiguration zu identifizieren, was diesen Prozess vereinfachen kann.
Quarkus bietet mehrere Erweiterungen für bekannte Bibliotheken, um sie „Native-Image-freundlich“ zu machen. Da ich jedoch mit einer vorhandenen Codebasis arbeitete und mein Ziel darin bestand, größere Umgestaltungen (oder Codeänderungen) zu vermeiden ), begnügte ich mich damit, die erforderlichen Konfigurationsdateien zu erstellen, die die vorhandenen Bibliotheken benötigten, um native Bilder erfolgreich zu erstellen.
Beachten Sie, dass das Kompilieren nativer Bilder ressourcenintensiv ist und im Vergleich zur Bytecode-Kompilierung, die auf eine Standard-JVM-Laufzeit abzielt, deutlich länger dauert. Es besteht die Möglichkeit, dass Sie einem Build-Knoten mehr RAM zuweisen müssen, um Speicherprobleme zu vermeiden. Dies sollte kein Problem darstellen, ist aber auf jeden Fall etwas, das Sie im Hinterkopf behalten sollten.
Nachdem ich meine nativen Image-Lambdas kompiliert und gepackt hatte, war es an der Zeit, sie in einer Testumgebung bereitzustellen. Normalerweise nutzen Java Lambdas zur Ausführung die Java Runtimes von AWS; Da wir jedoch versuchen, ein natives Image zu verwenden, bei dem es sich um ein binäres Artefakt handelt, das unseren App-Code in der Graal-JVM enthält, müssen wir eine der „benutzerdefinierten“ Amazon Linux-Umgebungen auswählen, die AWS anbietet.
Ich habe eine Postman-API-Sammlung verwendet, um Anfragen an alle 9 Lambdas zu senden und die Kaltstart-Reaktionszeiten für jede der oben genannten Techniken zu messen. Um sicherzustellen, dass es immer zu einem Kaltstart kommt, habe ich die Konfiguration des Ziel-Lambdas neu geladen, um sicherzustellen, dass beim nächsten Aufruf keine Instanz verwendet wird, die möglicherweise bereits warm ist. Alle Lambdas wurden mit 1 GB RAM konfiguriert. Ich habe auch einen einzelnen Aufruf für jede Konfiguration gemessen, da der Vorgang zeitaufwändig war; Die beobachteten Reaktionszeiten ergaben jedoch ein ziemlich klares Bild.
Hat es also funktioniert? Absolut! Hier sind die Ergebnisse:
Und der klare Gewinner ist: GraalVM Native Images – im Durchschnitt hat es zu einer dreifachen Beschleunigung im Vergleich zu den unveränderten Java Lambdas geführt – keine Timeouts mehr und viel bessere Antwortzeiten, was genau das ist, was ich wollte erreichen.
SnapStart funktionierte ohne Codeänderungen nicht so gut, wie ich es mir vorgestellt hatte. Wenn der C1-Compiler zusätzlich zur SnapStart-Funktion verwendet wurde, verkürzte er die Kaltstartzeiten weiter, übertraf aber immer noch nicht das native Image von GraalVM. Das soll nicht heißen, dass es sich nicht um eine praktikable Option als schnell und einfach umzusetzende Verbesserung handelt; Wenn wir jedoch unser Lambda so weit wie möglich optimieren möchten und etwas Zeit und Ressourcen haben, um die Konfiguration und unseren Build-Prozess anzupassen, ist GraalVM definitiv überlegen, wenn es um Leistung und Sicherheit geht.
Wie von GraalVM behauptet, benötigen native Images im Vergleich zu ihren regulären JVM-Gegenstücken weniger Ressourcen, um effektiv ausgeführt zu werden. Ich wollte sehen, wie sich die Kaltstart- und Warmstartleistung halten würde, wenn ich die Menge an RAM reduzieren würde, mit der diese Lambdas arbeiten mussten. Dieses Mal habe ich nur eine einzige Lambda-App ausgewählt, um diesen Test durchzuführen. Hier sind die Ergebnisse:
Und sie haben ihr Versprechen gehalten! Beim Versuch, eine Konfiguration mit 256 MB und weniger zu konfigurieren, reichte bei regulären JVM-Lambdas der Arbeitsspeicher aus, wohingegen das native Image scheinbar nicht phasengesteuert war und weiterhin ausgeführt wurde. Wenn 128 MB nicht die niedrigste verfügbare Speicheroption gewesen wären, frage ich mich, wie viel niedriger wir hätten gehen können. Native Images sind nicht nur beim Kaltstart schneller, sondern bieten auch bei der Arbeit mit begrenzten Ressourcen eine konstante Leistung, was sich in niedrigeren Betriebskosten niederschlägt.
Das Ökosystem von Java ist reichhaltig und umfangreich. Jeden Tag entstehen viele neue Technologien und Verbesserungen, die Java im Spiel halten, wenn es um serverlose Anwendungen geht. Eine dieser aufstrebenden Technologien ist GraalVM. Was als Forschungsprojekt begann, wird nun langsam angenommen und stellt eine praktikable Alternative zu einer Standard-JVM wie HotSpot dar. In diesem Blogbeitrag habe ich kaum an der Oberfläche dessen gekratzt, was GraalVM zu bieten hat, und ich möchte die Leser ermutigen, es weiter zu erkunden. Es gibt mehrere Erfolgsgeschichten von Unternehmen wie Adyen (Artikel-Link) oder Facebook (Artikel-Link), die GraalVM nutzen konnten, um Zeit und Geld zu sparen.
Wenn Sie also Java das nächste Mal als Option rabattieren, probieren Sie GraalVM aus. Und da Spring Boot 3 jetzt native GraalVM-Images sofort unterstützt, ist es einfacher denn je, sie für Ihre serverlosen Workloads zu nutzen, um von der Leistung, dem geringen Ressourcenverbrauch und der zusätzlichen Sicherheit zu profitieren, die GraalVM zu bieten hat.
Das obige ist der detaillierte Inhalt vonJava kann auch serverlos sein: Verwendung von GraalVM für schnelle Kaltstarts. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!