Docker

How to set memory limit for your Java containers?

By default, Docker containers have no resource constraints and can use as much as the host’s kernel memory and scheduler allows but there could be scenarios where you have to set it explicitly. For example in case if your container consumes too much of the host machine’s memory and if the kernel detects that there is not enough memory to perform system functions, the kernel would take over and start killing processes to free up memory.

This could effectively bring your application down, in this post we are going to look at how to set the memory limit for containers and in specific how to address the challenges in case of running Java applications on Docker containers.

This quickstart assumes basic understanding of Docker concepts, please refer to earlier posts for understanding on Docker & how to install and containerize applications.

#1. Setting memory limit for individual containers

To limit memory for the container, we can use the --memory flag or just  -m during the startup of the container. For example, we set the memory limit of NGINX server to only 256 MB of RAM.

Set memory limit for container
Image – Set memory limit for container

To check the memory usage run docker stats command to check the same.

Run docker stats command to check memory usage
Image – Run docker stats command to check memory usage

If you get below warning message during docker run command then cgroups swapping is disabled by default. cgroups (control groups) is basically a Linux kernel setting through which you can set limits the user accounts for the resource usage (CPU, memory, disk I/O, network, etc.) for a collection of processes. By this, you can isolate the processes and their resource usage from each other.

WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.

For enabling swap, add/modify the below line in /etc/default/grub file and update grub configuration by sudo update-grub command.

GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"

For the changes to take effect, reboot the docker host machine.

If you have multiple containers and if you’re using compose to start the containers, follow the next section on how to set memory limits.

#2. Setting memory limit on Docker Compose

Below is the sample compose file for sample application with Angular as front end, Spring Boot as API, and  Postgres as Database.

version: '3'
services:
   ui:
    build:
      context: .
      dockerfile: UIDockerfile
    ports:
      - '4200:4200'
    networks:
      - samplenet
    links:
      - 'api:api'
  api:
   build:
     context: .
     dockerfile: AppDockerfile
   ports:
     - '8080:8080'
   depends_on:
    - db
    - rabbitmq
   networks:
    - samplenet
   links:
    - 'db:db'
  db: 
   build:
    context: .
    dockerfile: DBDockerfile
  volumes:
   - 'postgresdb:/var/lib/postgresql/data'
  environment:
   POSTGRES_USER: postgres
   POSTGRES_PASSWORD: postgres
   POSTGRES_DB: testdb
  ports:
   - '5432:5432'
  healthcheck:
   test:
     - CMD-SHELL
     - 'pg_isready -U postgres'
   interval: 10s
   timeout: 5s
   retries: 5
  networks:
   - samplenet
 rabbitmq:
  image: 'rabbitmq:3.5.3-management'
  container_name: rabbitmq2
  ports:
   - '5672:5672'
   - '15672:15672'
  networks:
   - samplenet
networks:
samplenet: null
volumes:
postgresdb: {}

Above compose file defines 4 services as below:

  • ui : this is for angular application,it exposes port 4200 on the container to port 4200 on the host machine.
  • api : this is for spring boot application,it exposes port 8080 on the container to port 8080 on the host machine. Note this is dependent on db and rabbitmq services.
  • db : this is for postgresdb application,it exposes port 5432 on the container to port 5432 on the host machine.
  • rabbitmq : Uses rabbitmq:3.5.3-management public image from Dockerhub registry and uses ports 5672,15672
  • All the above services uses samplenet network and
  • Persistent volume postgresdb definition is for db service
  • Added healthcheck section for db service to keep tab on the health of the database
  • depends_on denotes the service dependencies. When you start the services, compose would start the dependent services as well.

For example, if you want to set memory limits for particular container say api services. We can use mem_limit option like below:

 api:
   build:
     context: .
     dockerfile: AppDockerfile
   ports:
     - '8081:8081'
   depends_on:
     - db
     - rabbitmq
   networks:
     - samplenet
   mem_limit: 1024MB 
   links:
    - 'db:db'

