viernes, 8 de agosto de 2008

JVM tuning: Parámetros de lanzamiento de JVM: Sun Hotspot

[Artículo actualizado el 3/11/2008 con información adicional sobre -XX:+DoEscapeAnalysis y -XX:+AggressiveOpts].

Parámetros de lanzamiento. JVM options. VM options. JDK option. Tuning parameter. Tunning.

O como queráis llamarlo. Por cierto que no se escribe Tunning, sino Tuning. a mi se me escapa mucho.

Sí, es tuning, pero no del de la foto :-)

La cuestión es de sobra comentada en este blog: cada vez el rendimiento sin tunear, o "Out-of-box performance" se aproxima más al mejor de los casos. Es decir en condiciones extremas puede haber una diferencia del 8% (que no es poco) pero puede ser un margen más que suficiente para no meterse en líos. Porque por toquitear podemos cagarla. Ahora o en un futuro. Pueden revisarse las reflexiones en la serie relacionada con la etiqueta Out-of-box.

A lo que vamos. Pero vale la pena intentarlo. Es más, casi diría que tenemos la obligación de intentarlo, cada aplicación es un mundo, los comportamientos de nuestros usuarios son otro mundo y las configuraciones y otros elementos para qué contarlo.

De todas formas, insisto en que no debe tunear por tunear, ni dejarse llevar por lo que supuesto expertos y teóricos aseguran en sus blogs (¿como este?). Las reglas de oro, en mi experiencia, son las siguientes:


  1. No te fíes de la teoría. O de los expertillos de salón. O de "tu intuición e inteligencia". Ni mucho menos de la de tu jefe. Prueba tú mismo la inclusión de opciones nuevas.
  2. No hagas Microbenchmarks. Mejor no hacerlos, puede llevar a conclusiones erróneas gracias a las optimizaciones (o no optimizaciones del Hotspot); aunque te creas más listo que nadie y pienses que lo estás simulando bien, lo más probable es que no sea así. Simula siempre con tu aplicación, no con un programita de esos que haces siempre para tomar tiempos.
  3. Paso 1: No tuning. Out-of-box. Déjalo como está (sin configurar o preconfigurado para tu servidor de aplicaciones), tocando únicamente los targets de memoria en cuanto a heap y revisando que aparezca el -server (por ejemplo en Glassfish V2, el comilador es -client por defecto). Si el rendimiento en el entorno de ejecución es adecuado, incluso en situaciones de carga, no lo toques. Si funciona no lo toques.
  4. Paso 2: Comienza a hacer pruebas de carga sobre tu sistema en pre-explotación. O en explotación. Pero pruebas de verdad, con un juego de pruebas coherente, con sus pausas aleatorias y la carga distribuida en distintos clientes con distintos anchos de banda simulado o real. Y ejecutado durante más de 2 horas cada uno. Lo mejor sería que las pruebas no fueran sintéticas 100% sino en base a los logs de ejecuciones pasadas, pausas, etc.
    1. Parte de la configuración base. Toma los tiempos de referencia.
    2. Sigue los pasos del apartado 4.2 del documento http://java.sun.com/performance/reference/whitepapers/tuning.html#section4.2. Empieza por el ejemplo 1. Aplica los parámetros y toma tiempos
    3. Repite el paso anterior con los ejemplo 2..7. Toma tiempos de todo ellos.
    4. Revisar las configuraciones de resultados publicados para los benchmarks del SPEC, y prueba con alguna de ellas (abajo pego un ejemplo).
    5. Por supuesto hay otras miles de opciones, muchas indocumentadas lamentablemente. Si tienes tiempo, ya sabes. ¡A probar!
    6. Toma tus propias conclusiones. Recuerda que un objetivo más importante que el rendimiento es la estabilidad al cabo del tiempo.
  5. Menos es más (esto también lo dicen los chicos de Usabilidad). Si dos alternativas dan resultados casi equivalentes, opta por la que menos opciones de lanzamiento tenga.
  6. Cada vez que cambies de versión de JDK, de versión de servidor de aplicaciones, de versión del kernel, o de hardware vuelve a hacer las comprobaciones, a no ser que en las notas de la versión dejen muy claro que sólo corrigen bugs que no te afectan. Apúntatelo en la cabecita. O en la libreta. O en la documentación. Un parámetro óptimo para una versión puede ser peor que la opción por defecto para la siguiente versión.
