h1

Tuning de JBoss 4.2: el baseline

03/08/2011

Wizdoc [Icon By Buuf]  Tips & Tricks.

Recuerda: el mejor rendimiento proviene del trabajo innecesario que no haces.

En estas últimas semanas estuvimos atareados porque debemos cerrar los últimos defectos y funcionalidad de la aplicación a nuestro cargo, programados a liberarse en una versión a publicar el próximo 7 de abril. ¿Por qué con tanta anticipación? Porque durante las próximas 4 semanas el equipo de integration and testing realizará las pruebas de regresión de toda la aplicación previas a la liberación de abril, y el equipo de desarrollo estará cerrando los defectos que aparezcan durante este tiempo.

A mí personalmente me tocaron resolver algunos problemas de desempeño que estaban presentando dos módulos de la aplicación. Esto porque en México nadie tiene suficiente conocimiento del código y de aquél lado (India y San Diego) estaban enfocados en resolver defectos más obvios, como cadenas sin traducir o errores por librerías AJAX. Al realizar el correspondiente análisis de desempeño apoyándome con el lead tester de San Diego, encontré que los servidores de aplicaciones no tenían tuning más allá de los clásicos -server -Xms -Xmx. El arquitecto original de la plataforma justificó esta configuración básica al asumir que con suficiente RAM y CPU se podría solventar cualquier problema. Sin embargo, como ocurre típicamente en estos casos, “otros sistemas tienen más prioridad que éste”, así que se dejó a la solución con un mínimo de hardware necesario que obviamente, no basta para las necesidades de operación diaria.

Así fue como me hice a la tarea de generar el tuning de los servidores de aplicaciones, sacándome un diez con algunos parámetros que permiten obtener un baseline sobre el cual podemos trabajar. Las especificaciones del sistema se proporcionan a continuación:

Característica Valor
Memoria 6 GB (5834 MB)
Procesador 2 x Intel® Xeon® CPU E5450 @ 3.00GHz (Quad Core)
Sistema Operativo Linux Red Hat 3.4.6-2
Servidor de aplicaciones JBoss 4.2.3
Módulos/Instancias de JBoss 6
Versión de Java jdk1.6.0_18
Características generales del hardware/middleware sobre el que correrá el sistema

Y las diferentes configuraciones que nos ayudaron a obtener un mejor rendimiento de hasta 28.5% en tiempos de respuesta y capacidad o throughput, fueron los siguientes:

1. Lo básico: versión de JVM, modo de ejecución y tamaño de pila (heap) de Java

Conviene tener la última versión de Java en la máquina donde se ejecutarán los servidores de aplicaciones. Al parecer la versión 1.6.0_18 es bastante estable, por lo que es una de las más recomendadas. Por otro lado, conviene verificar que se está asignando memoria más allá de la asignada por default (64 MB) y que el modo de ejecución de la JVM sea de “servidor”:


JAVA_HOME=/usr/java/jdk1.6.0_18
-server
-Xms512m
-Xmx512m

2. Impidiendo llamadas explícitas al recolector de basura

Algunas aplicaciones hacen uso de la instrucción System.gc(). Aunque esta instrucción no asegura el llamado al recolector de basura, en caso de ejecutarse puede provocar problemas de escalabilidad debido a que “congela el mundo” hasta que termina su ejecución. Por ejemplo si en algún lugar del código se implementa la siguiente instrucción:


for(int i = 0; i < some_number; i++) {
  System.gc();
}

Tenemos un potencial desastre. Para evitarlo sin meterse con el código – o debido a que librerías de terceros lo implementan – se utiliza la siguiente instrucción:

-XX:-DisableExplicitGC

3. Tuning del recolector de basura: corriendo en paralelo

