Hinweis:

GraalVM Native Image, Frühling und Containerisierung

Einführung

In dieser Übung lernen Entwickler, wie sie GraalVM Native Image-Anwendungen containerisieren.

Die GraalVM Native Image-Technologie kompiliert Java-Code im Voraus in einer nativen ausführbaren Datei. Nur der Code, der zur Laufzeit von der Anwendung benötigt wird, ist in der ausführbaren Datei enthalten.

Eine ausführbare Datei, die von Native Image erstellt wird, bietet mehrere wichtige Vorteile:

Viele der führenden Microservice-Frameworks unterstützen die Kompilierung im Voraus mit GraalVM Native Image, einschließlich Micronaut, Spring, Helidon und Quarkus.

Darüber hinaus gibt es Maven- und Gradle-Plug-ins für Native Image, damit Sie Java-Anwendungen einfach als ausführbare Dateien erstellen, testen und ausführen können.

Hinweis: Oracle Cloud Infrastructure (OCI) stellt GraalVM Enterprise ohne zusätzliche Kosten bereit.

Voraussichtliche Labordauer: 90 Minuten

Übungsziele

In dieser Übung führen Sie folgende Schritte aus:

HINWEIS: Wenn das Laptopsymbol in der Übung angezeigt wird, müssen Sie z.B. einen Befehl eingeben. Achten Sie darauf.

# This is where you will need to do something

Schritt 1: Verbindung zu einem virtuellen Host herstellen und Entwicklungsumgebung prüfen

Ihre Entwicklungsumgebung wird von einem Remotehost bereitgestellt: einer OCI Compute-Instanz mit Oracle Linux 8, 4 Cores und 32 GB Arbeitsspeicher. Die Luna Labs-Desktopumgebung wird angezeigt, bevor der Remote-Host bereit ist. Dies kann bis zu zwei Minuten dauern.

Sie stellen eine Verbindung zu Ihrem Remote-Host her, indem Sie ein Setup-Skript in Ihrer Luna Desktop-Umgebung ausführen. Dieses Skript ist über die Registerkarte "Ressourcen" verfügbar.

  1. Doppelklicken Sie auf das Luna Lab-Symbol auf dem Desktop, um den Browser zu öffnen.

    Symbol für Luna Desktop

  2. Die Registerkarte Ressourcen wird angezeigt. Die neben dem Titel Ressourcen angezeigte Kopie wird gestartet, während die Compute-Instanz in der Cloud bereitgestellt wird.

    Registerkarte "Luna Resource"

  3. Wenn die Instanz bereitgestellt ist (dies kann bis zu 2 Minuten dauern), wird Folgendes auf der Registerkarte Ressourcen angezeigt.

    Luna Resources (Registerkarte)

  4. Kopieren Sie das Konfigurationsskript, mit dem die VS-Codeumgebung eingerichtet wird, aus der Registerkarte "Ressourcen". Klicken Sie auf den Link Details anzeigen, um die Konfiguration anzuzeigen. Kopieren Sie dies wie im folgenden Screenshot gezeigt:

    Konfigurationsskript kopieren

  5. Öffnen Sie ein Terminal, wie im folgenden Screenshot gezeigt:

    Terminal öffnen

  6. Fügen Sie den Konfigurationscode in das Terminal ein. Dadurch wird der VS-Code für Sie geöffnet.

    Terminal 1 einfügen

    Terminal 2 einfügen

  7. Ein VS-Codefenster wird geöffnet und stellt automatisch eine Verbindung zur VM-Instanz her, die für Sie bereitgestellt wurde. Klicken Sie auf Weiter, um den Fingerprint des Rechners zu akzeptieren.

    VS-Code akzeptieren

Sie sind fertig! Herzlichen Glückwunsch. Sie sind jetzt erfolgreich mit einem Remotehost in Oracle Cloud verbunden.

Mit dem obigen Skript wird der VS-Code geöffnet, der mit der Remote-Compute-Instanz verbunden ist, während der Quellcode für die Übung geöffnet wird.

Als nächstes müssen Sie ein Terminal innerhalb des VS-Codes öffnen. Mit diesem Terminal können Sie mit dem Remotehost interagieren. Ein Terminal kann im VS-Code über das Menü geöffnet werden: Terminal > Neues Terminal.

