Comparing a REST H2 Spring versus Quarkus application on Raspberry Pi

Goal of this comparison

In my previous post “A Spring REST and H2 database application on the Raspberry Pi” an example was described to store sensors and measurements in a H2-database through REST API’s with a Spring application on the Raspberry Pi. This application takes some time to start on a Raspberry Pi, and Adam Bien who makes the airhacks.fm podcast asked me if I could compare this to a similar Quarkus application.

Challenge accepted and here are the results… ;-)

Cat fight between Quarkus and Spring

About the example application

Used tools

I used AdoptOpenJDK 11 and IntelliJ IDEA to build the application on PC. The sources of the Spring application are part of all the examples of my book “Getting Started with Java on Raspberry Pi”. In this post a similar application was made using Quarkus and Panache and the full code is also available on GitHub.

About Quarkus

Quarkus is open-source and initially developed by Red Hat, to provide “Supersonic Subatomic Java”. It’s a Kubernetes Native Java stack, tailored for OpenJDK HotSpot and GraalVM, using the best Java libraries and standards. It promises to use less memory and start faster compared to other cloud-native stacks.

About Panache

Panache allows you to simplify your database code with Quarkus, for a full description see “Quarkus - Simplified Hibernate ORM with Panache”. It builds on top of Hibernate ORM to map Java objects to relational database records.

What does the application do?

The goal of this example Quarkus application is exactly the same as the Spring proof-of-concept application: store data in a H2 database and provide REST API’s to add and read data from this database. So we are using the same diagram:

Database scheme

By integrating Swagger UI we can easily test the API’s:

Some code examples

As Quarkus and Panache extend Java EE, the code looks very similar to the Spring code, e.g. for a measurement entity:

@Entity
public class MeasurementEntity extends PanacheEntity {

    public String key;
    public Double value;
    public LocalDateTime timestamp;

    @ManyToOne
    @JoinColumn(name = "sensor_id", nullable = false)
    @JsonbTransient
    public SensorEntity sensorEntity;

    public MeasurementEntity() {
        // Required for mapping
    }

    public MeasurementEntity(SensorEntity sensorEntity, 
        String key, Double value) {
        this.sensorEntity = sensorEntity;
        this.key = key;
        this.value = value;
        this.timestamp = LocalDateTime.now();
    }
}

The rest controllers also need minimal code, e.g. to get all the measurements from the database:

@ApplicationScoped
@Path("/measurement")
public class MeasurementRepository implements
        PanacheRepository<MeasurementEntity> {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<MeasurementEntity> get() {
        return this.findAll().list();
    }
}

Live coding and testing

The dev-mode is a real eye-catcher when developing a Quarkus application. As soon as you have something which can run, start the application with:

$ mvn quarkus:dev

Any change in the code will immediately be recompiled and is ready to use and test. This means you can add extra REST-services in this example application and try them out in the Swagger UI without the need to compile or restart the application.

Testing on different systems and OS

To compare the start-up speeds of the Spring and Quarkus application, I used the Spring JAR, Quarkus JAR and Quarkus native application.

As JAR-file

The JAR files can be created in both projects with Maven and started with java like any other project with:

$ mvn clean package
$ java -jar target/java-spring-rest-db-0.0.1-SNAPSHOT.jar
$ java -jar target/javaquarkusrestdb-1.0-SNAPSHOT-runner.jar

As native application

On Linux PC

Using the provided Docker files, the application can be converted to a native application using GraalVM with one single Maven command. Keep in mind H2 can not be used as an integrated database into your application, and you need to run it standalone and adjust the application.properties. See below this article for detailed info.

$ mvn package -Pnative -Dquarkus.native.container-build=true
$ target/javaquarkusrestdb-1.0-SNAPSHOT-runner
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
22:10:15 INFO  [io.ag.pool]] (main) Datasource '<default>': Initial size smaller than min. Connections will be created when necessary
22:10:15 INFO  [io.quarkus]] (main) javaquarkusrestdb 1.0-SNAPSHOT native (powered by Quarkus 1.6.0.Final) started in 0.062s. Listening on: http://0.0.0.0:8080
22:10:15 INFO  [io.quarkus]] (main) Profile prod activated. 
22:10:15 INFO  [io.quarkus]] (main) Installed features: [agroal, cdi, hibernate-orm, hibernate-orm-panache, jdbc-h2, mutiny, narayana-jta, resteasy, resteasy-jsonb, smallrye-context-propagation, smallrye-openapi, swagger-ui]

