Spring Native in Google Cloud

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

Umfrage

Wie möchten Sie diese Anleitung verwenden?

Nur durchlesen Durchlesen und die Übungen absolvieren

Wie würden Sie Ihre Erfahrung mit Java bewerten?

Anfänger Fortgeschrittene Erfahren

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

Anfänger Fortgeschritten Erfahren

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.

5042e8e62a05a27.png

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: 6ecb403e9af1475e.png

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). d420322893640701.png
  • Dieser Build-Prozess kann viel Arbeitsspeicher (einige Gigabyte) beanspruchen. cda24e1eb11fdbea.png
  • 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:

e667f65a011c1328.png

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.

6dde63d35959b1bb.png

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:

Lizenz

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