VS-Code-Terminal

Wir nutzen dieses Terminal im Rest der Übung.

Hinweis zur Entwicklungsumgebung

Wir verwenden GraalVM Enterprise 22 als Java-Umgebung für diese Übung. GraalVM ist eine leistungsstarke JDK-Distribution von Oracle, die auf einer vertrauenswürdigen und sicheren Oracle Java SE basiert.

Ihre Entwicklungsumgebung ist mit GraalVM und den Tools für native Images vorkonfiguriert, die für diese Übung erforderlich sind.

Sie können dies einfach prüfen, indem Sie die folgenden Befehle im Terminal ausführen:

java -version

native-image --version

Schritt 2: Unsere Beispiel-Java-Anwendung kennenlernen

In dieser Übung erstellen Sie eine einfache Anwendung mit einer sehr minimalen REST-basierten API. Anschließend werden Sie diese Anwendung mit Docker in einen Container stellen. Sehen Sie sich zunächst die einfache Anwendung an.

Sie haben den Quellcode angegeben und Skripte für diese Anwendung erstellt. Der Ordner mit dem Quellcode wird im VS-Code geöffnet.

Die Anwendung basiert auf dem Spring Boot-Framework und nutzt das Spring Native Project (ein Spring-Inkubator zur Generierung nativer ausführbarer Dateien mit GraalVM Native Image).

Die Anwendung verfügt über zwei Klassen, die in src/main/java zu finden sind:

Was bewirkt die Anwendung? Wenn Sie den Endpunkt-REST /jibber aufrufen, der in der Anwendung definiert ist, gibt er einen Unsinnvers zurück, der im Stil des Jabberwocky-Poems von Lewis Carroll generiert wurde. Das Programm erreicht dies, indem es eine Markov Chain verwendet, um das ursprüngliche Gedicht zu modellieren (das ist im Wesentlichen ein statistisches Modell). Dieses Modell generiert einen neuen Text.

In der Beispielanwendung stellen wir der Anwendung den Text des Gedichts zur Verfügung und generieren dann ein Modell des Textes, mit dem die Anwendung dann einen neuen Text generiert, der dem ursprünglichen Text ähnlich ist. Wir nutzen die RiTa-Bibliothek für uns, um uns von Anfang an zu heben. Sie unterstützt das Erstellen und Verwenden von Markov Chains.

Darunter befinden sich zwei Snippets aus der Utilityklasse com.example.demo.Jabberwocky, die das Modell erstellen. Die Variable text enthält den Text des ursprünglichen Gedichts. Dieses Snippet zeigt, wie das Modell erstellt und dann mit text aufgefüllt wird. Dieser wird vom Klassenkonstruktor aufgerufen. Die Klasse wird als Singleton definiert (so dass nur eine Instanz der Klasse jemals erstellt wird).

this.r = new RiMarkov(3);
this.r.addText(text);

Hier sehen Sie die Methode zum Generieren neuer Zeilen von Versen aus dem Modell, basierend auf dem ursprünglichen Text.

public String generate() {
    String[] lines = this.r.generate(10);
    StringBuffer b = new StringBuffer();
    for (int i=0; i< lines.length; i++) {
        b.append(lines[i]);
        b.append("<br/>\n");
    }
    return b.toString();
}

Nehmen Sie sich etwas Zeit, um den Code anzuzeigen und sich damit vertraut zu machen.

Zum Erstellen der Anwendung verwenden Sie Maven. Die Datei pom.xml wurde mit Spring Initializr generiert und enthält Unterstützung zur Verwendung der Spring Native-Tools. Dies ist eine Abhängigkeit, die Sie Ihren Spring Boot-Projekten hinzufügen möchten, wenn Sie ein GraalVM Native Image ansprechen möchten. Wenn Sie Maven verwenden, wird durch Hinzufügen von Unterstützung für Spring Native das folgende Plug-in in Ihre Standard-Build-Konfiguration eingefügt.

