Goals of this tutorial:
- Compare GraalVM native image and JVM using a basic SpringBoot application
- Examine differences in size, execution time, and resource consumption
- Determine scenarios where GraalVM native image is more advantageous than JVM and vice versa
Download and install git for your operating system.
Docker requires WSL (Windows Subsystem for Linux)
wsl --install
Download and install Docker Desktop on Windows or Docker Engine and Docker Compose on Linux.
Download and install Python 3.11) (or later).
Because the two SpringBoot applications "graalvm-demo-book" and "graalvm-demo-book-utilizer" are built inside of Docker, there is no need to install GraalVM or Java locally.
graalvm-demo-root
├── graalvm-demo-book
│ ├── Main Application
│ └── Insert books into a MongoDB
│
├── graalvm-demo-book-utilizer
│ ├── Utilizer Application
│ └── Create a load-test for graalvm-demo-book
│
├── load-test
│ ├── Python Scripts
│ └── Run multiple load-tests and evalute the results
│
└── monitoring
├── Grafana and Prometheus config
└── Preconfigured Grafana dashboard with container monitoring
The Project Structure outlines the two key applications: graalvm-demo-book and graalvm-demo-book-utilizer.
The graalvm-demo-book application manages books and provides Rest endpoints to add, delete and get books. This is the application we use for the comparison.
The graalvm-demo-book-utilizer application creates multiple load tests for the graalvm-demo-book app.
The components of a book are as follows:
Name | Type | Description |
---|---|---|
id | String | Unique ID for the Book |
title | String | The book's title |
author | String | The book's author |
pageCount | Integer | The book's page count |
A simplified diagram of the main components.
+-------------------+ +-----------------------+
| | | |
| graalvm-demo-book |<------| graalvm-demo-utilizer |
| | | |
| POST /books | | POST /load-test |
| insert books | | - configure url, |
| | | endpoint |
+---------|---------+ | - set numBooks, |
| | numRequests, |
| | numUsers |
v | |
+-----------------+ +-----------------------+
| |
| MongoDB |
| Store books |
| |
+-----------------+
First, we have start with a normal JVM and look how our demo application behaves.
You can use any command line tool (Terminal, PowerShell, Bash, ...)
Clone the GitHub repository
git clone https://github.com/envite-consulting/showcase-graalvm.git
Navigate to the directory (the entire rest of this tutorial can be executed from this directory)
cd showcase-graalvm
Linux only: Prepare folder structure
mkdir target && chmod -R g+rX,o+rX target
chmod -R g+rX,o+rX monitoring
Pull all docker images, e.g. mongo db
docker compose pull --ignore-buildable
Build JVM image:
docker compose build graalvm-demo-book-jvm
Dockerfile: graalvm-demo-book/Dockerfile.jvm
- Docker multi-stage build
- SpringBoot layered image
Run app and mongodb
docker compose up -d graalvm-demo-book-jvm
Show running containers
docker compose ps
View start log and find the startup time of the application
docker compose logs -f graalvm-demo-book-jvm
On Windows use Git console or another bash like terminal to run the following curl
commands.
Alternatively you can install VS Code Plugin "Rest client" or ItelliJ's "Services" und run the http requests via requests.http
Create a new book
time curl -v -XPOST -d'{"id":"978-3-8477-1359-3","title":"Nils Holgerssons wunderbare Reise durch Schweden","author":"Selma Lagerlöf","pageCount":704}' -H'Content-Type: application/json; charset=utf-8' http://localhost:8080/books
How long did the first request take?
If you want, you can play a little bit with the API.
curl http://localhost:8080/books/978-3-8477-1359-3
curl http://localhost:8080/books
curl -v -XPOST http://localhost:8080/books/bulk \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
[
{"id":"978-0-345-40946-1","title":"The Demon-Haunted World","author":"Carl Sagan, Ann Druyan","pageCount":480},
{"id":"978-0-345-53943-4","title":"Cosmos","author":"Carl Sagan","pageCount":432}
]
EOF
curl http://localhost:8080/books/bulk/978-0-345-40946-1,978-0-345-53943-4
What did you discover? Is all good or do you think there are some issues especially related to Cloud?
What is GraalVM?
- AOT: Ahead-of-time compilation
- Low Memory Footprint
- Self-contained executables
- Reduced startup time
--> How does it solve our problem? Compilation to native binary moves stuff from runtime to compile time.
Build native image:
docker compose build graalvm-demo-book-native
Dockerfile: graalvm-demo-book/Dockerfile.native
- Docker multi-stage build
- ./mvnw -Pnative,musl native:compile # what happens here?
Run app and mongodb
docker compose up -d graalvm-demo-book-native
Show running containers
docker compose ps
View start log and find the startup time of the application
docker compose logs -f graalvm-demo-book-native
Create a new book
time curl -v -XPOST -d'{"id":"978-3-8477-1359-3","title":"Nils Holgerssons wunderbare Reise durch Schweden","author":"Selma Lagerlöf","pageCount":704}' -H'Content-Type: application/json; charset=utf-8' http://localhost:8084/books
How long did the first request take?
curl http://localhost:8084/books/978-3-8477-1359-3
What did you discover? What is better now?
Dive is a tool for exploring a docker image and layer contents.
Let's use it to analyze our two Docker images.
JVM:
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest graalvm-demo-book-jvm:0.1.0
Native:
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest graalvm-demo-book-native:0.1.0
Which differences do you see? Do you see problems, which one is better?
Stop all running containers
docker compose down -v
docker compose up -d prometheus cadvisor grafana
Open Grafana: http://localhost:3000/d/edonzk2655t6oc/lecture-graalvm
Start the two containers again
docker compose up -d graalvm-demo-book-jvm graalvm-demo-book-native
Which differences do you see on the Grafana Dashboard?
On Windows use Git console or another bash like terminal to run the following curl
commands.
Alternatively you can install VS Code Plugin "Rest client" or use ItelliJ's "Services" und run the http requests via requests_load-test.http.
These are the parameters of graalvm-demo-book-utilizer
Name | Type | Description |
---|---|---|
webClientUrl | String | The URL of the service to be load-tested |
webClientEndpoint | String | The endpoint of the service |
numberOfBooks | Integer | The number of books |
numberOfRequests | Integer | The number of requests |
concurrentUsers | Integer | The number of simulated concurrent users via parallel threading |
Each simulated user (via parallel threading) will send the specified number of requests (numberOfRequests).
User
├── Request-1
│ └── numberOfBooks books
│
├── Request-2
│ └── numberOfBooks books
...
└── Request-n
The total number of books that are written to the mongoDB are: concurrentUsers * numberOfRequests * numberOfBooks.
Build the utilizer:
docker compose build graalvm-demo-book-utilizer
Start the utilizer
docker compose up -d graalvm-demo-book-utilizer
Run a simple load-test on graalvm-demo-book-jvm
curl -v -XPOST http://localhost:8085/load-test \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
"webClientUrl": "http://graalvm-demo-book-jvm:8080",
"webClientEndpoint": "/books/bulk",
"numberOfBooks": 20,
"numberOfRequests": 10,
"concurrentUsers": 1
}
EOF
Run the same load-test on graalvm-demo-book-native
curl -v -XPOST http://localhost:8085/load-test \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
"webClientUrl": "http://graalvm-demo-book-native:8080",
"webClientEndpoint": "/books/bulk",
"numberOfBooks": 20,
"numberOfRequests": 10,
"concurrentUsers": 1
}
EOF
Try changing the parameters and compare the results.
Which differences do you see?
Windows: Create Python environment and install requirements
python -m 'venv' .venv
.venv/Scripts/activate
pip install -r ./load-test/requirements.txt
Linux: Create Python environment and install requirements
python -m 'venv' .venv
source .venv/bin/activate
pip install -r ./load-test/requirements.txt
Before running the load test, we should restart the containers. (Why?)
docker compose down -v graalvm-demo-book-jvm graalvm-demo-book-native prometheus
docker compose up -d prometheus
Wait some seconds and then start the book apps:
docker compose up -d graalvm-demo-book-jvm graalvm-demo-book-native
Hint: If you have a CPU with performance and efficiency cores, you can pin the containers to a specific CPU-set in the compose.yaml file. For example if you have an Intel CPU with 4 performance cores, and you want to ensure that the load test runs on them, set cpuset to "0-7".
graalvm-demo-book-jvm:
container_name: graalvm-demo-book-jvm
image: graalvm-demo-book-jvm:0.1.0
cpuset: "0-7"
Run load-test
python ./load-test/send_requests.py --urls "jvm,native" --runs 150
Evaluate the load-test
python ./load-test/requests_eval.py --images "jvm,native"
Which differences do you see? During and after the load-test?
For which kind of workload would you use which variant?
- JVM vs. GraalVM native: advantages and disadvantages
- In relation to Cloud Computing?
Additional Topics:
- JVM Optimizatios: Jlink, CDS, AOT
- GraalVM AOT, JIT