1. Übersicht
In diesem Codelab lernen wir das Spring Native-Projekt kennen, erstellen eine App, die es verwendet, und stellen sie in Google Cloud bereit.
Wir gehen auf die Komponenten, die bisherige Entwicklung des Projekts, einige Anwendungsfälle und natürlich die Schritte ein, die Sie ausführen müssen, um es in Ihren Projekten zu verwenden.
Das Spring Native-Projekt befindet sich derzeit in der Testphase. Daher ist für den Einstieg eine bestimmte Konfiguration erforderlich. Wie auf der SpringOne 2021 angekündigt, wird Spring Native jedoch mit erstklassiger Unterstützung in Spring Framework 6.0 und Spring Boot 3.0 integriert. Daher ist jetzt der perfekte Zeitpunkt, um sich das Projekt einige Monate vor der Veröffentlichung genauer anzusehen.
Die Just-in-Time-Kompilierung wurde zwar für Dinge wie langlaufende Prozesse sehr gut optimiert, aber es gibt bestimmte Anwendungsfälle, in denen vorkompilierte Anwendungen noch besser funktionieren. Darauf werden wir im Codelab näher eingehen.
Sie lernen,
- Cloud Shell verwenden
- Cloud Run API aktivieren
- Spring Native-Anwendung erstellen und bereitstellen
- Eine solche App in Cloud Run bereitstellen
Voraussetzungen
- Ein Google Cloud Platform-Projekt mit einem aktiven GCP-Abrechnungskonto
- Die gcloud-Befehlszeile ist installiert oder Sie haben Zugriff auf Cloud Shell.
- Grundlegende Kenntnisse in Java und XML
- Grundkenntnisse gängiger Linux-Befehle
Umfrage
Wie möchten Sie diese Anleitung verwenden?
Wie würden Sie Ihre Erfahrung mit Java bewerten?
Wie würden Sie Ihre Erfahrungen mit der Nutzung von Google Cloud-Diensten bewerten?
2. Hintergrund
Das Spring Native-Projekt nutzt mehrere Technologien, um Entwicklern eine native Anwendungsleistung zu bieten.
Um Spring Native vollständig zu verstehen, ist es hilfreich, einige dieser Komponententechnologien zu kennen, was sie uns ermöglichen und wie sie hier zusammenwirken.
AOT-Kompilierung
Wenn Entwickler javac normalerweise zur Kompilierungszeit ausführen, wird der .java-Quellcode in .class-Dateien kompiliert, die in Bytecode geschrieben sind. Dieser Bytecode ist nur für die Java Virtual Machine gedacht. Daher muss die JVM diesen Code auf anderen Maschinen interpretieren, damit wir unseren Code ausführen können.
Dieser Prozess ermöglicht die plattformübergreifende Ausführung von Java-Code – „once write, everywhere run“. Er ist jedoch im Vergleich zur Ausführung von nativem Code sehr aufwendig.
Glücklicherweise verwenden die meisten JVM-Implementierungen die Just-in-Time-Kompilierung, um diese Interpretationskosten zu senken. Dazu werden die Aufrufe einer Funktion gezählt. Wenn sie oft genug aufgerufen wird, um einen Grenzwert zu überschreiten ( standardmäßig 10.000), wird sie zur Laufzeit in nativen Code kompiliert, um eine weitere teure Interpretation zu vermeiden.
Bei der Vorabkompilierung wird der gesamte erreichbare Code zur Laufzeit in eine native ausführbare Datei kompiliert. Dadurch wird die Portabilität gegen eine bessere Arbeitsspeichernutzung und andere Leistungssteigerungen bei der Laufzeit getauscht.
Das ist natürlich ein Kompromiss, der sich nicht immer lohnt. Die AOT-Kompilierung kann jedoch in bestimmten Anwendungsfällen punkten, z. B.:
- Kurzlebige Anwendungen, bei denen die Startzeit wichtig ist
- Umgebungen mit sehr begrenztem Arbeitsspeicher, in denen JIT zu kostspielig sein kann
Außergewöhnlich ist, dass die AOT-Kompilierung in JDK 9 als experimentelle Funktion eingeführt wurde. Diese Implementierung war jedoch teuer in der Wartung und hat sich nie richtig durchgesetzt. Daher wurde sie in Java 17 stillschweigend entfernt, um Entwicklern die Nutzung von GraalVM zu ermöglichen.
GraalVM
GraalVM ist eine hochoptimierte Open-Source-JDK-Distribution mit extrem kurzen Startzeiten, AOT-nativer Image-Kompilierung und Polyglott-Funktionen, mit denen Entwickler mehrere Sprachen in einer einzigen Anwendung kombinieren können.
GraalVM befindet sich in der aktiven Entwicklung und es werden ständig neue Funktionen hinzugefügt und bestehende verbessert. Wir empfehlen Entwicklern, sich über die neuesten Entwicklungen auf dem Laufenden zu halten.
Einige der jüngsten Meilensteine:
- Neue, nutzerfreundliche Ausgabe für die Erstellung nativer Bilder ( 18. Januar 2021)
- Java 17-Support ( 18. Januar 2022)
- Die mehrstufige Kompilierung ist jetzt standardmäßig aktiviert, um die Polyglott-Kompilierungszeiten zu verbessern ( 2021-04-20)
Spring Native
Einfach ausgedrückt: Spring Native ermöglicht die Verwendung des Native-Image-Compilers von GraalVM, um Spring-Anwendungen in native ausführbare Dateien umzuwandeln.
Dabei wird Ihre Anwendung zur Kompilierungszeit statisch analysiert, um alle Methoden in Ihrer Anwendung zu finden, die vom Einstiegspunkt aus erreichbar sind.
Dadurch wird im Grunde eine „geschlossene Welt“ für Ihre Anwendung geschaffen, bei der davon ausgegangen wird, dass der gesamte Code zur Kompilierungszeit bekannt ist und während der Laufzeit kein neuer Code geladen werden darf.
Die Generierung von nativen Images ist ein speicherintensiver Prozess, der länger dauert als die Kompilierung einer regulären Anwendung. Außerdem gelten für bestimmte Aspekte von Java Einschränkungen.
In einigen Fällen sind keine Codeänderungen erforderlich, damit eine Anwendung mit Spring Native funktioniert. In einigen Fällen ist jedoch eine bestimmte native Konfiguration erforderlich, damit die Funktion ordnungsgemäß funktioniert. In solchen Fällen bietet Spring Native oft Native Hints, um diesen Prozess zu vereinfachen.
3. Einrichtung/Vorbereitung
Bevor wir mit der Implementierung von Spring Native beginnen, müssen wir unsere App erstellen und bereitstellen, um einen Leistungsgrundwert zu ermitteln, den wir später mit der nativen Version vergleichen können.
1. Projekt erstellen
Als Erstes laden wir unsere App von start.spring.io herunter:
curl https://start.spring.io/starter.zip -d dependencies=web \ -d javaVersion=11 \ -d bootVersion=2.6.4 -o io-native-starter.zip
Diese Starter-App verwendet Spring Boot 2.6.4, die aktuelle Version, die vom spring-native-Projekt zum Zeitpunkt der Erstellung unterstützt wird.
Seit der Veröffentlichung von GraalVM 21.0.3 können Sie für dieses Beispiel auch Java 17 verwenden. Wir verwenden für diese Anleitung weiterhin Java 11, um die erforderliche Konfiguration zu minimieren.
Sobald wir die ZIP-Datei in der Befehlszeile haben, können wir einen Unterordner für unser Projekt erstellen und den Ordner dort entpacken:
mkdir spring-native cd spring-native unzip ../io-native-starter.zip
2. Codeänderungen
Sobald wir das Projekt geöffnet haben, fügen wir schnell ein Lebenszeichen hinzu und zeigen die Leistung von Spring Native, sobald wir es ausführen.
Bearbeiten Sie die Datei „DemoApplication.java“ so, dass sie so aussieht:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.time.Instant;
@RestController
@SpringBootApplication
public class DemoApplication {
private static Instant startTime;
private static Instant readyTime;
public static void main(String[] args) {
startTime = Instant.now();
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/")
public String index() {
return "Time between start and ApplicationReadyEvent: "
+ Duration.between(startTime, readyTime).toMillis()
+ "ms";
}
@EventListener(ApplicationReadyEvent.class)
public void ready() {
readyTime = Instant.now();
}
}
Unsere Baseline-App ist jetzt einsatzbereit. Sie können also ein Image erstellen und lokal ausführen, um sich ein Bild von der Startzeit zu machen, bevor wir sie in eine native Anwendung umwandeln.
So erstellen wir unser Image:
mvn spring-boot:build-image
Mit docker images demo
können Sie sich auch ein Bild von der Größe des Referenzbilds machen:
So führen Sie unsere App aus:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
3. Referenz-App bereitstellen
Jetzt haben wir unsere App und können sie bereitstellen und die Zeiten notieren, die wir später mit den Startzeiten unserer nativen App vergleichen.
Je nach Art der Anwendung, die Sie erstellen, gibt es verschiedene Möglichkeiten, Ihre Inhalte zu hosten.
Da es sich bei unserem Beispiel jedoch um eine sehr einfache Webanwendung handelt, können wir es bei Cloud Run belassen.
Wenn Sie die Schritte auf Ihrem eigenen Computer ausführen, muss die gcloud CLI installiert und auf dem neuesten Stand sein.
Wenn Sie Cloud Shell verwenden, wird das alles automatisch erledigt. Sie können einfach Folgendes im Quellverzeichnis ausführen:
gcloud run deploy
4. Anwendungskonfiguration
1. Maven-Repositories konfigurieren
Da sich dieses Projekt noch in der experimentellen Phase befindet, müssen wir unsere App so konfigurieren, dass sie experimentelle Artefakte finden kann, die nicht im zentralen Maven-Repository verfügbar sind.
Dazu fügen wir unserer pom.xml die folgenden Elemente hinzu. Sie können dies in einem beliebigen Editor tun.
Fügen Sie der pom die folgenden Abschnitte „repositories“ und „pluginRepositories“ hinzu:
<repositories>
<repository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
2. Abhängigkeiten hinzufügen
Fügen Sie als Nächstes die Abhängigkeit „spring-native“ hinzu, die zum Ausführen einer Spring-Anwendung als natives Image erforderlich ist. Hinweis: Dieser Schritt ist nicht erforderlich, wenn Sie Gradle verwenden.
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.11.2</version>
</dependency>
</dependencies>
3. Unsere Plug-ins hinzufügen/aktivieren
Fügen Sie jetzt das AOT-Plug-in hinzu, um die Kompatibilität und den Footprint des nativen Images zu verbessern ( Weitere Informationen):
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>0.11.2</version>
<executions>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
Aktualisieren Sie nun das Spring-Boot-Maven-Plug-in, um die Unterstützung für native Images zu aktivieren, und erstellen Sie mit dem Paketo-Builder das native Image:
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
</plugins>
Das tiny-builder-Image ist nur eine von mehreren Optionen. Es ist eine gute Wahl für unseren Anwendungsfall, da es nur sehr wenige zusätzliche Bibliotheken und Dienstprogramme enthält, was dazu beiträgt, die Angriffsfläche zu minimieren.
Wenn Sie beispielsweise eine App entwickeln, die Zugriff auf einige gängige C-Bibliotheken benötigt, oder sich noch nicht sicher sind, welche Anforderungen Ihre App hat, ist der Full-Builder möglicherweise die bessere Wahl.
5. Native App erstellen und ausführen
Sobald das alles eingerichtet ist, sollten wir unser Image erstellen und unsere native, kompilierte App ausführen können.
Bevor Sie den Build ausführen, sollten Sie Folgendes beachten:
- Das dauert länger als ein normaler Build (einige Minuten).
- Dieser Build-Prozess kann viel Arbeitsspeicher (einige Gigabyte) beanspruchen.
- Für diesen Build-Prozess muss der Docker-Daemon erreichbar sein.
- In diesem Beispiel führen wir den Prozess manuell durch. Sie können Ihre Build-Phasen aber auch so konfigurieren, dass ein natives Build-Profil automatisch ausgelöst wird.
So erstellen wir unser Image:
mvn spring-boot:build-image
Sobald die App erstellt ist, können wir sie testen.
So führen Sie unsere App aus:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
Jetzt sind wir in einer guten Position, um beide Seiten der Gleichung für native Anwendungen zu sehen.
Wir haben ein wenig Zeit und zusätzlichen Speicherplatz bei der Kompilierung eingespart, erhalten aber im Gegenzug eine Anwendung, die viel schneller gestartet werden kann und je nach Arbeitslast deutlich weniger Speicherplatz verbraucht.
Wenn wir docker images demo
ausführen, um die Größe des nativen Bildes mit dem Original zu vergleichen, sehen wir eine drastische Reduzierung:
Bei komplexeren Anwendungsfällen sind außerdem zusätzliche Änderungen erforderlich, um den AOT-Compiler darüber zu informieren, was Ihre App zur Laufzeit tun wird. Bestimmte vorhersehbare Arbeitslasten (z. B. Batchjobs) eignen sich daher sehr gut dafür, während andere mehr Aufwand erfordern.
6. Native App bereitstellen
Damit wir unsere App in Cloud Run bereitstellen können, müssen wir unser natives Image in einen Paketmanager wie Artifact Registry hochladen.
1. Docker-Repository vorbereiten
Dazu erstellen wir ein Repository:
gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"
Als Nächstes prüfen wir, ob wir für das Pushen von Daten in unsere neue Registry authentifiziert sind.
Mit der gcloud CLI lässt sich dieser Vorgang erheblich vereinfachen:
gcloud auth configure-docker us-central1-docker.pkg.dev
2. Image in Artifact Registry veröffentlichen
Als Nächstes taggen wir unser Bild:
export PROJECT_ID=$(gcloud config list --format 'value(core.project)')
docker tag demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
Und dann können wir es mit docker push
an die Artifact Registry senden:
docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
3. In Cloud Run bereitstellen
Jetzt können wir das in Artifact Registry gespeicherte Image in Cloud Run bereitstellen:
gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
Da wir unsere App als natives Image erstellt und bereitgestellt haben, können wir sicher sein, dass unsere Anwendung unsere Infrastrukturkosten optimal nutzt.
Sie können die Startzeiten unserer Referenz-App mit dieser neuen nativen App selbst vergleichen.
7. Zusammenfassung/Bereinigung
Herzlichen Glückwunsch zum Erstellen und Bereitstellen einer Spring Native-Anwendung in Google Cloud.
Wir hoffen, dass Sie durch diese Anleitung das Spring Native-Projekt besser kennenlernen und es in Betracht ziehen, wenn es in Zukunft Ihren Anforderungen entspricht.
Optional: Dienst bereinigen und/oder deaktivieren
Unabhängig davon, ob Sie ein Google Cloud-Projekt für dieses Codelab erstellt oder ein vorhandenes wiederverwendet haben, sollten Sie unnötige Kosten für die verwendeten Ressourcen vermeiden.
Sie können die von uns erstellten Cloud Run-Dienste löschen oder deaktivieren, das von uns gehostete Image löschen oder das gesamte Projekt einstellen.
8. Zusätzliche Ressourcen
Das Spring Native-Projekt ist zwar noch relativ neu und experimentell, aber es gibt bereits eine Vielzahl guter Ressourcen, mit denen Early Adopter Probleme beheben und sich einbringen können:
Zusätzliche Ressourcen
Im Folgenden finden Sie Onlineressourcen, die für diese Anleitung relevant sein könnten:
- Weitere Informationen zu nativen Hinweisen
- Weitere Informationen zu GraalVM
- Mitmachen
- Fehler „Nicht genügend Arbeitsspeicher“ beim Erstellen nativer Images
- Fehler beim Starten der Anwendung
Lizenz
Dieser Text ist mit einer Creative Commons Attribution 2.0 Generic License lizenziert.