Mein Dilemma
Ich bin ein großer Fan von Docker und Kubernetes, da es mir die Möglichkeit gibt, kleine funktionale Einheiten zu bauen und sie später wieder zu großen zusammenzufügen. Ganz so, wie es mein Kollege Christopher Schmidt in seinen Kubernetes-Artikeln beschreibt. Das erfordert aber, dass ich kleine Container bauen kann. Mit „klein“ meine ich: sowohl klein in seiner Funktionalität als auch klein im Speicher zur Laufzeit und klein zum Download beim Deployment.
Nebenbei bin ich aber auch ein großer Fan von Scala, weil mir hier der Compiler eine ganze Klasse von Fehlern schon zur Entwicklungszeit abfangen kann.
Mein Dilemma besteht nun darin, dass ich es bisher noch nicht geschafft habe eine JVM-basierte Sprache in einem Container auszuführen (und das kann durchaus an meinem Unvermögen liegen), der …
- … etwas sinnvolles tut
- … weniger als 250 MB RAM zur Laufzeit verbraucht hat
- … weniger als 150 MB Image-Größe hatte
Zerlege ich dann ein System in minimale funktionale Einheiten, ist mein Entwicklungsrechner schon mal schnell überfordert, alle Umsysteme für den Entwicklungsbetrieb laufen zu lassen. Mal ganz davon abgesehen, dass sich hungrige Artefakte auch in den Betriebskosten niederschlagen.
Mein Hoffnungsschimmer
Oracles GraalVM kann offensichtlich vieles leisten. Doch mein Hauptinteresse gilt hier der Tatsache, dass sie JVM-Class-Dateien zu nativen Plattform-Binaries kompilieren kann, um so (laut Webseite) den Memory-Footprint zu verringern:
Native
Native images compiled with GraalVM ahead-of-time improve the startup time and reduce the memory footprint of JVM-based applications.
Das wollte ich mir doch mal genauer ansehen, in der Hoffnung, am Ende wunderschöne und in alle Dimensionen „kleine“ Container mit JVM-Sprachen-Inhalt bauen und deployen zu können.
Ärmel hoch - Hände dran
Die GraalVM gibt es sowohl in der Enterprise (EE) als auch in der Community Edition (CE). Weil ich Open Source mag habe ich mich für die CE entschieden, die bei GitHub heruntergeladen werden kann:
Um gleich ein bisschen vorwärts zu machen, habe ich mir einen Docker-Container für die Build-Umgebung gebaut, in den ich das TAR entpacken kann, ohne meine aktuelle Landschaft zu stören:
Dockerfile:
In dem TAR-Archiv der GraalVM befindet sich ein komplettes Java-JDK, das ich zum Kompilieren von Java-Sourcen zu Class-Dateien benutzen kann. Zusätzlich liegt in dem bin-Verzeichnis das Tool „native-image“, das mir das versprochene Binary aus Class-Dateien bauen soll.
Hello World
Als Nächstes wollte ich mal sehen, wie problemlos ein kleiner Shell-Befehl gebaut werden kann. Um zu demonstrieren, dass auch fachlich hoch anspruchsvolle Anwendungen umgesetzt werden können, möchte ich also die Worte „Hello World“ auf der Konsole ausgeben.
HelloWorld.java:
Es ist keine Überraschung, dass der ganze Vorgang funktioniert. Etwas überrascht hat mich aber tatsächlich, dass es so einfach war.
Also ab zum nächsten Schritt, in dem das Binary erzeugt wird:
Das Bauen hat einen Moment gedauert, das soll aber nicht weiter stören, denn das Ergebnis passt:
Und mit einer Größe von 5.2 MB ist das Binary auch noch in einem annehmbaren Rahmen.
Hello World Server
Um als Nächstes ein Gefühl für den Memory-Footprint zu bekommen, habe ich fix einen simplen Webserver kopiert und modifiziert.
Kompilieren und starten:
Messen:
Dann mal das Ganze als native Binary:
Auch wenn 10 MB in der JVM durchaus nicht viel sind, so sind 864 KB spürbar weniger. Das Binary für den HelloWorldServer ist dabei immer noch nur 5,9 MB klein.
Container
Als Letztes wollte ich versuchen, meine Artefakte in einem Docker-Container verpackt laufen zu lassen. Der naive Ansatz, wie ich ihn von Go kenne, besteht darin, ein Binary in einen scratch-Container zu packen und zu starten:
Dockerfile.scratch:
Schade – ein Fehler, hier verließ das Experiment den Pfad der Problemlosigkeit. Ein Blick in die im Stacktrace referenzierte Zeile der entsprechenden Go-Datei hat auch nicht viel zur Aufklärung beigetragen. Sie reicht lediglich einen Fehler weiter, den sie beim Ausführen des Binaries bekommt. Nach etwas Nachdenken, welche Datei das Binary überhaupt öffnen muss, kam ich zu dem Schluss, dass es sich eigentlich nur um eine Library handeln kann und das lässt sich ja rausfinden:
Wunderbar, jetzt muss ich nur noch das Dockerfile anpassen, um die gelinkten Dateien mit in den Container zu packen. Sie sollten ja in dem Build-Container zu finden sein:
Dockerfile.scratch2:
Und tatsächlich, es läuft. Analog startet auch der HelloWorldServer. Die paar Libraries fügen dem Docker-Image nochmal 3 MB hinzu, für mich ist das aber auch noch in einem annehmbaren Bereich, wenn man bedenkt, dass ein jre-alpine Basis-image mehr als 100 MB dazupacken würde.
Fazit
Die GraalVM macht es einem in dem aktuellen Stadium noch nicht einfach, an schlanke Docker-Images und Prozesse zu kommen, aber sie macht es möglich. Für mich bedeutet das einen großen Schritt in die richtige Richtung. Wenn ich wirklich kleine Deployment-Artefakte bauen will, wo bisher der Griff zu Go fast selbsverständlich war, habe ich nun wieder die Wahl der Programmiersprache und des Ökosystems. Anhänger der Programmiersprache Go müssen jetzt bestimmt lächeln, da sie die hier ermittelten Zahlen leicht übertreffen können. Für mich ist aber mit den Möglichkeiten der GraalVM eine Schmerzgrenze unterschritten, wodurch ich Java für Docker-Container wieder passend finde.
Ich bin gespannt darauf, was sich ergibt, wenn ich mal mit Maven-, SBT- und Gradel-Builds auf die GraalVM losgehe und ob es sogar machbar ist, auch das Scala-Ökosystem unter meine Schmerzgrenze zu wuchten.