Spring Native Microservices mit GraalVM - Blitzstart in der Cloud

Der perfekte Microservice skaliert in der Cloud optimal, so dass sprunghaft steigende Last fehlerfrei verarbeitet wird. Mit Spring Native und der GraalVM können wir Cloud native Images bauen. Container von nativen Images starten trotz geringerem Ressourcenverbrauch blitzschnell. Dazu zeige ich euch hier eine Demo.

AOT Kompilierung mit GraalVM

GraalVM ist eine Java VM (Virtuelle Maschine) und ein JDK (Java Development Kit) in den Java LTS Versionen 11 oder 17. Oracle entwickelt GraalVM und bietet es als freie Community Edition oder lizenzpflichtige Enterprise Edition an. Seit Version 19 im Mai 2019 gilt GraalVM als produktionsreif.

Die große Besonderheit von GraalVM ist die ahead-of-time (AOT) Kompilierung des Sourcecodes. Damit bauen wir in der Build-Phase (z.B. mit Maven) ein natives Image. Native Images enthalten alle benötigten Applikations Klassen, Dependencies, 3rd Party Bibliotheken und JDK Klassen. Das native Image ist eine alleinstehende, ausführbare Binärdatei, deren Vorteile ein deutlich schnellerer Start bei geringerem Speicherverbrauch sind.

Im Vergleich zur klassischen just-in-time (JIT) Kompilierung hat die AOT Kompilierung auch Nachteile. Der Spitzen-Durchsatz ist geringer und die maximales Latenz größer. Entscheidet euch je nach Anwendungsfall für die besser passende Alternative.

Vergleich von AOT und klassischer JIT Kompilierung

Da es mir hier um das native Image geht, schaut euch für weitere Infos die GraalVM Webseite an: https://www.graalvm.org/.

Installation GraalVM

Die Installation der GraalVM ist einfach: Herunterladen, Entpacken und in der IDE konfigurieren (im verlinktem Artikel habe ich das Oracle JDK in der IDE konfiguriert, die GraalVM Konfiguration funktioniert analog).
Die ausführliche Installationsanleitung findet ihr hier:
https://www.graalvm.org/docs/getting-started/#install-graalvm

Installation Docker

Native Images sind Docker Images. Damit wir diese später Bauen und Verwenden können, benötigen wir eine Docker Installation. In Blog Artikel java-in-docker.html habe ich bereits das Zusammenspiel von Java Anwendungen und Docker erklärt. Dort gibt es auch Erklärungen zum Installieren von Docker. Wenn ihr lokal native Images bauen wollt, benötigt ihr Docker.

Spring Native

Spring Native baut Spring Anwendungen als natives Image mittels GraalVM. So übernimmt die Spring Anwendung die Vorteile des GraalVM nativen Images:
  • Blitzstart mit direkter Bereitschaft Lastspitzen im Container zu verarbeiten
  • Geringer Speicherverbrauch im Vergleich zur herkömmlichen Spring Anwendung

Stand November 2022 hat Spring Native leider noch keine 1.0er Version. Trotzdem stelle ich es euch hier vor, da der Blitzstart extrem beeindruckend ist. Die Risiken einer Beta-Version innerhalb eines einfachen Microservices sind für mich überschaubar. Voraussichtlich kommt die 1.0er Version mit Spring 6 noch in diesem Jahr. 
Hier findet ihr die Dokumentation zu Spring Native für weitere Details: 
https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/ 

Spring Boot Projekt für GraalVM

Spring Boot unterstützt bereits Spring Native. Daher erstelle ich mein Projekt mit dem Spring Initializr.
 
Spring Native Projekt im Initializr

Spring Boot erstellt die Maven Konfiguration für uns. Ihr müsst dort keine eigenen Anpassungen vornehmen. Ich zeige euch hier nur die Unterschiede im Vergleich zu herkömmlich, neu erstellten Spring Boot Projekten:
  • Spring Native hat eine eigene Dependency, deren Version noch nicht von Spring Boot in Version 2.7.5 gemanagt wird:
    <dependency>
        <groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.12.1</version>
    </dependency>
  • Die Build-Konfiguration weicht deutlich vom Spring Boot Standard ab, da wir ein natives Image mit AOT Kompilierung bauen wollen:
