Bootified Code → Native Image
Spring Boot Native via Cloud Native Buildpacks
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.
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
╰─ pack suggest-builders
Suggested builders:
Google: gcr.io/buildpacks/builder:v1 Ubuntu 18 base image with buildpacks for .NET, Go, Java, Node.js, and Python
Heroku: heroku/buildpacks:18 heroku-18 base image with buildpacks for Ruby, Java, Node.js, Python, Golang, & PHP
Paketo Buildpacks: paketobuildpacks/builder:base Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang
Paketo Buildpacks: paketobuildpacks/builder:full Ubuntu bionic base image with buildpacks for Java, .NET, NodeJS, Golang, PHP, HTTPD and NGINX
Paketo Buildpacks: paketobuildpacks/builder:tiny Tiny base image (bionic build image, distroless run image) with buildpacks for Golang> This will list all the CNB based trusted reference implementation builders.
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.
╰─ pack inspect-builder paketobuildpacks/builder:tiny
Inspecting builder: paketobuildpacks/builder:tinyDetection Order:
├ Group #1:
│ └ paketo-buildpacks/java-native-image@4.5.0
│ └ Group #1:
│ ├ paketo-buildpacks/graalvm@3.4.0
│ ├ paketo-buildpacks/gradle@3.4.0 (optional)
│ ├ paketo-buildpacks/leiningen@1.2.1 (optional)
│ ├ paketo-buildpacks/maven@3.2.1 (optional)
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.
bootBuildImage {
pullPolicy = "IF_NOT_PRESENT"
imageName = "harbor.sys.humourmind.com/kna/spring-kloud-k8s:1.0"
environment = ["BP_BOOT_NATIVE_IMAGE" : "1", "BP_JVM_VERSION" : "11"]
builder = "paketobuildpacks/builder:tiny"
}---
bootBuildImage
> is the CNB build task
environment
> passes build time inputs to the builder
> BP_BOOT_NATIVE_IMAGE would activate the spring-graalvm-native image buildpack
builder
> to reference paketobuildpacks/builder:tiny builder during the build phase
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,
gradle bootBuildImage
This task builds the machine-optimized native container image. To build via pack-cli,
pack build harbor.sys.humourmind.com/kna/spring-kloud-k8s:2.0 -B paketobuildpacks/builder:tiny -e BP_BOOT_NATIVE_IMAGE=1 -e BP_JVM_VERSION=11
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,
environment = ["BP_BOOT_NATIVE_IMAGE" : "1", "BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS": " -H:TraceClassInitialization=true --initialize-at-build-time=org.springframework.boot.logging"]
We have the spring native image up and running in sub-seconds!
2020-12-02 09:06:32.921 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2020-12-02 09:06:32.923 INFO 1 --- [ main] i.h.kloudnative.KloudNativeApplication : Started KloudNativeApplication in 0.107 seconds (JVM running for 0.112)
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,
"Layers": [
"sha256:906014996d9387b59468387ffdc46993c459ddf00f90e6324312f25f04ca4380",
"sha256:0acf37ff77b2c3ece119a8c192a4dfb92f07d486fd70e69dac1f069ee430df07",
"sha256:583a10d296e30d621fc8ff791c7b3ebc0da7357b10dd03d92068e92c4c31ac7b",
"sha256:52da9578e67922c62d17a1e3d4a908e0b3e9e46f92da45d37aa5d615154073a2",
"sha256:e81d17f1c46a1828c5e9bf55c81ea8af5965e7ab47e917fd5bd472a3dd8463b6"
]
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.
pack rebase harbor.sys.humourmind.com/kna/spring-kloud-k8s:2.0 --run-image humourmind/cnb-bionic-run@sha256:eb7edb490c2661ce80087b404b29cb593a22a6d877aad188827b482949c23280
If we inspect the image again, the base layers would have been updated with the new bionic run-image reference,
"Layers": [
"sha256:c8d644964be895653b0d5334e287115920beba57953249711b701df70e23d81d",
"sha256:7f1c64917d142dd155ba2b0111689c11fc8e59f644bca95ff660b263246080c7",
"sha256:c009f0e83ff5223756125dc4a2ab0428723e146601f7cc593fcd0878019fb1e7",
"sha256:583a10d296e30d621fc8ff791c7b3ebc0da7357b10dd03d92068e92c4c31ac7b",
"sha256:52da9578e67922c62d17a1e3d4a908e0b3e9e46f92da45d37aa5d615154073a2",
"sha256:e81d17f1c46a1828c5e9bf55c81ea8af5965e7ab47e917fd5bd472a3dd8463b6"
]
If we rerun the app, everything is intact, and the ABI contract ensures the binary compatibility between the app and userspace layers.
2020-12-02 09:24:56.265 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2020-12-02 09:24:56.267 INFO 1 --- [ main] i.h.kloudnative.KloudNativeApplication : Started KloudNativeApplication in 0.15 seconds (JVM running for 0.153)
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