Exercise - Using GraalVM to create native Serverless Java function

Exercise - Using GraalVM to create native Serverless Java function

As mentioned in the previously, reducing the size of a unction container image is key.

In addition to JPMS and jlink, we can also use GraalVM, an open-source high-performance virtual machine developed by Oracle Labs, to further reduce the size of a function container image.

GraalVM offers many features but in this exercise, we will solely focus on GraalVM native-image feature. 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 init-image feature.

: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 (pom.xml, func.yaml, 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 (fnproject/fn-java-fdk-build, fnproject/fn-java-native, etc.).

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).

Conlusion

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.