Micronaut & GraalVM: together for Cloud Native

Micronaut & GraalVM

If you have developed web services on top of the JMV (so typically with Java and Spring) maybe you were very disappointed about Serverless platforms offered by the top Cloud Providers.

I work a lot with Spring and when I tried to deploy a web services in a serverless platform (ex: CloudRun) I immediately notices the Cold Start problem suffered by the JVM. If you don't know, Cold Start means the time needed by the serverless platform (Google Cloud Function or AWS Lambda) to start the JVM and then the service. And generally it happens all the times the service is switched off by the platform and then restarted (due to low requests for example).

Problem: ColdStart & JVM

The cold start can be a real problem, especially when the service response time take several seconds. For a REST API for example 1 or 2 seconds may be good, but what about 6 or 10 seconds?

Solution: Micronaut & GraalVM

Micronaut supports several plugins that can be used to compile a native image, or a docker image with high performances that doesn't need the JVM. Therefore using a native image in cloud environments makes the could start lower, opening new interesting horizons.

In this article I want to show you how to develop a simple Hello World with Micronaut in order to compile it both as a native image and as a Docker Container

You can find the full code [here on Github]((https://github.com/aitechnologies-it/cloud-frameworks-test/tree/main/mngraal) with some suggestions and instructions.

1 - Micronaut Project

Create a Micronaut project is really simple. I suggest to use the official CLI and SDKMAN

sdk install micronaut 2.5.7
mn create-app it.aitlab.tests.mngraal --build=maven

Then add the Hello World micronaut Controller.

@Controller("/")
public class HelloWorld {

    @Get
    public String getHello() {
        return "Hello World!!!";
    }
}

2 - Run the service

Now let's try to compile the code and start the service with the JVM, please note the startup time.

$ ./mvnw mn:run 

 __  __ _                                  _
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
  Micronaut (v2.5.7)

17:25:17.974 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 688ms. Server Running: http://localhost:8080

In my laptop it took 688ms to be available, without covering the time needed to launch the JVM. Even if locally seems reasonably fast, problems happen in cloud, with the JVM that took several seconds to become available.

For a fast service check you can use curl 127.0.0.1:8080.

3 - GraalVM native image

Ok now let's try to create a native image with GraalVM. We need a Graal Virtual Machine installed in the system along with the native image tool. So as usual I suggest SDKMAN.

# Example: Install Graal and Native Image
sdk install java 21.1.0.r11-grl
gu install native-image
# Point to the graal jdk and then package the app
sdk use java 21.1.0.r11-grl

On now our terminal (environment) points to the Java Graal VM. So we can use the Micronaut plugin to start the build (the plugin is already configured in the Micronaut pom).

# Compile
$ ./mvnw package -Dpackaging=native-image

# #Run 
$ ./target/mngraal
 __  __ _                                  _
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
  Micronaut (v2.5.7)

17:43:31.164 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 17ms. Server Running: http://localhost:8080

As you can see startup time are very good now, 17ms versus the 700ms of the example before. An enormous advantage, which must be added to that you get by not instantiating the JVM.

4 - Think about the Cloud: the Docker Container

As with the native image, Micronaut is already ready to create the docker image which in turn encapsulates it. To create the image you need Docker installed, and with adequate allocated memory (I recommend at least 8GB).

# Create the image locally
./mvnw package -Dpackaging=docker-native

Well now that the image is in the local repo, just run it.

# Run the image locally
docker run -p 8080:8080 mngraal

 __  __ _                                  _
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
  Micronaut (v2.5.7)

15:50:32.117 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 18ms. Server Running: http://ad1c24f29e22:8080

Even for docker the start time is a few tens of milliseconds. Now that you have a local docker image you can use your favorite provider for deployment and performance testing (will be the subject of future articles).

Conclusion

It's not true that Java is not suitable for Serverless solutions, the fact that it can be compiled in native image opens the way to many uses that were not considered before because of the JVM.

I'll leave it to you to do further performance testing by provider, and also comparisons at the memory-occupied level. Surely in the legacy or environments strongly linked to the Java world, GraalVM gives additional possibilities, especially for Serverless solutions.