Por default, el Garbage Collector de la máquina virtual de Java usa el modo “serial” de recolección, pero esto sólo sirve en máquinas con un solo CPU. Para servidores de aplicaciones con 2 o más cores, es recomendable seleccionar el modo “paralelo” mediante la opción -XX:+UseParallelGC. Por otro lado, éste se puede utilizar en modalidad de “pequeños impulsos frecuentes” (Concurrent Mark Sweep – CMS) mediante la opción -XX:+UseConcMarkSweepGC. Esta bandera disminuye un poco el rendimiento del sistema a cambio de no congelar la aplicación cada que se llena el GC. Finalmente, es posible realizar una recolección de basura de elementos nuevos (young generation; ver más abajo) en la pila de objetos, a través de la instrucción -XX:+UseParNewGC. Nota: La recolección paralela y la recolección de elementos nuevos son mutuamente excluyentes, así que al mezclar los tres pueden tenerse resultados inesperados:


-XX:+UseParallelGC
-XX:+UseConcMarkSweepGC

ó


-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC

4. Tuning del recolector de basura: lapso de ejecución para RMI

Para objetos creados durante la invocación de métodos remotos (Remote Method Invocation – RMI), la JVM tiene definido por defecto un minuto entre cada corrida del garbage collector. Esto significa que cada minuto podríamos estar “congelando al mundo” en un sistema distribuido. Por ello, se recomienda que en el caso específico del JBoss, se disminuya el número de ejecuciones a una corrida por hora:


-Dsun.rmi.dgc.client.gcInterval=3600000
-Dsun.rmi.dgc.server.gcInterval=3600000

5. Tuning del recolector de basura: hilos de ejecución

Por default, el número de hilos de ejecución asignados al GC son equivalentes al número de threads disponibles en el procesador. Sin embargo, esto puede ser muy ineficiente en sistemas con una buena cantidad de multithreading, siendo de 1/2 a 1 la proporción aconsejable.


-XX:ParallelGCThreads=6

6. Tuning del recolector de basura: incrementando la generación joven

Cuando se ejecuta un programa en Java, todos los objetos que se van creando pertenecen a tres generaciones, como se muestra en el siguiente esquema:

Java generation arrangement

Las tres generaciones de objetos en Java. (Fuente: java.sun.com)

Cuando se crea un objeto nuevo en Java con la instrucción new, éste inicialmente se encuentra en el espacio Edén (Eden space). Conforme se van ejecutando varios ciclos de recolección de basura o se van creando nuevos objetos, éstos van migrando a través de “espacios de supervivencia” (Survivor spaces) al ser copiados sobre áreas menos transaccionales de la memoria. La región tenured es la más importante pues en ésta se genera la mayoría de las operaciones en Java. Finalmente, aquellos objetos que han permanecido activos por mucho tiempo pasan a formar parte del espacio permanente (perm space) pues difícilmente serán eliminados.

Así entonces, en un ambiente altamente transaccional, es conveniente que entre el 6 y el 12% de la pila de memoria sea parte de la generación joven, pues se están creando muchos objetos en Java que serán migrados rápidamente. El tamaño por default es de apenas 2 MB y puede crecer de manera ilimitada, quemándose todo el espacio dedicado al tenured o perm, por lo que siempre es conveniente definir su tamaño explícitamente:


-XX:MaxNewSize=64m
-XX:NewSize=64m

7. Tuning del recolector de basura: pasando de generación en generación

El parámetro -XX:SurvivorRatio puede utilizarse para ajustar el tamaño de los espacios de supervivencia. Aunque no es tan importante para el rendimiento, sí permite ayudarnos a definir cuál será el espacio para el resto de las generaciones, pues si son demasiado pequeños, los nuevos objetos serán copiados directamente en el espacio tenured y si son demasiado grandes, se está desperdiciando memoria. Por ejemplo, -XX:SurvivorRatio=6 significa que existirá una relación de 6 a 1 entre el Edén y los survivor spaces.

Por otro lado, la JVM define por defecto un “porcentaje de ocupación” del 50% del survivor space actual para empezar a copiar los objetos que contiene al siguiente espacio. Esto puede significar un desperdicio del 50% de la memoria designada a los survivor spaces, por lo que conviene incrementarla para hacer un uso más eficiente de la misma. -XX:TargetSurvivorRatio permite definir el porcentaje de uso necesario para copiar los objetos del actual espacio al siguiente:


