Virtualization Technology News and Information
Article
RSS
Diving into container's depths and finding the best base image

According to New Relic's State of the Java Ecosystem 2023, over 70% of Java applications in production environments run inside a container. In recent years, containers have become a common tool in microservice Java architecture in the cloud. Their popularity is unsurprising - containers outperform traditional JVMs in cloud deployment. Today, containers are used for simplified management; easy installation; packaging; isolation and improved security; scalability; and more.

What is a container?

layer-structure-java-container

Java container represents a stack piled from several layers: Java runtime; OS packages for the application; libraries; frameworks; and the base image or parent image itself.

A typical way to build  a Java container is based on creating a fat JAR component. The fat JAR normally aggregates Java class files, associated metadata, and all sorts of dependencies into one file for distribution. So, the result is a self-contained executable that just needs a JRE to run.

The layered structure of the container allows each layer to be crafted according to the application goal, playing with OS, libraries, and the rest of its inlay. For example, one can include only the application code in the executable and exclude the dependencies (storing them in the local repository instead). This approach makes the fat JAR thinner and boosts container efficiency. One can also utilize class files without the JAR packaging, removing double compression, accelerating startup, and reducing compressed image size in the process.

Adapting each container application layer to one's goal significantly increases efficiency and dramatically reduces costs. The exercise can become endless, and the result of the game is seen in a variety of containers offered today: large, small - even distroless.

Container size matters in terms of application performance, but there is no good or bad container - only the right one that serves the operational needs. There are no unified criteria for OS images, so it is important to know the requirements before proceeding with container or image choice.

The image we use in a JDK container is built on someone else's work, called a parent or base image, and this part requires careful attention. We have freedom of choice here - we can use JIB, buildpacks, or base/parent images. However, the dominant method of container creation is still based on the Dockerfile.

CPU, bandwidth usage and increased cloud cost in container transfer

The layered structure affects container transfer. We transfer containers from one environment to another, from one registry to the development machine, and so on. Pulling images is part of the everyday routine when working with applications. So, each time we transfer containers, we pass the network, and this transfer implies a cost.

The network boundaries you cross when you upload and pull images also matter, and restricted bandwidth might crush your operations. If you rely on a big registry (and in development practice, this is often the case), you can start your own proxy to reduce the traffic. However, if you deal with large workloads in an enterprise-level routine, this option becomes ineffective as network usage and cloud storage (CPU) costs become too high. So a solution for smooth, cost-efficient work with containers is required. This process can be delegated to a third party, such as a Docker Hub, software developer, etc.

To improve the container's transfer efficiency, you can change the Dockerfile build instructions so that downloading and installing dependencies occur before the source code is copied to the container. You can also put the container layers that get frequent updates on top so they will be pulled first. By doing so, you decrease the time spent on the pull process.

In summary, container transfer is a handover of structured information consisting of different layers. These layers are saved on the server and transferred via the network, so the process involves extra costs, time, and possible bandwidth restrictions.

To solve the issue of rising energy and cost consumption in development, the container sizes are being optimized. Small lightweight containers allow higher performance in the cloud, and they create less traffic and use less storage. Small containers are based on small images, such as Alpaquita Linux.

How native images differ from other base images

The GraalVM project started with the revolutionary idea of delivering one virtual machine to combine all languages, and their initial proposal of low-footprint, fast-startup Java packaged in a native image received widespread adoption in the developers' community.  Today, GraalVM equals native images for many of us.

GraalVM compiles Java bytecode ahead-of-time into a platform dependent executable called native image. Native images include only the code required at run time, which are the application classes, standard-library classes, the language runtime, and statically-linked native code from the JDK. To create a native image GraalVM executable first uses static analysis of code to determine the classes and methods that are reachable when the application runs; and then it compiles classes, methods, and resources into a binary. This entire process is called build time, and it clearly distinguishes from the Java source code compilation to bytecode.

Native images are currently supported by all the leading application frameworks. They allow deployment in containers and are ideal for the cloud.

A Native image executable file has important advantages over any other JDK image. It starts in milliseconds, delivers peak performance immediately, and fully skips a warmup phase, which is a modern pain for large and complex Java workloads. Native images can be packaged into a lightweight container image, and we'll explore how such containers differ below.

Native images in action

In comparison to the Java container based on the JDK parent image, a run time container with Liberica NIK (based on the GraalVM native image) will look as follows:

java-container-native-image 

As you can notice there are only three layers in this container and the fat one is the Native Image, with the other two occupying less space.

