Spring Native in Google Cloud

1. Übersicht

In diesem Codelab lernen wir das Spring Native-Projekt kennen, erstellen eine Anwendung damit und stellen sie in Google Cloud bereit.

Wir gehen auf die Komponenten, die jüngste Geschichte des Projekts und einige Anwendungsfälle ein und erläutern natürlich die Schritte, die erforderlich sind, um es in Ihren Projekten zu verwenden.

Das Spring Native-Projekt befindet sich derzeit in der Testphase. Daher ist eine bestimmte Konfiguration erforderlich, um loszulegen. Wie auf der SpringOne 2021 angekündigt, soll Spring Native jedoch mit erstklassigem Support in Spring Framework 6.0 und Spring Boot 3.0 integriert werden. Dies ist also 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 lang laufende Prozesse sehr gut optimiert, es gibt aber bestimmte Anwendungsfälle, in denen im Voraus kompilierte Anwendungen noch besser abschneiden. Das werden wir im Codelab besprechen.

In diesem Codelab lernen Sie, wie Sie

  • Cloud Shell verwenden
  • die Cloud Run API aktivieren
  • eine Spring Native-Anwendung erstellen und bereitstellen
  • eine solche Anwendung in Cloud Run bereitstellen

Voraussetzungen

Umfrage

Wie werden Sie diese Anleitung verwenden?

Nur lesen Lesen und die Übungen durcharbeiten

Wie würden Sie Ihre Erfahrungen mit Java bewerten?

Anfänger Fortgeschritten Experte

Wie würden Sie Ihre Erfahrungen mit der Verwendung von Google Cloud-Diensten bewerten?

Anfänger Fortgeschritten Experte

2. Hintergrund

Das Spring Native-Projekt nutzt mehrere Technologien, um Entwicklern die Leistung nativer Anwendungen zu bieten.

Um Spring Native vollständig zu verstehen, ist es hilfreich, einige dieser Komponententechnologien zu kennen, zu wissen, was sie für uns ermöglichen und wie sie hier zusammenarbeiten.

AOT-Kompilierung

Wenn Entwickler „javac“ normalerweise zur Kompilierzeit ausführen, wird unser .java-Quellcode in .class-Dateien kompiliert, die in Bytecode geschrieben sind. Dieser Bytecode ist nur für die Java Virtual Machine bestimmt. Daher muss die JVM diesen Code auf anderen Computern interpretieren, damit wir unseren Code ausführen können.

Dieser Prozess verleiht Java seine charakteristische Portabilität, sodass wir Code einmal schreiben und überall ausführen können. Er ist jedoch teuer im Vergleich zur Ausführung von nativem Code.

Glücklicherweise verwenden die meisten Implementierungen der JVM die Just-in-time-Kompilierung, um diese Interpretationskosten zu senken. Dazu werden die Aufrufe für eine Funktion gezählt. Wenn sie oft genug aufgerufen wird, um einen Schwellenwert zu überschreiten ( standardmäßig 10.000), wird sie zur Laufzeit in nativen Code kompiliert, um weitere teure Interpretationen zu vermeiden.

Die Ahead-of-time-Kompilierung verfolgt den umgekehrten Ansatz, indem sie den gesamten erreichbaren Code zur Kompilierzeit in eine native ausführbare Datei kompiliert. Dadurch wird die Portabilität gegen Speichereffizienz und andere Leistungssteigerungen zur Laufzeit eingetauscht.

5042e8e62a05a27.png

Das ist natürlich ein Kompromiss, der nicht immer sinnvoll ist. Die AOT-Kompilierung kann jedoch in bestimmten Anwendungsfällen von Vorteil sein, z. B.:

  • Kurzlebige Anwendungen, bei denen die Startzeit wichtig ist
  • Umgebungen mit stark eingeschränktem Arbeitsspeicher, in denen JIT zu teuer sein kann

Die AOT-Kompilierung wurde als experimentelles Feature in JDK 9 eingeführt. Diese Implementierung war jedoch teuer in der Wartung und setzte sich nie durch. Daher wurde sie in Java 17 zugunsten von GraalVM entfernt.

GraalVM

