Skip to content

Latest commit

 

History

History
597 lines (373 loc) · 25.2 KB

QUARKUS-REACTIVE_VS_QUARKUS-REACTIVE-NATIVE.md

File metadata and controls

597 lines (373 loc) · 25.2 KB

CHAPTER 1: THERE'S ONLY ONE RULE IN THE JUNGLE: WHEN THE LION'S HUNGRY, HE EATS!

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.


CHAPTER 2: BRILLIANCE SHOULD BE ACKNOWLEDGED.

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.

Gradle Build Script

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.


CHAPTER 3: A GENTLEMAN'S QUOTE IS A GENTLEMAN'S WORD.

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.


CHAPTER 4: THE YOUNG SUCCEED THE OLD!

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

CHAPTER 5: NOW STARTS THE ALPHA DANCE.

As you can find, most of the in-docker solutions are pretty poor in comparison with non-docker performance.

On macOS and Windows, for example, standard Linux-based Docker containers aren’t actually running directly on the OS, since the OS isn’t Linux. And the image filesystem from the container itself is typically mounted with some sort of overlay filesystem, which can slow things down, so for anything I/O bound you want to use a bind-mounted volume. "Itamar Turner-Trauring"

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.

Regarding the volume, Bind mounts have limited functionality compared to volumes. When you use a bind mount, a file or directory on the host machine is mounted into a container. The file or directory is referenced by its absolute path on the host machine. By contrast, when you use a volume, a new directory is created within Docker’s storage directory on the host machine, and Docker manages that directory’s contents.

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.


BONUS: I'LL HAVE A PINT AND A PICKLED EGG.

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.


BACK TO THE MAIN PAGE