The difference here is due to the GraalVM executable replacing a typical JAR in a container with a JAR file. The JAR requires a JVM, and JVM uses a Just In Time (JIT) compiler to generate native code during runtime.

Then for AOT compilation at build time, you will need a container with Liberica NIK, but at run time you will only require the base OS layer.

To build the native executable, GraalVM uses Ahead Of Time compilation (AOT). In this compilation type we spend our resources just once, without further need to adapt the container layers for better performance continuously. Via a single AOT compilation, we receive the perfect result we reach in many steps of enhancements in a Docker-based container.

Native image generation performs aggressive optimizations eliminating unused code in the JDK and its dependencies, heap snapshotting, and static code initializations. This results in a fat native image layer in a container with much thinner other layers. These other layers have been already optimized by an AOT compiler, and we do not need to waste time on restructuring them. The container with the native image is small and lightweight by default. AOT compilation requires fewer resources, delivering peak performance immediately with no warmup, and reduces attack surface.

The GraalVM-based container in this light seems a favorable option for any cloud-based Java application. Apart from efficient performance, it provides by far the biggest startup benefits compared to other solutions to deal with the slow warmup currently being offered. Native images not only contain code, but also data as a pre-initialized heap that is created by running user-defined static initializers.

It should be noted that constructing a GraalVM native image build is a complicated process which can consume a lot of memory and CPU, resulting in an extended build time. The GraalVM philosophy, however, is to provide the most energy- and cost-efficient execution for Java programs, and the GraalVM community is continuously working on improving build-time performance.

The recently launched Layered Native Image project  is focused on this problem. A new feature currently in research will divide the layered structure into separate libraries, allowing you to use only a small library relevant to your container and consisting of the set of native images required. The expected results of the Layered Native Image project are an increased compilation speed, the removal of many redundant container images, and - as a consequence - a significant reduction in rebuild or build time.

GraalVM solutions is on top of the list for reducing resource requirements and is a key component to keeping costs down in modern sustainable Java applications. Recent release of Spring Boot 3  enlarges GraalVM Native Image support, allowing Spring Boot applications to be converted into native executables using the standard Maven or Gradle plugins without special configuration. Today you can use GraalVM functionality in Spring framework through buildpacks or native build tools. Both options are perfect for creating your ideal container in the cloud.

Optimizing containers

Optimization is the path one should always search for in software development. The performance, security, production, and development costs of the same two containers presented above can be further advanced; you just have to choose more suitable components.

The first optimization option is based on a buildpack. A buildpack is a key element for your container's structure when your team lacks the time and prefers ready-made instruments.  A buildpack transforms an application source code into runnable artifacts by analyzing the code and determining the best way to build it. Containers based on buildpacks will have optimized metrics for all layers. Designing a JVM or native image based container using a buildpack is a relatively easy task. You only need to choose an appropriate buildpack.

The next level of advanced container optimization arrives via musl technology. Musl is a more effective technology for Java containers, and switching a development routine to a musl supported software is not complicated. Using Alpaquita Linux with musl allows for an enhanced JDK container with thinner layers and greater performance metrics. Liberica Native Image Kit on musl is available to create a native image-based container that will deliver a smaller size and faster speeds along with a reduced surface for possible attacks.

Containers on musl outperform the standard ones. They are more effective in development and operation, their transfer requires less resources and time, they reduce the cost and deliver an easier Java development practice, thereby speeding up both phases - development and production.

Containers have taken over the Java microservice architecture. There are many tools and ways to design them. While buildpacks are good for teams working on short deadlines and using a ready-made component for container structures, musl-supported Alpaquita Linux and Liberica NIK are great options for advanced development, delivering sustainability in cost and usage containers with increased security and performance metrics.

##

ABOUT THE AUTHOR

Dmitry Chuyko 

Dmitry Chuyko  is a Senior Performance Architect at BellSoft, an OpenJDK committer, and a public speaker. Prior to joining BellSoft, Dmitry worked on the Hotpot JVM at Oracle, and before that he had many years of programming experience in Java. He is currently focused on optimizing HotSpot for x86 and ARM, previously being involved in rolling out JEP 386, which enables the creation of the smallest JDK containers. Dmitry continues his journey in the containerization process and is happy to share his insights and expertise in this field.

Published Thursday, August 15, 2024 7:14 AM by David Marshall
Comments
There are no comments for this post.
To post a comment, you must be a registered user. Registration is free and easy! Sign up now!
Calendar
<August 2024>
SuMoTuWeThFrSa
28293031123
45678910
11121314151617
18192021222324
25262728293031
1234567