Dicho esto, y sabiendo que cada sistema y aplicación es un mundo, y los objetivos de negocio otro, voy a poner a continuación las opciones que a mi más mejor me funcionan.

  • Mi configuración HW y soft base, resumidamente es: máquinas x64, 2 procesadores XEON Quadcore (8 núcleos por máquina en total), 16 Gbytes de RAM, discos SAS en RAID 1, CentOS 5.1 kernel 2.6.18.
  • Viendo las configuraciones de resultados publicados en SPECjbb2005, una configuración que se repite con frecuencia es más o menos:
    • Para Hotspot: -Xms3650m -Xmx3650m -Xmn2000m -server -XX:+UseBiasedLocking -XX:+AggressiveOpts -XX:+UseParallelOldGC -Xss128k -XX:+UseLargePages -Xbatch
    • Para JRockit: -Xms3650m -Xmx3650m -Xns3000m -XXaggressive -XXlazyunlocking -Xlargepages -Xgc:genpar -XXtlasize:min=4k,preferred=1024k -XXcallprofiling
Lo que más me ha llamado la atención es que en casi ninguna configuración se abusa del tamaño de heap, aunque la JVM sea de 64bits en los tests no pasan de 3 GBytes de heap máximo. ¿Por qué? por tiempo de recolección de basura por supuesto. Esta puede ser una configuración válida para un benchmarking, pero en mi opinión es absurdo no aprovechar todo lo que nos da la máquina (o casi todo).

Los parámetros que mejores resultados me dan a mi en mi sistema y con mis circunstancias, y con estabilidad, son los siguientes, tanto en JDK 1.5 como en Java 6 (aunque en Java 6 algunas de ellas son valores ya por defecto): "-server -Xmx12288m -Xms12288m -XX:MaxPermSize=256m -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy -XX:+UseBiasedLocking -XX:+EliminateLocks -XX:+AggressiveOpts -Xverify:none -Djava.net.preferIPv4Stack=true"

De todo ello, lo más importante es elegir una buena estrategia de recolección de basura. En uno de los enlaces abajo están perfectamente explicadas todas las estrategias actuales, en mi caso el "stop-the-world" apenas se nota frente a una diferencia importante de rendimiento con estrategias concurrentes, quizás sea porque mi gráfico de objetos sea muy simple, un número medio de objetos con gran tamaño.

