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 .