Codes are a puzzle


Turbocharge Java Microservices with Quarkus and GraalVM Native Image

In the dynamic landscape of modern software development, microservices have become the favored architectural approach. While this methodology offers numerous advantages, it is not without its challenges. Issues such as large memory footprints, extended start times, and high CPU usage often accompany traditional JVM-based services. These challenges not only impact the technical aspects but also have financial implications that can significantly affect the overall cost of running and maintaining software solutions.

What is GraalVM Native Image?

GraalVM Native Image is a key feature of the GraalVM, which is a high-performance runtime that provides support for various programming languages and execution modes. Specifically, GraalVM Native Image allows you to compile Java applications ahead-of-time into standalone native executables, bypassing the need for a Java Virtual Machine (JVM) during runtime. This innovative approach yields executable files that exhibit nearly instantaneous startup times and significantly reduced memory consumption compared to their traditional JVM counterparts. These native executables are meticulously crafted, containing only the essential classes, methods, and dependent libraries indispensable for the application’s functionality. Beyond its technical prowess, GraalVM Native Image emerges as a strategic solution with far-reaching implications. It not only surmounts technical challenges but also introduces a compelling financial case. By facilitating the development of efficient, secure, and instantly scalable cloud-native Java applications, GraalVM becomes instrumental in optimizing resource utilization and fostering cost-effectiveness. In essence, it plays a pivotal role in elevating the performance and financial efficiency of software solutions in contemporary, dynamic environments.

Technical Challenges and Financial Implications

1. Large Memory Footprints

Technical Impact: Traditional JVM-based services often incur substantial memory overhead due to classloading and metadata for loaded classes.

Financial Case: High memory consumption translates to increased infrastructure costs. GraalVM’s elimination of metadata for loaded classes and other optimizations leads to a more efficient use of resources, resulting in potential cost savings.

2. Extended Start Times

Technical Impact: Cold starts in microservices can lead to higher response times, impacting user experience and potentially causing service degradation.

Financial Case: Extended start times not only affect user satisfaction but also contribute to higher operational costs. GraalVM’s optimizations, such as eliminating classloading overhead and pre-generating image heap during the build, drastically reduce startup times, potentially minimizing operational expenses.

3. High CPU Usage

Technical Impact: Traditional JVMs often burn CPU cycles for profiling and Just-In-Time (JIT) compilation during startup.

Financial Case: Excessive CPU usage results in increased cloud infrastructure costs. GraalVM’s avoidance of profiling and JIT-ing overhead directly contributes to reduced CPU consumption, translating to potential cost savings in cloud usage.

Tackling the Cold Start Problem

Microservices, especially in serverless or containerized environments, often face the Cold Start Problem, impacting response times and user experience. GraalVM addresses this challenge by implementing several optimizations:

1. No Classloading Overhead

  • Traditional Java applications rely on classloading at runtime to dynamically load and link classes. This process introduces overhead, particularly during the startup phase. GraalVM minimizes this overhead through a process known as static or ahead-of-time (AOT) compilation. This involves pre-loading, linking, and partially initiating all classes that the application requires. As a result, there is no need for runtime classloading during application startup.

2. Elimination of Interpreted Code

  • Traditional Java Virtual Machines rely on an interpreted execution mode before applying Just-In-Time (JIT) compilation. This can contribute to startup delays and increased CPU usage. Native executables contain no interpreted code, further contributing to faster startup times.

3. No Profiling and JIT-ing Overhead

  • GraalVM bypasses the need to start the Just-In-Time (JIT) Compiler, reducing CPU usage during startup.

4. Image Heap Generation at Build Time

  • GraalVM’s native image utility enables the execution of initialization processes for specific classes during the build process. This results in the generation of an image heap that includes pre-initialized portions, speeding up the application’s startup.

Oracle GraalVM’s native image utility has demonstrated startup times almost 100 times faster than traditional JVM-based applications. The graph below illustrates the substantial reduction in runtime memory requirements, showcasing GraalVM’s efficiency compared to HotSpot(Figure 1).

Executables start up

Figure 1 – Native executables start up almost instantly(oracle.com)


Achieving a Leaner Memory Footprint

GraalVM contributes to lower memory footprints through the following optimizations:

1. No Metadata for Loaded Classes

  • GraalVM avoids storing metadata for dynamically loaded classes in the non-heap memory. During the build process, the necessary class information is pre-loaded and linked, minimizing the need for additional metadata at runtime.