<plugin>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-aot-maven-plugin</artifactId>
    <version>${spring-native.version}</version>
    <executions>
        <execution>
            <id>test-generate</id>
            <goals>
                <goal>test-generate</goal>
            </goals>
        </execution>
        <execution>
            <id>generate</id>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Erstellen Sie nun die Anwendung. Führen Sie im Root-Verzeichnis des Repositorys die folgenden Befehle in der Shell aus:

mvn clean package

Dadurch wird eine "ausführbare" JAR-Datei generiert, die alle Abhängigkeiten der Anwendung sowie eine richtig konfigurierte MANIFEST-Datei enthält. Sie können diese JAR-Datei ausführen und dann den Endpunkt der Anwendung "ping" ausführen, um Ihre Eingaben anzuzeigen. Setzen Sie den Befehl mit & in den Hintergrund, damit Sie die Eingabeaufforderung zurückgeben.

java -jar ./target/jibber-0.0.1-SNAPSHOT-exec.jar &

Rufen Sie den Endpunkt mit dem Befehl curl in der Befehlszeile auf.

Wenn Sie den Befehl an Ihr Terminal senden, werden Sie möglicherweise aufgefordert, die URL in einem Browser zu öffnen. Schließen Sie einfach den Dialog, wie unten dargestellt.

VS-Code

Führen Sie Folgendes aus, um den HTTP-Endpunkt zu testen:

curl http://localhost:8080/jibber

Hast du den Unsinn-Vers zurück bekommen? Nachdem Sie eine funktionierende Anwendung erstellt haben, beenden Sie sie, und fahren Sie fort, um sie zu containerisieren. Setzen Sie die Anwendung in den Vordergrund, damit Sie sie beenden können.

fg

Geben Sie <ctrl-c> ein, um die Anwendung jetzt zu beenden.

<ctrl-c>

Schritt 3: Java-Anwendung mit Docker in Container aufnehmen

Die Containerisierung Ihrer Java-Anwendung als Docker-Container ist sehr einfach. Sie können ein neues Docker-Image basierend auf einem Image erstellen, das eine JDK-Distribution enthält. In dieser Übung verwenden Sie also einen Container, der bereits ein JDK, container-registry.oracle.com/java/openjdk:17-oraclelinux8, enthält. Hierbei handelt es sich um ein Oracle Linux 8-Image mit OpenJDK.

Im Folgenden finden Sie eine Aufschlüsselung der Dockerfile, in der beschrieben wird, wie das Docker-Image erstellt wird. Weitere Informationen finden Sie in den Kommentaren.

FROM container-registry.oracle.com/java/openjdk:17-oraclelinux8 # Base Image

ARG JAR_FILE                   # Pass in the JAR file as an argument to the image build

EXPOSE 8080                    # This image will need to expose TCP port 8080, as this is the port on which your app will listen

COPY ${JAR_FILE} app.jar       # Copy the JAR file from the `target` directory into the root of the image 
ENTRYPOINT ["java"]            # Run Java when starting the container
CMD ["-jar","app.jar"]         # Pass in the parameters to the Java command that make it load and run your executable JAR file

Sie finden die Dockerfile zur Containerisierung Ihrer Java-Anwendung im Verzeichnis 00-containerise.

Um ein Docker-Image zu erstellen, das Ihre Anwendung enthält, führen Sie die folgenden Befehle vom Terminal aus:

docker build -f ./00-containerise/Dockerfile \
             --build-arg JAR_FILE=./target/jibber-0.0.1-SNAPSHOT-exec.jar \
             -t localhost/jibber:java.01 .

Fragen Sie Docker ab, um sich das neu erstellte Image anzusehen:

docker images | head -n2

Es sollte ein neues Bild aufgeführt werden. Führen Sie dieses Bild wie folgt aus:

docker run --rm -d --name "jibber-java" -p 8080:8080 localhost/jibber:java.01

Rufen Sie den Endpunkt dann wie zuvor mit dem Befehl curl auf:

curl http://localhost:8080/jibber

Hast du den Unsinnvers gesehen? Prüfen Sie nun, wie lange die Anwendung zum Hochfahren dauerte. Sie können diese Daten aus den Logs extrahieren, da Spring Boot-Anwendungen die Startzeit in die Logs schreiben:

docker logs jibber-java

