Exercise - Using GraalVM to create native Serverless Java function
As mentioned in the previously, reducing the size of a unction container image is key.
GraalVM offers many features but in this exercise, we will solely focus on GraalVM
native-image is an AOT (Ahead-of-Time) compiler that will compile Java code into a native binary executable.
Boostrap the function
To create a function that uses GraalVM
native-init we will use the Fn
:bulb: This is not a typo! There’s
native-init, a GraalVM feature and
init-image, an Fn feature!
fn init --init-image fnproject/fn-java-native-init graalfunc
The parameter passed (“fnproject/fn-java-native-init”) to
init-image is a Docker image that will produce all the artifacts required by the function, including GraalVM AOT compiler support.
If you look at the content of the newly created “graalfunc” directory, you will see familiar content (
HelloFunction.java, etc.) but also a
Dockerfile that Fn will use to build the actual container image of the function.
Build and Deploy the Function
You can build, deploy and run the function, as usual, using the Fn CLI.
fn create someapp fn deploy --app someapp graalfunc fn invoke someapp graalfunc
Using GraalVM `native-image’
If we inspect the
Dockerfile, we can notice that it’s a multi-stage build as it uses multiple images (
First our Java function is compiled using Maven and Fn Java FDK image, this result in a regular jar.
The key part of the
Dockefile is the following set of commands wehre a GraalVM container image (
fnproject/fn-java-native) is used to invoke the
native-image utility to compile the function JAR into a native executbale. The benefit of this approach, i.e. buidling into containers, is that nothing is required on the developer machine, no GraalVM setup nor any Java installation!
FROM fnproject/fn-java-native:latest as build-native-image WORKDIR /function COPY --from=build /function/target/*.jar target/ COPY --from=build /function/src/main/conf/reflection.json reflection.json COPY --from=build /function/src/main/conf/jni.json jni.json RUN /usr/local/graalvm/bin/native-image \ --static \ --delay-class-initialization-to-runtime=com.fnproject.fn.runtime.ntv.UnixSocketNative \ -H:Name=func \ -H:+ReportUnsupportedElementsAtRuntime \ -H:ReflectionConfigurationFiles=reflection.json \ -H:JNIConfigurationFiles=jni.json \ -classpath "target/*"\ com.fnproject.fn.runtime.EntryPoint
The rest of the
Dockerfile is pretty straight forward, i.e. the function image is created from a
busybox:glibc base image. The native executable generated in the privous step is then copied into this image (
COPY --from=build-native-image /function/func func).
You can see that creating and using a GraalVM
native-image is trivial as everything is handled by Fn via its
init-image feature. To appreciate the underlying benefits, you should measure the size of the produced image. You can do that using
docker images and the name of the function container image. If you didn’t write the container image name earlier,
fn inspect someapp modularfunc will give you all the details of the function, including its container image name. Another benefit is that the startup time of the “Java” function is improved as it is now invoked as a native executable.
docker images graalfunc:0.0.2 REPOSITORY TAG IMAGE ID CREATED SIZE david 0.0.2 e7a57e4c755b 1 minute ago 20MB
:mega: You might want to check
dive, a convenient tool to explore container image and its layers.
You can see that our function container image only weight 20MB and it includes everything (and just that!) to run our Serverless function, i.e. the operating system and our
Java function that has been compiled and linked into a native Linux executable. It should be mentioned that this executable doesn’t require any external Java runtime as it also embeds SubstrateVM. As said earlier, the smaller the container image is, the faster it will be loaded from the registry when it is invoked.
For more details on GraalVM integration in Fn, you can check this article.