GraalVM ist eine hochoptimierte Open-Source-JDK-Distribution mit extrem schnellen Startzeiten, AOT-Kompilierung nativer Images und polyglotten Funktionen, mit denen Entwickler mehrere Sprachen in einer einzigen Anwendung kombinieren können.

GraalVM wird aktiv weiterentwickelt und erhält ständig neue Funktionen und Verbesserungen. Entwickler sollten also auf dem Laufenden bleiben.

Einige aktuelle Meilensteine sind:

  • Eine neue, nutzerfreundliche Ausgabe für den Build nativer Images ( 18.01.2021)
  • Java 17-Support ( 18.01.2022)
  • Standardmäßig aktivierte mehrstufige Kompilierung zur Verbesserung der Kompilierzeiten für polyglotte Anwendungen ( 20.04.2021)

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 zur Kompilierzeit eine statische Analyse Ihrer Anwendung durchgeführt, um alle Methoden in Ihrer Anwendung zu finden, die vom Einstiegspunkt aus erreichbar sind.

Dadurch entsteht im Wesentlichen eine „Closed-World“-Konzeption Ihrer Anwendung, bei der der gesamte Code zur Kompilierzeit bekannt ist und zur Laufzeit kein neuer Code geladen werden darf.

Die Generierung nativer Images ist ein speicherintensiver Prozess, der länger dauert als die Kompilierung einer regulären Anwendung und bestimmte Einschränkungen für Java mit sich bringt.

In einigen Fällen sind keine Codeänderungen erforderlich, damit eine Anwendung mit Spring Native funktioniert. In anderen Situationen ist jedoch eine bestimmte native Konfiguration erforderlich, damit alles richtig funktioniert. In diesen Situationen 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 Anwendung erstellen und bereitstellen, um eine Leistungsbaseline zu erstellen, mit der wir die native Version später vergleichen können.

1. Projekt erstellen

Zuerst rufen wir unsere Anwendung von start.spring.io ab:

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 zum Zeitpunkt der Erstellung dieses Codelabs die neueste Version ist, die vom Spring Native-Projekt 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 in dieser Anleitung jedoch weiterhin Java 11, um die Konfiguration zu minimieren.

Sobald wir unsere ZIP-Datei in der Befehlszeile haben, können wir ein Unterverzeichnis 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 „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();
    }
}

An diesem Punkt ist unsere Baseline-Anwendung bereit. Sie können also ein Image erstellen und es lokal ausführen, um eine Vorstellung von der Startzeit zu erhalten, bevor wir es in eine native Anwendung umwandeln.

So erstellen Sie unser Image:

mvn spring-boot:build-image

Sie können auch docker images demo verwenden, um eine Vorstellung von der Größe des Baseline-Images zu erhalten: 6ecb403e9af1475e.png

So führen Sie unsere Anwendung aus:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

3. Baseline-Anwendung bereitstellen

Jetzt stellen wir unsere Anwendung bereit und notieren uns die Zeiten, die wir später mit den Startzeiten unserer nativen Anwendung vergleichen.

Je nach Art der Anwendung, die Sie erstellen, gibt es verschiedene Möglichkeiten, Ihre Inhalte zu hosten Ihre Inhalte.

Da unser Beispiel jedoch eine sehr einfache Webanwendung ist, können wir es einfach halten und auf Cloud Run setzen.

Wenn Sie die Schritte auf Ihrem eigenen Computer ausführen, müssen Sie das Tool gcloud CLI installiert und aktualisiert haben.

Wenn Sie Cloud Shell verwenden, ist das alles erledigt und 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 Testphase befindet, müssen wir unsere Anwendung so konfigurieren, dass sie experimentelle Artefakte finden kann, die nicht im zentralen Maven-Repository verfügbar sind.

Dazu müssen wir unserer Datei „pom.xml“ die folgenden Elemente hinzufügen. Das können Sie im Editor Ihrer Wahl tun.

Fügen Sie unserer Datei „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 erforderlich ist, um eine Spring-Anwendung als natives Image auszuführen. 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. Plug-ins hinzufügen/aktivieren

Fügen Sie jetzt das AOT-Plug-in hinzu, um die Kompatibilität und den Speicherbedarf nativer 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>

