23 mars 2020
By Yann Albou.
This blog post was originally published on Medium
Why having a very fast startup, low memory footprint, native compilation with standard frameworks fit perfectly in Docker and Kubernetes improving scalability, resiliency and security.
Photo by Guillaume Jaillet on Unsplash
In this blog post, I show you how Quarkus can run a Java application natively (by compiling Java bytecode to machine code with GraalVM) inside Docker using the most minimal image (and most secure) in Docker "FROM SCRATCH" and deploying it on kubernetes.
A Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM, crafted from the best of breed Java libraries and standards.
Quarkus is a framework developed by RedHat which was designed for the container world and which has the following characteristics:
To demonstrate this approach I use a demo application based on https://github.com/quarkusio/quarkus-quickstarts/tree/master/getting-started
This is a minimal CRUD service exposing a couple of endpoints over REST. Under the hood, this demo uses RESTEasy to expose the REST endpoints.
you can find the sources in the following GitHub repository: sokube/quarkus-scratch
### Image for getting maven dependencies and then acting as a cache for the next image FROM maven:3.6.3-jdk-11 as mavencache ENV MAVEN_OPTS=-Dmaven.repo.local=/mvnrepo COPY pom.xml /app/ WORKDIR /app RUN mvn test-compile dependency:resolve dependency:resolve-plugins ### Image for building the native binary FROM oracle/graalvm-ce:19.3.1-java11 AS native-image ENV MAVEN_OPTS=-Dmaven.repo.local=/mvnrepo COPY --from=mavencache /mvnrepo/ /mvnrepo/ COPY . /app WORKDIR /app ENV GRAALVM_HOME=/usr RUN gu install native-image && ./mvnw package -Pnative -Dmaven.test.skip=true && # Prepare everything for final image mkdir -p /dist && cp /app/target/*-runner /dist/application ###*/ Final image based on scratch containing only the binary FROM scratch COPY --chown=1000 --from=native-image /dist /work # it is possible to add timezone, certificat and new user/group # COPY --from=xxx /usr/share/zoneinfo /usr/share/zoneinfo # COPY --from=xxx /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ # COPY --from=xxx /etc/passwd /etc/passwd # COPY --from=xxx /etc/group /etc/group EXPOSE 8080 USER 1000 WORKDIR /work/ CMD ["./application", "-Djava.io.tmpdir=/work/tmp"]
This multistage docker build is composed of 3 parts:
To execute this multi-stage build use the following command line:
docker build -t quarkus-app .
It is quite long and CPU-intensive to compile a native executable with GraalVm. This is therefore preferable to delegate this process to your CI/CD pipeline. The good news it that, without native compilation and during your development phase, the Quarkus hot reload is very efficient.
To run the generated image:
docker run -it --rm --name quarkus -p 8888:8080 quarkus-app
It will generate the following output:
2020-03-15 18:19:39,643 INFO [io.quarkus] (main) getting-started 1.0-SNAPSHOT (running on Quarkus 1.2.1.Final) started in 0.028s. Listening on: http: //0.0.0.0:8080 2020-03-15 18:19:39,644 INFO [io.quarkus] (main) Profile prod activated. 2020-03-15 18:19:39,644 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]
Notice the startup time of 0.028s
You can test the application in your browser : http://localhost:8888/hello/greeting/SoKube
Do not think this is a simple hello World demo: Under the hood the Quarkus framework use CDI and Resteasy as you would do with a real java application that needs to serve business requests…
Another interesting test is to limit memory and cpus:
docker run -it --rm --name quarkus -p 8888:8080 --cpus="0.05" --memory="4m" --memory-swap="4m" quarkus-app
With 0.05 of a CPU and 4m of memory the application starts in 2.503s :
2020-03-15 18:35:18,137 INFO [io.quarkus] (main) getting-started 1.0-SNAPSHOT (running on Quarkus 1.2.1.Final) started in 2.503s. Listening on: [http://0.0.0.0:8080](http://0.0.0.0:8080/) 2020-03-15 18:35:18,137 INFO [io.quarkus] (main) Profile prod activated. 2020-03-15 18:35:18,137 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]
Still impressive in regard of the allocated resources!
So first create the kubernetes cluster with:
k3d create --name quarkus-cluster --api-port 6555 --publish 8085:80 export KUBECONFIG="$(k3d get-kubeconfig --name='quarkus-cluster')" kubectl cluster-info
Deploy the application using https://github.com/sokube/quarkus-scratch/blob/master/deploy.yaml
Clone this Git repo and change directory to "quarkus-scratch"
k3d import-images --name quarkus-cluster quarkus-app kubectl apply -f deploy.yaml
The first line imports the previously created image called "quarkus-app" in the k3s cluster. And the second line deploys the application.
Then you should be able to reach the application using: http://localhost:8085/hello/greeting/SoKube
In the Pod spec I defined the request and limit resources to use a maximum of 1 millicore of CPU (1000 millicore = 1 CPU) and 4Mi of Memory
resources: limits: memory: "4Mi" cpu: "1m" requests: cpu: "1m" memory: "4Mi"
The command "kubectl logs -l app=quarkus" shows the startup logs:
2020-03-17 10:42:26,239 INFO [io.quarkus] (main) getting-started 1.0-SNAPSHOT (running on Quarkus 1.2.1.Final) started in 0.170s. Listening on: [http://0.0.0.0:8080](http://0.0.0.0:8080/) 2020-03-17 10:42:26,241 INFO [io.quarkus] (main) Profile prod activated. 2020-03-17 10:42:26,241 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]
Still very fast despite the resource limitations ! Try such a scenario with your JEE or SpringBoot app 😀
With a so fast startup and low memory footprint you can easily scale your application with 50 replicas on your laptop:
kubectl scale deployment/quarkus --replicas 50 NAME READY UP-TO-DATE AVAILABLE AGE quarkus 50/50 50 50 1h
and then scale it down also very quickly:
kubectl scale deployment/quarkus --replicas 1 NAME READY UP-TO-DATE AVAILABLE AGE quarkus 1/1 1 1 1h
Deploying such an application is not for the "Wahoo effect" (at least not only!).
Relying on Quarkus using native compilation and deploying on kubernetes is good but not sufficient!
Your application needs to be designed as a Cloud Native Application (CNA) and I am not talking about micro-services. You can write a CNA as a "normal" service but that respects some principles and avoid, for instance, a startup init of your application that load during several minutes a huge cache in memory… The design of your application is very important, none of the frameworks, tools, products can compensate for a bad design!
A drawback of the native compilation are the restrictions, especially around the use of reflection and dynamic class loading. This makes it harder (at least for now) to move all applications to native binaries, but with every release of Graal, compatibility is improving. It is why Quarkus supports a limited list of extensions but it is growing and already contains lot of extensions.
Another aspect related to the minimalist docker image is debuging ! No way to exec a command in the container! So how to debug an image that doesn’t contain a shell, tools like curl, wget …or even ls, chmod, chown, mkdir… ? I won’t go into detail on how to achieve this but the short story is to use an image like busybox and inject from this image to the minimalist image the shell and the needed tools…
Quarkus fit perfectly in the Cloud era, where containers, Kubernetes, micro-services or services, function-as-a-service and applications natively designed for the Cloud allow to reach high levels of productivity and efficiency.
Services, instant scalability, and high density platforms like Kubernetes require applications with a small memory footprint and fast start-up. Java was not well positioned because it favors processing times at the expense of the CPU and RAM. Combining Quarkus with GraalVM, it is not anymore the case!
Kubernetes is here to stay, so let’s prepare our developments, applications and platforms for maximum flexibility, efficiency and security.