Те вещи, которые ограничены Java и Docker

Java задняя часть JVM Docker
[Примечание редактора] Java и Docker не являются естественными друзьями. Docker может устанавливать лимиты памяти и ЦП, которые Java не может определить автоматически. Используя идентификатор Java Xmx (утомительный/дублированный) или новый экспериментальный идентификатор JVM, мы можем это исправить.

Расширенная интеграция контейнера Docker с Java 10 — официальный блог DockerЭта проблема полностью решена в OpenJ9 и OpenJDK10 последней версии Java.

Несоответствие в виртуализации

Сочетание Java и Docker не является идеальным сочетанием, и поначалу оно довольно далеко от идеального сочетания. Для начала вся идея JVM заключается в том, что виртуальная машина может делать программы независимыми от базового оборудования.

Итак, каковы преимущества упаковки нашего Java-приложения в JVM, а затем помещения всего этого в контейнер Docker? В большинстве случаев вы просто дублируете JVM и контейнеры Linux, не получая никакой выгоды, кроме траты памяти. Это так глупо.

Однако Docker может упаковать ваши программы, настройки, определенные JDK, настройки Linux и серверы приложений, а также другие инструменты вместе как единое целое. С точки зрения DevOps/Cloud такой полный контейнер имеет более высокий уровень инкапсуляции.

Проблема 1: Память

По сей день подавляющее большинство производственных приложений все еще используют Java 8 (или более раннюю версию), и это может быть проблематично. Java 8 (до обновления 131) плохо работает с Docker. Проблема в том, что на вашем компьютере объем доступной памяти и ЦП для JVM не соответствует объему доступной памяти и ЦП, который позволяет вам использовать Docker.

Например, если вы ограничиваете свой контейнер Docker только 100 МБ памяти, более старые версии Java не распознают это ограничение. Java не видит этого ограничения. JVM потребует больше памяти, намного превышающей этот предел. Если используется слишком много памяти, Docker примет меры и уничтожит процесс внутри контейнера! Процесс JAVA убит, что явно не то, что нам нужно.

Чтобы решить эту проблему, вам нужно указать максимальный лимит памяти для Java. В более старых версиях Java (до 8u131) вам нужно было установить-Xmxчтобы ограничить размер кучи. Это кажется неправильным, и вы не хотите определять эти ограничения дважды, и вы действительно не хотите определять их в своем контейнере.

К счастью, теперь у нас есть лучший способ решить эту проблему. Начиная с Java 9 (8u131+) в JVM были добавлены следующие флаги:
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap

Эти флаги заставляют JVM проверятьcgroupконфигурация, Docker выполняется черезcgroupдля достижения максимальной настройки памяти. Теперь, если ваше приложение достигает предела, установленного Docker (скажем, 500 МБ), JVM увидит этот предел. JVM попытается выполнить операции GC. Если лимит памяти все же превышен, JVM делает то, что должна, бросаяOutOfMemoryException. То есть JVM умеет видеть эти настройки из Docker.

Начиная с Java 10 (см. тест ниже), эти флаги опыта включены по умолчанию, и их также можно использовать.-XX:+UseContainerSupportвключить (вы можете установить-XX:-UseContainerSupportзапретить эту практику).

Проблема 2: ЦП

Второй вопрос похож, но он связан с процессором. Короче говоря, JVM будет смотреть на оборудование и определять количество процессоров. Это оптимизирует время выполнения для использования этих процессоров. Но опять же, вот еще одно несоответствие, Docker может не позволить вам использовать все эти процессоры. К сожалению, это не было исправлено в Java 8 или Java 9, но было исправлено в Java 10.

Начиная с Java 10, расчет доступных процессоров будет решать эту проблему по-другому (по умолчанию) (опять же,UseContainerSupport).

Тесты обработки памяти Java и Docker

В качестве забавного упражнения давайте проверим и протестируем, как Docker справляется с нехваткой памяти с несколькими разными версиями/флагами JVM или даже с разными JVM.

Сначала создадим тестовое приложение, которое просто «ест» память и не освобождает ее.
java
import java.util.ArrayList;
import java.util.List;

public class MemEat {
public static void main(String[] args) {
    List l = new ArrayList<>();
    while (true) {
        byte b[] = new byte[1048576];
        l.add(b);
        Runtime rt = Runtime.getRuntime();
        System.out.println( "free memory: " + rt.freeMemory() );
    }
}
} 

Мы можем запустить контейнер Docker и запустить приложение, чтобы посмотреть, что произойдет.

Тест 1: Java 8u111

Во-первых, мы начнем с контейнера с более старой версией Java 8 (обновление 111).
shell
docker run -m 100m -it java:openjdk-8u111 /bin/bash

мы компилируем и запускаемMemEat.javaдокумент:
shell
javac MemEat.java

java MemEat
...
free memory: 67194416
free memory: 66145824
free memory: 65097232
Killed

