Hello, world,
The next step in my journey throw the jungles of frameworks is Quarkus. SUPERSONIC? SUBATOMIC?
Ok, Ok. Let's test it.
Before all, I would like to tell you that I would like to test the reactive approach due to the fact, that in previous research this solution brings us a pretty good system speed up.
So, I've took a Quarkus Reactive + Reactive Postgres Client with it's architecture.
I hope you are familiar with my previous performance results regarding the Spring Web (as Native), and Spring Reactive (as Native). If not, please take a look on it.
This article is not about Quarkus internal architecture and design, its paradigms, and the solutions that Quarkus Team brings to life. This article is about performance. I will not dive deeper into the Quarkus Reactive stack and the business description of my application you could always read it on your own in my previous research and in the documentation.
And today I will check the performance of a native executable (including in the docker solution) and a default one (including inD solution as well).
The lion is hungry. And I am going to eat.
So, we are going to create our application based on Quarkus. Hopefully, the developers who are familiar with Spring Framework could easily migrate to Quarkus. Quarkus brings a number of pros and cons. And it depends on you what is more appropriate for your business. I will just provide you the link to the sources.
The languages, frameworks, and tools I used.
JDK | GC | Gradle | Quarkus |
---|---|---|---|
17 | G1 | 7.5.1 | 2.13.1.Final |
I will highlight some of the configurations here.
plugins {
id 'java'
id 'io.quarkus' version '2.13.1.Final'
}
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
//region quarkus
implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")
implementation("io.quarkus:quarkus-resteasy-reactive-jackson")
implementation("io.quarkus:quarkus-reactive-pg-client")
implementation("io.quarkus:quarkus-config-yaml")
implementation("io.quarkus:quarkus-logging-json")
implementation("io.quarkus:quarkus-arc")
//endregion
//region lombok
annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
implementation("org.projectlombok:lombok:${lombokVersion}")
//endregion
}
group 'by.vk.quarkusweb'
version '1.0-SNAPSHOT'
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
compileJava {
options.encoding = 'UTF-8'
options.compilerArgs << '-parameters'
}
And application.yml, for sure.
quarkus:
banner:
enabled: false
container-image:
build: true # set false for non image building procedure
datasource:
db-kind: postgresql
username: postgres
password: postgres
reactive:
url: postgresql://localhost:5433/a2b?currentSchema=a2b
As I already mentioned, I decided to check all possible types of launching the application, such as jar, jar in docker, native executable, and in docker native solutions as well.
In Quarkus you have several ways of building a docker image:
- JIB (both for UBI and Distroless images);
- Docker;
- S2I;
- Buildpack.
And a bit more of building the native executable, especially as a part of the docker image. And I am going to check all of these cases.
So, being bounded by my env I will have results for:
- Typical fast and uber jars and 4 different images that had been built with JIB, Docker, S2I, and Buildpack;
- Native native executable, manually using the micro base image, manually using the minimal base image, using a Distroless base image, using buildpack;
- For the best-rated result of the native executable I will provide the results of compression using UPX both for max and min compression.
Just do it.
There are results of non-native solutions.
- FAST JAR
Global information:
Requests:
Requests per second:
Responses per second:
Response time for first minute:
Response time for all time:
You could download the FAST JAR Performance Tests Results and check it on your own.
- UBER JAR
Global information:
Requests:
Requests per second:
Responses per second:
Response time for first minute:
Response time for all time:
You could download the UBER JAR Performance Tests Results and check it on your own.
- JIB with default UBI base image
Global information:
Requests:
Requests per second:
Responses per second:
Response time for first minute:
Response time for all time:
Docker image investigation:
You could download the UBI Performance Tests Results and check it on your own.
- JIB with Distroless base image
Global information:
Requests:
Requests per second:
Responses per second:
Response time for first minute:
Response time for all time:
Docker image investigation:
You could download the Distroless Performance Tests Results and check it on your own.
- Docker
Global information:
Requests:
Requests per second:
Responses per second:
Response time for first minute:
Response time for all time:
Docker image investigation:
You could download the Docker Performance Tests Results and check it on your own.
- S2I
No Openshift manifests were generated so no s2i process will be taking place.
So, if you have any setup on Openshift, you could try to check it on your own using my source and contribute my researches.
- Buildpack
I've tried the set of builders and no one, even provided by Daniel Oh on his youtube channel did work correctly.
Let's gather all the information:
TYPE | BUILD TIME (s) | ARTIFACT SIZE (MB) | BOOT UP (s) | ACTIVE USERS | RPS | RESPONSE TIME (95th pct) (ms) | SATURATION POINT | JVM HEAP (MB) | JVM NON-HEAP (MB) | JVM CPU (%) | THREADS (MAX) | POSTGRES CPU (%) |
---|---|---|---|---|---|---|---|---|---|---|---|---|
FAST JAR | 4 | N/A | 0.987 | 10246 | 755.434 | 13686 | 1971 | 999 | 55 | 9 | 25 | 99 |
UBER JAR | 8 | 17,7 | 1.884 | 10258 | 753.933 | 14111 | 2149 | 934 | 55 | 5 | 23 | 99 |
JIB/ubi8 | 16 | 384 | 1.151 | 10244 | 593.275 | 20170 | 1305 | 999 | 55 | 8 | 26 | 70 |
JIB/distroless | 14 | 249 | 1.088 | 10202 | 428.563 | 33060 | 1339 | 915 | 55 | 15 | 26 | 93 |
DOCKER | 39 | 416 | 0.948 | 10238 | 540.492 | 24206 | 1315 | 207 | 55 | 18 | 21 | 53 |
Move on.
Now it's a time to compare previous solutions with native ones.
- Native Executable
Global information:
Requests:
Requests per second:
Responses per second:
Response time for first minute:
Response time for all time:
You could download the Docker Performance Tests Results and check it on your own.
- MANUALLY - Native Micro Base Image
Global information:
Requests:
Requests per second:
Responses per second:
Response time for first minute:
Response time for all time:
Docker image investigation:
You could download the Docker Performance Tests Results and check it on your own.
- MANUALLY - Native Minimal Base Image
Global information:
Requests:
Requests per second:
Responses per second:
Response time for first minute:
Response time for all time:
Docker image investigation:
You could download the Docker Performance Tests Results and check it on your own.
- MANUALLY - Native Distroless Image
Global information:
Requests:
Requests per second:
Responses per second:
Response time for first minute:
Response time for all time:
Docker image investigation:
You could download the Docker Performance Tests Results and check it on your own.
(!) Now let's try UPX and compare the results after ultra-brute compression.
UPX works by compressing the sections stored within the Section Table of the PE file. A strong indicator of UPX being used is the renaming of the header names (UPX0/UPX1). The main purpose of UPX is to reduce file size, this helps mask the malware as a .jpg or to spread through emails.
Unfortunately, after several trys to compress image with upx I didn't get the output...
TYPE | BUILD TIME (s) | ARTIFACT SIZE (MB) | BOOT UP (s) | ACTIVE USERS | RPS | RESPONSE TIME (95th pct) (ms) | SATURATION POINT | RAM (MB) | JVM CPU (%) | THREADS (MAX) | POSTGRES CPU (%) |
---|---|---|---|---|---|---|---|---|---|---|---|
NATIVE EXECUTABLE | 180 | 49.3 | 0.223 | 10232 | 697.563 | 16426 | 1967 | 646 | 10 | 15 | 99 |
NATIVE EXECUTABLE - UPX MAX | 741 | 15 | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A |
MANUALLY MICRO BASE IMAGE | 301 | 78.6 | 0.031 | 10253 | 507.971 | 25637 | 1282 | 690 | 20 | 8 | 57 |
MANUALLY MINIMAL BASE IMAGE | 301 | 152 | 0.032 | 10238 | 448.231 | 35777 | 914 | 669 | 17 | 8 | 61 |
DISTROLESS BASE IMAGE | 238 | 72.1 | 0.032 | 10260 | 473.458 | 30156 | 1747 | 622 | 23 | 8 | 45 |
As you can find, most of the in-docker solutions are pretty poor in comparison with non-docker performance.
It's about two points:
- Security;
- I/O and volumes operations.
What about the security, docker uses one mechanism named seccomp and the issue had not been fixed yet. I would like to mention that Podman and Kubernetes have no issues with it. So, if we are going to check the performance running your CI/CD - it could be or not enabled. Up to you. Chose the best approach for your business.
Now, I am going to test the best in-docker solution for Spring and Quarkus with:
- --security-opt seccomp=unconfined;
- use volumes to create a directory within Docker's storage.
I've tried to compare it on Quarkus native distroless base image. After tuning we've got a boost in performance of up to 11%. All results you could find in chapter 6.
These stunts are performed by trained professional, don't try this on production.
CHAPTER 6: IF YOU WISH TO BE THE KING OF THE JUNGLE, IT'S NOT ENOUGH TO ACT LIKE A KING. YOU MUST BE THE KING.
Let's compare all the results including the Spring Web, Spring Reactive and their native solutions as well.
FRAMEWORK | APPLICATION TYPE | BUILD TYPE | BUILD TIME (s) | ARTIFACT SIZE (MB) | BOOT UP (s) | ACTIVE USERS | TOTAL REQUESTS | OK | KO(%) | RPS | RESPONSE TIME (95th pct) (ms) | SATURATION POINT | RAM (MB) | CPU (%) | THREADS (MAX) | POSTGRES CPU (%) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
SPRING | WEB | NATIVE BUILD PACK * | 751 | 144,79 | 1,585 | 10201 | 453012 | 339759 | 25 | 374.566 | 47831 | 584 | 310 | 12,5 | 64 | 99 |
NATIVE BUILD TOOLS * | 210 | 116,20 | 0,310 | 8759 | 480763 | 342782 | 29 | 414.785 | 32175 | 1829 | 263 | 8 | 52 | 99 | ||
UNDERTOW | 5 | 49,70 | 3,59 | 10311 | 523756 | 396071 | 24 | 381.127 | 50977 | 1611 | 658 | 11 | 33 | 99 | ||
UNDERTOW IN DOCKER | 46 | 280 | 5,20 | 10264 | 430673 | 289692 | 33 | 448.682 | 29998 | 916 | 840 | 15 | 32 | 99 | ||
REACTIVE + R2DBC | NATIVE BUILD PACK * | 1243 | 98,5 | 0,103 | 10268 | 691487 | 573983 | 17 | 615.75 | 17891 | 1904 | 685 | 30 | 14 | 70 | |
JAR | 3,1 | 40,6 | 2,55 | 10326 | 1168782 | 1079847 | 8 | 1091,3 | 10406 | 4391 | 1823 | 8 | 31 | 70 | ||
JAR IN DOCKER | 39 | 271 | 3,95 | 10258 | 699180 | 581761 | 17 | 631.599 | 18955 | 2250 | 883 | 29 | 31 | 70 | ||
QUARKUS | REACTIVE + R2DBC | FAST JAR | 4 | N/A | 0,987 | 10246 | 828711 | 718773 | 13 | 755.434 | 13686 | 1971 | 1054 | 9 | 25 | 99 |
UBER JAR | 8 | 17,7 | 1,884 | 10258 | 826311 | 716252 | 13 | 753.933 | 14111 | 2149 | 989 | 5 | 23 | 99 | ||
JIB WITH UBI | 16 | 384 | 1.151 | 10244 | 661502 | 120360 | 18 | 593.275 | 20170 | 1305 | 1054 | 8 | 26 | 70 | ||
JIB WITH DISTROLESS | 14 | 249 | 1.088 | 10202 | 473991 | 486400 | 20 | 540.492 | 33060 | 1339 | 970 | 8 | 26 | 93 | ||
DOCKER | 39 | 416 | 0.948 | 10238 | 609675 | 343384 | 28 | 428.563 | 24206 | 1315 | 262 | 18 | 21 | 53 | ||
NATIVE EXECUTABLE | 180 | 49.3 | 0.223 | 10232 | 768017 | 654382 | 15 | 697.563 | 16426 | 1967 | 646 | 10 | 15 | 99 | ||
+ UPX-MAX | NATIVE EXECUTABLE | 741 | 15 | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | |
NATIVE MICRO BASE IMAGE | 301 | 78.6 | 0.031 | 10253 | 570959 | 445872 | 22 | 507.971 | 25637 | 1282 | 690 | 20 | 8 | 57 | ||
NATIVE MINIMAL BASE IMAGE | 301 | 152 | 0.025 | 10238 | 523534 | 395079 | 25 | 448.231 | 35777 | 914 | 669 | 17 | 8 | 61 | ||
NATIVE DISTROLESS BASE IMAGE * | 238 | 72.1 | 0.032 | 10260 | 546371 | 419297 | 23 | 473.458 | 30156 | 1747 | 622 | 23 | 8 | 45 | ||
NATIVE DISTROLESS BASE IMAGE * + ** | 238 | 72.1 | 0.037 | 10259 | 584874 | 460724 | 21 | 515.762 | 23786 | 2254 | 628 | 17 | 8 | 47 |
- is experimental feature; ** with --security-opt seccomp=unconfined and volume creation.
If your eyes are bleeding from the numbers, I've prepared some charts for you. Let's continue to bleed from charts :)
- Let's compare basic solutions that provides us with JARS after the build:
- JAR IN DOCKER:
- NATIVES:
- NATIVE IN DOCKER:
Actually, I could share my thoughts about Quarkus and compare it with Reactive solutions in Spring:
- A lot of different approaches out of the box;
- Some of these approaches don't work;
- Artifacts sizes are less than those created by Spring;
- A bit less build time for native solutions;
- Resources consumption is less;
- Saturation point and RPS are less :(
What to bring into production is up to you. But Quarkus provides its solution as ready for production, Spring Native is an experimental feature at the moment.
This article is the 3rd in my performance journey.
Next, I will bring you details regarding the Micronaut, Vert.x, Helidon, and Ktor.
So, will be in touch.
HAVE A NICE DAY.