Java has been in a bit of an awkward spot since containers took off a few years ago. In the world of Kubernetes, microservices, and serverless, it has been getting harder and harder to ignore that Java applications are, by today’s standards, bloated. Well, until now. In this article, I explore the basics of Quarkus, a Kubernetes-native Java framework built to specifically address Java’s bloatedness problem.
Java of yore
For years, many of us looked the other way when confronted with the bloatedness of Java. Who cares if my server-side app:
- Needed hundreds of megabytes worth of class files.
- Created gigabytes worth of runtime memory footprint.
- Took up to a minute (or five) to start up.
I definitely didn’t care, because my Java application would then run reliably on a powerful piece of hardware or virtual machine for months, if not years, serving hundreds of requests concurrently. Not to mention, as a language, Java gave organizations pretty much everything they needed to maintain software for a long time using a large team of professionals with varying levels of skills.
The Java Virtual Machine (JVM) has long promised and been delivered as a "write once, run (almost) anywhere" platform. Type safety, object-oriented programming support, and an unrivaled set of options in tooling and libraries have long made Java an excellent choice for team-based software development. Further, with enterprise-grade application servers (for example, EAP, WebLogic, and Tomcat), organizations also had a resilient and feature-rich platform for their Java web applications. These applications simply needed to comply with JavaEE standards around describing deployment concerns (think web.xml
).
Any JavaEE-compliant application server would then take care of operational concerns like security, logging, connecting to databases/queues, and scaling. It’s no surprise that for years Java has dominated the programming language landscape as the de facto standard for the enterprise.
Kubernetes: The new application server
It has been observed before that Kubernetes is the new application server. Containers and Kubernetes have taken the "write once run anywhere" paradigm of the JVM and extended it to most other programming languages. Now applications written in any language can leverage Kubernetes for operational concerns and decouple themselves from runtime infrastructure. These applications just have to be delivered in compliant Linux containers.
With this structure in place, developers can code their applications in their favorite programming language and count on Kubernetes to handle operational concerns like logging, scaling, healing, and networking. Add in the Istio service mesh and you even have out-of-box fault tolerance and application-level metrics without a single line of application code. Today we find ourselves in a tech landscape that overwhelmingly prefers horizontal scaling of automated cattle over vertical scaling of manually cared for pets. Microservices and serverless/Function-as-a-Service (FaaS) applications have become all the rage, and both benefit greatly from low memory footprints and blazing-fast startup times.
So, with all of this said, it becomes increasingly harder to ignore that my Java container images are even larger in size as well as memory footprint, and they take quite a bit longer to start up, especially when compared to a language like Golang. Modern cloud-native frameworks like Spring Boot or Dropwizard have helped, but startup times are still at least 10 seconds or more, and runtime memory footprint is at least in the hundreds of megabytes.
Enter Quarkus
Quarkus aims to tackle the bloatedness problem of Java head-on. Marketed as Supersonic Subatomic Java, Quarkus leverages GraalVM and HotSpot to provide developers with a framework to create applications from Java code with fast boot times and low RSS memory. The following figure from quarkus.io does a good job illustrating the benefits. Notice the drastic difference in both RSS memory and boot time between Quarkus native and the traditional cloud-native stack.
Source: ">
OpenJDK and GraalVM
As evident from the figure above, Quarkus has two modes: JVM and native. The native mode uses GraalVM to create a standalone executable that doesn’t run in a Java VM, and the greatest efficiency gains come from running a Quarkus application in this mode. However, not every JVM feature works in native mode, and the most notorious of these lost features is reflection.
This fact can be a huge problem as many frameworks and libraries that Java developers depend on for everyday development rely heavily on reflection. GraalVM works around this by allowing classes to be registered for reflection at compile time. While this process can be cumbersome when working directly with GraalVM, Quarkus streamlines the registration process by detecting and auto-registering as many of your code’s reflection candidates as possible.
While Quarkus does a pretty good job with auto-registering most reflection candidates, you might still run into instances where you have to explicitly register some of your classes using Quarkus’s RegisterForReflection
annotation. This process might become more trouble than it's worth in some projects. For this reason, as well as just general flexibility, Quarkus also offers the JVM mode. In JVM mode, Quarkus apps are packaged as JAR files and run on the OpenJDK HotSpot JVM.
Show me the code!
So having set the stage, let’s look at some code. To get started with Quarkus, I put together a JAX-RS application following the excellent getting started guides from Quarkus. See my repo for the application's code. This application is a simple service that can be used to store, update, retrieve, and delete arbitrary text values. I mostly just followed the guide as I wrote my code. I built the application out in the following stages.
Core application
In this stage, I created the core application with all the API endpoints. I started by generating an app skeleton using the quarkus-maven-plugin
and adding the resteasy-jackson
extension for JSON support:
mvn io.quarkus:quarkus-maven-plugin:1.3.2.Final:create \ -DprojectGroupId=org.saharsh \ -DprojectArtifactId=sample-quarkus-app \ -DclassName="org.saharsh.samples.quarkus.resources.ValuesResource" \ -Dpath="/api/values" mvn quarkus:add-extension -Dextensions="resteasy-jackson"
Some changes I made include getting rid of .dockerignore
and the Dockerfile
examples generated by the quarkus-maven-plugin
's create task. Instead, I prefer to use a multi-stage Dockerfile (see JVM and Native) to keep my build concerns in one file. After this, I just added my application code as captured in this tag (or commit).
Metrics and health checks
Metrics and health checks are crucial in creating twelve-factor applications. Quarkus leverages Microprofile, which makes adding these features pretty straightforward:
mvn quarkus:add-extension -Dextensions="metrics"
See this tag (and commit) for the metrics I added for the application. My application collects timing metrics for all of its exposed API endpoints. It also contains a gauge of the value store’s size. These metrics are published at the /metrics
endpoint, which contains base, vendor, and application metrics. Each one of those subgroups also has its own endpoint (for example, /metrics/application
):
mvn quarkus:add-extension -Dextensions="health"
Similarly, see this tag (and commit) for the health checks. I added a liveness check and a readiness check. The /health
endpoint can be accessed for all health checks aggregated into one. However, you typically separate these into liveness and readiness probes. For this reason, /health/live
and /health/ready
endpoints are also automatically provided.
Persistence
The core app I put together in the first stage uses an in-memory storage service. This means that the storage is local to each instance of the application and gets wiped when that instance goes down. To build an actual stateless application that can be scaled up and have persistent storage, let’s offload the application state to a MySQL database:
mvn quarkus:add-extension -Dextensions="hibernate-orm,jdbc-mysql"
See this tag (and commit) for changes related to persistence. The highlights are:
- Making zero code changes to switch to persistent mode because my resource class depends on the
StorageService
interface abstraction. - Picking my storage service implementation at runtime lets me introduce three things:
- A
sample.storage.type
property. - A
producer
class to create the right bean based on the property. - A
Qualifier
annotation (ConfiguredStorage
) for my resource class to specify that it intends to use the bean produced by theproducer
class.
- A
- Leveraging the
application.properties
pattern to use in-memory storage as the default storage type.
For this last one, I intend to use environment variables to override these properties and switch over to persistent storage. There is one catch, however. Quarkus does much of its configuration and bootstrap at build time. Most properties will then be read and set during the build-time step. To change them, make sure to repackage your application. In my application.properties
file, quarkus.hibernate-orm.dialect
, quarkus.datasource.driver
, and quarkus.datasource.health.enabled
cannot be overridden at runtime. The good news is that the rest can.
And that’s it
I have a couple more commits around adding native build support and documentation. However, the application is ready to go. My repo's README.md
does a good job of walking through the details of building and running this application locally. You can use the following steps as a reference for running the application on Red Hat OpenShift or Red Hat CodeReady Containers:
# Create a new project oc new-project samples # Standup MySQL oc new-app --name=valuesdb mysql-ephemeral \ -p DATABASE_SERVICE_NAME=valuesdb \ -p MYSQL_ROOT_PASSWORD=password \ -p MYSQL_USER=valsuser \ -p MYSQL_PASSWORD=password \ -p MYSQL_DATABASE=valsdb # Create the application schema in MySQL oc rsh valuesdb-1-[pod_id] bash -c "mysql -uvalsuser -ppassword valsdb" mysql> CREATE TABLE vals ( id BIGINT AUTO_INCREMENT PRIMARY KEY, value VARCHAR(255) NOT NULL, date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); # Create API application from Github repo oc new-app --name valuesapi https://github.com/saharsh-samples/sample-quarkus-app # Expose a route oc expose svc/valuesapi && oc get routes # To use persistent storage, first create a secret containing DB configuration oc create secret generic valuesapi-properties \ --from-literal=SAMPLE_STORAGE_TYPE=persistent \ --from-literal=QUARKUS_DATASOURCE_URL="jdbc:mysql://valuesdb/valsdb" \ --from-literal=QUARKUS_DATASOURCE_USERNAME=valsuser \ --from-literal=QUARKUS_DATASOURCE_PASSWORD=password # Turn the fields of the secret into environment variables for the API app oc set env dc/valuesapi --from=secret/valuesapi-properties # Add liveness and readiness probes oc set probe dc/valuesapi --liveness --get-url=http://:8080/health/live oc set probe dc/valuesapi --readiness --get-url=http://:8080/health/ready
Conclusion
Quarkus is an exciting new development in the Java ecosystem. I will make sure to share more articles and code as I explore Quarkus in relation to serverless architecture, reactive programming, and Kafka. In the meantime, check out the following links to dig deeper:
Last updated: February 11, 2024