[Примечание редактора] Java и Docker не являются естественными друзьями. Docker может устанавливать лимиты памяти и ЦП, которые Java не может определить автоматически. Используя идентификатор Java Xmx (утомительный/дублированный) или новый экспериментальный идентификатор JVM, мы можем это исправить.
Расширенная интеграция контейнера Docker с Java 10 — официальный блог DockerЭта проблема полностью решена в OpenJ9 и OpenJDK10 последней версии Java.
Итак, каковы преимущества упаковки нашего Java-приложения в JVM, а затем помещения всего этого в контейнер Docker? В большинстве случаев вы просто дублируете JVM и контейнеры Linux, не получая никакой выгоды, кроме траты памяти. Это так глупо.
Однако Docker может упаковать ваши программы, настройки, определенные JDK, настройки Linux и серверы приложений, а также другие инструменты вместе как единое целое. С точки зрения DevOps/Cloud такой полный контейнер имеет более высокий уровень инкапсуляции.
Например, если вы ограничиваете свой контейнер Docker только 100 МБ памяти, более старые версии Java не распознают это ограничение. Java не видит этого ограничения. JVM потребует больше памяти, намного превышающей этот предел. Если используется слишком много памяти, Docker примет меры и уничтожит процесс внутри контейнера! Процесс JAVA убит, что явно не то, что нам нужно.
Чтобы решить эту проблему, вам нужно указать максимальный лимит памяти для Java. В более старых версиях Java (до 8u131) вам нужно было установить
К счастью, теперь у нас есть лучший способ решить эту проблему. Начиная с Java 9 (8u131+) в JVM были добавлены следующие флаги:
Эти флаги заставляют JVM проверять
Начиная с Java 10 (см. тест ниже), эти флаги опыта включены по умолчанию, и их также можно использовать.
Начиная с Java 10, расчет доступных процессоров будет решать эту проблему по-другому (по умолчанию) (опять же,
Сначала создадим тестовое приложение, которое просто «ест» память и не освобождает ее.
Мы можем запустить контейнер Docker и запустить приложение, чтобы посмотреть, что произойдет.
мы компилируем и запускаем
Как и ожидалось, Docker убил наш Java-процесс. Не то, что мы хотели (!). Вы также можете увидеть вывод: Java считает, что ей еще нужно выделить много памяти.
Мы можем исправить это, предоставив Java максимальную память с помощью флага -Xmx:
После предоставления нашего собственного ограничения памяти процесс изящно останавливается, и JVM понимает предел, на котором он работает. Однако проблема в том, что теперь вы устанавливаете эти ограничения памяти дважды, один раз для Docker и один раз для JVM.
(На момент написания этой статьи версия этого образа OpenJDK Java — Java 8u144)
Далее компилируем и снова запускаем
Все еще есть та же проблема. Но теперь мы можем дать экспериментальные флаги, упомянутые выше, чтобы попробовать:
На этот раз мы не сообщали JVM, какой предел был, мы просто сказали JVM проверить правильность установки предела! Чувствую себя лучше.
Когда я тестировал его, он изначально не работал. На момент написания этой статьи образ AdoptAJDK OpenJDK10 идентичен
Протестировал код (даже вручную указав необходимые флаги):
Однако в этом
успех! Без каких-либо флагов Java 10 по-прежнему правильно определяет ограничение памяти Dockers.
пожалуйста в моемследующий пост в блогеУзнайте больше об OpenJ9 в .
Он работает быстро, имеет очень хорошее управление памятью и работает исключительно хорошо, часто экономя до 30-50% памяти для наших микросервисов. Это в значительной степени определяет приложение Spring Boot как «микро» со временем выполнения всего 100-200 МБ вместо 300+ МБ. Я планирую написать об этом статью в ближайшее время.
Но, к моему удивлению, OpenJ9 не
Добавляем флаги OpenJDK (флаги, которые OpenJ9 игнорирует):
Упс, JVM снова убит Docker.
Я очень надеюсь, что аналогичная опция будет добавлена в OpenJ9 в ближайшее время, так как я хочу запустить эту опцию в продакшене без необходимости дважды указывать max memory. Eclipse/IBM прилагает все усилия, чтобы исправить это, были подняты проблемы, и даже были отправлены PR для решения проблем.
В этом случае размер кучи ограничен памятью, выделенной для экземпляра Docker, что относится к более старым JVM и OpenJ9. Это, конечно, неправильно, потому что сам контейнер и другие части JVM за пределами кучи также используют память. Но вроде работает, видимо Docker в этом случае разрешительный. Может быть, какой-нибудь bash-бог сделает лучшую версию, которая вычитает часть байтов из других процессов.
В любом случае, не делайте этого, это может не сработать.
Последний ночной выпуск OpenJ9, в котором есть две вещи:
ТАДААА, в процессе ремонта!
Как ни странно, этот флаг не включен по умолчанию в OpenJ9, как в Java 10. Опять же: убедитесь, что вы хотите запустить Java в контейнере Docker.
Если вы используете Java в контейнере Docker, убедитесь, что вы установили ограничение памяти Docker и ограничение в JVM или что ваша JVM понимает эти ограничения.
Если вы не можете обновить версию Java, используйте
Для Java 8 и Java 9 обновите до последней версии и используйте:
Для Java 10 убедитесь, что он поддерживает UseContainerSupport (обновите до последней версии).
Для OpenJ9 (который я настоятельно рекомендую, может эффективно уменьшить объем памяти в производственных средах) теперь используйте
Оригинальная ссылка:Java and Docker, the limitations(перевести:kelvinji)
Расширенная интеграция контейнера 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, в котором есть две вещи:
- Еще один проблемный параметр PATH, который необходимо исправить в первую очередь.
- 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)