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.
Windows only: Docker requires WSL (Windows Subsystem for Linux)
wsl --installDownload 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.gitNavigate to the directory (the entire rest of this tutorial can be executed from this directory)
cd showcase-graalvmLinux only: Prepare folder structure
mkdir target && chmod -R g+rX,o+rX target
chmod -R g+rX,o+rX monitoringPull all docker images, e.g. mongo db
docker compose pull --ignore-buildableBuild JVM image:
docker compose build graalvm-demo-book-jvmℹ️ For Apple M4 laptops, there was an issue with Java in Docker versions before 4.39.0. If your build fails on an M4 machine, make sure that your Docker is on version >=4.39.0
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 psView start log and find the startup time of the application
docker compose logs -f graalvm-demo-book-jvmOn 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/booksHow 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-3curl http://localhost:8080/bookscurl -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}
]
EOFcurl http://localhost:8080/books/bulk/978-0-345-40946-1,978-0-345-53943-4What 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-nativeDockerfile: 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 psView start log and find the startup time of the application
docker compose logs -f graalvm-demo-book-nativeCreate 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/booksHow long did the first request take?
curl http://localhost:8084/books/978-3-8477-1359-3What 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.0Native:
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest graalvm-demo-book-native:0.1.0Which differences do you see? Do you see problems, which one is better?
Stop all running containers
docker compose down -vdocker compose up -d prometheus cadvisor grafanaOpen Grafana: http://localhost:3000/d/edonzk2655t6oc/lecture-graalvm
Start the two containers again
docker compose up -d graalvm-demo-book-jvm graalvm-demo-book-nativeWhich 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-nThe total number of books that are written to the mongoDB are: concurrentUsers * numberOfRequests * numberOfBooks.
Build the utilizer:
docker compose build graalvm-demo-book-utilizerStart the utilizer
docker compose up -d graalvm-demo-book-utilizerRun 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
}
EOFRun 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
}
EOFTry 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.txtLinux: Create Python environment and install requirements
python -m 'venv' .venv
source .venv/bin/activate
pip install -r ./load-test/requirements.txtBefore 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 prometheusWait some seconds and then start the book apps:
docker compose up -d graalvm-demo-book-jvm graalvm-demo-book-nativeHint: 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 150Evaluate 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