Снова здравствуйте. В преддверии старта курса «Разработчик Java» подготовили перевод еще одного небольшого материала.
Недавно у одного из пользователей Plumbr APM возникла странная проблема с аварийной остановкой docker-контейнера с кодом 137. Конфигурация была простейшая с несколькими вложенными контейнерами и виртуальными машинами, похожая на матрешку:
В ходе расследования проблемы мы нашли документацию Docker по этой теме. Стало понятно, что причина либо в ручной остановке контейнера, либо в нехватке памяти и последующим вмешательством oomkiller (Out-Of-Memory Killer).
Смотрим syslog и видим, что, действительно, был вызван oomkiller:
Как видно, java-процесс достиг лимита в 3145728 кБ (это около 3ГБ), что и вызвало остановку контейнера. Это довольно странно, так как сам docker был запущен с ограничением в 4 ГБ (в файле
Вы наверняка знаете, что JVM также накладывает свои ограничения на использование памяти. Хотя непосредственно docker был настроен на лимит в 4 ГБ, а JVM запущена с параметром
Пока мы не понимаем, что происходит. Docker должен разрешить использовать 4 ГБ. Так почему же OOMkiller сработал на 3 ГБ? Дальнейший поиск информации привел нас к тому, что есть еще одно ограничение памяти в ОС, которая была развернута на железе.
Скажите спасибо cgroups (control groups, контрольные группы). cgroups — это механизм ядра Linux для ограничения, контроля и учета использования ресурсов группами процессов. По сравнению с другими решениями (команда
В нашей ситуации cgroups ограничивали использование памяти в 3 ГБ (через
Исследование памяти и событий GC с помощью Plumbr показало, что большую часть времени JVM использовало около 700 МБ. Исключение было только непосредственно перед остановкой, когда происходил всплеск выделения памяти. За ним следовала длительная пауза GC. Итак, кажется, происходило следующее:
Понимая это, мы настроили все ограничения в 2.5 ГБ для docker и в 1.5 для java. После этого JVM могла обработать
Даже в такой простой ситуации, было три ограничения памяти:
Таким образом, когда вы сталкиваетесь с OOM killer, вам следует обратить внимание на все задействованные ограничения памяти.
Еще один вывод для разработчиков docker. Кажется, нет смысла разрешать запускать такие «матрешки», в которых ограничение памяти вложенного контейнера выше, чем ограничение cgroups. Простая проверка этого при запуске контейнера с отображением соответствующего предупреждения может сэкономить сотни часов отладки для ваших пользователей.
На этом все. Ждем вас на курсе.
Недавно у одного из пользователей Plumbr APM возникла странная проблема с аварийной остановкой docker-контейнера с кодом 137. Конфигурация была простейшая с несколькими вложенными контейнерами и виртуальными машинами, похожая на матрешку:
- свой железный сервер с Ubuntu;
- множество docker-контейнеров с Ubuntu внутри;
- Java Virtual Machine внутри docker-контейнеров.
В ходе расследования проблемы мы нашли документацию Docker по этой теме. Стало понятно, что причина либо в ручной остановке контейнера, либо в нехватке памяти и последующим вмешательством oomkiller (Out-Of-Memory Killer).
Смотрим syslog и видим, что, действительно, был вызван oomkiller:
[138805.608851] java invoked oom-killer: gfp_mask=0xd0, order=0, oom_score_adj=0
[138805.608887] [<ffffffff8116d20e>] oom_kill_process+0x24e/0x3b0
[138805.608916] Task in /docker/264b771811d88f4dbd3249a54261f224a69ebffed6c7f76c7aa3bc83b3aaaf71 killed as a result of limit of /docker/264b771811d88f4dbd3249a54261f224a69ebffed6c7f76c7aa3bc83b3aaaf71
[138805.608902] [<ffffffff8116da84>] pagefault_out_of_memory+0x14/0x90
[138805.608918] memory: usage 3140120kB, limit 3145728kB, failcnt 616038
[138805.608940] memory+swap: usage 6291456kB, limit 6291456kB, failcnt 2837
[138805.609043] Memory cgroup out of memory: Kill process 20611 (java) score 1068 or sacrifice child
Как видно, java-процесс достиг лимита в 3145728 кБ (это около 3ГБ), что и вызвало остановку контейнера. Это довольно странно, так как сам docker был запущен с ограничением в 4 ГБ (в файле
docker-compose
).Вы наверняка знаете, что JVM также накладывает свои ограничения на использование памяти. Хотя непосредственно docker был настроен на лимит в 4 ГБ, а JVM запущена с параметром
Xmx=3GB
. Это может еще больше запутать, но имейте в виду, что JVM может использовать больше памяти, чем было указано в -Xmx (см. статью про анализ использования памяти в JVM).Пока мы не понимаем, что происходит. Docker должен разрешить использовать 4 ГБ. Так почему же OOMkiller сработал на 3 ГБ? Дальнейший поиск информации привел нас к тому, что есть еще одно ограничение памяти в ОС, которая была развернута на железе.
Скажите спасибо cgroups (control groups, контрольные группы). cgroups — это механизм ядра Linux для ограничения, контроля и учета использования ресурсов группами процессов. По сравнению с другими решениями (команда
nice
или /etc/security/limits.conf
), cgroups предоставляют большую гибкость, так как они могут работать с (под)наборами процессов.В нашей ситуации cgroups ограничивали использование памяти в 3 ГБ (через
memory.limit_in_bytes
). У нас определённый прогресс!Исследование памяти и событий GC с помощью Plumbr показало, что большую часть времени JVM использовало около 700 МБ. Исключение было только непосредственно перед остановкой, когда происходил всплеск выделения памяти. За ним следовала длительная пауза GC. Итак, кажется, происходило следующее:
- Java-код, запущенный внутри JVM, пытается получить много памяти.
- JVM, проверив, что до ограничения Xmx в 3 ГБ еще далеко, просит выделить память у операционной системы.
- Docker также проверяет и видит, что его предел в 4 ГБ тоже не достигнут.
- Ядро ОС проверяет ограничение cgroup в 3 ГБ и убивает контейнер.
- JVM останавливается вместе с контейнером, прежде чем может обработать свой
OutOfMemoryError
.
Понимая это, мы настроили все ограничения в 2.5 ГБ для docker и в 1.5 для java. После этого JVM могла обработать
OutOfMemoryError
и бросить исключение OutOfMemoryError. Это позволило Plumbr сделать свою магию — получить снапшот памяти с соответствующими дампами стека и показать, что был один запрос к базе данных, который в определенной ситуации пытался загрузить целиком почти всю базу данных.Выводы
Даже в такой простой ситуации, было три ограничения памяти:
- JVM через параметр
-Xmx
- Docker через параметры в файле
docker-compose
- Операционная система через параметр
memory.limit_in_bytes у cgroups
Таким образом, когда вы сталкиваетесь с OOM killer, вам следует обратить внимание на все задействованные ограничения памяти.
Еще один вывод для разработчиков docker. Кажется, нет смысла разрешать запускать такие «матрешки», в которых ограничение памяти вложенного контейнера выше, чем ограничение cgroups. Простая проверка этого при запуске контейнера с отображением соответствующего предупреждения может сэкономить сотни часов отладки для ваших пользователей.
На этом все. Ждем вас на курсе.