Docker Meets Java 10

Docker Support in Java 10

Java on Docker should no longer suck!

Docker is a really popular technology that is used by developers for containerising JVM-based applications that offers consistent environments for development and deployment and proper isolation between applications when deployed.

So what is the Problem?

The Java JVM, until now, didn’t provide support to understand that it’s running inside a container — and that it has some resources that are memory and CPU restricted. Because of that, you can’t let the JVM ergonomics make the decision by itself regarding the maximum heap size.

We tend to think that containers are just like Virtual Machines(VM) where we can completely define a number of virtual CPUs and virtual memory.

Containers are more similar to isolation mechanisms where the resources (CPU, memory, filesystem, network, etc.) for one process are isolated from another. This isolation is possible due to a Linux kernel feature called cgroups.

However, some applications that collect information from the execution environment have been implemented before the existence of cgroups. Tools like ‘top‘, ‘free‘, ‘ps‘, and even the JVM is not optimized for executing inside a container, a highly-constrained Linux process. Let’s check it out.

The Solution(Docker Support in Java 10:

Several changes targeted at improving the Java execution experience when running in docker containers have been integrated into JDK 10.

Key Improvements provided in Java 10 that fix the above problems related to running JVM inside a Docker Container are discussed below.

Many applications that run in a Java Virtual Machine (JVM), including data services such as Apache Spark and Kafka and traditional enterprise applications, are run in containers.

Until recently, running the JVM in a container presented problems with memory and cpu sizing and usage that led to performance loss. This was because Java didn’t recognize that it was running in a container.

With the release of Java 10, the JVM now recognizes constraints set by container control groups (cgroups). Both memory and cpu constraints can be used to manage Java applications directly in containers, these include:

  • adhering to memory limits set in the container
  • setting available cpus in the container
  • setting cpu constraints in the container

Supported Platforms: 
Java 10 improvements are realized in both Docker for Mac or Windows and Docker Enterprise Edition environments including Community Editions of Linux.

Some Key Improvements part of Java 10 for a Better Docker Support:

  1. Improve docker container detection and resource configuration usage
    (JDK-8146115)

This change eliminates the need to use experimental options for enabling the VM to size the Java heap based on available container memory. The change adds cgroup detection and allows the JVM to configure it’s memory and cpu usage based on container settings. It will not only examine cpusets but will also respond to cpu shares, quotas and periods.

This change also adds a new JVM option to allow users to select the number of processors that the JVM will use internally to determine how many threads to create for it’s subsystems.

-XX:ActiveProcessorCount={xxx}

2. Allow more flexibility in selecting Heap % of available RAM
(JDK-8186309)

Prior to this change, the JVM only provided the ability to select a ratio of container memory. This change adds the ability to select a percentage of container memory to be used for the maximum heap size.

3. jcmd attach in linux should be relative to /proc/pid/root and namespace aware
(JDK-8179498)

This change corrects a problem not allowing a host JVM to attach to a Java process running inside of a docker container.

Container Memory Limits

Until Java 9 the JVM did not recognize memory or cpu limits set by the container using flags. In Java 10, memory limits are automatically recognized and enforced.

Java defines a server class machine as having 2 CPUs and 2GB of memory and the default heap size is ¼ of the physical memory. For example, a Docker Enterprise Edition installation has 2GB of memory and 4 CPUs. Compare the difference between containers running Java 8 and Java 10. First, Java 8:

docker container run -it -m512 --entrypoint bash openjdk:latest
$ docker-java-home/bin/java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
uintx MaxHeapSize := 524288000 {product}
openjdk version "1.8.0_162"

The max heap size is 512M or ¼ of the 2GB set by the Docker EE installation instead of the limit set on the container to 512M. In comparison, running the same commands on Java 10 shows that the memory limit set in the container is fairly close to the expected 128M:

docker container run -it -m512M --entrypoint bash openjdk:10-jdk
$ docker-java-home/bin/java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
size_t MaxHeapSize = 134217728 {product} {ergonomic}
openjdk version "10" 2018-03-20

Setting Available CPUs

By default, each container’s access to the host machine’s CPU cycles is unlimited. Various constraints can be set to limit a given container’s access to the host machine’s CPU cycles. Java 10 recognizes these limits:

docker container run -it --cpus 2 openjdk:10-jdk
jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 2

All CPUs allocated to Docker EE get the same proportion of CPU cycles. The proportion can be modified by changing the container’s CPU share weighting relative to the weighting of all other running containers. The proportion will only apply when CPU-intensive processes are running. When tasks in one container are idle, other containers can use the leftover CPU time. The actual amount of CPU time will vary depending on the number of containers running on the system. These can be set in Java 10:

docker container run -it --cpu-shares 2048 openjdk:10-jdk
jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 2

The cpuset constraint sets which CPUs allow execution in Java 10.

docker run -it --cpuset-cpus="1,2,3" openjdk:10-jdk
jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 3

Allocating memory and CPU

With Java 10, container settings can be used to estimate the allocation of memory and CPUs needed to deploy an application. Let’s assume that the memory heap and CPU requirements for each process running in a container has already been determined and JAVA_OPTS set. For example, if you have an application distributed across 10 nodes; five nodes require 512Mb of memory with 1024 CPU-shares each and another five nodes require 256Mb with 512 CPU-shares each. Note that 1 CPU share proportion is represented by 1024.

For memory, the application would need 5Gb allocated at minimum.

512Mb x 5 = 2.56 Gb

256Mb x 5 = 1.28 Gb

The application would require 8 CPUs to run efficiently.

1024 x 5 = 5 CPUs

512 x 5 = 3 CPUs

Best practice suggests profiling the application to determine the memory and CPU allocations for each process running in the JVM. However, Java 10 removes the guesswork when sizing containers to prevent out of memory errors in Java applications as well allocating sufficient CPU to process work loads.

References: https://blog.docker.com/2018/04/improved-docker-container-integration-with-java-10/

About the Author: Srikanth Jallapuram shares amongst other things a very high passion for Enterprise Java Architecture especially the latest versions of Java 8 and Java 10 and Cloud based Serverless Computing including Microservices, Containers based End-to-End Application development.

To get your Java 10 Applications developed, reach out at 
http://www.technovature.com/microservices.html or info@technovature.com

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.