Перейти к основному содержимому

Как настроить размер кучи Java внутри контейнера Docker

· 9 мин. чтения

1. Обзор

Когда мы запускаем Java в контейнере, мы можем захотеть настроить его, чтобы наилучшим образом использовать доступные ресурсы.

В этом руководстве мы увидим, как установить параметры JVM в контейнере, который запускает процесс Java. Хотя следующее относится к любому параметру JVM, мы сосредоточимся на общих флагах -Xmx и -Xms .

Мы также рассмотрим распространенные проблемы контейнеризации программ, работающих с определенными версиями Java, и способы установки флагов в некоторых популярных контейнеризованных Java-приложениях.

2. Настройки кучи по умолчанию в контейнерах Java

JVM довольно хорошо определяет подходящие настройки памяти по умолчанию .

В прошлом JVM не знала о памяти и ЦП, выделенных для контейнера . Итак, в Java 10 появился новый параметр: +UseContainerSupport (включен по умолчанию) для устранения основной причины , и разработчики перенесли исправление в Java 8 в 8u191 . JVM теперь вычисляет свою память на основе памяти, выделенной контейнеру.

Тем не менее, мы все еще можем захотеть изменить настройки по умолчанию в некоторых приложениях.

2.1. Автоматический расчет памяти

Когда мы не устанавливаем параметры -Xmx и -Xmx , JVM определяет размер кучи в соответствии со спецификациями системы .

Давайте посмотрим на этот размер кучи:

$ java -XX:+PrintFlagsFinal -version | grep -Ei "maxheapsize|maxram"

Это выводит:

openjdk version "15" 2020-09-15
OpenJDK Runtime Environment AdoptOpenJDK (build 15+36)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 15+36, mixed mode, sharing)
size_t MaxHeapSize = 4253024256 {product} {ergonomic}
uint64_t MaxRAM = 137438953472 {pd product} {default}
uintx MaxRAMFraction = 4 {product} {default}
double MaxRAMPercentage = 25.000000 {product} {default}
size_t SoftMaxHeapSize = 4253024256 {manageable} {ergonomic}

Здесь мы видим, что JVM устанавливает размер кучи примерно на 25% доступной оперативной памяти. В этом примере было выделено 4 ГБ в системе с 16 ГБ.

В целях тестирования создадим программу, которая выводит размеры кучи в мегабайтах:

public static void main(String[] args) {
int mb = 1024 * 1024;
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
long xmx = memoryBean.getHeapMemoryUsage().getMax() / mb;
long xms = memoryBean.getHeapMemoryUsage().getInit() / mb;
LOGGER.log(Level.INFO, "Initial Memory (xms) : {0}mb", xms);
LOGGER.log(Level.INFO, "Max Memory (xmx) : {0}mb", xmx);
}

Давайте поместим эту программу в пустой каталог, в файл с именем PrintXmxXms.java .

Мы можем протестировать его на нашем хосте, если у нас установлен JDK. В системе Linux мы можем скомпилировать нашу программу и запустить ее из терминала, открытого в этом каталоге:

$ javac ./PrintXmxXms.java
$ java -cp . PrintXmxXms

В системе с 16 ГБ ОЗУ вывод:

INFO: Initial Memory (xms) : 254mb
INFO: Max Memory (xmx) : 4,056mb

Теперь давайте попробуем это в некоторых контейнерах.

2.2. До JDK 8u191

Давайте добавим следующий файл Dockerfile в папку, содержащую нашу Java-программу:

FROM openjdk:8u92-jdk-alpine
COPY *.java /src/
RUN mkdir /app \
&& ls /src \
&& javac /src/PrintXmxXms.java -d /app
CMD ["sh", "-c", \
"java -version \
&& java -cp /app PrintXmxXms"]

Здесь мы используем контейнер, который использует более старую версию Java 8, которая предшествует поддержке контейнеров, доступной в более современных версиях. Построим его образ:

$ docker build -t oldjava .

Строка CMD в Dockerfile — это процесс, который по умолчанию запускается при запуске контейнера. Поскольку мы не указали флаги -Xmx или -Xms JVM, параметры памяти будут установлены по умолчанию.

Запустим этот контейнер:

$ docker run --rm -ti oldjava
openjdk version "1.8.0_92-internal"
OpenJDK Runtime Environment (build 1.8.0_92-...)
OpenJDK 64-Bit Server VM (build 25.92-b14, mixed mode)
Initial Memory (xms) : 198mb
Max Memory (xmx) : 2814mb