Please take a look a few lines above, indeed start-up time is 0.062s!!! I tried several times and got between 0.040s and 0.078s. Faster than light :-)

For the Raspberry Pi

The Raspberry Pi 3 and 4 hava a 64-bit chip, but Raspberry Pi OS is - at this moment - a 32-bit system.

Running on 32-bit Raspberry Pi OS

There is no GraalVM (yet?) for 32-bit systems, so it is not possible to compile Quarkus to a native application for Raspberry Pi OS.

The JAR version was used. Starting the Spring application:

$ java -jar target/java-spring-rest-db-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.8.RELEASE)

22:49:42.131  INFO 1702 --- [           main] b.w.j.JavaSpringRestDbApplication        : Starting JavaSpringRestDbApplication v0.0.1-SNAPSHOT on ubuntu with PID 1702 (/home/ubuntu/JavaOnRaspberryPi/Chapter_10_Spring/java-spring-rest-db/target/java-spring-rest-db-0.0.1-SNAPSHOT.jar started by ubuntu in /home/ubuntu)
22:49:42.148  INFO 1702 --- [           main] b.w.j.JavaSpringRestDbApplication        : No active profile set, falling back to default profiles: default
...
22:50:16.205  INFO 1702 --- [           main] b.w.j.JavaSpringRestDbApplication        : Started JavaSpringRestDbApplication in 36.606 seconds (JVM running for 39.212)

Starting the Quarkus application:

$ java -jar target/javaquarkusrestdb-1.0-SNAPSHOT-runner.jar
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
22:20:46 INFO  [io.ag.pool]] (main) Datasource '<default>': Initial size smaller than min. Connections will be created when necessary
22:20:50 INFO  [io.quarkus]] (main) javaquarkusrestdb 1.0-SNAPSHOT on JVM (powered by Quarkus 1.6.0.Final) started in 9.259s. Listening on: http://0.0.0.0:8080

Running on 64-bit Ubuntu

