2008-10-16 17 views
22

En el trabajo, una de nuestras plataformas de destino es un mini servidor con recursos limitados que ejecuta Linux (núcleo 2.6.13, distribución personalizada basada en un viejo núcleo de Fedora). La aplicación está escrita en Java (Sun JDK 1.6_04). El asesino OOM de Linux está configurado para matar procesos cuando el uso de memoria excede los 160MB. Incluso durante cargas elevadas, nuestra aplicación nunca supera los 120 MB y junto con algunos otros procesos nativos que están activos, nos mantenemos dentro del límite de OOM.¿De qué versión de Linux kernel/libc es Java Runtime.exec() seguro con respecto a la memoria?

Sin embargo, resulta que el método Java Runtime.getRuntime(). Exec(), la forma canónica de ejecutar procesos externos desde Java, tiene un particularly unfortunate implementation on Linux que hace que los procesos secundarios generados (temporalmente) requieran la misma cantidad de memoria como el proceso principal ya que el espacio de direcciones se copia. El resultado neto es que nuestra aplicación es asesinada por el asesino OOM en cuanto hacemos Runtime.getRuntime(). Exec().

Actualmente trabajamos en esto teniendo un programa nativo separado para ejecutar todos los comandos externos y nos comunicamos con ese programa a través de un socket. Esto es menos que óptimo.

Después de posting about this problem online obtuve algunos comentarios que indican que esto no debería ocurrir en versiones "más nuevas" de Linux ya que implementan el método posix fork() usando copy-on-write, lo que significa que solo copiará las páginas que necesita modifique cuando se requiera en lugar de todo el espacio de direcciones inmediatamente.

Mis preguntas son:

  • Es esto cierto?
  • ¿Esto es algo en el núcleo, la implementación de libc o en otro lugar completamente?
  • ¿De qué versión de kernel/libc/whatever está disponible copy-on-write para fork()?
+0

Es memoria * virtual *, no * física * que se necesita entre la llamada fork() y la siguiente ejecución(). Dudo mucho que se esté quedando sin memoria virtual, dado el tamaño del espacio de direcciones en comparación con su límite de memoria física. –

+0

Absolutamente, no nos estamos quedando sin memoria física, pero una parte de Linux parece pensar que sí. –

Respuesta

10

Esto es más o menos la forma en que * nix (y linux) han funcionado desde el comienzo de los tiempos (o hasta el amanecer de mmus).

Para crear un nuevo proceso en * nixes llame a fork(). fork() crea una copia del proceso de llamada con todas sus asignaciones de memoria, descriptores de archivos, etc. Las asignaciones de memoria se realizan con copia-en-escritura para que (en casos óptimos) no se copie la memoria, solo las asignaciones. Una llamada siguiente de exec() reemplaza la asignación de memoria actual con la del nuevo ejecutable. Entonces, fork()/exec() es la forma de crear un nuevo proceso y eso es lo que usa la JVM.

La advertencia es que con procesos enormes en un sistema ocupado, el padre puede continuar ejecutándose por un tiempo antes de que el niño exec() 's cause una gran cantidad de memoria a causa de copiar-en-escribir . En las VM, la memoria se puede mover mucho para facilitar un recolector de basura que produce aún más copia.

La "solución alternativa" es hacer lo que ya ha hecho, crear un proceso liviano externo que se encargue de generar nuevos procesos, o utilizar un enfoque más ligero que fork/exec para generar procesos (que Linux no tiene - y de todos modos requeriría un cambio en el jvm mismo). Posix especifica la función posix_spawn(), que en teoría se puede implementar sin copiar la asignación de memoria del proceso de llamada, pero en Linux no.

+1

Llamaría a este proceso liviano ssh y usar http://www.jcraft.com/jsch/ para conectarme a localhost. – JoG

1

1: Sí. 2: Esto se divide en dos pasos: Cualquier llamada al sistema como fork() está envuelta por glibc en el kernel. La parte del kernel de la llamada al sistema está en kernel/fork.c 3: No lo sé. Pero apostaría que tu kernel lo tiene.