Давайте теперь ограничим память контейнера до 1 ГБ.

$ docker run --rm -ti --memory=1g oldjava
openjdk version "1.8.0_92-internal"
OpenJDK Runtime Environment (build 1.8.0_92-...)
OpenJDK 64-Bit Server VM (build 25.92-b14, mixed mode)
Initial Memory (xms) : 198mb
Max Memory (xmx) : 2814mb

Как мы видим, результат точно такой же. Это доказывает, что старая JVM не учитывает распределение памяти контейнера.

2.3. После JDK 8u130

С той же тестовой программой давайте воспользуемся более современной JVM 8, изменив первую строку Dockerfile :

FROM openjdk:8-jdk-alpine

Затем мы можем проверить это снова:

$ docker build -t newjava .
$ docker run --rm -ti newjava
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (IcedTea 3.12.0) (Alpine 8.212.04-r0)
OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)
Initial Memory (xms) : 198mb
Max Memory (xmx) : 2814mb

Здесь снова используется вся память узла докера для расчета размера кучи JVM. Однако, если мы выделим контейнеру 1 ГБ ОЗУ:

$ docker run --rm -ti --memory=1g newjava
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (IcedTea 3.12.0) (Alpine 8.212.04-r0)
OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)
Initial Memory (xms) : 16mb
Max Memory (xmx) : 247mb

На этот раз JVM рассчитала размер кучи на основе 1 ГБ оперативной памяти, доступной для контейнера.

Теперь мы понимаем, как JVM вычисляет свои значения по умолчанию и почему нам нужна современная JVM, чтобы получить правильные значения по умолчанию, давайте рассмотрим настройку параметров.

3. Настройки памяти в популярных базовых изображениях

3.1. OpenJDK и принять OpenJDK

Вместо жесткого кодирования флагов JVM непосредственно в команде нашего контейнера рекомендуется использовать переменную среды, такую как JAVA_OPTS . Мы используем эту переменную в нашем Dockerfile , но ее можно изменить при запуске контейнера:

FROM openjdk:8u92-jdk-alpine
COPY src/ /src/
RUN mkdir /app \
&& ls /src \
&& javac /src/com/foreach/docker/printxmxxms/PrintXmxXms.java \
-d /app
ENV JAVA_OPTS=""
CMD java $JAVA_OPTS -cp /app \
com.foreach.docker.printxmxxms.PrintXmxXms

Давайте теперь создадим образ:

$ docker build -t openjdk-java .

Мы можем выбрать параметры памяти во время выполнения, указав переменную среды JAVA_OPTS :

$ docker run --rm -ti -e JAVA_OPTS="-Xms50M -Xmx50M" openjdk-java
INFO: Initial Memory (xms) : 50mb
INFO: Max Memory (xmx) : 48mb

Следует отметить, что существует небольшая разница между параметром -Xmx и максимальным объемом памяти, сообщаемым JVM. Это связано с тем, что Xmx устанавливает максимальный размер пула распределения памяти, который включает кучу, оставшееся пространство сборщика мусора и другие пулы.

3.2. Томкэт 9

Контейнер Tomcat 9 имеет свои собственные сценарии запуска, поэтому для установки параметров JVM нам нужно работать с этими сценариями.

Сценарий bin/catalina.sh требует, чтобы мы установили параметры памяти в переменной окружения CATALINA_OPTS .

Давайте сначала создадим военный файл для развертывания на Tomcat.

Затем мы поместим его в контейнер с помощью простого Dockerfile , где мы объявим переменную среды CATALINA_OPTS :

