In the realm of Java, who hasn't heard one of these statements at least a dozen times:
- "Java is slow"
- "Java needs too much memory!"
- "Java Services/Servers need too long to start!"
- "Java isn't ready for microservices!"
I'm glad to tell you, with this post, that none of these are true anymore. Actually, they haven't been true anymore for a long time.
The last time I had a slow boot of an old-school Application Server was in the age of Tomcat or JBoss 4.x, and those are very, very old.
The - hopefully final - nail in the coffin of these myths is named Quarkus, or as the people behind this wonderful creation call it: "Supersonic Subatomic Java".
So what is it actually? Again, let's use the words of the creators: "A Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM, crafted from the best of breed Java libraries and standards."
Alright, that doesn't really tell us much, it also introduces another Buzzword: GraalVM.
GraalVM? What is that now?
Oracle created something wonderful with GraalVM, here is their sales-pitch on it:
And let me tell you, those are not just empty promises! GraalVM is amazingly fast and combined with Quarkus, you are in for one hell of a joy-ride.
While evaluating Quarkus for a Microservice Landscape, we did our homework first and compared three different environments:
- Payara Micro + JVM
- Quarkus + JVM
- Quarkus Native Image
While Payara Micro is already considered "fast" and "small footprint", Quarkus and a Quarkus Native Image respectively, are considerably faster and even smaller in overhead.
Don't believe me? I'll back those claims up in just a second.
Here are some pictures of a loadtest we created. The application was equipped with JAX-RS and JPA, targeting a (brutally oversized) in-memory database (so we can reduce the impact of database speed or, god forbid, slow network traffic).
The test itself consisted of 5000 Requests in 5 concurrent threads and a minimal database payload (it was just a string tbh).
All we monitored were CPU-Load, Time and Memory-Usage of the service image itself.
It's not really a detailed or scientific test, but the numbers are sufficient enough to name a clear winner.
Amazing how fast a native Image can be, right?
As you can see, we got the following advantages, just by using quarkus native images:
- we reduced the time to first request from 7.3 seconds, to 0.104 seconds. This would typically include the costly bootstrapping of a jvm/application-server runtime, as well as the preparation of the JAX-RS Endpoint
- memory usage decreased from ~700 MByte to ~50 MByte
- the CPU-Load went from a wild rollercoaster of 50%-90% to consistent 30% during the whole lifecycle
While working off the 5000 Requests hasn't sped up as drastically, the memory consumption consistently stayed very low.
If that isn't a noticeable improvement, I don't know what is.
A demo project, including the following Dockerfile can be found here
Are you ready to dive in? So here we go!
First a basic Dockerfile to build some Java stuff
FROM quay.io/quarkus/centos-quarkus-maven:19.3.1-java11 AS build # Since JVM has not been ported to alpines musl yet # and quarkus still relies on gcc for native binaries # We'll use the quarkus maven image as build-base # Lots of workarounds and setup for graalvm and quarkus to build the native binary # thankfully none of them end up in the final image WORKDIR /usr/src/app USER root RUN chown -R quarkus /usr/src/app USER quarkus COPY --chown=quarkus app/ . RUN ./mvnw clean package -Pnative # Due to the very same reason of musl not playing along just yet # we will use the redhat minimal image for delivery FROM registry.access.redhat.com/ubi8/ubi-minimal WORKDIR /app # Since the user should always be nobody (imho) and the USER-Directive only affects RUN, CMD and ENTRYPOINT # But not WORKDIR, we have to modify ownership of the workdir RUN chown nobody. /app # Copy as nobody COPY --from=build --chown=nobody /usr/src/app/target/*-runner /app/quarkusapp # Set up permissions for user `nobody` RUN chmod 775 /app \ && chmod -R "g+rwX" /app \ && chown -R nobody. /app EXPOSE 8080 USER nobody # Tell quarkus to listen on all interfaces, instead of localhost CMD ["./quarkusapp", "-Dquarkus.http.host=0.0.0.0"]
This handles the build part for you.
You might be asking yourself now "Yeah, this is nice and all. But what if I need more than just your lame little JAX-RS Service? This doesn't really help me much..."
Don't worry friend, quarkus has a bootstrapping service, like most other cool cloud-ready frameworks today, where you can check some boxes and get the skeleton project ready to use!
All this does basically, is change the dependencies in the resulting pom.xml, which the quarkus-maven-plugin picks up and builds your image with.
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-jsonb</artifactId> </dependency>
With this short, but hopefully sweet, little introduction, you should be ready to go and quarkus(ify?) your applications. And don't forget, always try to have fun doing it!
If I missed anything or you need some further information, don't shy away from leaving a comment, I'll make sure to leave none of you hanging :)