Как и ожидалось, Docker убил наш Java-процесс. Не то, что мы хотели (!). Вы также можете увидеть вывод: Java считает, что ей еще нужно выделить много памяти.

Мы можем исправить это, предоставив Java максимальную память с помощью флага -Xmx:
shell
javac MemEat.java

java -Xmx100m MemEat
...
free memory: 1155664
free memory: 1679936
free memory: 2204208
free memory: 1315752
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at MemEat.main(MemEat.java:8)

После предоставления нашего собственного ограничения памяти процесс изящно останавливается, и JVM понимает предел, на котором он работает. Однако проблема в том, что теперь вы устанавливаете эти ограничения памяти дважды, один раз для Docker и один раз для JVM.

Тест 2: Java 8u144

Как упоминалось ранее, с добавлением новых флагов для решения проблемы JVM теперь может следовать настройкам, предоставленным Docker. Мы можем протестировать его с более новой версией JVM.
shell
docker run -m 100m -it adoptopenjdk/openjdk8 /bin/bash

(На момент написания этой статьи версия этого образа OpenJDK Java — Java 8u144)

Далее компилируем и снова запускаемMemEat.javaфайл без каких-либо флагов:
shell
javac MemEat.java

java MemEat
...
free memory: 67194416
free memory: 66145824
free memory: 65097232
Killed

Все еще есть та же проблема. Но теперь мы можем дать экспериментальные флаги, упомянутые выше, чтобы попробовать:
shell
javac MemEat.java
java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap MemEat
...
free memory: 1679936
free memory: 2204208
free memory: 1155616
free memory: 1155600
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at MemEat.main(MemEat.java:8)

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

Тест 3: Java 10u23

Некоторые люди упоминали в комментариях и на Reddit, что Java 10 все исправляет, делая экспериментальный флаг новым значением по умолчанию. Это поведение можно отключить, отключив этот флаг:-XX:-UseContainerSupport.

Когда я тестировал его, он изначально не работал. На момент написания этой статьи образ AdoptAJDK OpenJDK10 идентиченjdk-10+23Упакуйте вместе. Эта JVM, очевидно, все еще не понимаетUseContainerSupportфлаг, процесс все еще убит Docker.
shell
docker run -m 100m -it adoptopenjdk/openjdk10 /bin/bash

Протестировал код (даже вручную указав необходимые флаги):
shell
javac MemEat.java

java MemEat
...
free memory: 96262112
free memory: 94164960
free memory: 92067808
free memory: 89970656
Killed

java -XX:+UseContainerSupport MemEat

Unrecognized VM option 'UseContainerSupport'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

Тест четвертый: Java 10u46 (ночной)

Я решил попробовать последнюю версию AdoptAJDK OpenJDK 10.nightlyПостроить. Версия, которую он содержит, — это Java 10+46, а не Java 10+23.
shell
docker run -m 100m -it adoptopenjdk/openjdk10:nightly /bin/bash

Однако в этомngithlyВ сборке была проблема, из-за которой экспортированный PATH указывал на старый каталог Java 10+23 вместо 10+46, нам нужно это исправить.
shell
export PATH=$PATH:/opt/java/openjdk/jdk-10+46/bin/

javac MemEat.java

java MemEat
...
free memory: 3566824
free memory: 2796008
free memory: 1480320
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at MemEat.main(MemEat.java:8)

успех! Без каких-либо флагов Java 10 по-прежнему правильно определяет ограничение памяти Dockers.

Тест пятый: OpenJ9

Я также недавно экспериментировал с OpenJ9, эта бесплатная альтернатива JVM была открыта из IBM J9 и теперь поддерживается Eclipse.

пожалуйста в моемследующий пост в блогеУзнайте больше об OpenJ9 в .

Он работает быстро, имеет очень хорошее управление памятью и работает исключительно хорошо, часто экономя до 30-50% памяти для наших микросервисов. Это в значительной степени определяет приложение Spring Boot как «микро» со временем выполнения всего 100-200 МБ вместо 300+ МБ. Я планирую написать об этом статью в ближайшее время.

Но, к моему удивлению, OpenJ9 неcgroupОпция флага ограничения памяти (backported). Если мы применим предыдущий тестовый пример к последней сборке AdoptAJDK OpenJDK 9 + OpenJ9:
shell
docker run -m 100m -it adoptopenjdk/openjdk9-openj9 /bin/bash

Добавляем флаги OpenJDK (флаги, которые OpenJ9 игнорирует):
shell
java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap MemEat
...
free memory: 83988984
free memory: 82940400
free memory: 81891816
Killed

Упс, JVM снова убит Docker.

Я очень надеюсь, что аналогичная опция будет добавлена ​​в OpenJ9 в ближайшее время, так как я хочу запустить эту опцию в продакшене без необходимости дважды указывать max memory. Eclipse/IBM прилагает все усилия, чтобы исправить это, были подняты проблемы, и даже были отправлены PR для решения проблем.

