Running a Microservice in Quarkus on GraalVM
This is the fifth part of a series on building a microservice in Quarkus. The other parts were:
- Part One: Building the framework and adding persistence
- Part Two: Implementing CDC with Kafka Connect and Debezium
- Part Three: Connecting to third party APIs and testing with Wiremock
- Part Four: Securing the service with OpenID Connect
In this part we are going to deploy the service running in GraalVM.
GraalVM
GraalVM is a game-changing technology that has only been production ready since 2019. It is a Java VM that contains a JIT compiler and supports building of native images allowing AOT (Ahead Of Time) compilation of java applications. The executable file built by the native image does not run on a JVM but as a platform-specific native application. The binary image contains all of the necessary libraries and dependencies significantly reducing the startup and execution time.
Installing GraalVM
You can get instructions for downloading GraalVM for your OS of choice here.
Next configure the runtime. Here is how you would do it for macOS
export GRAALVM_HOME=/Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.1.0/Contents/Home
For building native images you will need to install the native image tool
$ {GRAALVM_HOME}/bin/gu install native-image
Once you have installed the native image tool you can create native image binaries using the tool
$ native-image my-app.jar
This creates a native binary in the current directory. You can now invoke the binary as follows:
$ ./my-app
Building the Native Image
You can follow along with the code by pulling the branch for this article
git clone git@github.com:iainporter/sms-service.git
cd sms-service
git checkout part_five
Up until now we have built the SMS microservice to run in a JVM. To run it as a native image we need to either pass an argument to maven on the command line:
mvn install -Dquarkus.package.type=native
Or we can add a profile and use the same argument which also allows us to run integration tests against the native image. Before we do that however let’s add a docker file to build a minimal image in which to run the native executable.
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1
WORKDIR /deployments/
COPY --chown=1001:root target/*-runner /deployments/application
EXPOSE 8080
USER 1001
CMD ./application -Dquarkus.http.host=0.0.0.0
The profile in the pom
Now we can install the executable with the maven command
mvn clean install -Pnative
It is here that we run into trouble. In order to build the native executable the GraalVM compiler needs to run a static analysis of the code to perform ahead of time compilation. It has to make a closed world assumption about the code, meaning that all classes have to be available at compile time. No dynamic class loading at runtime or java agents such as JMX, and the use of reflection is limited. All of these limitations are what make the application fast to boot so compromises have to be made. Quarkus is built to run on GraalVM and is optimised for that purpose.
The SMS microservice makes use of the OKHttp client that falls over at compilation time:
Error: No instances of sun.security.provider.NativePRNG are allowed in the image heap as this class should be initialized at image runtime. To see how this object got instantiated use -H:+TraceClassInitialization.
Detailed message:
Trace: object java.security.SecureRandom
There are a bunch of properties that you can use to tweak the compiler behaviour but in this instance it gives us an opportunity to rip out the rest client and use the rest client in Quarkus which won’t have compilation issues.
The Rest client interface can be found here and the implementation here. Once we have replaced the rest client the application compiles to a native executable.
Memory Footprint and Boot Time
Now that we can build the native application we can compare the memory size and startup time. The normal JVM image size is 559MB
The startup time for the JVM image is over 15s
The native image size is less than half of the JVM image
The startup time is lightening fast. In fact it is so quick that is starts up before the postgres database is ready and so fails to start. To fix this we need to add a wait utility to the container
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.2.1/wait /wait
RUN chmod +x /wait
EXPOSE 8080
USER 1001
CMD /wait && ./application -Dquarkus.http.host=0.0.0.0
We make use of this in the docker-compose file
environment:
WAIT_HOSTS: postgres-db:5432
The sms service will wait until postgres is available until starting up. When it does start up the result is less than 2 seconds.
This might seem like a long time but consider that the service has to connect to the database, run several flyway migrations, as well as set up the debezium connector to tail the database logs and set up kafka listeners. Compared to the JVM image it is incredibly fast.
The code for this whole series on building a production-ready microservice in Quarkus is available here.
The other parts in this series are:
- Part One: Building the framework and adding persistence
- Part Two: Implementing CDC with Kafka Connect and Debezium
- Part Three: Connecting to third party APIs and testing with Wiremock
- Part Four: Securing Microservices in Quarkus with OpenIdConnect
- Part Six: Containerizing your Microservice with Jib
- Part Seven: Building a CI pipeline for a Microservice with CircleCI