CHAPTER 1: SILENCE, DEVELOPER! MY NAME IS DARTH VADER. I AM AN EXTRATERRESTRIAL FROM THE PLANET REACTIVE!
Hello, world,
Now I would like to continue my comparing the performance of Spring Family Solutions. Today I am about Spring Reactive and Spring Boot Reactive as Native. For sure, I will use Spring Data R2DBC as well.
I hope you are familiar with my previous performance results regarding the Spring Web and Spring Web as Native. If not, please take a look on it.
I believe, my reader, are familiar with Spring Reactive but I will provide you a brief quote from official documentation on what is it: Reactive processing is a paradigm that enables developers to build non-blocking, asynchronous applications that can handle back-pressure (flow control).
I will not dive deeper into the Spring 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.
So, what I am going to check? I would like to check the performance of applications using Netty, JIB-built image, and Native Application using both native build tools and build packs. And now I will concentrate my attention on performance comparing.
Let's move forward.
CHAPTER 2: IF MY CALCULATIONS ARE CORRECT, WHEN THIS BABY HITS 1k RESPONSES PER SECONDS, YOU'RE GONNA SEE SOME SERIOUS SHIT.
Now we need to create the application that we are going to test.
The initial implementation will be on Spring Boot Reactive. The sources you could find there.
The languages, frameworks, and tools I used.
JDK | GC | Gradle | Spring Boot |
---|---|---|---|
17 | G1 | 7.5.1 | 2.7.4 |
I will highlight some of the configurations here.
plugins {
id 'org.springframework.boot' version '2.7.4'
id 'io.spring.dependency-management' version '1.0.14.RELEASE'
id 'java'
id("com.google.cloud.tools.jib") version "3.3.0"
}
group = 'by.vk'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
//region spring
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework:spring-context-indexer")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
implementation("org.springdoc:springdoc-openapi-ui:1.6.11")
//endregion
//region logback
implementation("ch.qos.logback.contrib:logback-jackson:0.1.5")
implementation("ch.qos.logback.contrib:logback-json-classic:0.1.5")
//endregion
//region lombok
annotationProcessor("org.projectlombok:lombok")
implementation("org.projectlombok:lombok")
//endregion
//region postgres
implementation("io.r2dbc:r2dbc-postgresql:0.8.13.RELEASE")
//endregion
}
jib {
to {
image = 'reactive-service-a2b:latest'
}
from {
image = "gcr.io/distroless/java17"
}
container {
jvmFlags = ['-noverify', '-XX:+UseContainerSupport', '-XX:MaxRAMPercentage=75.0', '-XX:InitialRAMPercentage=50.0', '-XX:+OptimizeStringConcat', '-XX:+UseStringDeduplication', '-XX:+ExitOnOutOfMemoryError', '-XX:+AlwaysActAsServerClassMachine', '-Xmx512m', '-Xms128m', '-XX:MaxMetaspaceSize=128m', '-XX:MaxDirectMemorySize=256m', '-XX:+HeapDumpOnOutOfMemoryError', '-XX:HeapDumpPath=/opt/tmp/heapdump.bin']
ports = ['8080']
labels.set([maintainer: 'Vadzim Kavalkou <[email protected]>', appname: 'a2b-service', version: '0.0.1-SNAPSHOT'])
creationTime = 'USE_CURRENT_TIMESTAMP'
}
}
And application.yml, for sure.
server:
compression:
enabled: true
spring:
main:
banner-mode: off
web-application-type: reactive
cache:
type: none
webflux:
base-path: "api/v1/"
r2dbc:
url: "r2dbc:postgresql://localhost:5433/a2b"
username: "postgres"
password: "postgres"
management:
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
endpoint:
health:
enabled: true
probes:
enabled: true
show-components: never
show-details: never
group:
readiness:
include: readinessState, db
metrics.enabled: true
prometheus.enabled: true
endpoints.web.exposure.include: "*"
metrics.export.prometheus.enabled: true
logging.level:
ROOT: info
by.vk.springbootreactive: info
org.springframework: info
As I already mentioned, I decided to check all possible types of launching the application, such as jar, docker and native image and in docker native solution as well. I'm creating the image using JIB to be able to create images without Docker Engine and took the distroless as a base image due to the next benefits of it:
"Distroless" images contain only your application and its runtime dependencies. They do not contain package managers, shells or any other programs you would expect to find in a standard Linux distribution.
Let's check the results.
- REACTIVE
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 Jar Performance Tests Results and In Docker Performance Tests Results and check it on your own.
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 (%) |
---|---|---|---|---|---|---|---|---|---|---|---|---|
JAR | 3,1 | 40,6 | 2,55 | 10326 | 1091,3 | 10406 | 4391 | 1730 | 93 | 8 | 31 | 90 |
JAR IN DOCKER | 39 | 271 | 3,95 | 10258 | 631.599 | 18955 | 2250 | 790 | 93 | 29 | 31 | 37 |
As you can see, in most cases basic approach using jar launching seems pretty good and has the best saturation point. Move on.
Now it's a time to compare previous solution with native one.
STRONGLY RECOMMEND NOT TO USE SPRING NATIVE IN PRODUCTION AT THE MOMENT. IT COULD LEAD TO UNPREDICTABLE LOSSES.
JDK | GC | Gradle | Spring Boot | Spring AOT |
---|---|---|---|---|
17 | G1 | 7.5.1 | 2.7.4 | 0.12.1 |
import org.springframework.aot.gradle.dsl.AotMode
plugins {
id 'org.springframework.boot' version '2.7.4'
id 'io.spring.dependency-management' version '1.0.14.RELEASE'
id 'java'
id 'org.springframework.experimental.aot' version '0.12.1'
}
group = 'by.vk'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
maven { url 'https://repo.spring.io/release' }
mavenCentral()
}
dependencies {
//region spring
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-actuator") {
exclude group: 'io.micrometer', module: 'micrometer-core'
}
//endregion
//region logback
implementation("ch.qos.logback.contrib:logback-jackson:0.1.5")
implementation("ch.qos.logback.contrib:logback-json-classic:0.1.5")
//endregion
//region lombok
annotationProcessor("org.projectlombok:lombok")
implementation("org.projectlombok:lombok")
//endregion
//region postgres
implementation("io.r2dbc:r2dbc-postgresql:0.8.13.RELEASE")
//endregion
}
bootBuildImage {
buildpacks = ["gcr.io/paketo-buildpacks/java-native-image:7.32.1"]
builder = "paketobuildpacks/builder:tiny"
environment = [
"BP_NATIVE_IMAGE": "true"
]
}
springAot {
mode = AotMode.NATIVE
debugVerify = false
removeXmlSupport = true
removeSpelSupport = true
removeYamlSupport = false
removeJmxSupport = true
verify = true
}
And application.yml:
server:
compression:
enabled: true
spring:
main:
banner-mode: off
web-application-type: reactive
cache:
type: none
webflux:
base-path: "api/v1/"
r2dbc:
url: "r2dbc:postgresql://localhost:5433/a2b" #for in docker solution replace 'localhost' with 'postgres-a2b'
username: "postgres"
password: "postgres"
properties:
schema: "a2b"
management:
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
endpoint:
health:
enabled: true
probes:
enabled: true
show-components: never
show-details: never
group:
readiness:
include: readinessState, db
metrics.enabled: true
prometheus.enabled: true
endpoints.web.exposure.include: "*"
metrics.export.prometheus.enabled: true
logging.level:
ROOT: info
by.vk.springbootreactive: info
org.springframework: info
Let's build both solutions, and check its performance.
Commands to build:
sdk use java 22.2.r17-grl
./gradlew clean nativeCompile --parallel
./docker/docker-compose up
./build/native/nativeCompile/spring-boot-reactive-native
Than you can check the stats of you process. Firstly, we are going to find the pid:
ps aux | grep :8080 (or you can find by name spring-boot-reactive-native)
Then check it:
top -pid ${PID}
- NATIVE BUILD TOOLS
You could download the Native Build Tools Performance Tests Results, Build Pack Performance Tests Results and check it on your own. JFYI: The native solution uses Serial GC.
Let's sum it:
TYPE | BUILD TIME (s) | ARTIFACT SIZE (MB) | BOOT UP (s) | ACTIVE USERS | RPS | RESPONSE TIME (95th pct) (ms) | SATURATION POINT | RAM (MB) | CPU (%) | THREADS (MAX) | POSTGRES CPU (%) |
---|---|---|---|---|---|---|---|---|---|---|---|
BUILD PACK | 1243 | 98,5 | 0.103 | 10268 | 615.75 | 17891 | 1904 | 685 | 30 | 14 | 65 |
NATIVE BUILD TOOLS | 187 | 71,7 | 0,107 | 10224 | 934.147 | 12591 | 3038 | 634 | 32 | 23 | 70 |
So what do we have? Let's compare all the results including the WEB and WEB as Native.
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 (%) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
WEB | BUILD PACK | 751 | 144,79 | 1,585 | 10201 | 453012 | 339759 | 25 | 374.566 | 47831 | 584 | 310 | 12,5 | 64 | 99 |
WEB | NATIVE BUILD TOOLS | 210 | 116,20 | 0,310 | 8759 | 480763 | 342782 | 29 | 414.785 | 32175 | 1829 | ✅ 263 | ✅ 8 | 52 | 99 |
WEB | UNDERTOW | 5 | 49,70 | 3,59 | 10311 | 523756 | 396071 | 24 | 381.127 | 50977 | 1611 | 658 | 11 | 33 | 99 |
WEB | UNDERTOW IN DOCKER | 46 | 280 | 5,20 | 10264 | 430673 | 289692 | 33 | 448.682 | 29998 | 916 | 840 | 15 | 32 | 99 |
REACTIVE | BUILD PACK | 1243 | 98,5 | ✅ 0,103 | 10268 | 691487 | 573983 | 17 | 615.75 | 17891 | 1904 | 685 | 30 | ✅ 14 | ✅ 70 |
REACTIVE | NATIVE BUILD TOOLS | 187 | 71,7 | ✅ 0,107 | 10224 | 1013549 | 915094 | 10 | 934.147 | 12591 | 3038 | 634 | 32 | 23 | ✅ 70 |
REACTIVE | JAR | ✅ 3,1 | ✅ 40,6 | 2,55 | 10326 | ✅ 1168782 | 1079847 | ✅ 8 | ✅ 1091,3 | ✅ 10406 | ✅ 4391 | 1823 | ✅ 8 | 31 | ✅ 70 |
REACTIVE | JAR IN DOCKER | 39 | 271 | 3,95 | 10258 | 699180 | 581761 | 17 | 631.599 | 18955 | 2250 | 883 | 29 | 31 | ✅ 70 |
So, let's compare Spring Reactive (as Native as well) and Spring Web (as Native as well).
The two points for not choosing the Reactive Jar solution are:
- If you would like to have a better boot-up time, please consider to chose the native compilation.
- If you would like to have less resource consumption, please take a look at Native Build Tools for Web.
In case you are going to have both of these properties at the top, please chose the Reactive Native Build Tools approach.
The pros of using the Reactive approach instead of the Web one:
- Less artifact size;
- On average better boot-up time;
- Handling 2,23 times more requests;
- RPS is better 2,43 times;
- Response time is less in 2,88 times;
- Saturation point is higher in 2,3 times.
Cons:
- If you chose the Reactive Jar approach you could have RAM consumption increasing 3 times. In other cases, it's pretty similar and even sometimes better.
This article is the second in my performance journey.
Next, I will bring you details regarding the Quarkus, Micronaut, Vert.x, Helidon, and Ktor.
So, will be in touch.
HAVE A NICE DAY.