Jetzt aktualisieren wir das Plug-in „spring-boot-maven“, um die Unterstützung für native Images zu aktivieren und den Paketo-Builder zu verwenden, um unser natives Image zu erstellen:

<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>

Beachten Sie, dass das Tiny-Builder-Image nur eine von mehreren Optionen ist. 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 Anwendung erstellen, die Zugriff auf einige gängige C-Bibliotheken benötigt, oder Sie sich noch nicht sicher sind, welche Anforderungen Ihre Anwendung hat, ist der Full-Builder möglicherweise besser geeignet.

5. Native Anwendung erstellen und ausführen

Sobald alles eingerichtet ist, sollten wir unser Image erstellen und unsere native, kompilierte Anwendung ausführen können.

Bevor Sie den Build ausführen, sollten Sie Folgendes beachten:

So erstellen Sie unser Image:

mvn spring-boot:build-image

Sobald das Image erstellt wurde, können wir die native Anwendung in Aktion sehen.

So führen Sie unsere Anwendung aus:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

An diesem Punkt können wir beide Seiten der Gleichung für native Anwendungen sehen.

Wir haben zur Kompilierzeit etwas Zeit und zusätzlichen Arbeitsspeicher in Kauf genommen, erhalten aber im Gegenzug eine Anwendung, die viel schneller gestartet werden kann und deutlich weniger Arbeitsspeicher verbraucht (je nach Arbeitslast).

Wenn wir docker images demo ausführen, um die Größe des nativen Images mit dem Original zu vergleichen, sehen wir eine drastische Reduzierung:

e667f65a011c1328.png

In komplexeren Anwendungsfällen sind zusätzliche Änderungen erforderlich, um den AOT-Compiler darüber zu informieren, was Ihre Anwendung zur Laufzeit tun wird. Aus diesem Grund eignen sich bestimmte vorhersehbare Arbeitslasten (z. B. Batchjobs) sehr gut dafür, während andere mehr Aufwand erfordern.

6. Native Anwendung bereitstellen

Um unsere Anwendung in Cloud Run bereitzustellen, müssen wir unser natives Image in einen Paketmanager wie Artifact Registry hochladen.

1. Docker-Repository vorbereiten

Wir können diesen Prozess starten, indem wir ein Repository erstellen:

gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"

Als Nächstes müssen wir uns vergewissern, dass wir authentifiziert sind, um Inhalte hochzuladen in unsere neue Registry.

Die gcloud CLI kann diesen Prozess erheblich vereinfachen:

gcloud auth configure-docker us-central1-docker.pkg.dev

2. Image in Artifact Registry hochladen

Als Nächstes taggen wir unser Image:

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

Dann können wir es mit docker push an 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 Image, das wir in Artifact Registry gespeichert haben, 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 Anwendung als natives Image erstellt und bereitgestellt haben, können wir sicher sein, dass unsere Anwendung unsere Infrastrukturkosten optimal nutzt.

Vergleichen Sie die Startzeiten unserer Baseline-Anwendung mit denen der neuen nativen Anwendung.

6dde63d35959b1bb.png

7. Zusammenfassung/Bereinigung

Herzlichen Glückwunsch zur Erstellung und Bereitstellung einer Spring Native-Anwendung in Google Cloud.

Hoffentlich regt Sie diese Anleitung dazu an, sich mit dem Spring Native-Projekt vertraut zu machen und es im Hinterkopf zu behalten, falls es in Zukunft Ihren Anforderungen entspricht.

Optional: Dienst bereinigen und/oder deaktivieren

Unabhängig davon, ob Sie für dieses Codelab ein Google Cloud-Projekt erstellt oder ein vorhandenes Projekt wiederverwendet haben, sollten Sie unnötige Kosten für die von uns 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 herunterfahren.

8. Zusätzliche Ressourcen

Das Spring Native-Projekt ist zwar noch neu und experimentell, es gibt aber bereits eine Vielzahl guter Ressourcen, die Early Adopters bei der Fehlerbehebung und der Beteiligung am Projekt helfen können:

Zusätzliche Ressourcen

Im Folgenden finden Sie Online-Ressourcen, die für diese Anleitung relevant sein können:

Lizenz

Dieser Text ist mit einer Creative Commons Attribution 2.0 Generic License lizenziert.