How to build a Graal native-image from a Scala app
Graal is currently on everyone’s lips and for good reason: as developers that deploy their software on top of the JVM, we are used to long startup times and high memory requirements. Graal’s native-image promises to solve both of these problems, enabling a new and exciting use case: real time serverless instances on platforms such as “Google Cloud Run”.
Prerequisites
In this tutorial we will take a simple http4s server application and walk through the Graal native-image configuration and build process. Start by checking out the sample code from the GitHub project taig/scala-graal.
Building the Scala app
At first we have to generate a far jar from our Scala app. This is done via sbt-assembly. The dependency has already been added to the project/plugins.sbt
file, so proceed by kicking it off via sbt assembly
which will generate ./target/scala-2.13/scala-graal-assembly-0.1.0-SNAPSHOT.jar
.
Building the native-image
The Dockerfile in docker/build
has all it takes to turn the jar file from the previous step into a Graal native-image. Simply run the command below from the project root (this might take a while).
If you are on macOS, make sure your Docker daemon has at least 4GB of RAM assigned.
docker run --rm -it -v $PWD:/app/ $(docker build -f docker/build -q .)
The docker/build
image is based on oracle/graalvm-ce:20.1.0-java11
, I couldn’t get it to work with the more recent 20.2.0 yet.
Building a Graal native-image relies heavily on configuration. You can find the respective configuration at ./src/main/resources/META-INF/native-image/io.taig/scala-graal
but you will see that the files are mostly empty. This is due to the simplicity of this example project. A real world project with plenty of dependencies can be incredibly difficult or even impossible to configure. I generally tend to struggle with the Google Cloud Java libraries, as they heavily rely on reflection. I didn’t manage to get doobie working (probably due to JDBC), but skunk works just fine. Be warned and prepared for painful and tedious trial and error sessions.
Running the executable
Now the executable should already be waiting for you in the project root directory. The easiest way to execute it is via a docker scratch
container with the command below.
docker run --init --rm -p 8080:8080 $(docker build -f docker/deploy -q .)
Make sure it works via a simple curl request:
$ curl http://localhost:8080/
> ¡Hola mundo!
To get a better feeling for the startup time, you may want to build the deploy image separately:
$ docker build --tag scala-graal-deploy -f docker/deploy .
$ docker run --init --rm -p 8080:8080 scala-graal-deploy
Which should start the server instantaneously compared to running on the JVM, which takes slightly longer.
java -jar target/scala-2.13/scala-graal-assembly-0.1.0-SNAPSHOT.jar
Conclusion
Graal native-image is an exciting technology that makes JVM-based languages an interesting candidate in the world of serverless cloud computing. However, the configuration process can be incredibly tedious and does not warrant to Graal native-image all the things just yet. Unfortunately it’s mostly the Java ecosystem which is to blame here. Java libraries are omnipresent but still tend to rely heavily on reflection.
Further reading
- Building Serverless Scala Services with GraalVM Native Image by Noel Welsh
- Benchmarking Web Services using GraalVM Native Image by Noel Welsh
- GraalVM Native Image Tips & Tricks by James Ward
- GraalVM Native Image: Real Life by Tarasov Aleksandr
- http4s documentation: Deployment
- Graal documentation: Static Native Images