FROM tomcat:9.0
COPY ./target/*.war /usr/local/tomcat/webapps/ROOT.war
ENV CATALINA_OPTS="-Xms1G -Xmx1G"

Затем мы создаем образ контейнера и запускаем его:

$ docker build -t tomcat .
$ docker run --name tomcat -d -p 8080:8080 \
-e CATALINA_OPTS="-Xms512M -Xmx512M" tomcat

Следует отметить, что когда мы запускаем это, мы передаем новое значение в CATALINA_OPTS. Однако если мы не предоставим это значение, мы зададим некоторые значения по умолчанию в строке 3 Dockerfile .

Мы можем проверить примененные параметры времени выполнения и убедиться, что наши параметры -Xmx и -Xms присутствуют:

$ docker exec -ti tomcat jps -lv
1 org.apache.catalina.startup.Bootstrap <other options...> -Xms512M -Xmx512M

4. Использование плагинов сборки

Maven и Gradle предлагают плагины, которые позволяют нам создавать образы контейнеров без Dockerfile . Сгенерированные изображения обычно можно параметризовать во время выполнения с помощью переменных среды.

Давайте рассмотрим несколько примеров.

4.1. Использование весенней загрузки

Начиная с Spring Boot 2.3, плагины Spring Boot Maven и Gradle могут создавать эффективный контейнер без Dockerfile .

С помощью Maven мы добавляем их в блок < configuration> в плагине spring-boot-maven:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>com.foreach.docker</groupId>
<artifactId>heapsizing-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- dependencies... -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>heapsizing-demo</name>
</image>
<!--
for more options, check:
https://docs.spring.io/spring-boot/docs/2.4.2/maven-plugin/reference/htmlsingle/#build-image
-->
</configuration>
</plugin>
</plugins>
</build>
</project>

Чтобы собрать проект, запустите:

$ ./mvnw clean spring-boot:build-image

В результате будет создан образ с именем <идентификатор-артефакта>:<версия>. В этом примере demo-app:0.0.1-SNAPSHOT . Под капотом Spring Boot используются Cloud Native Buildpacks в качестве базовой технологии контейнеризации.

Плагин жестко кодирует настройки памяти JVM. Однако мы все еще можем переопределить их, установив переменные среды JAVA_OPTS или JAVA_TOOL_OPTIONS:

$ docker run --rm -ti -p 8080:8080 \
-e JAVA_TOOL_OPTIONS="-Xms20M -Xmx20M" \
--memory=1024M heapsizing-demo:0.0.1-SNAPSHOT

Вывод будет примерно таким:

Setting Active Processor Count to 8
Calculated JVM Memory Configuration: [...]
[...]
Picked up JAVA_TOOL_OPTIONS: -Xms20M -Xmx20M
[...]

4.2. Использование JIB-файла Google

Как и плагин maven Spring Boot, Google JIB создает эффективные образы Docker без Dockerfile . Плагины Maven и Gradle настраиваются аналогичным образом. Google JIB также использует переменную среды JAVA_TOOL_OPTIONS в качестве механизма переопределения параметров JVM.

Мы можем использовать плагин Google JIB Maven в любой среде Java, способной генерировать исполняемые файлы jar. Например, его можно использовать в приложении Spring Boot вместо плагина spring-boot-maven для создания образов контейнеров:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<!-- dependencies, ... -->

<build>
<plugins>
<!-- [ other plugins ] -->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>2.7.1</version>
<configuration>
<to>
<image>heapsizing-demo-jib</image>
</to>
</configuration>
</plugin>
</plugins>
</build>
</project>

Образ создается с использованием цели maven jib:DockerBuild :

$ mvn clean install && mvn jib:dockerBuild

Теперь мы можем запустить его как обычно:

$ docker run --rm -ti -p 8080:8080 \
-e JAVA_TOOL_OPTIONS="-Xms50M -Xmx50M" heapsizing-demo-jib
Picked up JAVA_TOOL_OPTIONS: -Xms50M -Xmx50M
[...]
2021-01-25 17:46:44.070 INFO 1 --- [ main] c.foreach.docker.XmxXmsDemoApplication : Started XmxXmsDemoApplication in 1.666 seconds (JVM running for 2.104)
2021-01-25 17:46:44.075 INFO 1 --- [ main] c.foreach.docker.XmxXmsDemoApplication : Initial Memory (xms) : 50mb
2021-01-25 17:46:44.075 INFO 1 --- [ main] c.foreach.docker.XmxXmsDemoApplication : Max Memory (xmx) : 50mb

5. Вывод

В этой статье мы рассмотрели необходимость использования современной JVM для получения настроек памяти по умолчанию, которые хорошо работают в контейнере.

Затем мы рассмотрели рекомендации по установке параметров -Xms и -Xmx в пользовательских образах контейнеров и способы работы с существующими контейнерами приложений Java для установки в них параметров JVM.

Наконец, мы увидели, как использовать инструменты сборки для управления контейнеризацией приложения Java.

Как всегда, исходный код примеров доступен на GitHub .