Runtime.exec заставляет дублирующуюся JVM зависать бесконечно, пока не будет убит (Solaris 10) - PullRequest
4 голосов
/ 21 июля 2009

Все,

Мы запускаем приложение J2EE на сервере WebLogic 9.2 MP2 с 64-разрядной JVM jrockit (27.3.1) в Solaris 10.

Мы вызываем использование runtime.exec для вызова исполняемого файла jfmerge для создания PDF-документов.

Мы обнаружили, что в Solaris при вызове runtime.exec временно запускается дублирующаяся JVM, чтобы запустить процесс jfmerge. Хотя это неэффективно (наша JVM составляет 5 ГБ, поэтому дублированная оболочка JVM также составляет 5 ГБ), основная проблема заключается в том, что при большой нагрузке на эту функциональность (создание PDF) в нашем приложении иногда дублируется JVM никогда не выходит.

Когда JVM зависает, серверы создают большие проблемы (чрезмерная медлительность приложений и завершенные пользовательские сеансы), так как весь дубликат JVM получает все свои 5 ГБ размера процесса, записанные на подкачку диска.

Мы отметили следующий зависший поток, связанный с зависшим процессом JVM, до тех пор, пока процесс не будет уничтожен вручную:

"[STUCK] ExecuteThread: '17' для очереди: 'weblogic.kernel.Default (самонастройка) '"id = 3463 idx = 0x158 tid = 3460 prio = 1 жив, на родном языке, демон в JRockit / IO / FileNativeIO.readBytesPinned (Ljava / IO / FileDescriptor; [БИИ) Я (Родной Метод) в jrockit / io / FileNativeIO.readBytes (FileNativeIO.java:30) в java / io / FileInputStream.readBytes ([BII) I (FileInputStream.java) в java / io / FileInputStream.read (FileInputStream.java:194) в Java / языки / UNIXProcess $ DeferredCloseInputStream.read (UNIXProcess.java:227) в java / io / BufferedInputStream.fill (BufferedInputStream.java:218) в java / io / BufferedInputStream.read (BufferedInputStream.java:235) ^ - Удерживающий замок: Java / IO / BufferedInputStream @ 0xfffffffec6510470 [тонкий замок] в г / v3 / общий / formgeneration / SessionBean / FormsBean.getProcessStatus (FormsBean.java:809) в г / v3 / общий / formgeneration / SessionBean / FormsBean.createPDF (FormsBean.java:750) в г / v3 / общий / formgeneration / SessionBean / FormsBean.getTemplateDetails (FormsBean.java:450) в г / v3 / общий / formgeneration / SessionBean / FormsBean.generateSinglePDF (FormsBean.java:1371) в г / v3 / общий / formgeneration / SessionBean / FormsBean.generatePDF (FormsBean.java:263) в г / v3 / общий / formgeneration / SessionBean / FormsBean.endorseDocument (FormsBean.java:2377) в г / v3 / общий / formgeneration / SessionBean / Forms_qaco28_EOImpl.endorseDocument (Forms_qaco28_EOImpl.java:214) в г / v3 / делегаты / общее / FormsAndNoticesDelegate.endorseDocument (FormsAndNoticesDelegate.java:128) в г / v3 / действия / общий / EndorseDocumentAction.executeRequest (EndorseDocumentAction.java:68) в г / v3 / FWK / контроллер / распорки / действие / V3CommonDispatchAction.dispatchToExecuteMethod (V3CommonDispatchAction.java:532) в г / v3 / FWK / контроллер / распорки / действие / V3CommonDispatchAction.executeBaseAction (V3CommonDispatchAction.java:336) в г / v3 / FWK / контроллер / распорки / действие / V3BaseDispatchAction.execute (V3BaseDispatchAction.java:69) в орг / Apache / распорки / действие / RequestProcessor.processActionPerform (RequestProcessor.java:484) в г / v3 / FWK / контроллер / распорки / requestprocessor / V3TilesRequestProcessor.processActionPerform (V3TilesRequestProcessor.java:384) в орг / Apache / распорки / действие / RequestProcessor.process (RequestProcessor.java:274) в орг / Apache / распорки / действие / ActionServlet.process (ActionServlet.java:1482) в орг / Apache / распорки / действие / ActionServlet.doGet (ActionServlet.java:507) в г / v3 / FWK / контроллер / распорки / сервлет / V3ControllerServlet.doGet (V3ControllerServlet.java:110) в javax / servlet / http / HttpServlet.service (HttpServlet.java:743) в javax / servlet / http / HttpServlet.service (HttpServlet.java:856) в WebLogic / сервлет / внутренний / StubSecurityHelper $ ServletServiceAction.run (StubSecurityHelper.java:227) в WebLogic / сервлет / внутренний / StubSecurityHelper.invokeServlet (StubSecurityHelper.java:125) вWebLogic / сервлет / внутренний / ServletStubImpl.execute (ServletStubImpl.java:283) в WebLogic / сервлет / внутренний / ServletStubImpl.execute (ServletStubImpl.java:175) в WebLogic / сервлет / внутренний / WebAppServletContext $ ServletInvocationAction.run (WebAppServletContext.java:3231) в WebLogic / безопасность / ACL / внутренний / AuthenticatedSubject.doAs (AuthenticatedSubject.java:321) в WebLogic / безопасность / сервис / SecurityManager.runAs (SecurityManager.java:121) в WebLogic / сервлет / внутренний / WebAppServletContext.securedExecute (WebAppServletContext.java:2002) в WebLogic / сервлет / внутренний / WebAppServletContext.execute (WebAppServletContext.java:1908) в WebLogic / сервлет / внутренний / ServletRequestImpl.run (ServletRequestImpl.java:1362) в weblogic / work / ExecuteThread.execute (ExecuteThread.java:209) в weblogic / work / ExecuteThread.run (ExecuteThread.java:181) в jrockit / vm / RNI.c2java (JJJJJ) V (родной метод) - конец трассы

Мы бы хотели сделать пару вещей:

1.) Предотвратите появление дублирующейся JVM, так как нам не нужны никакие из ее функций при выполнении простого исполняемого файла jfmerge, и это создает огромные накладные расходы.

2.) В краткосрочной перспективе, по крайней мере, следует предотвратить бесконечную передачу этой дублированной JVM на неопределенный срок.

