Building Cloud-Native Java Applications With AOT and Kubernetes
A combination of technologies like GraalVM, Micronaut, and Kubernetes unlocks a whole new world for developers looking to build and implement Java cloud solutions.
Join the DZone community and get the full member experience.
Join For FreeNative cloud applications use services and infrastructure provided by AWS, Microsoft Azure, and Google Cloud among others. Essentially, this means you can code, test, maintain, and run your app all in the cloud.
This approach gives developers an opportunity to:
Build and release applications faster
Connect mammoth enterprise apps with nimble end-user applications via APIs
Segment bulky apps into self-sufficient services, which can be managed and updated at scale using Kubernetes
Increase system reliability
With modern platforms like Kubernetes and Istio, the need to have smaller runtimes that can scale up, down, and even all the way down to zero is becoming critical. However, applications built on a traditional Java stack (even if optimized for cloud-native environments) are memory-intensive and take longer to start than software created using other popular programming languages.
In this article, I’ll provide a brief overview of the technologies and tools that allow software engineers to simultaneously launch more Java applications in the cloud and sonically improve their performance.
Barriers to Implementing Java in a Cloud-Native Way
- Inefficient memory usage. When you launch a Java application in the cloud, the Java Virtual Machine (JVM) starts converting bytecode into native code using just-in-time (JIT) compilers. With this process going on, you cannot execute other applications and use the server capacity to the fullest.
- High execution time. “Write once, run anywhere” is the essence of Java applications. The cross-platform functionality is enabled through code compilation. The time it takes a JIT compiler to optimize the bytecode is added to the overall execution time.
- Communication overhead. A containerized Java app needs additional libraries to communicate efficiently in a cloud-native and microservices environment. These include service discovery, circuit breaker, and distributed tracing instrumentation libraries.
Optimizing JVM Memory Usage and Startup Time With Ahead of Time (AoT) Compilation
To resolve the issues with significant memory usage and startup time, Java 9 supports Ahead-Of-Time (AOT) compilation. The technique helps compile bytecode to native before executing an application, so that we don’t have to spend system resources on JIT compilation at run time.
There are two ways to convert a Java application into fully compiled native code, which is also known as native image:
Create your Java archive (JAR) file as a usual application
Create a native image from your JAR file
xxxxxxxxxx
native-image -cp build/hello-native-0.1-all.jar
[hello-native:19] classlist: 6,238.75 ms
[hello-native:19] (cap): 1,046.36 ms
[hello-native:19] setup: 2,673.82 ms
[hello-native:19] (typeflow): 48,004.19 ms
[hello-native:19] (objects): 24,675.85 ms
[hello-native:19] (features): 2,264.51 ms
[hello-native:19] analysis: 78,074.57 ms
[hello-native:19] (clinit): 1,048.50 ms
[hello-native:19] universe: 3,079.67 ms
[hello-native:19] (parse): 9,075.28 ms
[hello-native:19] (inline): 50,002.33 ms
[hello-native:19] (compile): 117,425.27 ms
[hello-native:19] compile: 187,438.96 ms
[hello-native:19] image: 15,428.14 ms
[hello-native:19] write: 2,457.20 ms
[hello-native:19] [total]: 295,887.91 ms
Then you can run the native code and use it without any dependency on Java Runtime Environment (JRE). This helps reduce the JVM startup time to just 54 milliseconds.
xxxxxxxxxx
./hello-native
11:32:09.499 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 54ms. Server Running: http://localhost:8080
If we execute the same application using the default JIT compilation functionality provided by the virtual machine, the startup time will jump to 1677 milliseconds.
xxxxxxxxxx
java -jar ./hello-native-0.1-all.jar
17:20:50.910 [main] [INFO] mio.micronaut.runtime.Micronaut - Startup completed in 1677ms. Server Running: http://localhost:8080
Java application performance with AOT vs. JIT compilation
Parameters |
Native Application |
Standard Java Application |
Startup Time (ms) |
54 |
1677 |
Memory Footprint (MB) |
61 |
415 |
Choosing a Web Framework With AOT Compilation Capabilities
The traditional Java development frameworks like Spring or Java EE are not suitable for building native images. As a result, several solutions with robust AOT capabilities emerged as an alternative. These include Helidon by Oracle, Quarkus by RedHat, and Micronaut by Object Computing.
Framework / Attributes |
Micronaut |
Quarkus |
Helidon |
Developed by |
Object Computing |
Red Hat |
Oracle |
Launched in |
2017 |
2018 |
2018 |
Supported languages |
Java, Kotlin, Groovy |
Java, Kotlin |
Java |
To show you how to create a simple Java app with Micronaut and run it as a native image, I’ve uploaded a sample project on GitHub.
Below you will find an application entry point:
xxxxxxxxxx
package micronaut.hello.native1;
import io.micronaut.runtime.Micronaut;
public class Application {
public static void main(String[] args) {
Micronaut.run(Application.class);
}
}
And here’s a sample controller for the Micronaut application:
x
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import java.util.HashMap;
import java.util.Map;
"/issues") (
public class IssuesController {
"/{number}") (
public Map<String, String> issue( Integer number) {
return new HashMap<String, String>() {{
put("key", "Issue # " + number + "!");
}};
}
}
Now we can compile the bytecode to native, run it, and see the results.
xxxxxxxxxx
> curl http://localhost:8080/issues/1
{"key":"Issue # 1!"}
Enabling Communication Between Java Application Services With Kubernetes and Istio
To further optimize the performance of a containerized Java application in the cloud, you can utilize the Kubernetes system. This approach allows developers to:
Scale all the microservices that comprise a Java app in the same transparent way
Minimize the infrastructure configuration and management efforts
Establish efficient communication between microservices
You can also use the Istio service to facilitate communication between microservices, which allows developers to run sidecar applications against every microservice and handle communication through a local proxy. For example, you make a call to a sidecar proxy, and the server will resolve and forward the request to the appropriate component of the service mesh. This method simplifies the application logic and moves load-balancing, circuit breaking, and service discovery logic to the service mesh.
You can install Istio using the Helm package manager:
xxxxxxxxxx
helm install --name istio-init --namespace istio-system istio.io/istio-init
helm install --name istio --namespace istio-system istio.io/istio
To create a communication point, you need to launch a Kubernetes service. Istio will intercept and enhance communication between your microservices without any additional actions.
x
apiVersion v1
kind Service
metadata
name istio-service-b
labels
app istio-service-b
service istio-service-b
spec
ports
port3000
name http
selector
app istio-service-b
Take-Home Message
A combination of technologies like GraalVM, Micronaut, and Kubernetes unlocks a whole new world for developers looking to build and implement Java cloud solutions. The technology stack allows us to deploy eight times more microservices comprising a containerized app while using the same cloud capacity. With Istio, we can further shift the execution of complex features, such as circuit breaker, call tracing, and service discovery from the application side to the cloud infrastructure and simultaneously enhance communication between the microservices.
You can find all the related code for your project in my GitHub repository.
Opinions expressed by DZone contributors are their own.
Trending
-
MLOps: Definition, Importance, and Implementation
-
Building and Deploying Microservices With Spring Boot and Docker
-
Cypress Tutorial: A Comprehensive Guide With Examples and Best Practices
-
Tomorrow’s Cloud Today: Unpacking the Future of Cloud Computing
Comments