-XX:SurvivorRatio=8
-XX:TargetSurvivorRatio=90

8. Tuning del recolector de basura: incrementando la generación permanente

Así como estamos definiendo un tamaño para la generación joven, también es posible definir uno para el espacio permanente (perm space) que en aplicaciones con muchos objetos estáticos y utilerías (sobre todo en Ajax) pueden generar el temido java.lang.OutOfMemoryError: PermGen space. Cabe destacar que si estamos encontrando constantemente errores de este tipo aunque incrementemos el espacio considerablemente (>25% del espacio asignado al heap), significa que tenemos un problema de objetos no recolectados que requiere echarse un clavado en el código.


-XX:MaxPermSize=128m

9. Paginación de la memoria

El objetivo de la paginación en Java es optimizar los búferes de traducción y búsqueda en memoria (Translation-Lookaside Buffers – TLB). Estos son en pocas palabras, caches que almacenan los últimos mapeos de memoria virtual a física. Modificando los valores correspondientes se incrementa la eficiencia del uso memoria. En la mayoría de los casos no se recomienda pasar de 6 MB de paginación pues puede ser contraproducente.


-XX:+UseLargePages
-XX:LargePageSizeInBytes=5m

10. Non-Uniform Memory Architecture (NUMA)

Debido a que una buena parte de las arquitecturas multiprocesador están basadas en el uso de memoria de acuerdo a posiciones relativas a otro procesador o a través de memoria compartida entre procesadores, es posible utilizar una opción de “escopetazo” denominada NUMA. El uso de este parámetro en combinación con -XX:+UseParallelGC puede incrementar significativamente el desempeño:


-XX:+UseParallelGC
-XX:+UseNUMA

El detalle consiste en que algunas arquitecturas no soportan esta funcionalidad, por lo que el valor aportado por este parámetro debe ser comprobado después de una serie de pruebas de desempeño.

5 comentarios

  1. interesante tu post. Voy a ver tus datos y probar en mi server de pruebas. Gracias


  2. que te parecen estos parametros
    -Xms4000m -Xmx4000m
    -XX:MaxPermSize=128m
    -XX:PermSize=128m
    -XX:SurvivorRatio=128
    -XX:MaxTenuringThreshold=0
    -XX:+UseTLAB
    -XX:+CMSClassUnloadingEnabled
    -XX:+CMSClassUnloadingEnabled
    -XX:MaxNewSize=1000m
    -XX:NewSize=1000m
    -XX:+UseParNewGC
    -XX:+UseConcMarkSweepGC
    -XX:CMSInitiatingOccupancyFraction=70


    • Se ve bastante bien, sobre todo por la bandera -XX:+CMSClassUnloadingEnabled que también realiza una depuración del PermGen. En proyectos que hacen un uso intensivo de clases generadas dinámicamente (sobre todo por los famosos ORMs) ésta puede ayudarte a evitar los fastidiosos java.lang.OutOfMemoryError: PermGen space. El tema es: ¿la máquina donde estas corriendo esto tiene al menos una memoria de 6GB cierto? de lo contrario te estás comiendo toda la RAM y el servidor no dejará disponible memoria para ningún otro proceso corriendo en la máquina.


      • si tiene 6 gb, otra pregunta este par de parametros
        -XX:MaxNewSize=1000m
        -XX:NewSize=1000m

        me llama la atencion que tu explicacion lo configuras solo con 64m , 1000m no sera un numero muy grande???


      • En nuestro caso tenemos seis módulos o JVMs conviviendo en la misma máquina (64MB x 6 = 384 MB). Como ese parámetro debe ser entre 6 y 12% del total de la memoria (3072 MB, configurados con -Xms512m; -Xmx512m en el ejemplo), es correcto. En tu caso, asumo que sólo es una instancia del servidor de aplicaciones; ahora que lo mencionas sí podrías bajar ese valor a la mitad, pero deberías hacer la prueba con ambos valores para validar cuál es el que ayuda más al desempeño de tu aplicación.



Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: