Optimize Java Serverless Functions in Kubernetes
Achieve faster startup and a smaller memory footprint to run serverless functions on Kubernetes.
Join the DZone community and get the full member experience.Join For Free
A faster startup and smaller memory footprint always matter in Kubernetes due to the expense of running thousands of application pods and the cost savings of doing it with fewer worker nodes and other resources. Memory is more important than throughput on containerized microservices on Kubernetes because:
- It's more expensive due to permanence (unlike CPU cycles)
- Microservices multiply the overhead cost
- One monolith application becomes N microservices (e.g., 20 microservices ≈ 20GB)
This significantly impacts serverless function development and the Java deployment model. This is because many enterprise developers chose alternatives such as Go, Python, and Nodejs to overcome the performance bottleneck—until now, thanks to Quarkus, a new Kubernetes-native Java stack. This article explains how to optimize Java performance to run serverless functions on Kubernetes using Quarkus.
Traditional frameworks in the Java ecosystem come at a cost in terms of the memory and startup time required to initialize those frameworks, including configuration processing, classpath scanning, class loading, annotation processing, and building a metamodel of the world, which the framework requires to operate. This is multiplied over and over for different frameworks.
Quarkus helps fix these Java performance issues by "shifting left" almost all of the overhead to the build phase. By doing code and framework analysis, bytecode transformation, and dynamic metamodel generation only once, at build time, you end up with a highly optimized runtime executable that starts up super fast and doesn't require all the memory of a traditional startup because the work is done once, in the build phase.
More importantly, Quarkus allows you to build a native executable file that provides performance advantages, including amazingly fast boot time and incredibly small resident set size (RSS) memory, for instant scale-up and high-density memory utilization compared to the traditional cloud-native Java stack.
Here is a quick example of how you can build the native executable with a Java serverless function project using Quarkus.
1. Create the Quarkus Serverless Maven Project
This command generates a Quarkus project (e.g.,
quarkus-serverless-native) to create a simple function:
$ mvn io.quarkus:quarkus-maven-plugin:1.13.4.Final:create \ -DprojectGroupId=org.acme \ -DprojectArtifactId=quarkus-serverless-native \ -DclassName="org.acme.getting.started.GreetingResource"
2. Build a Native Executable
You need a GraalVM to build a native executable for the Java application. You can choose any GraalVM distribution, such as Oracle GraalVM Community Edition (CE) and Mandrel (the downstream distribution of Oracle GraalVM CE). Mandrel is designed to support building Quarkus-native executables on OpenJDK 11.
pom.xml, and you will find this
native profile. You'll use it to build a native executable:
<profiles> <profile> <id>native</id> <properties> <quarkus.package.type>native</quarkus.package.type> </properties> </profile> </profiles>
Note: You can install the GraalVM or Mandrel distribution locally. You can also download the Mandrel container image to build it (as I did), so you need to run a container engine (e.g., Docker) locally.
Assuming you have started your container runtime already, run one of the following Maven commands.
$ ./mvnw package -Pnative \ -Dquarkus.native.container-build=true \ -Dquarkus.native.container-runtime=docker
$ ./mvnw package -Pnative \ -Dquarkus.native.container-build=true \ -Dquarkus.native.container-runtime=podman
The output should end with
Run the native executable directly without Java Virtual Machine (JVM):
The output will look like this:
__ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ INFO [io.quarkus] (main) quarkus-serverless-native 1.0.0-SNAPSHOT native (powered by Quarkus xx.xx.xx.) Started in 0.019s. Listening on: http://0.0.0.0:8080 INFO [io.quarkus] (main) Profile prod activated. INFO [io.quarkus] (main) Installed features: [cdi, kubernetes, resteasy]
Supersonic! That's 19 milliseconds to startup. The time might be different in your environment.
It also has extremely low memory usage, as the Linux
ps utility reports. While the app is running, run this command in another terminal:
$ ps -o pid,rss,command -p $(pgrep -f runner)
You should see something like:
PID RSS COMMAND 10246 11360 target/quarkus-serverless-native-1.0.0-SNAPSHOT-runner
This process is using around 11MB of memory (RSS). Pretty compact!
Note: The RSS and memory usage of any app, including Quarkus, will vary depending on your specific environment and will rise as application experiences load.
You can also access the function with a REST API. Then the output should be
$ curl localhost:8080/hello Hello RESTEasy
3. Deploy the Functions to Knative Service
If you haven't already, create a namespace (e.g.,
quarkus-serverless-native) on OKD (OpenShift Kubernetes Distribution) to deploy this native executable as a serverless function. Then add a
quarkus-openshift extension for Knative service deployment:
$ ./mvnw -q quarkus:add-extension -Dextensions="openshift"
Append the following variables in
src/main/resources/application.properties to configure Knative and Kubernetes resources:
quarkus.container-image.group=quarkus-serverless-native quarkus.container-image.registry=image-registry.openshift-image-registry.svc:5000 quarkus.native.container-build=true quarkus.kubernetes-client.trust-certs=true quarkus.kubernetes.deployment-target=knative quarkus.kubernetes.deploy=true quarkus.openshift.build-strategy=docker
Build the native executable, then deploy it to the OKD cluster directly:
$ ./mvnw clean package -Pnative
Note: Make sure to log in to the right project (e.g.,
quarkus-serverless-native) using the
oc login command ahead of time.
The output should end with
BUILD SUCCESS. It will take a few minutes to complete a native binary build and deploy a new Knative service. After successfully creating the service, you should see a Knative service (KSVC) and revision (REV) using either the
oc command tool:
$ kubectl get ksvc NAME URL [...] quarkus-serverless-native http://quarkus-serverless-native-[...].SUBDOMAIN True $ kubectl get rev NAME CONFIG NAME K8S SERVICE NAME GENERATION READY REASON quarkus-serverless-native-00001 quarkus-serverless-native quarkus-serverless-native-00001 1 True
4. Access the Native Executable Function
Retrieve the serverless function's endpoint by running this
$ kubectl get rt/quarkus-serverless-native
The output should look like:
NAME URL READY REASON quarkus-serverless-native http://quarkus-serverless-restapi-quarkus-serverless-native.SUBDOMAIN True
Access the route
URL with a
$ curl http://quarkus-serverless-restapi-quarkus-serverless-native.SUBDOMAIN/hello
In less than one second, you will get the same result as you got locally:
When you access the Quarkus running pod's logs in the OKD cluster, you will see the native executable is running as the Knative service.
Subscribe to bit.ly/danielohtv to find more technical demos.
You can optimize Java serverless functions with GraalVM distributions to deploy them as serverless functions on Knative with Kubernetes. Quarkus enables this performance optimization using simple configurations in normal microservices.
The next article in this series will guide you on making portable functions across multiple serverless platforms with no code changes.
Published at DZone with permission of Daniel Oh. See the original article here.
Opinions expressed by DZone contributors are their own.