<build>
    <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>
        <plugin>
            <groupId>org.springframework.experimental</groupId>
            <artifactId>spring-aot-maven-plugin</artifactId>
            <version>0.12.1</version>
            <executions>
                <execution>
                    <id>generate</id>
                    <goals>
                        <goal>generate</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

  • Das Plugin spring-boot-maven-plugin hat eine eigene Konfiguration für native Images.
  • Außerdem gibt es ein 2. Plugin spring-aot-maven-plugin, welches die AOT Transformation aufruft.
  • Für Plugins und Dependencies werden spezielle Maven Repositories von Spring konfiguriert, da sich die Beta Version von Spring Native und ihre Tools nicht in den offiziellen Maven Repositories befinden. Das wird sich dann mit dem 1.0 Release ändern, bis dahin werden diese Repositories benötigt: 
    <repositories>
        <repository>
    <id>spring-releases</id>
    <name>Spring Releases</name>
    <url>https://repo.spring.io/release</url>
    <snapshots>
        <enabled>false</enabled>
    </snapshots>
</repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
    <id>spring-releases</id>
    <name>Spring Releases</name>
    <url>https://repo.spring.io/release</url>
    <snapshots>
        <enabled>false</enabled>
    </snapshots>
</pluginRepository>
    </pluginRepositories>
  • Außerdem gibt es noch ein Maven Profil, welches weitere Dependencies und Plugins festlegt:
<profile>
    <id>native</id>
    <properties>
        <repackage.classifier>exec</repackage.classifier>
    </properties>
    <build>
        <plugins>
    <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <version>0.9.16</version>
                <extensions>true</extensions>
                <executions>
                    <execution>
                        <id>build-native</id>
                        <phase>package</phase>
                        <goals>
                            <goal>build</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</profile>
  • Mit dem Plugin native-maven-plugin wird schließlich das native GraalVM Image gebaut. Bei aktive Maven Profil native geschieht das dann in der package Phase des Maven Builds. Das machen wir dann im nächsten Abschnitt.

Cloud natives Microservice Image bauen und starten

Das zuvor generierte Spring native Projekt hat noch keine Anwendung. Im Screenshot hatte ich Spring WebFlux als Web Framework ausgewählt - ihr könntet z. B. den Java Code aus diesem Blog-Beitrag verwenden: webflux.html

Alternativ ist auch der klassische Servlet Stack als Web Framework möglich. Dazu könntet ihr den Code aus diesem Artikel verwenden: rest-json-apis-in-java-leicht-gemacht.html
Der Applikationscode ist natürlich unabhängig vom nativen Image und kann alles mögliche sein. Ein REST Controller mit HTTP GET Methode bietet sich zum Ausprobieren an, weil er mit wenigen Zeilen Code geschrieben ist und dann im Browser getestet werden kann.

Natives Image mit Maven bauen

Mit dem Maven Plugin native-maven-plugin baut Maven in der package Phase ein natives Docker Image. Maven speichert das native Image es in der lokalen Docker Registry. Passend zur zuvor gezeigten Maven Konfiguration bietet uns Spring Boot folgendes Maven Kommando an, um Applikation und natives Image zu bauen:
    mvn spring-boot:build-image

Der Maven Build zum Bauen des nativen Images dauert deutlich länger als ein herkömmlicher Build. Das liegt an der AOT Kompilierung mit der GraalVM. Den Vorteil des schnellen Container Starts zum Beginn der Laufzeit, erkauft man sich durch die vorab ausgeführte (AOT) Kompilierung während der Build-Zeit. Die Build-Zeit wird am Ende von Maven geloggt. Bei mir dauerte es für das hier gezeigte einfache Demo-Projekt immer über 5 Minuten:
...
[INFO] Successfully built image 'docker.io/library/graalvm:0.5'
[INFO] 
[INFO] --------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] --------------------------------------------------------
[INFO] Total time:  05:19 min

Zum Vergleich das Bauen der jar-Datei ohne natives Image dauert mit Maven bei mir circa 30 Sekunden. Wie Anfangs erwähnt wird das native Docker Image von Maven in der lokalen Docker Registry gespeichert. Der folgende Docker Befehl listet alle Images in unserer Docker Registry auf:
    docker images --all

    REPOSITORY                  TAG           SIZE
    paketobuildpacks/run        tiny-cnb      17.4MB
    graalvm                     0.5           86.2MB
    paketobuildpacks/builder    tiny          599MB

