При работе одной из наших целевых платформ является мини-сервер с ограниченными ресурсами под управлением Linux (ядро 2.6.13, специальный дистрибутив на основе старого ядра Fedora). Приложение написано на Java (Sun JDK 1.6_04). Убийца Linux OOM настроен на завершение процессов, когда использование памяти превышает 160 МБ. Даже при высокой нагрузке наше приложение никогда не превышает 120 МБ, и вместе с некоторыми другими активными собственными процессами мы остаемся в пределах ограничений OOM.
Однако оказывается, что метод Java Runtime.getRuntime (). Exec (), канонический способ выполнения внешних процессов из Java, имеет особенно неудачная реализация в Linux, который заставляет порожденные дочерние процессы (временно) требовать тот же объем памяти, что и родительский процесс, поскольку адресное пространство копируется. В итоге наше приложение убивает убийца OOM, как только мы выполняем Runtime.getRuntime (). Exec ().
В настоящее время мы работаем над этим, имея отдельную собственную программу, выполняющую все внешние команды, и мы общаемся с этой программой через сокет. Это менее чем оптимально.
После публикация об этой проблеме в Интернете я получил некоторую обратную связь, указывающую на то, что этого не должно происходить в «более новых» версиях Linux, поскольку они реализуют метод posix fork () с использованием копирования при записи, что, предположительно, означает, что он будет копировать только те страницы, которые ему нужно изменить, когда он требуется вместо всего адресного пространства сразу.
Мои вопросы:
Безусловно, у нас не заканчивается физическая память, но какая-то часть Linux, кажется, думает, что это так.




1: Да. 2: Это делится на два этапа: Любой системный вызов, такой как fork (), привязывается glibc к ядру. Ядро часть системного вызова находится в kernel / fork.c 3: я не знаю. Но держу пари, что в вашем ядре он есть.
Убийца OOM срабатывает, когда возникает угроза нехватки памяти на 32-битных коробках. У меня никогда не было проблем с этим, но есть способы держать OOM в страхе. Эта проблема может быть связана с проблемой конфигурации OOM.
Поскольку вы используете приложение Java, вам следует подумать о переходе на 64-битный Linux. Это обязательно должно исправить. Большинство 32-битных приложений могут без проблем работать на 64-битном ядре, если установлены соответствующие библиотеки.
Вы также можете попробовать ядро PAE для 32-битной Fedora.
Почему переход на 64-битную JVM исправит это?
@AlastairMcCormack 32-битные системы Linux использовали разделение 3 ГБ / 1 ГБ для пользовательского пространства и ядра. Часть 1 ГБ зарезервирована для процессов пользовательского пространства, которым требуется доступ к пространству ядра через системные вызовы. Это ТОЛЬКО 128М. Я считаю, что нехватка памяти как в ядре 128 МБ, так и в пользовательском пространстве 3 ГБ вызовет OOM. Хотя в пространстве 3 ГБ могут быть другие процессы, JVM с большей вероятностью станет самым большим процессом и, следовательно, целью OOM. Кроме того, процесс разветвления, такой как OPs JVM, имеет более высокий балл OOM, поскольку каждый ребенок добавляет к баллу родителей. 64 Bit не имеет разделения памяти или какого-либо разделения ядра / пользователя.
Что ж, я лично сомневаюсь, что это правда, поскольку fork () Linux выполняется через копирование при записи, потому что Бог знает, когда (по крайней мере, в ядрах 2.2.x он был, а это было где-то в 199x).
Поскольку OOM killer считается довольно грубым инструментом, который, как известно, пропускает зажигание (например, он не обязательно убивает процесс, который фактически выделил большую часть памяти) и который следует использовать только в качестве последнего ответа, неясно, как мне, почему вы настроили его для стрельбы на 160M.
Если вы хотите наложить ограничение на выделение памяти, то вам в помощь ulimit, а не OOM.
Мой совет - оставить OOM в покое (или отключить его вообще), настроить ulimits и забыть об этой проблеме.
Спасибо за информацию, но, к сожалению, у нас нет контроля над платформой: аппаратное обеспечение, ОС и ее конфигурация в основном высечены из камня (мы, так сказать, «просто» развертываем на ней).
Да, это абсолютно верно даже для новых версий Linux (мы используем 64-битную Red Hat 5.2). У меня была проблема с медленно работающими подпроцессами около 18 месяцев, и я никогда не мог понять проблему, пока не прочитал ваш вопрос и не проверил тест, чтобы проверить его.
У нас есть блок на 32 ГБ с 16 ядрами, и если мы запустим JVM с такими настройками, как -Xms4g и -Xmx8g, и запустим подпроцессы с помощью Runtime.exec () с 16 потоками, мы не сможем запустить наш процесс быстрее, чем примерно 20 вызовов процесса в секунду.
Попробуйте это с помощью простой команды «date» в Linux примерно 10 000 раз. Если вы добавляете код профилирования, чтобы наблюдать, что происходит, он запускается быстро, но со временем замедляется.
Прочитав ваш вопрос, я решил попробовать понизить настройки памяти до -Xms128m и -Xmx128m. Теперь наш процесс выполняется со скоростью около 80 вызовов в секунду. Я изменил только настройки памяти JVM.
Кажется, он не забирает память таким образом, чтобы у меня когда-либо не хватало памяти, даже когда я пробовал это с 32 потоками. Просто необходимо каким-то образом выделить дополнительную память, что приводит к большим затратам на запуск (и, возможно, остановку).
В любом случае, похоже, что должна быть настройка, чтобы отключить это поведение Linux или, может быть, даже в JVM.
Это в значительной степени способ, которым * nix (и Linux) работали с незапамятных времен (или на заре mmus).
Чтобы создать новый процесс в * nixes, вы вызываете fork (). fork () создает копию вызывающего процесса со всеми его отображениями памяти, файловыми дескрипторами и т. д. Отображения памяти выполняются копированием при записи, поэтому (в оптимальных случаях) фактически не копируется никакая память, а только отображения. Следующий вызов exec () заменяет текущее отображение памяти сопоставлением нового исполняемого файла. Итак, fork () / exec () - это способ создания нового процесса, и это то, что использует JVM.
Предостережение заключается в том, что с огромными процессами в загруженной системе родительский элемент может продолжать работать некоторое время, прежде чем дочерний exec () вызовет копирование огромного объема памяти из-за копирования при записи. В виртуальных машинах память можно много перемещать, чтобы упростить сборщик мусора, который производит еще большее копирование.
«Обходной путь» состоит в том, чтобы сделать то, что вы уже сделали, создать внешний легкий процесс, который заботится о создании новых процессов - или использовать более легкий подход, чем fork / exec, для создания процессов (чего в Linux нет - и в любом случае требуется изменение самого jvm). Posix определяет функцию posix_spawn (), которая теоретически может быть реализована без копирования отображения памяти вызывающего процесса, но в Linux это не так.
Я бы назвал этот легкий процесс ssh и использовал jcraft.com/jsch для подключения к localhost.
Это виртуальный, а не память физический, которая необходима между вызовом fork () и последующим exec (). Я очень сомневаюсь, что у вас заканчивается виртуальная память, учитывая размер адресного пространства по сравнению с вашим пределом физической памяти.