- PVSM.RU - https://www.pvsm.ru -

Java и ограничения памяти в контейнерах: LXC, Docker и OpenVZ

Недавно была опубликована информативная статья Мэтта Уильямса о Java в Docker и существующих ограничениях памяти. Автор поднимает интересную тему о скрытой проблеме ограничения памяти, с которой пользователи могут столкнуться во время работы с контейнерами.

Большое количество репостов и лайков показывает, что данная тема довольно популярна среди Java-разработчиков.image

Поэтому хотелось бы более подробно проанализировать данную проблему и определить возможные пути ее решения.

Проблема

Мэтт описывает свое ночное «путешествие» в контейнере Docker со стандартным поведением памяти JVM. Он обнаружил, что ограничения RAM отображаются [1] некорректно внутри контейнера. В результате, приложение Java, или любое другое, видит общий объем ресурсов оперативной памяти, выделенной для всей хост-машины, а JVM не может указать, сколько ресурсов было предоставлено родительскому контейнеру для работы. Это приводит к ошибке OutOfMemoryError, вызванной неправильным поведением динамической памяти JVM в контейнере.

Фабио Кунг, из Heroku, подробно описал основные причины возникновения этой проблемы в своей недавней статье "Память внутри контейнеров Linux. Или почему в контейнере Linux не работает free и top? [2]"

Большинство инструментов Linux, предоставляющих метрики ресурсов системы, были созданы в то время, когда cgroups еще не существовали (например: free и top, как у procps). Они обычно читают метрики памяти из файловой системы proc: /proc/meminfo, /proc/vmstat, /proc/PID/smaps и других.

К сожалению, /proc/meminfo, /proc/vmstat и пр. не находятся в контейнерах. Это означает, что они не управляются cgroup. Они всегда отображают количество памяти хост-системы (физической или виртуальной машины) в целом, что является бесполезным для современных контейнеров Linux (Heroku, Docker и т.д.). Процессы внутри контейнера, необходимые для определения количества памяти, требуемой им для работы, не могут полагаться на free, top и др.; они подлежат ограничениям, налагаемыми cgroups и не могут использовать всю имеющуюся память хост-системы.

Автор подчеркивает важность видимости пределов реальной памяти. Это позволяет оптимизировать работу приложений и устранить проблемы внутри контейнеров: утечку памяти, использование подкачки, снижение производительности и т.д. Кроме того, в некоторых случаях полагаются на вертикальное масштабирование для оптимизации использования ресурсов внутри контейнеров путем автоматического изменения количества рабочих приложений, процессов или потоков. Вертикальное масштабирование обычно зависит от количества памяти, имеющейся в конкретном контейнере, поэтому ограничения должны быть видны внутри контейнера.

Решение

Сообщество “Открытые контейнеры” инициирует работы по улучшению runC [3] для замещения файлов /proc. LXC также создает файловую систему lxcfs [4], которая позволяет контейнерам иметь виртуализированные файловые системы cgroup и виртуализованный вид файлов /proc. Так что этот вопрос находится под пристальным вниманием системных администраторов контейнера. Я считаю, что упомянутые усовершенствования могут помочь решить эту проблему на базовом уровне.

Мы также столкнулись с той же проблемой в Jelastic и уже нашли способы ее решения для наших пользователей. Поэтому мы хотели бы рассказать детали реализации.

Прежде всего, давайте вернемся к мастеру установки Jelastic, выберем провайдера услуг [5] для тестовой учетной записи и создадим контейнер Java Docker [6] с заранее заданными ограничениями памяти — например, 8 клаудлет, которые эквивалентны 1 Гб оперативной памяти.

image

Перейдите к Jelastic SSH gate [7] (1), выберите ранее созданную тестовую среду (2), и выберите контейнер (3). Находясь внутри, можете проверить доступную память с помощью инструмента free (4).

image

Как мы можем видеть, ограничение памяти равно 1 Гб, определенному ранее. Теперь проверим инструмент top.

image

Все работает должным образом. Для двойной проверки, мы повторим тест Мэтта, связанного с вопросом эвристического поведения Java, описанного в его статье.

image

Как и следовало ожидать, мы получаем MaxHeapSize = 268435546 (~ 256 Мб), что составляет 1/4 от оперативной памяти контейнера в соответствии со стандартным поведением динамической памяти Java.

В чем секрет нашего решения? Конечно же, в правильном сочетании «ингредиентов». В нашем случае, это сочетание технологий OpenVZ и Docker, которое дает больший контроль с точки зрения безопасности и изоляции, а также возможность использовать такие функции как живая миграция [8] и гибернация контейнеров. Ниже приведена высокоуровневая схема контейнера Docker в Jelastic.

image

В OpenVZ каждый контейнер имеет виртуализированный вид псевдо-файловой системы /proc. В частности, /proc/meminfo внутри контейнера является «специальной» версией, показывающей информацию о каждом контейнере, а не хоста. Поэтому, когда такие инструменты, как top и free работают внутри контейнера, они показывают оперативную память и использование своп с ограничениями, специфичными для данного конкретного контейнера.

Стоит отметить, что своп внутри контейнеров не реальный, а виртуальный (отсюда и название всей технологии — VSwap [9]). Основная идея состоит в том, что когда контейнер с активированным VSwap превышает заданное ограничение оперативной памяти, некоторая часть из его памяти переходит в так называемый кэш свопа. Никакого реального перекачивания не происходит, а это означает, что нет необходимости в вводе/выводе, если, конечно же, нет недостатка глобальной оперативной памяти. Кроме того, контейнер, который использует VSwap, и имеющий превышение ограничения оперативной памяти, «наказывается» замедлением, изнутри это выглядит, как будто происходит реальная подкачка. Эта технология приводит к контролю памяти контейнера и использования свопа.

Такая реализация позволяет запускать Java и другие системы без необходимости адаптировать приложения под Jelastic PaaS [10]. Но если вы не используете Jelastic, возможным обходным путем будет указывать размер динамической памяти для виртуальной машины Java и не зависеть от эвристики (согласно советам Мэтта [1]). Для остальных языков требуется более глубокое исследование. Пожалуйста, свяжитесь с нами, если вы можете поделиться своим опытом в этом направлении, и мы будем рады расширить эту статью.

Автор: Jelastic

Источник [11]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/163155

Ссылки в тексте:

[1] ограничения RAM отображаются: http://matthewkwilliams.com/index.php/2016/03/17/docker-cgroups-memory-constraints-and-java-cautionary-tale/

[2] Память внутри контейнеров Linux. Или почему в контейнере Linux не работает free и top?: https://fabiokung.com/2014/03/13/memory-inside-linux-containers/

[3] улучшению runC: https://github.com/opencontainers/runc/issues/400

[4] файловую систему lxcfs: https://linuxcontainers.org/lxcfs/manpages/man1/lxcfs.1.html

[5] выберем провайдера услуг: https://jelastic.cloud/

[6] создадим контейнер Java Docker: https://docs.jelastic.com/dockers-management

[7] Jelastic SSH gate: https://docs.jelastic.com/ssh-access

[8] живая миграция: http://blog.jelastic.com/2015/12/14/live-containers-migration-across-data-centers-aws-and-azure-integration/

[9] VSwap: https://openvz.org/VSwap

[10] Jelastic PaaS: https://jelastic.com/

[11] Источник: https://habrahabr.ru/post/306474/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best