Das gebaute native Image ist graalvm mit dem Tag 0.5. Die beiden anderen Images wurden von Maven während dem Bauen verwenden und sind deshalb auch in unserer lokalen Registry. 

Natives Image mit Docker starten

Das Starten des nativen Images funktioniert genau so, wie wir es von Docker kennen (siehe dazu auch java-in-docker.html):
    docker run -p 8080:8080 graalvm:0.5

Dann erscheinen die von Spring bekannten Logs in der Shell:
2022-07-23 06:26:05.642  INFO 1 --- [main] ...NativeListener :
AOT mode enabled
...
 :: Spring Boot ::                (v2.7.1)
2022-07-23 06:26:05.644  INFO 1 --- [main] ...GraalvmApplication : Starting GraalvmApplication using Java 17.0.3.1 on 230a54500910 with PID 1 (/workspace/de.bsi.graalvm.GraalvmApplication started by cnb in /workspace)
2022-07-23 06:26:05.644  INFO 1 --- [main] ...GraalvmApplication :
No active profile set, falling back to 1 default profile: "default"
2022-07-23 06:26:05.667  INFO 1 --- [main] ...NettyWebServer :
Netty started on port 8080
2022-07-23 06:26:05.668  INFO 1 --- [main] ...GraalvmApplication :
Started GraalvmApplication in 0.033 seconds (JVM running for 0.037)

In der letzten Log-Zeile sehen wir das Highlight bzw. den Blitzstart von nativen Images mit der GraalVM: Started GraalvmApplication in 0.033 seconds 🚀
Wir haben eine Spring Anwendung in unter einer Zehntelsekunde gestartet! Bei meinen Demos von herkömmlichen Spring Anwendungen dauert das meist circa 3 Sekunden - das ist ein erheblicher Unterschied.

Wenn ihr den Code aus einem meiner vorherigen Artikeln verwendet habt, sollte die Web Applikation auf Port 8080 lauschen. Deshalb ist im Docker Run-Befehl das Port Mapping -p 8080:8080, so dass ihr die Anwendung im Container mit dem Browser testen könnt: http://localhost:8080/<Pfad_zum_Controller>

Fazit

Durch den extrem schnellen Start ist die GraalVM bzw. die mit ihr erzeugten nativen Images eine sehr gute Wahl für Anwendungen, die aufgrund spontaner Lastspitzen schnell skalieren müssen. Gegenüber einer herkömmlichen JVM hat sie klare Vor- und Nachteile, so dass ihr je nach Anwendungsfall die besser passende Lösung wählen könnt. Mich hat am meisten beeindruckt, dass die Startzeit der nativen Images bei meinen Tests mindestens um den Faktor 10 schneller war 😲

Spring Native ist Stand November 2022 noch in der Beta-Phase. Mit Spring 6 und Spring Boot 3 sollte sich das aber noch in diesem Jahr ändern.

Den Code zum Artikel findest Du bei GitHub: https://github.com/elmar-brauch/graalvm

Unterschiede zur früheren Version dieses Blog Artikels von Juni 2021:
  • GraalVM unterstütze im Juni 2021 nur Java 11, mittlerweile gibt es die Java 17 Version von GraalVM. Es gab aber keine GraalVM Version zu den Java Versionen zwischen den LTS Versionen 11 und 17.
  • Der Maven Build für das native Image benötigte auf demselben Notebook im Juni 2021 mit der Spring Native Version 0.10 über 6 Minuten. In der hier verwendeten, aktuellen Version 0.12.1 war der Build circa 1 Minute schneller.
  • Die Startzeit des Docker Containers mit dem nativen Image von Spring 0.10 dauerte auf demselben Notebook im Juni 2021 circa 0,05 Sekunden. Mit der Version 0.12.1 dauerte der Start circa 0,03 Sekunden.

Kommentare

Beliebte Posts aus diesem Blog

OpenID Connect mit Spring Boot 3

Authentifizierung in Web-Anwendungen mit Spring Security 6

Reaktive REST-Webservices mit Spring WebFlux