The myth of slow Java
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 solution to a non-existing problem
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:
"GraalVM is an ecosystem and shared runtime offering performance advantages not only to JVM-based languages such as Java, Scala, and Kotlin, but also to other programming languages such as JavaScript, Ruby, Python, and R. Additionally, it enables the execution of native code via an LLVM front-end, and WebAssembly programs on the JVM."
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.
We want the numbers Mason!
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.
Payara Micro with OpenJDK 11.0.6 (Provided by GraalVM 20.0 CE):
Quarkus with OpenJDK 11.0.6 (Provided by GraalVM 20.0 CE):
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.
So let's build something with Quarkus!
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 :)
Top comments (8)
Nice! 2 cents: graalVM compiled is not java anymore. It is the same as golang: a compiled code with a management runtime. You have to compare it with go, rust not with java. IMHO.
I perfectly understand the case for native images, but comparisons with plain java is oranges and apples.
I ignored payara micro. I've spent some time with micronaut and honestly , while I wait for quarkus, micronaut hotreload and hackability are way superior to me now (testing with kotlin: no plain java is a must to me). Quarkus hotreload constantly chrashes with kotlin for me.
Do uou intentionally skip micronaut gor any reason?! Also: I have to give payara a try!
While that is certainly true, the comparison, as unfair as it is, resulted from the fact that most of our microservices were implemented in some kind of JVM framework. Naturally, we were interested in the performance-claims by just "changing the runtime/build".
Rewriting the services in a new language was proposed, but ultimately unanimously dropped, because we rely heavily on EE-Frameworks.
It would have been much more work than just switching the runtime to quarkus.
We have quite a wide range of implementations:
Pretty much each team worked with the framework they felt comfortable with, when building their services.
That is interesting. I haven't tried the hotreload extensively yet, since a complete maven-build works pretty fast and gives me a chance to grab a new cup of coffee. I'll take a look at that!
To be honest, I haven't heard of micronaut before, but at first glance it looks pretty cool! I'll be sure to check it out.
Yes, this is why I understand the case for compiled java code too. I agree: brownfield always has backward compatibility as a top requirement.
Hi, thanks for sharing it.
How do you got that 50 MB container? because when I run the example, I got this:
aironman@MacBook-Pro-de-Alonso ~/g/quarkus-quickstart (master)> docker container ls | grep quarkus
0a3165e9f300 quarkus "./quarkusapp -Dquar…" 26 hours ago Up 26 hours 0.0.0.0:8080->8080/tcp hardcore_goldstine
aironman@MacBook-Pro-de-Alonso ~/g/quarkus-quickstart (master)> docker images | grep quarkus
quarkus latest b09b95ad10eb 26 hours ago 204MB
I am running Catalina 10.15.4 (19E287) Docker 2.3.1.0 (45408)
Hi Alonso
The 50 MB you are referencing is the RAM usage of the Quarkus Native Binary.
The Image size itself is the same ~200M for me
quarkus latest cc034ce75edb 51 seconds ago 204MB
The native binary is ~30MB, the ubi-minimal image is ~140MB, adding the docker magic results in ~200M on disk.
As soon as alpine works with quarkus, we will hopefully get 50MB images too :D
Great write up. We similarly were seeing too many issues with Spring Boot launched on tomcat and did some research. This nailed it on the head.
Thank you for reading!
I'm curios, what made you decide to use Tomcat?
Nice and short article which proves quarkus claims for better performance and memory usage.