Обновление: (взломать не рекомендуется)

Немного уродливый/хакерский способ исправить это — использовать следующие комбинированные флаги:
shell
java -Xmx`cat /sys/fs/cgroup/memory/memory.limit_in_bytes` MemEat
...
free memory: 3171536
free memory: 2127048
free memory: 2397632
free memory: 1344952
JVMDUMP039I Processing dump event "systhrow", detail "java/lang/OutOfMemoryError" at 2018/05/15 14:04:26 - please wait.
JVMDUMP032I JVM requested System dump using '//core.20180515.140426.125.0001.dmp' in response to an event
JVMDUMP010I System dump written to //core.20180515.140426.125.0001.dmp
JVMDUMP032I JVM requested Heap dump using '//heapdump.20180515.140426.125.0002.phd' in response to an event
JVMDUMP010I Heap dump written to //heapdump.20180515.140426.125.0002.phd
JVMDUMP032I JVM requested Java dump using '//javacore.20180515.140426.125.0003.txt' in response to an event
JVMDUMP010I Java dump written to //javacore.20180515.140426.125.0003.txt
JVMDUMP032I JVM requested Snap dump using '//Snap.20180515.140426.125.0004.trc' in response to an event
JVMDUMP010I Snap dump written to //Snap.20180515.140426.125.0004.trc
JVMDUMP013I Processed dump event "systhrow", detail "java/lang/OutOfMemoryError".
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at MemEat.main(MemEat.java:8)

В этом случае размер кучи ограничен памятью, выделенной для экземпляра Docker, что относится к более старым JVM и OpenJ9. Это, конечно, неправильно, потому что сам контейнер и другие части JVM за пределами кучи также используют память. Но вроде работает, видимо Docker в этом случае разрешительный. Может быть, какой-нибудь bash-бог сделает лучшую версию, которая вычитает часть байтов из других процессов.

В любом случае, не делайте этого, это может не сработать.

Тест 6: OpenJ9 (ночной)

Кто-то предложил использовать последнюю версию OpenJ9nightlyВерсия.
shell
docker run -m 100m -it adoptopenjdk/openjdk9-openj9:nightly /bin/bash

Последний ночной выпуск OpenJ9, в котором есть две вещи:
  1. Еще один проблемный параметр PATH, который необходимо исправить в первую очередь.
  2. JVM поддерживает новый флаг UseContainerSupport (как и в Java 10).

shell
export PATH=$PATH:/opt/java/openjdk/jdk-9.0.4+12/bin/

javac MemEat.java

java -XX:+UseContainerSupport MemEat
...
free memory: 5864464
free memory: 4815880
free memory: 3443712
free memory: 2391032
JVMDUMP039I Processing dump event "systhrow", detail "java/lang/OutOfMemoryError" at 2018/05/15 21:32:07 - please wait.
JVMDUMP032I JVM requested System dump using '//core.20180515.213207.62.0001.dmp' in response to an event
JVMDUMP010I System dump written to //core.20180515.213207.62.0001.dmp
JVMDUMP032I JVM requested Heap dump using '//heapdump.20180515.213207.62.0002.phd' in response to an event
JVMDUMP010I Heap dump written to //heapdump.20180515.213207.62.0002.phd
JVMDUMP032I JVM requested Java dump using '//javacore.20180515.213207.62.0003.txt' in response to an event
JVMDUMP010I Java dump written to //javacore.20180515.213207.62.0003.txt
JVMDUMP032I JVM requested Snap dump using '//Snap.20180515.213207.62.0004.trc' in response to an event
JVMDUMP010I Snap dump written to //Snap.20180515.213207.62.0004.trc
JVMDUMP013I Processed dump event "systhrow", detail "java/lang/OutOfMemoryError".
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

ТАДААА, в процессе ремонта!

Как ни странно, этот флаг не включен по умолчанию в OpenJ9, как в Java 10. Опять же: убедитесь, что вы хотите запустить Java в контейнере Docker.

в заключении

Короче говоря: следите за несоответствиями лимита ресурсов. Проверьте настройки памяти и флаги JVM, ничего не предполагайте.

Если вы используете Java в контейнере Docker, убедитесь, что вы установили ограничение памяти Docker и ограничение в JVM или что ваша JVM понимает эти ограничения.

Если вы не можете обновить версию Java, используйте-XmxУстановите свои собственные ограничения.

Для Java 8 и Java 9 обновите до последней версии и используйте:
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap

Для Java 10 убедитесь, что он поддерживает UseContainerSupport (обновите до последней версии).

Для OpenJ9 (который я настоятельно рекомендую, может эффективно уменьшить объем памяти в производственных средах) теперь используйте-XmxСтавьте лимиты, но скоро появится поддержкаUseContainerSupportвариант логотипа.

Оригинальная ссылка:Java and Docker, the limitations(перевести:kelvinji)