viernes, 29 de agosto de 2008

Es la guerra: IO vs "non blocking NIO"


En esta entrada anterior hice una pequeña y somera referencia hacia que los procesadores multicore y el uso de NPTL y otras librerías de threads optimizadas, pueden haber cambiado las reglas del juego. Y no estar tan claro que la E/S no bloqueante merezca la pena frente a una IO bloqueante "de toda la vida".

Parece que a lo largo de 2008 se ha armado un pequeño revuelo / revolución a este respecto. Como muestra estos dos botones: En definitiva, mi conclusión personal es la siguiente para máquinas de 8-32 cores:
  1. Servidores con poca concurrencia esperada: IO síncrona
  2. Servidores con concurrencia media esperada: IO síncrona
  3. Servidores con concurrencia alta esperada: depende, habría que probarlo
  4. Servidores con concurrencia brutal esperada: NIO asíncrona

En los escenarios 1 y 2, seguramente el problema de rendimiento y escalabilidad lo tengamos en otro sitio así que este no es un tema tan crítico en esos casos.

En cualquier caso, depende del uso y de lo que entendamos por poca o mucha concurrencia. El otro día un colega al que respeto un montón me comentaba que lo estamos viendo con un punto de vista exclusivamente de "contenidos de hipertexto"; en un entorno de servir contenidos multimedia el tiempo de cambio de contexto entre threads es inasumible de ninguna de las maneras.

domingo, 24 de agosto de 2008

Java SE 6u6-p Performance Release - YA DISPONIBLE EN OTRAS PLATAFORMAS

[Notas de octubre'08 y noviembre'08 ==>

Querido googler: si llegas a esta página -como muchos- buscando información de las performance releases del JRE 6, quizás te interesen los artículos posteriores 6u6-p: Pues en Linux x64 peta!!!!!!! y Identificación de bugs en la versión 6u6p y workaround ]


En la entrada Java SE 6u6p Performance Release remarqué que en julio estaba ya disponible la versión 6u6-p Performance Release, pero sólo lo estaba para Solaris 64 bits.

Pues bien, por fin ya está disponible para un número más que interesante de otras plataformas, y la sopresa es que no sólo de 64 bits sino también algunas de 32 bits:



Ala, a rellenar el cuestionario en http://java.sun.com/javase/performance/survey.html y descargar, a ver qué tal...

miércoles, 20 de agosto de 2008

Allocation, deallocation and escape analysis for Java

Enlazo con este interesante artículo en IBM Developer Works: Básicamente el anterior post trata dos temas muy interesantes desde el punto de vista teórico:
  • El mínimo coste de la creación de objetos y reserva de memoria en Java frente a C/C++. Esto es gracias a la arquitectura interna con intervención del GC.
  • La inclusión del Escape Analysis en Java 6 y cómo eso beneficia al alojamiento de objetos en el stack frente al heap, lo cual puede ser una signfiicativa mejora de rendimiento...
Pues bien, el segundo de ellos parece que todavía no es cierto en Java 6 (el artículo es de 2005). Básicamente, hasta donde yo sé, sí que está implementado el análisis de escape (con la opción -XX:+DoEscapeAnalysis) pero sólo se usa para eliminar bloqueos, no para el alojamiento en stack. Quizás en Java 7 la optimización sea completa...

En este hilo puede verse la misma conclusión: http://forums.java.net/jive/thread.jspa?threadID=22617&tstart=120. Sin embargo en este otro post de 2007 se asegura que el Escape Analysis se eliminó de Mustang, por lo que me surge la duda si en actualizaciones posteriores sí se hizo o si simplemente es que hay desinformación por ahí... ver: http://blog.nirav.name/2007/02/escape-analysis-in-mustang-ii.html.

Y en cualquier caso con los microbenchmarks que yo he hecho (Linux, kernel 2.6, 8 cores) no parece que se gane mucho en cuanto a eliminación de locks, sí que parece suponer una mínima mejora pero nada comparable con el biased locking. Con todo el miedo que tengo a los microbenchmarks, en cualquier caso lo cierto es que la opción UseBiasedLocking está activada por defecto en Java 6, mientras que DoEscapeAnalysis hay que activarla.

El sentido común me dice que la decisión de poner opciones por defecto (o no ponerlas) por algo será (si partimos de la premisa de que los ingenieros de Sun no son imbéciles profundos ;-) y por lo tanto seguramente es un análisis experimental e imperfecto, así que tendremos que esperar a Java 7 para que ambas líneas de aprovechamiento del "análisis de escape" funcionen adecuadamente.

P.D: en otro post comentaré acerca del peligro de los microbenchmarks y cómo deben hacerse para no obtener resultados equivocados (en teoría, aunque tampoco me fío del todo).

[Actualización: puede verse información más detallada sobre esta y otras opciones de optimizaciones sobre el modelo de Threading en el post ¿Funcionan las optimizaciones sobre el modelo de threading?]

[Actualización 06-09-2008: Definición de Escape analysis en la Wikipedia: http://en.wikipedia.org/wiki/Escape_analysis]

[Actualización 04-11-2008: Conceptos generales de rendimiento en Java, incluyendo estos nombrados: http://en.wikipedia.org/wiki/Java_performance]

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...