Ответы [ 3 ]

6 голосов
/ 01 июня 2010

Этот ответ задерживается, но у нас та же проблема, и проблема для нас заключается в том, как Solaris управляет памятью.

Проблема в том, что когда у нас есть сервер приложений, использующий в моем случае большой объем памяти 10 ГБ, и мы хотим запустить простое «ls», для запуска нового процесса требуется 10 ГБ.

Солярису требуются дополнительные 10 ГБ, доступные на нашем сервере, в Linux используется функция, известная как «копирование при записи». Эта функция уменьшает накладные расходы на создание нового процесса

http://developers.sun.com/solaris/articles/subprocess/subprocess.html

Историческая справка и описание проблемы

Традиционно в Unix был только один способ создания нового процесса: использование системного вызова fork (), за которым часто следует системный вызов exec (). Вызов fork () создает копию адресного пространства всего родительского процесса, а exec () превращает эту копию в новый процесс.

(Примечание. В ОС Solaris термин пространство подкачки используется для описания комбинации физической памяти и дискового пространства подкачки, настроенных для системы. Однако в других системах Unix этот термин может означать пространство подкачки на диске, также известное в качестве резервного хранилища. Чтобы избежать путаницы, я буду использовать термин «виртуальная память» (VM) для обозначения физической памяти и дискового пространства.)

Обычно метод fork / exec работал достаточно хорошо. Однако в некоторых случаях он имеет недостатки, такие как нехватка памяти без уважительной причины и низкая производительность разветвления.

Недостаточно памяти: для процесса с большой памятью системный вызов fork () может завершиться ошибкой из-за недостаточного количества виртуальных машин, поскольку fork () требует вдвое больше родительской памяти. Это может произойти, даже если после fork () сразу же следует вызов exec (), который освободит большую часть этой дополнительной памяти. Когда это происходит, приложение обычно завершает работу.

Например, предположим, что 64-разрядное приложение в данный момент потребляет 6 гигабайт (Гбайт) виртуальной машины, и ему необходимо создать подпроцесс для запуска команды ls (1). Родительский процесс выдает вызов fork (), который завершится успешно только в том случае, если на данный момент доступно еще 6 ГБ виртуальной машины. Если в системе недостаточно доступной виртуальной машины (что часто встречается), fork () завершится с ошибкой ENOMEM. Очевидно, что команде ls (1) не нужно где-то около 6 Гбайт памяти для запуска, но fork () этого не знает.