El asesino OOM se activa cuando la memoria baja está amenazada en cajas de 32 bits. Nunca tuve un problema con esto, pero hay formas de mantener a OOM a raya. Este problema podría ser un problema de configuración de OOM.

Dado que está utilizando una aplicación Java, debería considerar cambiarse a Linux de 64 bits. Eso definitivamente debería arreglarlo. La mayoría de las aplicaciones de 32 bits se pueden ejecutar en un kernel de 64 bits sin problemas, siempre que se instalen bibliotecas relevantes.

También podría probar el kernel PAE para fedora de 32 bits.

+0

¿Por qué cambiar a Jbit de 64 bits soluciona esto? –

+1

@AlastairMcCormack Los sistemas Linux de 32 bits usaron una división de 3 GB/1 GB para el espacio de usuario y el kernel. Parte de 1 GB está reservado para los procesos de espacio del usuario que necesitan acceso al espacio del núcleo a través de llamadas al sistema. Esto es SÓLO 128M. Creo que la presión de la memoria tanto en el kernel de 128M como en el espacio de usuario de 3GB activará OOM. Si bien puede haber otros procesos en el espacio de 3GB, una JVM es más probable que se convierta en el proceso más grande, y por lo tanto un objetivo OOM.También un proceso de bifurcación como los OPs JVM tiene un puntaje OOM más alto ya que cada niño agrega al puntaje de los padres. 64 Bit no tiene división de memoria ni ninguna de las divisiones kernel/usuario. – Bash

5

Bueno, yo personalmente dudo que esto sea cierto, ya que el fork() de Linux se realiza mediante copy-on-write ya que Dios sabe cuándo (al menos, los núcleos 2.2.x lo tenían, y estaba en algún lugar en el 199x) .

Dado que OOM killer se cree que es un instrumento bastante tosco que se sabe que falla (es decir, no es necesario que mate el proceso que realmente asignó la mayor parte de la memoria) y que debe usarse solo como último recuerdo, no tengo claro por qué lo tienes configurado para disparar en 160M.

Si desea imponer un límite a la asignación de memoria, entonces ulimit es su amigo, no OOM.

Mi consejo es dejar OOM solo (o deshabilitarlo por completo), configurar ulimits y olvidarse de este problema.

+0

Gracias por la información, lamentablemente no tenemos control sobre la plataforma: el hardware, el sistema operativo y la configuración de la misma son en su mayoría inamovibles (nosotros simplemente "implementamos", por así decirlo). –

2

Sí, este es absolutamente el caso incluso con nuevas versiones de Linux (estamos en Red Hat 5.2 de 64 bits). He estado teniendo problemas con subprocesos de ejecución lenta durante aproximadamente 18 meses, y nunca pude resolver el problema hasta que leí su pregunta y realicé una prueba para verificarlo.

Tenemos una caja de 32 GB con 16 núcleos, y si ejecutamos la JVM con configuraciones como -Xms4g y -Xmx8g y ejecutamos subprocesos usando Runtime.exec() con 16 hilos, no podremos ejecutar nuestro proceso más rápido que aproximadamente 20 llamadas de proceso por segundo.

Pruebe esto con el simple comando "date" en Linux unas 10,000 veces. Si agrega código de perfil para ver lo que está sucediendo, comienza rápidamente pero se ralentiza con el tiempo.

Después de leer su pregunta, decidí intentar bajar la configuración de mi memoria a -Xms128m y -Xmx128m. Ahora nuestro proceso se ejecuta en aproximadamente 80 llamadas de proceso por segundo. La configuración de la memoria JVM fue todo lo que cambié.

No parece estar absorbiendo memoria de tal manera que alguna vez se me acabó la memoria, incluso cuando la probé con 32 hilos. Es solo que la memoria adicional tiene que ser asignada de alguna manera, lo que causa un fuerte inicio (y tal vez cierre) de costos.

De todos modos, parece que debería haber una configuración para deshabilitar este comportamiento Linux o tal vez incluso en la JVM.

Cuestiones relacionadas