Secrets behind tiny Docker containers for Java microservices
Published January 13, 2021
In view of the upcoming Liberica JDK release, we want to lift the curtain just a bit and talk about what makes BellSoft images so small. You will learn the two main image reduction methods and get tools to minimize containers for your project.
Every organization knows that speed is the key. This truth applies fully to your software; the quicker it transfers and deploys, the more you will process and gain. Hence the trend for tiny Docker container images. Fast and productive, they save precious time — which equals revenue — in all environments: development, staging, and production.
Building an application, we imagine a classic container consisting of multiple layers: the app itself, a framework, libraries, various packages. If you’d like to look into it more, head over to our previous article on how code becomes a microservice. Besides this structure, it delineates different technologies used in creating this architecture and the ways they interweave with one another.
But when it comes to optimization (e.g., for the microservice architecture), it’s easier to see a Linux container as a tale of two parts: base image + JDK. These are the components that always affect the footprint. Now let us see which changes we’ve made to these components to shrink containers more than ever.
How to reduce container size
1. Switch to another OS image
The first possible answer is rather simple. Instead of a heavyweight full version of CentOS, take CentOS slim. Still not enough? Switch to Debian. Or consider the optimal choice: Alpine musl. We at BellSoft advocate for Alpine Linux as an auspicious operating system for development since its base image is the lightest among the Linux family.
Take a look at these charts below. Here we compare the sizes of Liberica JDK containers (versions 11.0.10 + 11.0.10 Early Access builds) with three Linux distributions.
As you can see, the Liberica EA binaries released on Jan 19 are smaller by 3–6 MB, which amounts to 14.7% for the Alpine musl java.base image. The average improvement is 7.6%.
If there’s no need to compile applications inside of a Docker image, you may use either JRE or java.base. We observe similar positive changes for Liberica JRE EA: on average, these packages have experienced an impressive 16% reduction.
BellSoft currently offers three flavors of Liberica JDK:
- Full, complete with LibericaFX (based on OpenJFX), MinimalVM, and DIO APIs, is best suited for large-scale software development.
- Standard is the optimal build for most desktops/servers.
- Lite is a lightweight Liberica flavor, making it the perfect fit for cloud instances and minimizing resources.
The best part is that BellSoft Liberica Lite is not some kind of a lackluster version that will limit your project. It is a full-fledged TCK-verified runtime 100% compatible with Java SE specifications. Judging from the charts, it’s self-evident that this is the flavor we prefer in our minuscule Docker containers, for both high performance and ultimate speed.
In case you are wondering whether reduced static footprint influences functionality, the answer is it does not. The binaries with reduced static footprint remain compatible with the Java SE Specification and provide all the JVM functionality that is found in the standard binaries, including all JIT compilers (C1, C2, Graal JIT Compiler), Garbage Collectors (Serial, Parallel, CMS, G1, Shenandoah, ZGC) and serviceability features, where applicable.
Each release undergoes thorough QC. As part of this process, we have evaluated the binaries with major industry-standard benchmarks and measured performance with Java Microbenchmark Harness tests. Our intent was to prove no differences in functioning or slower startup times for the Lite binaries relative to Liberica Standard. A subset of the performance results for 11.0.10 EA is as follows.
- JDK 11.0.10
- 4 repeats, average data presented
- Cascade Lake Intel CPU, 48 cores, 128 Gb RAM
- Options for backend:
-server -XX:+PrintFlagsFinal -XX:+UseParallelGC -Xnoclassgc -XX:-UseAdaptiveSizePolicy -XX:+AlwaysPreTouch -XX:+UseBiasedLocking -XX:-UsePerfData -XX:-UseNUMA -XX:-UseNUMAInterleaving -XX:InlineSmallCode=20k -XX:CompileThreshold=1000 -XX:-SegmentedCodeCache -XX:MaxInlineLevel=15 -XX:ParallelGCThreads=56 -Xmx102400m -Xms102400m -Xmn76800m -XX:SurvivorRatio=130 -XX:TargetSurvivorRatio=66 -XX:MaxTenuringThreshold=15
- System options: Page Size = 512 Mb
DaCapo Benchmark Suite
- JDK 11.0.10
- 9.12-bach-MR1 benchmark revision
- 20 repeats, standard deviation computed from there.
- Skylake Intel CPU, 4 cores
Besides testing against subbenchmarks, we have calculated the geometric mean, which you can also see in the chart below.
As can be seen from these graphs, no statistically significant differences with Standard have been found. We have also extensively tested with other benchmarks and have not found any benchmarks where the new lite version is worse performance-wise with statistical significance. The conducted tests lead us to conclude that Liberica Lite EA binaries v. 11.0.10 are as performant and fast to launch as Standard ones.
You may download the latest packages right now or see what BellSoft has in store.
2. Trim down JDK
Another option is to minimize the runtime by excluding modules you don’t need with
jlink. We’ll show you how it’s done with an example.
The code inside this file is a small app you may work with to follow the steps.
First, launch the Java Dependency Analysis Tool (JDeps). It processes Java bytecode, i.e., class files or the JARs that contain them, and examines the statically declared dependencies between classes. But we also see another way of using it. Being fully aware of the module system, JDeps compiles a list of JDK modules your Java application depends on. Leave only those in your native image and get rid of the rest.
jdeps duke_ascii.jar duke_ascii.jar -> java.base bellsoft.duke -> java.io java.base bellsoft.duke -> java.lang java.base
Having analyzed the code with
jdeps, we see that it only uses
java.base. In the case of a bigger app with more dependencies and libraries, we’d use
jlink to reduce the runtime. Luckily, BellSoft already has a Docker image with
java.base. Here’s the pre-built image with this application on DockerHub.
Let’s run our app with:
docker run --rm bellsoft/liberica-openjdk-demos-asciiduke.
&&& &&&&&&& &&&&&&&&& &&&&&&&&&&& &&&&&&&&&&&&&&& &&&&&&&&&&&&&&&&& &&&&% %%%%%%&&&& &&&&%%%%%%%%%%%%%&&&& && &&%%%%%%%%%%%%&& &&& &&& &&%%%%%%%&&& &&&&& && && & && & & & && &&&&& &&&&& &&&& &&&& &&&&& &&&&&& & &&&&&&&&& & & && &&& && &&
For most similar CLI-like applications, a single
java.base module will suffice. The whole sample image with Liberica JDK Lite and Alpine Linux musl is going to be as little as 40.4 MB with the app itself!
Packed and run, this tiny image gives us the smallest container there is.
Lots of developers build Docker image files and containers on their own because they are dissatisfied with what the market offers. Our instructions and lightweight Liberica JRE/JDK images got you covered so that you focus energy on your project instead.
BellSoft is full of ambition and is planning further work to cut down images. Liberica’s 11.0.10 Early Access binaries show an 8 to 16 percent reduction in size over regular ones. If you would like to be among the first to know about new advanced features, contact BellSoft engineers. During the free consultation, our experts with 15+ years of Java™ experience will explain how to get EA to releases that will revolutionize your projects and optimize your solutions — whether they are desktop, server, or cloud-based.