Та же проблема может возникнуть не только в приложениях, но и в собственных инструментах Sun. Например, для dbx была подана следующая Sun RFE (запрос на улучшение): «Оболочка 4748951 dbx должна использовать posix_spawn () для не встроенных команд, а не для fork (2)».

RFE 4748951 появился, когда утилита клиента вызвала dbx для чтения огромного файла ядра, используя скрипт, который также должен был выполнить команду cut (1) из dbx. Они получили сообщение не может - попробуйте еще раз сообщение об ошибке, приводящее к прерыванию dbx. Расследование показало, что dbx использовал fork / exec для выполнения этой крошечной команды cut (1) и исчерпал ВМ во время вызова fork ().

Виртуальная машина Java Solaris (JVM) также страдает от той же проблемы в настоящее время, как описано в этом Sun RFE: "5049299 Используйте posix_spawn, а не fork, на S10, чтобы избежать исчерпания подкачки".


Итак, у вас есть 3 варианта.

1.- Выполнить функцию Runtime.exec ранее.

2.- Создайте межпроцессное взаимодействие с другим сервером Java и выполните команду Runtime.exec.

3.- Создайте класс JNI для вызова системной функции C. Я беру этот вариант, и он отлично работает.

Я положил здесь свой пример кода.

Java-код.

public class CallOS {
    static {
            System.loadLibrary("CallOS");
    }

    public native int exec(java.lang.String cmd);

    public static void main(String[] args) {
            int returnValue = 0;
            returnValue = new CallOS().exec("ls -la");
            System.out.println("- " + returnValue);
    }
}

Код заголовка C. Это генерируется с помощью javah -jni CallOS

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class CallOS */

#ifndef _Included_CallOS
#define _Included_CallOS
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     CallOS
 * Method:    exec
 * Signature: (Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_CallOS_exec
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

C-код.

#include "CallOS.h"
#include <stdlib.h>

JNIEXPORT jint JNICALL Java_CallOS_exec
  (JNIEnv *env, jobject obj, jstring cmd)
{
   jint  retval;
   jbyte *str;

   str = (*env)->GetStringUTFChars(env, cmd, NULL);
   if(str == NULL) return NULL;

   retval = system(str);

   (*env)->ReleaseStringUTFChars(env, cmd, str);
   return retval;
};

Надеюсь, это поможет вам.

0 голосов
/ 08 декабря 2009

Как и предполагал Брайан, в unix стандартный способ запуска другого процесса другим процессом - это переход к родительскому процессу и дочернему процессу. Затем дочерний процесс вызывает exec, чтобы заменить себя новой программой. JVM должна сделать это, чтобы запустить вашу программу jfmerge.

Обычно размер памяти дочернего процесса не является проблемой, поскольку ОС использует копирование при записи, чтобы два процесса совместно использовали один и тот же образ памяти, пока дочерний процесс не вызовет exec. Может случиться так, что модель JVM для дочерних процессов требует, чтобы она дважды работала с внуком, исполняющим jfmerge, и дочерним процессом, который управляет внуком. Это объясняет, почему вы видите дублирующийся процесс JVM, который вы видите. Трассировка стека показывает заблокированное чтение процесса из входного потока. Возможно, jfmerge работает медленно, и процесс просто зависает, ожидая, пока jfmerge выдаст какой-то вывод.

Что вы можете сделать, это запустить какой-то другой процесс для запуска jfmerge вместо вашей 5-гигабайтной JVM. Напишите отдельную программу, которая просто запускает jfmerge по требованию и обеспечивает связь с основным процессом через некую форму межпроцессного взаимодействия. Этому автономному серверу jfmerge не потребовалось бы столько памяти для работы, поэтому влияние разветвленных дочерних процессов не было бы таким большим.

0 голосов
/ 21 июля 2009

Правильно ли вы обрабатываете порожденный процесс stdout / stderr? Вы должны использовать оба в отдельных потоках, чтобы надежно предотвратить блокировку. См. этот ответ для деталей. Может случиться так, что процесс запуска вашего процесса работает правильно для некоторых заданий и для других (из-за количества stdout / err, которое вызывает зависание).

В отношении дублирующих процессов я бы ожидал, что JVM fork / exec. Это дублирует процесс Java (fork), а затем он должен заменить его новым процессом (exec). Интересно, это то, что вы видите? Также обратите внимание, что я ожидаю, что ОС будет реализовывать COW (копирование при записи) для дублирования только тех страниц памяти, которые отличаются между изображениями, поэтому в обычных условиях дублирование JVM не потребляет столько памяти, сколько вы можете подумать.

...