Beispiel: Die Anwendung wurde in 3.896s gestartet. Hier ist der Extrakt aus den Logs:

2022-03-09 19:48:09.511  INFO 1 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 3.896 seconds (JVM running for 4.583)

OK, beenden Sie den Container, und fahren Sie fort:

docker kill jibber-java

Sie können auch Docker abfragen, um die Größe des Images abzurufen. Wir haben ein Skript angegeben, das dies für Sie tut. Führen Sie im Terminal den folgenden Befehl aus:

echo $((`docker inspect -f "" localhost/jibber:java.01`/1024/1024))

Dadurch wird die Größe des Bildes in MB gedruckt, also 606 MB.

Schritt 4: Native ausführbare Datei erstellen

Zusammenfassen, was Sie bisher haben:

  1. Sie haben eine Spring Boot-Anwendung mit einem HTTP-Endpunkt /jibber erstellt.
  2. Sie haben den Container erfolgreich ausgeführt

Jetzt erfahren Sie, wie Sie mit GraalVM Native Image eine native ausführbare Datei aus Ihrer Anwendung erstellen können. Diese native ausführbare Datei wird eine Reihe interessanter Eigenschaften haben, nämlich:

  1. Es wird wirklich schnell anfangen
  2. Er verwendet weniger Ressourcen als die entsprechende Java-Anwendung

Mit dem Native Image Tooling, das mit GraalVM installiert ist, können Sie eine native ausführbare Datei einer Anwendung über die Befehlszeile erstellen. Da Sie Maven bereits verwenden, wenden Sie jedoch die nativen GraalVM-Build-Tools für Maven an. Dadurch können Sie das Build mit Maven problemlos fortsetzen.

Eine Möglichkeit, Unterstützung zum Erstellen einer nativen ausführbaren Datei hinzuzufügen, besteht in der Verwendung eines Maven-Profils, mit dem Sie entscheiden können, ob Sie nur die JAR-Datei oder eine native ausführbare Datei erstellen möchten.

In der bereitgestellten Maven-Datei pom.xml wurde ein Profil hinzugefügt, das eine native ausführbare Datei erstellt. Detaillierte Informationen:

Zuerst müssen Sie das Profil deklarieren und ihm einen Namen geben.

<profiles>
    <profile>
        <id>native</id>
        <!-- Rest of profile hidden, to highlight relevant parts -->
    </profile>
</profiles>

Als Nächstes wird im Profil das Plug-in zur Erstellung von GraalVM Native Image hinzugefügt und an die Phase package in Maven angehängt. Das bedeutet, dass er als Teil der Phase package ausgeführt wird. Beachten Sie, dass Sie Konfigurationsargumente mit dem Abschnitt <buildArgs> an das zugrunde liegende Build-Tool für native Images übergeben können. In einzelnen buildArg-Tags können Sie Parameter genau wie mit dem Tool native-image übergeben. Sie können also alle Parameter verwenden, die mit dem Tool native-image funktionieren:

<build>
    <plugins>
        <plugin>
            <groupId>org.graalvm.buildtools</groupId>
            <artifactId>native-maven-plugin</artifactId>
            <version>${native-buildtools.version}</version>
            <extensions>true</extensions>
            <executions>
                <execution>
                    <id>build-native</id>
                    <phase>package</phase>
                    <goals>
                        <goal>build</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <imageName>jibber</imageName>
                <buildArgs>
                    <buildArg>-H:+ReportExceptionStackTraces</buildArg>
                </buildArgs>
            </configuration>
        </plugin>
        <!-- Rest of profile hidden, to high-light relevant parts -->
    </plugins>
</build>

Führen Sie nun den Maven-Build mit dem Profil wie unten aus (beachten Sie, dass der Profilname mit dem Kennzeichen -P angegeben wird):

mvn package -Pnative

Dadurch wird eine native ausführbare Datei für die Plattform im Verzeichnis target mit dem Namen jibber generiert. Sehen Sie sich die Größe der Datei an:

ls -lh target/jibber

Führen Sie diese native ausführbare Datei aus, und testen Sie sie. Führen Sie den folgenden Befehl im Terminal aus, um die native ausführbare Datei auszuführen und mit & in den Hintergrund zu stellen:

