Java Serverless on Steroids With fn+GraalVM Hands-On
In this blog, we will build a Java serverless application using the fn project on JVM and then on GraalVM and compare the performance.
Join the DZone community and get the full member experience.Join For Free
Function-as-a-Service or Serverless is the most economical way to run code and use the cloud resources to the minimum. The serverless approach runs the code when a request is received. The code boots up, executes, handles the requests, and shuts down. Thus, utilizing the cloud resources to the optimum. This provides a highly available, scalable architecture, at the most optimal costs. However, serverless architecture demands a faster boot, quicker execution, and shutdown.
GraalVM native images (ahead of time) is the best runtime. GraalVM native images have a very small footprint, they are fast to boot and they come with embedded VM (Substrate VM).
I had blogged about GraalVM here. Please refer to the following blogs, for better understanding of the architecture of Graal VM and how it builds on top of Java Virtual Machine
Episode 1: "The Evolution" - Java JIT Hotspot & C2 compilers (the current episode...scroll down)Episode 2: "The Holy Grail" - GraalVM
In these blogs, I will talk about how GraalVM embraces polyglot, providing interoperability between various programming languages. I will then cover how it extends from Hotspot, and provides faster execution, and smaller footprints with 'Ahead-of-time' compilations and other optimizations.
In this blog, let's focus on building a simple KG to Pounds converter function in Java. First, we will build a serverless application with Java, and then later build it using GraalVM native image. We will then compare how fast and small GraalVM implementation is.
1. Starting the fn Daemon
Start the fn daemon server using
The fn server runs in docker, you can check that by running
docker ps The screenshot below shows, what I am able to see on my computer.
2. Generating the fn Boilerplate Code
Now we can generate the boilerplate code with:
fn init --runtime java converterFunc
This creates a folder
converterFunc with all the boilerplate code:
Let's inspect what is inside that folder. You will see a
pom.xml & a
func.yml is the main manifest yaml file that has the key information about the class that implements the function and the entry point. Let's inspect that:
- name: The name of the function, we can see the name of the function that we specified in our command-line
- version: Version of this function.
- runtime: Java Virtual machine as the runtime.
- build_image: The docker image that should be used to build the java code, in this case, we see it's JDK 11.
- run_image: The docker image that should be used as a runtime. In this case, it is JRE11.
- cmd: This is the entry point, which is the ClassName:MethodName.
fn has all the information that it needs in this yaml to build and run the function when it is invoked.
Now let's look at the maven file (pom.xml):
We see the repository from where the fn dependencies are to be pulled:
and the dependencies
src folder we will find
HelloFunction.java, which is the default boilerplate code that is generated by fn.
The code is very straight forward. It has a
handleRequest () method, which takes in the
String as a input and returns
String as an output. we can write our function logic in this method. This is the method that fn, calls when we invoke the function.
3. Writing Our Logic
Let's build our converter application. I am going to deploy it into the path
src/main/java/com/abvijay/converter , and the name of my Class is
The code is very straight forward. I am just expecting a kgs value in
String, converting that to
Double and calculating pound value and returning that back as a
String. (I did not write a lot of exception handling, to check for edge conditions, to keep it simple.)
Now we need to update the
func.yaml to point to our new Class.
Check line number 7, which is changed to point to the new class and method.
4. Build and Deploy the Serverless Container to Local Docker
Functions are grouped into applications. an application can have multiple functions. That helps in grouping them and managing them. So we need to create a
fn create app converter-app
Once the app is created, we can now deploy the app.
fn deploy --app converter-app --local
fn deploy command will build the code using maven, package it as a docker image, and deploy it to the local docker runtime. fn can also be used to deploy to the cloud or K8s cluster directly.
Let's now use the
docker images command to check if our image is built.
We can also use
fn inspect to get all the details about the function, this helps in the discovery of the services.
fn inspect function converter-app converterfunc
5. Running and Testing
Now let's invoke the service, since our function expects a input argument in number, we can pass it using a echo command and pipe the output to
fn invoke to invoke our function
echo -n '10' | fn invoke converter-app converterfunc
We can see the result coming from the function. Now let's run the same logic on GraalVM
6. Run on GraalVM, as a Native-image
The base image for GraalVM is different, we use
fnproject/fn-java-native-init, as the base, and initialize our fn project with that:
This fn configuration works differently. It also generates a Dockerfile, with all the necessary Docker build commands. This is a multi-stage docker build file. Let's inspect this docker file:
- line 17: The image will be built using
- line 18: setting the working directory to /function.
- line 19-23: Then the maven environment is configured.
- line 25-40: Using the base image as
fnproject/fn-java-native, the GraalVM is configured and fn runtime is compiled. This is a very important step, this is what makes our serverless runtime faster and with a smaller footprint.
- line 43-47: Using
busybox:glibc(which is the minimal version of linux+glibc) base image, the native images are copied.
- Line 48: is the function entry point. The func.yml in this way of building the serverless image has no information. fn will use the docker file to perform the build (along with maven) and deploy the image to the repository.
Now we need to change line 48 to point to our class. let's replace that with:
CMD [ "com.abvijay.converter.ConverterFunction::handleRequest" ]
Another important configuration file, that we need to change
src/main/conf This JSON file has the manifest information about the class name and the method:
Let's change that to:
Now let's create a new app and deploy this app and run and see:
There you go, our code is now running on GraalVM. So what's the big deal. When I ran
docker images, I see the size of the Java image as 223 MB and the GraalVM image is just 20MB. That is a 10-times smaller footprint.
When I timed the function calls, the Java function took around 700ms while GraalVM took around 460ms. That is almost 30% faster. For functions with more complex logic, the differences will be much more significant.
Java hotspot might catch up with this number, but that is provided the function runs longer, and the Just in time Compiler kicks in to optimize the code. Since most of the functions are expected to be quick and short running, it does not make sense to compare these JIT benchmarks.
There you go... I hope this was fun...Talk to you later!
Published at DZone with permission of A B Vijay Kumar. See the original article here.
Opinions expressed by DZone contributors are their own.