Java in Containers

Optimising Java for Containers

Reference URL: https://www.infoq.com/presentations/openjdk-containers/

  • Java Runtime Layer: Starting with Java 9, the JLink tool has been provided which can create your own custom runtime without changes to your code. The Java Runtime with all its modules could be 168MB. JLink allows you to only include relevant modules, bringing down the Java Runtime size to 50MB for a very basic Hello World application. Removing the header file and man pages further reduces this to 44MB.

  • Operating System Layer: The OS layer can also be optimised by perhaps using the Alpine image. However, Alpine Linux uses musl instead of glibc, while OpenJDK relies on glibc. For this, Project Portola is available which basically compiles the OpenJDK on top of musl. OpenJDK can then be used on top of Alpine. However, Portola does not go through all the testing usually done with OpenJDK, so this approach may not be the best option.

  • Java Startup Time: The Java startup time increased slightly from Java 8 to 9. JDK 14 startup times have improved over JDK 13 and with JDK 15, it would be even faster.

Docker Images with Spring Boot

Reference URL: https://spring.io/blog/2020/08/14/creating-efficient-docker-images-with-spring-boot-2-3

  • When containerising a java application, the common approach is to create a fat jar which is not ideal for creating container images that are based on layers.

  • There is always a certain amount of overhead when running a fat jar. In a containerised environment, this can be noticeable.

  • Frequently compiling and updating the code and all its dependencies is not efficient with docker images which are built on layers.

  • As of Spring Boot 2.3.0.M1, there is better efficiency to this process with buildpack support and layered jars.

Buildpacks

  • Till now, buildpacks were tightly coupled to the application platform. Cloud Native Buildpacks allow you to create Docker compatible images that can run almost anywhere.

  • Spring Boot 2.3.0.M1 includes buildpack support directly for Maven and Gradle.

Note: However, as of August 2020, I have not been able to get this working.

Layered Jars

  • The concept behind Layered Jars is that such a jar when extracted, has its files placed in root level folders differentiated by frequently changing custom application code files to less frequently changing library files.

  • These root level folders are dependencies (least likely to change), spring-boot-loader, snapshot-dependencies and application (most likely to change). In that order of likelihood of changing.

  • We enable layered jars in the pom.xml file as shown below.

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
     <layers>
        <enabled>true</enabled>
     </layers>
  </configuration>
</plugin>

We then extract the layered jar in a transient docker build image and copy these root level folders to the final runnable image. For the Dockerfile shown below, the assumption is that the src/ folder contains the Spring Boot Java code to be packaged and containerised.

 1# Create a transient container to build the application
 2FROM maven:3-openjdk-14-slim AS java-build
 3WORKDIR /app-build/
 4COPY pom.xml .
 5COPY src src
 6RUN mvn clean package
 7RUN java -Djarmode=layertools -jar target/*.jar extract
 8
 9# Prepare the final runtime image
10FROM openjdk:14-slim
11WORKDIR application
12COPY --from=java-build /app-build/dependencies/ ./
13COPY --from=java-build /app-build/spring-boot-loader ./
14COPY --from=java-build /app-build/snapshot-dependencies/ ./
15COPY --from=java-build /app-build/application/ ./
16EXPOSE 8080
17ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
  • The Dockerfile shown above builds a layered jar from the source code in a transient docker image called java-build.

  • In reality, to take advantage of the maven cache, what’s more efficient is to build the layered jar on the development machine, push and copy this over to the transient docker image and execute line 6 onwards as shown above. The Dockerfile shown above is more for completeness and reference.

  • Line 6 builds the jar file. Remember that a Layered Jar is built because it is specified in the earlier pom.xml file.

  • Line 7 extracts the jar file into its layer complaint folder structure.

  • Line 12 to 15 then copies the right folders and files into the runnable image docker file. The layers are written in the order they should be added to the Docker/OCI image:

    • dependencies: for any dependency whose version does not contain SNAPSHOT.

    • spring-boot-loader: for the jar loader classes.

    • snapshot-dependencies: for any dependency whose version contains SNAPSHOT.

    • application: for application classes and resources.

  • The order of the layers are important as it determines how likely previous layers are to be cached when part of the application changes.

  • More details can be found at https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/html/#build-image