Из какой версии ядра Linux/libc является Java Runtime.exec() безопасным в отношении памяти?
На работе одна из наших целевых платформ - это ограниченный ресурсами мини-сервер под управлением Linux (ядро 2.6.13, настраиваемый дистрибутив на базе старого Fedora Core). Приложение написано на Java (Sun JDK 1.6_04). Убийца Linux OOM настроен на то, чтобы убивать процессы, когда использование памяти превышает 160 МБ. Даже при высокой нагрузке наше приложение никогда не выходит за 120 Мбайт и вместе с некоторыми другими активными процессами, которые активны, мы остаемся в пределах предела OOM.
Однако, оказывается, что метод Java Runtime.getRuntime(). exec(), канонический способ выполнения внешних процессов с Java, имеет в частности неудачная реализация в Linux, которая вызывает порождаемые дочерние процессы (временно), требует того же объема памяти, что и родительский процесс, поскольку адресное пространство копируется. Конечным результатом является то, что наше приложение будет убито убийцей OOM, как только мы выполним Runtime.getRuntime(). Exec().
В настоящее время мы работаем над этим, имея отдельную собственную программу, выполняющую все внешние команды, и мы общаемся с этой программой через сокет. Это менее оптимально.
После публикация об этой проблеме в Интернете У меня появилась некоторая обратная связь, указывающая на то, что это не должно происходить в "новых" версиях Linux, поскольку они реализуют posix fork(), используя copy-on-write, предположительно означает, что он будет копировать только те страницы, которые необходимо изменить, когда это необходимо, вместо всего адресного пространства.
Мои вопросы:
- Это правда?
- Это что-то в ядре, реализация libc или где-то еще?
- В какой версии ядра /libc/what есть копирование на запись для fork()?
Ответы
Ответ 1
Это в значительной степени способ, с помощью которого nix (и linux) работали с самого начала (или atleat the dawn of mmus).
Чтобы создать новый процесс на * nixes, вы вызываете fork(). fork() создает копию вызывающего процесса со всеми его сопоставлениями памяти, файловыми дескрипторами и т.д. Сопоставления памяти выполняются copy-on-write, поэтому (в оптимальных случаях) копия памяти фактически не копируется, а только сопоставления. Следующий вызов exec() заменяет текущее сопоставление памяти с новым исполняемым файлом. Таким образом, fork()/exec() - это способ создания нового процесса и того, что использует JVM.
Предостережение с огромными процессами в загруженной системе, родительский элемент может продолжать работать некоторое время, прежде чем дочерний exec() приведет к копированию огромного объема памяти из-за копирования на запись. В виртуальных машинах память может перемещаться по множеству, чтобы облегчить сборщик мусора, который производит еще большее копирование.
"Обходной путь" заключается в том, чтобы сделать то, что вы уже сделали, создать внешний легкий процесс, который заботится о появлении новых процессов, или использовать более легкий подход, чем fork/exec для создания процессов (в том, что linux не имеет - и в любом случае потребует изменения самого jvm). Posix указывает функцию posix_spawn(), которая теоретически может быть реализована без копирования картографирования памяти вызывающего процесса, но на Linux это не так.
Ответ 2
Я лично сомневаюсь, что это правда, поскольку Linux fork() выполняется с помощью copy-on-write, так как Бог знает, когда (по крайней мере, у ядер 2.2.x это было, и это было где-то в 199x).
Поскольку убийца OOM считается довольно грубым инструментом, который, как известно, пропускает пропуски (fe, он не требует уничтожения процесса, который фактически выделял большую часть памяти) и который должен использоваться только как последний resport, это непонятно, почему вы настроили его на 160M.
Если вы хотите наложить ограничение на распределение памяти, тогда ulimit - ваш друг, а не OOM.
Мой совет - оставить ООМ самостоятельно (или вообще отключить его), настроить ulimits и забыть об этой проблеме.
Ответ 3
Да, это абсолютно так в случае с новыми версиями 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.
Ответ 4
1: Да.
2: Это разделено на два этапа: любой системный вызов, например fork(), заверяется glibc в ядро. Ядро часть системного вызова находится в ядре /fork.c
3: Я не знаю. Но я бы поспорил, что у вашего ядра есть это.
Убийца OOM запускается, когда на 32-битных коробках находится угроза низкой памяти. У меня никогда не было проблемы с этим, но есть способы держать ООМ в страхе. Эта проблема может быть проблемой конфигурации OOM.
Поскольку вы используете Java-приложение, вам стоит подумать о переходе на 64-битный Linux. Это должно обязательно исправить это. Большинство 32-разрядных приложений могут работать на 64-битном ядре без каких-либо проблем, пока установлены соответствующие библиотеки.
Вы также можете попробовать ядро PAE для 32-битной Fedora.