Bootified Code → Native Image

Spring Boot Native via Cloud Native Buildpacks

Srinivasa Vasu
Geek Culture
6 min readDec 4, 2020

--

The GraalVM native image is gaining a lot of popularity for serverless style scale to zero, on-demand scale-out/in architectures where the container boot time and runtime memory footprint play a massive part in both technical and commercial sense. Traditionally java apps are just in time compiled by the runtime, and it usually does a lot of runtime optimizations. The memory requirements are generally high because of the infrastructure's need to accommodate these runtime optimizations. GraalVM enables ahead of time compilation, and the resulting binary is machine optimized. This machine-optimized native binary has everything it needs, and hence the resulting image doesn’t need to pack a JRE runtime. Since these binaries are pre-compiled into a native machine code, it dramatically improves the start-up time as these apps can boot in sub-seconds. Also, the need to factor in the additional infrastructure to do the runtime optimizations is not required because it is pre-compiled, and parts of the application can be initialized at build time. This reduces the overall image size and memory requirements to run the native image. This makes it best suited for container-based environments as such, we build it once and run it anywhere the container runtime exists.

Though it is still in the early days, it opens up many possibilities in the cloud-native space where Spring plays a significant part. The initial support for native images was provided from Spring 5.1. Since then, a lot of effort has been going on between the Spring and GraalVM teams to improve the overall support. Spring Graal support is still experimental, and the spring-graalvm-native repo has examples and instructions to build the native image from the source code.

The best way to get started with Spring native image is through Cloud Native Buildpacks (CNB). Support for building OCI conformant images from the source code using cloud native buildpacks (CNB) has been added since Spring boot 2.3. paketo-buildpacks — one of the reference implementations of CNB does support building native images for spring boot based apps. paketo-buildpacks/java-native-image is the meta buildpack that contributes the necessary buildpack bits to create the native image.

Spring boot native

Let’s have a simple spring boot app and have the paketo-buildpacks native image builder to build the native image. If you want to know more about CNB and paketo-buildpacks, I have written a separate article.

The source code used in this article is available here. The quickest way to explore CNB is through pack-cli

paketo-buildpacks has three builders in particular — full, base, and tiny, each differs by the libraries and packages bundled in the base image. Let’s use the tiny builder in this case as the runtime image provided by that builder is distroless — just enough to run the apps. Let’s inspect the tiny builder to find out the bundled buildpacks.

It does have a java-native-image buildpack in the detection order. Based on the build time inputs, lifecycle in the builder orchestrates these buildpacks to generate the OCI conformant runnable image.

Another alternative to pack-cli is to use Spring’s native-build tool integration support. Spring boot has added a build task to leverage the cloud native buildpacks feature with maven/gradle. If the CNB build task is enabled, it uses the paketo-buildpacks builder by default. However, this can be customized during build time. More info related to gradle build customization is available here.

Let’s look at the changes in the gradle build file to enable the CNB build step.

Graal VM native image tool uses byte code instrumentation, and hence it needs to know all the bytecode that’s reachable during the image generation. It is a time-consuming and memory-intensive operation. So, it’s better to have higher memory (maybe at least 6GB) for the docker daemon allocated in the local workstation so that the image generation gets through without any OOM issues.

There are two ways to build the image — either by gradle or by pack-cli. To build via gradle,

This task builds the machine-optimized native container image. To build via pack-cli,

We may face some issues with class initialization during image creation. The initial design of Graal favored class initialization during the build time, which has been changed to runtime with Graal release 19.x and above. If we happen to hit the problem, then we can pass the build time inputs --initialize-at-build-time and --initialize-at-run-time to fix those class initialization-related issues. Multiple classes or packages can be passed as comma-separated values. Another build input that would be useful to understand the class initialization hierarchy is +H:TraceClassInitialization=true Sample input would be like,

We have the spring native image up and running in sub-seconds!

This image is built based on Ubuntu bionic, and that’s the base run-image being provided by the paketobuildpacks/builder:tiny. We can find various connected layers and image structure using dive, an excellent tool to inspect the image. If we examine the built image, the base layers will look something similar,

Suppose we want to swap the base OS filesystem (userspace) layer with a different run-image. In that case, traditionally, we have to rebuild the app and other intermediate images with the updated/patched base layer. This generally would be time-consuming and operationally intensive when we had to do it at scale. With cloud native buildpacks, updating/patching the base layers is as simple as a code rebase where the app’s manifest metadata needs to be updated with the base layer’s info. pack rebase does the job and the layer rebasing happens in a few seconds.

If we inspect the image again, the base layers would have been updated with the new bionic run-image reference,

If we rerun the app, everything is intact, and the ABI contract ensures the binary compatibility between the app and userspace layers.

Though this is not an actual full-blown application, the intent is to give you a perspective of how the integration is coming along and what might be the quickest way to play around with this. The Spring support is still in alpha. That means getting a more comprehensive range of Spring projects to be native enabled would take some time. The early signs are promising, and the road ahead looks quite interesting.

References:
CNB
https://buildpacks.io/
paketo-buildpacks
https://paketo.io/
dive
https://github.com/wagoodman/dive
pack-cli
https://github.com/buildpacks/pack
Source
https://github.com/srinivasa-vasu/spring-boot-k8s
Spring-GraalVM
https://github.com/spring-projects-experimental/spring-graalvm-native

--

--

Srinivasa Vasu
Geek Culture

Aspiring Software Artist | views expressed on this blog are solely mine |