Java Serverless on Steroids with fn+GraalVM Hands-On
Function-as-a-Service or Serverless is the most economical way to run code and use the cloud resources to the minimum. 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 high, available, scalable architecture, at the most optimum 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 & 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 & other optimisations
fn Project
fn project is a great environment to build serverless applications. fn supports building serverless applications in Go, Java, JavaScript, Python, Ruby, C#. It is a very simple and rapid application development environment that comes with fn daemon & a CLI which provides most of the scaffolding to build serverless applications.
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.
Prerequisite
- Install docker (refer to https://www.docker.com/ for latest instructions)
- Install fn (refer to https://fnproject.io/ for latest instructions)
1. Starting fn daemon
Start the fn daemon server using fn start
The fn server runs in docker, you can check that by running docker ps
The screenshot below shows, what I am able to see in 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
cd converterFunc
Let’s inspect what is inside that folder. You will see a func.yaml
, pom.xml
& a src
folder
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
schema_version: 20180708
name: converterfunc
version: 0.0.1
runtime: java
build_image: fnproject/fn-java-fdk-build:jdk11-1.0.118
run_image: fnproject/fn-java-fdk:jre11-1.0.118
cmd: com.example.fn.HelloFunction::handleRequest
- name: The name of the function, we can see the name of the function that we specified in our command line
fn init
- 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 its 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 com.fnproject.fn.api
, com.fnproject.fn.testing-core
, com.fnproject.fn.testing-junit4
.
In the src
the folder we will find HelloFunction.java
, which is the default boilerplate code that is generated by fn.
The code is very straightforward. It has a handleRequest ()
method, which takes in the String
an 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 ConverterFunction.java
The code is very straightforward. I am just expecting a kgs value in String
, converting that to Double
and calcualting 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 & Deploy the serverless container to the 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 converter-app
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.
Lets now use 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 lets 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
fn init --init-image fnproject/fn-java-native-init converterfuncongraalcd cnverterfuncongraal
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. Lets inspect this dockerfile
- line 17: The image will be built using
fnproject/fn-java-fdk-build
. - line 18: setting the working directory to /function
- line 19–23: Then the maven environment is configured.
- line 25–40: Using 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 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 dockerfile 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 reflection.json
under 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
fn create app graal-converter-app
fn deploy --app graal-converter-app --local
echo -n '20' | fn invoke graal-converter-app converterfuncongraal
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…ttyl :-)