Java и Docker. Часть 1

Сейчас Java всё чаще используется в контейнерах, обычно в Docker. Формально в Docker можно положить любую версию Java, и это даже как-то будет работать. Однако когда вышла Java 8, про работу в Docker ещё никто и не думал. В результате Java-приложение хоть и работает, но не понимает, что находится в ограниченном окружении. Не видит ограничение Docker-а на использование процессора и памяти.

Это приводит к весьма неприятным последствиям. Например, приложение не учитывает ограничение на процессоры и может создавать пулы с большим количеством потоков или не видит ограничение по памяти и неожиданно закрывается Docker-ом при превышении лимита.

Работа в контейнере начала улучшаться, начиная c Java 9 (есть и обратные порты в Java 8), в релизе Java 10 был добавлен новый параметр UseContainerSupport. Этот параметр сообщает Java-приложению, что оно запущено в контейнере и необходимо учитывать наложенные ограничения, а не использовать все ресурсы операционной системы без ограничений.

Давайте посмотрим, как это работает. По умолчанию UseContainerSupport включён, т. е. приложение видит ограничения контейнера. Положим в контейнер простое приложение, которое выводит количество доступных процессоров:

Runtime.getRuntime().availableProcessors().

Соберём образ и запустим контейнер такой командой:

docker run --memory=100m --cpus 2 java-docker

Вводим ограничение для контейнера: память 100 Мб и 2 процессора. В контейнере запускается такая команда:

CMD java -XX:+PrintFlagsFinal -version | grep MaxHeapSize

Получаем результат:

size_t MaxHeapSize    = 52428800

Это примерно 52 Мб, т. е. примерно половина лимита контейнера.Теперь посмотрим на доступные процессоры:

CMDjava -jartestJavaAppl.jar

Возвращает:

Runtime.getRuntime().availableProcessors(): 2

Видим, что Java-приложение учитывает в своей работе лимиты, заданные контейнером. Давайте повторим эксперимент, но на этот раз отключим режим UseContainerSupport — это фактически вернёт нас во времена Java 8. Ограничения контейнера те же. Смотрим память:

CMD java -XX:-UseContainerSupport -XX:+PrintFlagsFinal -version | grep MaxHeapSize

Получаем:

size_tMaxHeapSize   = 8415870976

Java не видит ограничение в 100 Мб и пытается использовать память «на всю котлету». А теперь смотрим, что происходит с процессором:

CMD java -XX:-UseContainerSupport -jar testJavaAppl.jar

Получаем:

Runtime.getRuntime().availableProcessors(): 12

И по процессорам Java пытается использовать все системные ресурсы, игнорируя ограничения Docker-а. Очевидно, что для корректной работы Java-приложения в Docker-е желательно использовать версию Java 10 или новее.

В следующей заметке затронем другие аспекты работы Java-приложения в контейнере. А более подробно посмотрим на это дело на открытом вебинаре «Java и Docker».