Above option would work only if you’re using compose version 2.x starting from 3.x , compose is aligned with Swarm and you have to use resources option like below

 api:
   build:
     context: .
     dockerfile: AppDockerfile
   ports:
     - '8081:8081'
   depends_on:
     - db
     - rabbitmq
   networks:
     - samplenet
   resources:
    limits:
     memory:1024M 
   links:
    - 'db:db'

Having seen how to set memory limits for containers, the next step is to address the memory-related challenges with respect to Java applications running on containers. I am sure, most of you who have running applications on Docker have faced Out of memory exceptions or improper heap memory issues. In the next section, we are going to check out how to resolve those issues.

Does JVM know whether it is running on container?

For setting memory limits, like in any conventional Java application the maximum Java heap size can be set explicitly via JVM command-line option -Xmx. When running applications on Docker, you can use the same command-line option to restrict the memory but consider the scenario that you have not mentioned any of the JVM command-line options, when a Java application is using Java SE 8u121 and earlier on Docker container, the JVM would automatically use the underlying host configuration of memory i.e., JVM would not know that its running on container and more likely your Java process would get killed if its taking too much memory.

Luckily from Java SE 8u131 and in later editions, JVM is made Docker aware with respect to Docker memory limits and starts to adjust like its running on the bare machine. To enable this, there are few command-line options.

-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1

If the above JVM command-line options are specified and -Xmx is not specified, the JVM will look at the Linux cgroup configuration for setting up the memory limits. With -XX:MaxRAMFraction=1 set to 1, we are instructing JVM to use almost all the available memory as max heap.

In the compose file, you can pass these options as an environment variable like in the example below :

environment:
- "JAVA_OPTS=-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1"

Java Options in Docker Compose File
Image – Passing Java Options in Docker Compose File

Next steps, we will check if JVM is able to detect how much memory is available when running inside a Docker container for different JDKs. We are going to run the container with 1GB memory and measure how much Max Heap is set.

#1.Open JDK 8

OpenJDK8 Docker Memory Limits
Image – OpenJDK8 Docker Memory Limits

#2.Open JDK 9

 Open JDK 9 Memory limits
Image – Open JDK 9 Docker Memory limits

#3.Open JDK 10

For JDK 10, -XX:MaxRAM parameter is deprecated and JVM will correctly detect the memory.

Open JDK 10 Docker Memory Limits
Image – Open JDK 10 Docker Memory Limits

Congrats! Today we have learned how to set memory limit for your Java containers and also we have analyzed and set JVM environment variables for different JDKs.

Before we close off, let us look at key considerations while setting up memory.

Key considerations while setting memory

  1. Run performance/load tests to understand the memory requirements of your application and configure the memory limit values and they should be refined iteratively. If there are memory leaks, fix those ones before moving on to containers.
  2. Swap is slower and less performant than memory but it can provide a buffer if running out of memory.
  3. Use memory limits for your containers as explained above so that it does not overuse the host memory.
  4. Check container runtime metrics. Use docker stats command to analyze the container’s runtime metrics. It supports CPU, memory usage, memory limit, and network IO metrics.
  5. Analyze cgroup metrics to understand the internals. In the case of Linux Containers, control groups are used to track groups of processes. CPU, memory, and block I/O usage metrics are exposed via pseudo-filesystem like /sys/fs/cgroup.

Like this post? Don’t forget to share it!

Additional Resources:

Summary
How to set memory limit for your Java containers?
Article Name
How to set memory limit for your Java containers?
Description
In this post we are going to look at how to set the memory limit for containers and in specific how to address the challenges in case of running Java applications on Docker containers.
Author
Publisher Name
upnxtblog
Publisher Logo

Average Rating

5 Star
0%
4 Star
0%
3 Star
0%
2 Star
0%
1 Star
0%

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Previous post Monitoring Docker containers using Prometheus + cAdvisor + Grafana
school Next post Managing Innovation and Design Thinking Specialization from Coursera