./target/jibber &

Rufen Sie den Endpunkt mit dem Befehl curl auf:

curl http://localhost:8080/jibber

Jetzt haben Sie eine native ausführbare Datei der Anwendung, die wirklich schnell beginnt!

Beenden Sie die Anwendung, bevor Sie fortfahren. Setzen Sie die Anwendung in den Vordergrund:

fg

Beenden Sie sie mit <ctrl-c>:

<ctrl-c>

Schritt 5: Natives ausführbares Programm in Container aufnehmen

Da Sie nun über eine native, ausführbare Version Ihrer Anwendung verfügen und diese ordnungsgemäß funktioniert, verpacken Sie sie.

Wir haben eine einfache Dockerfile zum Packen dieser nativen ausführbaren Datei bereitgestellt: Sie befindet sich im Verzeichnis native-image/containerisation/lab/01-native-image/Dockerfile. Die Inhalte werden im Folgenden zusammen mit Kommentaren angezeigt, die jede Zeile erläutern.

FROM container-registry.oracle.com/os/oraclelinux:8-slim

ARG APP_FILE                 # Pass in the native executable
EXPOSE 8080                  # This image will need to expose TCP port 8080, as this is port your app will listen on

COPY ${APP_FILE} app  # Copy the native executable into the root directory and call it "app"
ENTRYPOINT ["/app"]          # Just run the native executable :)

Führen Sie zum Erstellen am Terminal den folgenden Befehl aus:

docker build -f ./01-native-image/Dockerfile \
             --build-arg APP_FILE=./target/jibber \
             -t localhost/jibber:native.01 .

Sehen Sie sich das neu erstellte Bild an:

docker images | head -n2

Jetzt können Sie diesen Befehl ausführen und wie folgt vom Terminal testen:

docker run --rm -d --name "jibber-native" -p 8080:8080 localhost/jibber:native.01

Rufen Sie den Endpunkt über das Terminal mit curl auf:

curl http://localhost:8080/jibber

Auch hier hätte man mehr Unsinnvers im Stil des Gedichts Jabberwocky sehen sollen. Prüfen Sie, wie lange die Anwendung zum Starten dauerte, indem Sie die von der Anwendung erstellten Logs prüfen. Führen Sie in Ihrem Terminal den folgenden Befehl aus, und suchen Sie nach der Startzeit:

docker logs jibber-native

Das Folgende zeigt, dass die App in 0.074s gestartet wurde. Das ist eine große Verbesserung im Vergleich zum Original von 3.896s!

2022-03-09 19:44:12.642  INFO 1 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.074 seconds (JVM running for 0.081)

Beenden Sie den Container, und fahren Sie mit dem nächsten Schritt fort:

docker kill jibber-native

Bevor Sie zum nächsten Schritt gehen, schauen Sie sich die Größe des erzeugten Containers an:

echo $((`docker inspect -f "" localhost/jibber:native.01`/1024/1024))

Die Größe des Container-Images betrug 199 MB. Viel kleiner als unser ursprünglicher Java-Container.

Schritt 6: Mostly Static Executable erstellen und in einem Distroless-Image verpacken

Wiederholen Sie, was Sie bisher getan haben:

  1. Sie haben eine Spring Boot-Anwendung mit einem HTTP-Endpunkt /jibber erstellt.
  2. Sie haben den Container erfolgreich ausgeführt
  3. Sie haben eine native ausführbare Datei Ihrer Anwendung mit den Build-Tools für native Images für Maven erstellt.
  4. Sie haben Ihre native ausführbare Datei in einem Container abgelegt

Es wäre großartig, wenn wir Ihre Containergröße noch weiter verkleinern könnten, da kleinere Container schneller heruntergeladen und gestartet werden können. Mit GraalVM Native Image können Sie Systembibliotheken statisch mit der von Ihnen generierten nativen ausführbaren Datei verknüpfen. Wenn Sie eine statisch verknüpfte native ausführbare Datei erstellen, können Sie die native ausführbare Datei direkt in einem leeren Docker-Image verpacken, das auch als scratch-Container bezeichnet wird.