2. No Profiling Data or JIT Optimizations

  • Since the bytecode is already in native code, GraalVM eliminates the need for collecting profiling data for JIT optimizations, reducing memory overhead.

3. Isolation Technology

  • GraalVM introduces Isolates, a technology that partitions the heap into smaller, independent " heaps," enhancing efficiency, particularly in request processing scenarios.

In common, it consumes up to x5 times less memory compared to running on a JVM(Figure 2)

Memory compared to Go or Java HotSpot

Figure 2 – Native executables memory compared to Go or Java HotSpot(oracle.com)


In conclusion, GraalVM’s native image utility offers a transformative solution to the challenges posed by microservices, addressing startup time, memory footprint, and CPU usage concerns. By adopting GraalVM, developers can create cloud-native Java applications that are not only efficient and secure but also provide a superior user experience.

Native Java with Quarkus

To compile your Quarkus service into a native image, various methods are available. While this article won’t delve deeply into the Quarkus native build procedure, it does provide an overview of the essential steps.

Before proceeding with any approach for building a native image, it’s crucial to set up the proper native profile in your pom.xml file. Add the following profile:

1<profiles>
2  <profile>
3    <id>native</id>
4    <properties>
5      <quarkus.package.type>native</quarkus.package.type>
6    </properties>
7  </profile>
8</profiles>
  1. Producing a Native Executable with Installed GraalVM

Check your GraalVM version using the following command:

1./gu info native-image

This command will display the installed GraalVM version:

Downloading: Component catalog from www.graalvm.org
Filename : https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.3.0/native-image-installable-svm-java19-linux-amd64-22.3.0.jar
Name     : Native Image
ID       : native-image
Version  : 22.3.0
GraalVM  : 22.3.0
Stability: Experimental
Component bundle native-image cannot be installed
        - The same component Native Image (org.graalvm.native-image[22.3.0.0/55b341ca1bca5219aafa8ed7c8a2273b81d184dd600d8261c837fc32d2dedae5]) is already installed in version 22.3.0

And to create a native executable, use:

1./mvnw install -Dnative

These commands generate a *-runner binary in the target directory, allowing you to run the native executable:

1./target/*-runner
  1. Creating a Native Executable without installed GraalVM

If installing GraalVM locally poses challenges, an in-container build can be used:

1./mvnw install -Dnative -Dquarkus.native.container-build=true -Dquarkus.native.builder-image=graalvm

This command initiates the build within a Docker container and provides the necessary image file. You can then start the application with:

1./target/*-runner

In cases where building the native image proves challenging, the RedHat team provides a specialized distribution of GraalVM designed for the Quarkus framework called Mandrel. Mandrel streamlines GraalVM, focusing solely on the native-image capabilities essential for Quarkus applications. To use Mandrel, follow these steps:

  1. Identify the appropriate Mandrel version Mandrel repository

  2. Set the Mandrel version in your application.properties file:

1quarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel-builder-image:23.0.1.2-Final-java17
  1. Run the Maven build command:
1./mvnw clean install -Pnative

Manually Creating a Container

For those who prefer manual control over container creation, a multi-stage Docker build can be employed.

 1FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:23.0.1.2-Final-java17 AS build
 2COPY --chown=quarkus:quarkus mvnw /app/mvnw
 3COPY --chown=quarkus:quarkus .mvn /app/.mvn
 4COPY --chown=quarkus:quarkus pom.xml /app/
 5USER quarkus
 6WORKDIR /app
 7RUN ./mvnw -B org.apache.maven.plugins:maven-dependency-plugin:3.6.1:go-offline
 8COPY src /app/src
 9RUN ./mvnw package -Dnative
10
11FROM quay.io/quarkus/quarkus-micro-image:2.0
12WORKDIR /app/
13COPY --from=build /app/target/*-runner /app/application
14
15RUN chmod 775 /app /app/application \
16  && chown -R 1001 /app \
17  && chmod -R "g+rwX" /app \
18  && chown -R 1001:root /app
19
20EXPOSE 8080
21USER 1001
22
23CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

This Dockerfile orchestrates a multi-stage build, resulting in a Docker image with your Quarkus application. Execute this Dockerfile to produce the Docker image, ready to run your Quarkus application.

Summary

GraalVM Native Image is a powerful technology that can revolutionize the way you develop and deploy Java microservices. By adopting GraalVM Native Image, you can create microservices that are:

  • Faster
  • More scalable
  • Simpler to deploy
  • More cost-effective

GraalVM Native Image is a key enabler of cloud-native Java development and can help you achieve the performance, scalability, and cost savings that your business demands.