At Raspberry they are working on a 64-bit pre-release version of Raspberry Pi OS, but for this test I flashed a new SD card with the Raspberry Imager tool with “Ubuntu 20.04 LTS (Raspberry Pi 3/4/, 64-bit server OS for arm64 architectures”.

Selecting Ubuntu in the Raspberry Pi Imager tool

Some additional steps were needed to be able to run and compile the test application on the new SD card, for the full steps see at the bottom of this post.

The same JAR’s were used for the overview table below as on the 32-bit system.

Unfortunately I didn’t manage to create a native application on the Pi itself. The native build process took many hours and somewhere in the process kept failing with an out-of-memory exception. The Docker build process also didn’t work on the Pi. It’s probably possible to cross-compile in Docker on PC for aarch64 for the Raspberry Pi, but that’s out of my “comfort zone” at this moment, so I didn’t follow that path further.

Results

“Amazingly fast boot time” - as promised on the Quarkus website - is proven, at least on PC. But also on the Raspberry Pi the start-up is a lot faster.

Framework PC Pi3 Pi3 Pi4 Pi4
(1) (2-32b) (2-64b) (3-32b) (3-64b)
Spring JAR 7.20s 66.14s 60.24s 36.60s 34.67s
Quarkus JAR 2.30s 17.29s 16.44s 9.26s 9.20s
Quarkus Native 0.06s - - - -

The native one is extremely fast but I only got it working on my PC…



Further detailed info

Using stand-alone H2 database

See https://o7planning.org/en/11895/installing-h2-database-and-using-h2-console

$ wget https://h2database.com/h2-2019-10-14.zip
$ unzip h2-2019-10-14.zip
$ cd h2/bin
$ chmod +x h2.sh
$ ./h2.sh

To be able to connect to the web console from any other PC in your network, see https://www.h2database.com/html/tutorial.html:

$ nano /home/ubuntu/.h2.server.properties

webAllowOthers=true

Connection settings to be used in the web console:

Driver Class: org.h2.Driver JDBC URL: jdbc:h2:tcp://localhost/~/h2.db User Name: sa Password: {empty}

To use the Quarkus application with the stand-alone H2 database, the application.properties need to be changed to:

# H2 database
quarkus.datasource.url=jdbc:h2:tcp://localhost/~/test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
quarkus.datasource.driver=org.h2.Driver
quarkus.datasource.username=sa
quarkus.datasource.password=

Compiling Quarkus native application on Raspberry Pi

For completeness, this is the step-by-step I used to try to build the native application from the sources of the Quarkus application.

# Install Maven
$ sudo apt install maven

$ mvn -v
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 11.0.8, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-arm64
Default locale: en, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-1013-raspi", arch: "aarch64", family: "unix"

$ java -version
openjdk version "11.0.8" 2020-07-14
OpenJDK Runtime Environment (build 11.0.8+10-post-Ubuntu-0ubuntu120.04)
OpenJDK 64-Bit Server VM (build 11.0.8+10-post-Ubuntu-0ubuntu120.04, mixed mode)

# Spring DB application
$ git clone https://github.com/FDelporte/JavaOnRaspberryPi.git
$ cd JavaOnRaspberryPi/Chapter_10_Spring/java-spring-rest-db/
$ mvn clean package
$ java -jar target/java-spring-rest-db-0.0.1-SNAPSHOT.jar

# Quarkus DB application (first as JAR)
$ git clone https://github.com/FDelporte/JavaQuarkusRestDb.git
$ cd JavaQuarkusRestDb
$ nano src/main/resources/application.properties
quarkus.datasource.url=jdbc:h2:file:/home/ubuntu/database.db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
$ java -jar target/javaquarkusrestdb-1.0-SNAPSHOT-runner.jar

# Quarkus DB application (then as native)
# Building with Docker doesn't seem to work, so install GraalVM first
# https://www.graalvm.org/docs/getting-started/linux
$ cd /home/ubuntu
$ wget https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.1.0/graalvm-ce-java11-linux-aarch64-20.1.0.tar.gz
$ tar -xzf graalvm-ce-java11-linux-aarch64-20.1.0.tar.gz
$ export PATH=/home/ubuntu/graalvm-ce-java11-20.1.0/bin:$PATH
$ which java
/home/ubuntu/graalvm-ce-java11-20.1.0/bin/java
$ export GRAALVM_HOME=/home/ubuntu/graalvm-ce-java11-20.1.0/
$ export JAVA_HOME=/home/ubuntu/graalvm-ce-java11-20.1.0/
# Make sure GRAALVM_HOME and JAVA_HOME are set as part of PATH
$ echo $PATH
$ echo GRAALVM_HOME
$ echo JAVA_HOME
# Or use gu to install correctly
$ /home/ubuntu/graalvm-ce-java11-20.1.0/bin/gu install native-image

# You will will also need GCC, and the glibc and zlib headers
# https://quarkus.io/guides/building-native-image
$ sudo apt-get install build-essential libz-dev zlib1g-dev

# Tried to compile to native up to 4g xmx but each with out of memory exception
$ mvn package -Pnative -Dquarkus.native.native-image-xmx=2g

$ mvn package -Pnative -DskipTests -Dquarkus.native.native-image-xmx=4g
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.google.inject.internal.cglib.core.$ReflectUtils$1 (file:/usr/share/maven/lib/guice.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
WARNING: Please consider reporting this to the maintainers of com.google.inject.internal.cglib.core.$ReflectUtils$1
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
[INFO] Scanning for projects...
[INFO] 
[INFO] -------------------< be.webtechie:javaquarkusrestdb >-------------------
[INFO] Building javaquarkusrestdb 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ javaquarkusrestdb ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ javaquarkusrestdb ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ javaquarkusrestdb ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/ubuntu/JavaQuarkusRestDb/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ javaquarkusrestdb ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-surefire-plugin:2.22.1:test (default-test) @ javaquarkusrestdb ---
[INFO] Tests are skipped.
[INFO] 
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ javaquarkusrestdb ---
[INFO] 
[INFO] --- quarkus-maven-plugin:1.6.0.Final:build (default) @ javaquarkusrestdb ---
[INFO] [org.jboss.threads] JBoss Threads version 3.1.1.Final
[INFO] [org.hibernate.Version] HHH000412: Hibernate ORM core version 5.4.18.Final
[INFO] [io.quarkus.deployment.pkg.steps.JarResultBuildStep] Building native image source jar: /home/ubuntu/JavaQuarkusRestDb/target/javaquarkusrestdb-1.0-SNAPSHOT-native-image-source-jar/javaquarkusrestdb-1.0-SNAPSHOT-runner.jar
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Building native image from /home/ubuntu/JavaQuarkusRestDb/target/javaquarkusrestdb-1.0-SNAPSHOT-native-image-source-jar/javaquarkusrestdb-1.0-SNAPSHOT-runner.jar
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Running Quarkus native-image plugin on GraalVM Version 20.1.0 (Java Version 11.0.7)
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] /home/ubuntu/graalvm-ce-java11-20.1.0/bin/native-image -J-Dsun.nio.ch.maxUpdateArraySize=100 -J-DCoordinatorEnvironmentBean.transactionStatusManagerEnable=false -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory -J-Dvertx.disableDnsResolver=true -J-Dio.netty.leakDetection.level=DISABLED -J-Dio.netty.allocator.maxOrder=1 -J-Duser.language=en -J-Dfile.encoding=UTF-8 --initialize-at-build-time= -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime -H:+JNI -jar javaquarkusrestdb-1.0-SNAPSHOT-runner.jar -H:FallbackThreshold=0 -H:+ReportExceptionStackTraces -J-Xmx4g -H:-AddAllCharsets -H:-IncludeAllTimeZones -H:EnableURLProtocols=http,https --enable-all-security-services -H:NativeLinkerOption=-no-pie --no-server -H:-UseServiceLoaderFeature -H:+StackTrace javaquarkusrestdb-1.0-SNAPSHOT-runner
-H:IncludeAllTimeZones and -H:IncludeTimeZones are now deprecated. Native-image includes all timezonesby default.
[javaquarkusrestdb-1.0-SNAPSHOT-runner:31956]    classlist:  37,726.34 ms,  1.18 GB
[javaquarkusrestdb-1.0-SNAPSHOT-runner:31956]        (cap):   3,495.30 ms,  1.18 GB
[javaquarkusrestdb-1.0-SNAPSHOT-runner:31956]        setup:  13,476.39 ms,  1.18 GB
04:55:45,582 INFO  [org.hib.Version] HHH000412: Hibernate ORM core version 5.4.18.Final
04:55:45,631 INFO  [org.hib.ann.com.Version] HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
04:55:45,948 INFO  [org.hib.dia.Dialect] HHH000400: Using dialect: io.quarkus.hibernate.orm.runtime.dialect.QuarkusH2Dialect
04:57:24,391 INFO  [org.jbo.threads] JBoss Threads version 3.1.1.Final
Error: Image build request failed with exit status 137
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  04:24 min
[INFO] Finished at: 2020-07-28T04:58:44Z
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal io.quarkus:quarkus-maven-plugin:1.6.0.Final:build (default) on project javaquarkusrestdb: Failed to build quarkus application: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
[ERROR]         [error]: Build step io.quarkus.deployment.pkg.steps.NativeImageBuildStep#build threw an exception: java.lang.RuntimeException: Failed to build native image
[ERROR]         at io.quarkus.deployment.pkg.steps.NativeImageBuildStep.build(NativeImageBuildStep.java:366)
[ERROR]         at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
[ERROR]         at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
[ERROR]         at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
[ERROR]         at java.base/java.lang.reflect.Method.invoke(Method.java:566)
[ERROR]         at io.quarkus.deployment.ExtensionLoader$2.execute(ExtensionLoader.java:932)
[ERROR]         at io.quarkus.builder.BuildContext.run(BuildContext.java:277)
[ERROR]         at org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
[ERROR]         at org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:2046)
[ERROR]         at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1578)
[ERROR]         at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1452)
[ERROR]         at java.base/java.lang.Thread.run(Thread.java:834)
[ERROR]         at org.jboss.threads.JBossThread.run(JBossThread.java:479)
[ERROR] Caused by: java.lang.RuntimeException: Image generation failed. Exit code was 137 which indicates an out of memory error. Consider increasing the Xmx value for native image generation by setting the "quarkus.native.native-image-xmx" property
[ERROR]         at io.quarkus.deployment.pkg.steps.NativeImageBuildStep.imageGenerationFailed(NativeImageBuildStep.java:411)
[ERROR]         at io.quarkus.deployment.pkg.steps.NativeImageBuildStep.build(NativeImageBuildStep.java:344)
[ERROR]         ... 12 more
[ERROR] -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

On PC I also managed to create a native application with Docker, but didn’t manage to succeed with it on Raspberry Pi…

# Build native Quarkus appplication with Docker (skipping tests to speed up a bit...)
$ sudo apt install docker.io
$ sudo gpasswd -a ubuntu docker
$ newgrp docker
$ mvn package -Pnative -Dquarkus.native.container-build=true -DskipTests