Eine weitere Möglichkeit besteht darin, eine so genannte meist statisch verknüpfte native ausführbare Datei zu erzeugen. Damit verknüpfen Sie statisch in allen Systembibliotheken mit Ausnahme der C-Standardbibliothek glibc. Mit einer solchen nativen ausführbaren Datei können Sie einen kleinen Container verwenden, wie Google Distroless, der die glibc-Bibliothek, einige Standarddateien und SSL-Sicherheitszertifikate enthält. Der Distroless-Standardcontainer hat eine Größe von etwa 20 MB.

Sie erstellen eine meist statisch verknüpfte ausführbare Datei und verpacken sie dann in einem Distroless-Container.

Wir haben ein weiteres Maven-Profil hinzugefügt, um diese größtenteils statisch verknüpfte native ausführbare Datei zu erstellen. Dieses Profil heißt distroless. Der einzige Unterschied zwischen diesem Profil und dem vorher verwendeten Profil, native, besteht darin, dass ein Parameter -H:+StaticExecutableWithDynamicLibC übergeben wird. Wie Sie vermuten können, weist dies native-image an, eine meist statisch verknüpfte native ausführbare Datei zu erstellen.

Sie können Ihre meist statisch verknüpfte native ausführbare Datei wie folgt erstellen:

mvn package -Pdistroless

Es ist einfach genug. Die generierte native ausführbare Datei befindet sich im Zielverzeichnis jibber-distroless.

Jetzt in einem Distroless Container packen. Die hierfür erforderliche Dockerfile finden Sie im Verzeichnis native-image/containerisation/lab/02-smaller-containers/Dockerfile. Sehen Sie sich den Inhalt der Dockerfile an, die Kommentare enthält, um jede Zeile zu erklären:

FROM gcr.io/distroless/base # Our base image, which is Distroless

ARG APP_FILE                # Everything else is the same :)
EXPOSE 8080

COPY ${APP_FILE} app
ENTRYPOINT ["/app"]

Führen Sie zum Erstellen am Terminal den folgenden Befehl aus:

docker build -f ./02-smaller-containers/Dockerfile \
             --build-arg APP_FILE=./target/jibber-distroless \
             -t localhost/jibber:distroless.01 .

Sehen Sie sich das neu erstellte Distroless-Bild an:

docker images | head -n2

Jetzt können Sie sie wie folgt ausführen und testen:

docker run --rm -d --name "jibber-distroless" -p 8080:8080 localhost/jibber:distroless.01

curl http://localhost:8080/jibber

Großartig! Es hat funktioniert. Aber wie klein oder groß ist Ihr Container? Verwenden Sie das Skript, um die Bildgröße zu prüfen:

echo $((`docker inspect -f "" localhost/jibber:distroless.01`/1024/1024))

Die Größe beträgt ca. 107 MB! Also haben wir den Container um 92 MB geschrumpft. Ein langer Weg von unserer Ausgangsgröße, für den Java-Container, von etwa 600 MB.

Schlussfolgerung

Wir hoffen, Sie haben dieses Labor genossen und einige Dinge auf dem Weg gelernt. Wir haben uns vorgestellt, wie Sie eine Java-Anwendung containerisieren können. Anschließend wurde untersucht, wie diese Java-Anwendung in eine native ausführbare Datei konvertiert werden kann, die wesentlich schneller als die Java-Anwendung startet. Anschließend containerisierte wir die native ausführbare Datei und konnten feststellen, dass die Größe des Docker-Images mit der nativen ausführbaren Datei wesentlich kleiner ist als das Java Docker-Image.

Schließlich haben wir uns angesehen, wie wir meist statisch verknüpfte native ausführbare Dateien mit Native Image erstellen können. Diese können in kleineren Containern verpackt werden, wie Distroless. So können wir die Größe des Docker-Images noch weiter verringern.

Weitere Informationen

Weitere Lernressourcen

Sehen Sie sich andere Übungen zu docs.oracle.com/learn an, oder greifen Sie auf weitere Inhalte für kostenloses Lernen im Oracle Learning YouTube-Kanal zu. Außerdem besuchen Sie education.oracle.com/learning-explorer, um Oracle Learning Explorer zu werden.

Produktdokumentation finden Sie im Oracle Help Center.