Un breve comentario de ellas:
  • -server: Entre otras diferencias con el modo cliente es el número de iteraciones antes de compilar una zona de código, 10000 frente a 1500. Se puede cambiar con la opción -XX:CompileThreshold, pero curiosamente mis pruebas han dado que un número ideal tiende a 7000-10000, indicando 0 o similar da resultados catastróficos... parece ser que por los inlinings y elecciones correctas, qué curioso. Nota: la opción -client puede ser la adecuada si lo que necesitas es buen tiempo de arranque o en situaciones en las que cambian mucho los JSP (cada JSP recompila a una nueva versión de la clase Servlet generada), pero algunas de las siguientes opciones no funcionarán.
  • -Xmx: tamaño máximo del heap, aún no tengo claro si incluye o no la zona de PermGen. Regla de oro: curiosamente la máquina donde se ejecutan JVM JAMÁS debe utilizar el swap a disco, la GC sería un desastre si tiene que ir moviendo páginas de un lado a otro.
  • -Xms: tamaño inicial del heap, en todos los sitios se recomienda igualarlo al máximo por rollos de fragmentación. Nota: esto no significa que el proceso consuma desde el principio esa cantidad de memoria física.
  • -XX:MaxPermSize: tamaño de la zona de generación permanente, donde nunca se hace GC (creo) ahí se alojan los objetos "clase" por ejemplo. Normalmente con 64 megas es más que suficiente pero si tienes muchos JSPs o muchos EJBs, mejor subirlo por si acaso (en Java CAPS por defecto es 192 megas).
  • -XX:+UseParallelOldGC: estrategia de máxima paralelización en la recolección de basura, no sólo usa múltples threads (por defecto tantos como threads reales máximos soporta tu máquina) en la recolección en las zonas jóvenes, sino también paraleliza la recolección en la zona antigua del heap. La opción por defecto con -server es UseParallelGC, en mi caso había una diferencia perceptible entre ambas. En muchos casos la opción adecuada debería ser -XX:+UseConcMarkSweepGC, en el que virtualmente no hay pausas perceptibles, pero el rendimiento global es menor. Por cierto, si vas al límite de consumo de heap o tienes algún memory leak, esta opción puede ser un desastre porque los tiempos de "stop de world" se hacen infinitos ya que no encuentra nada que liberar...
  • -XX:+UseAdaptiveSizePolicy: para no liarse con los tamaños de cada zona del heap, en mi caso lo mejor ha sido indicar este parámetro para que sea Hotspot quien vaya decidiendo y redimensionando por mi. Obviamente lo mejor es que no haya redimensiones y usar parámetros como -Xmn, -XX:NewRatio=2 y similares, pero hay que tener mucho cuidado porque pueden empezar a saltar OutOfMemory's cuando no deberían...
  • -XX:+AggressiveOpts: Aplica (si las hay) optimizaciones experimentales que se liberarán oficialmente en siguientes versiones del JDK.Habilita aquellas opciones de rendimiento que se prevén como habilitadas por defecto en siguientes releases del Hotspot, es decir muchas de las que explícitamente se especifican a continuación en realidad no es necesario indicarlas si aparece esta opción. Yo lo he usado siempre y nunca ha habido un comportamiento raro. En mi caso sí que aportaba un 1-2% de mejora sin afectar a la estabilidad, si no es el caso mejor no ponerlo por si acaso.
  • -XX:+UseBiasedLocking: Utiliza una estrategia de bloqueo que beneficia escenarios en los que sólo un thread pasa por una región de exclusión mutua (reduce la "contención") pero penaliza algunas estrategias de bloqueo. Teniendo en cuenta que la mitad del JDK está sincronizado y que no suele unsarse un String o un ByteArray desde dos threads concurrentes, es una gran idea. En Java 6 está habilitado por defecto, en JDK 1.5 hay que habilitarlo o especificar +AggresiveOpts.
  • -XX:+EliminateLocks: Otra técnica de optimización que se nota levemente, básicamente unifica regiones de exclusión mutua en un mismo thread. Una vez más el diseño de las clases del JDk hace que esta estrategia sea beneficiosa. Creo que no está habilitado por defecto en Java 6, salvo que especifiques +AggresiveOpts.
  • -Xverify:none: no verifica todas las clases contenidas en el bootclasspath y resto de cargas(ncluyendo el escaneo de todos los JARs) al arrancar. Acelera la fase de lanzamiento pero más vale ir con ojo porque está deshabilitando un control que mejor activar tras un par de semanas de funcionamiento. Cuidado por tanto con deshabilitar a la ligera la verificación si desplegamos código no confiable o de proveedores externos en nuestros servidores de aplicaciones, ya que esto puede derivar en alguna vulnerabilidad.
  • -Djava.net.preferIPv4Stack=true: Bueno, básicamente que no utilice el stack de IPv6 para el networking. Si no usas IPv6, activa esta opción y verás...
  • [-XX:+DoEscapeAnalysis: Esta es una opción que no está en JDK 1.5 y sí en Java 6 y JDK 7, en mis microbenckmarks daba peores resultados que desactivándolo curiosamente, aunque la teoría dice que debería funcionar para eliminar locks en regiones por las que sólo puede pasar un thread a la vez] NOTA DE REVISIÓN DE 3/11/2008: esta opción se habilita al establecer +AggresiveOpts, por lo que los microbenchmarks habría que revisarlos, porque los benchmars reales dan mejor resultado con +AggresiveOpts que sin ella.
  • [Otras opciones como -XX:+UseFastAccessorMethods y -XX:+StringCache no las he probado explícitamente. Al menos el primero de ellos creo que stá habilitado por defecto en todas las VM, y el segundo no lo tengo claro pero estoy casi convencido de que se ve afectado por +AggresiveOpts.]
Insisto, lo mejor es dejar el máximo número de opciones a su valor por defecto, aunque a todos nos gusta tunear. Y no te fies de los blogs. ¡Ni de este! Prueba, prueba y prueba

Ver algunas explicaciones y enlaces de interés en:
P.D: Empiezan los Juegos. Que gane el mejor y que nadie se ahogue entre la polución de Pekín...

1 comentario:

Anónimo dijo...

Muy bueno tu Post, te hago una consulta, como hago para aplicarlos? como debo de hacer? , gracias desde ya.