Posts Tagged ‘tuning’

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.

h1

Tuning de Glassfish 2.1: el baseline

02/26/2010

Wizdoc [Icon By Buuf]  Tips & Tricks.
Cuando realizamos un tuning o puesta a punto para mejorar el desempeño de una aplicación, muchos de nosotros no sabemos por donde empezar. Con anterioridad hemos visto que lo mejor es iniciar tuning del código aplicativo, pues muchas veces es lo más fácil o que mayor impacto logra en el desempeño de la solución, para después ir bajando a las capas más abstractas de la arquitectura (contenedores de aplicaciones o middleware, luego sistema operativo y finalmente hardware). Ahora bien, con bastante regularidad, la mayoría de los encargados de hacer despliegues de aplicaciones saben qué parámetros modificar para hacer que la Máquina Virtual de Java (JVM) dé lo mejor de sí, pero en cuanto al servidor de aplicaciones, todos se quedan en blanco.

Hace poco ayudé a unos compañeros con este tipo de parámetros, pues su aplicación se caía con tan sólo un par de deployments, marcando el temido java.lang.OutOfMemoryError en muy poco tiempo. Entonces, aquí presento los parámetros más socorridos para mejorar el desempeño de una aplicación, con pequeños ejemplos para el servidor de aplicaciones Glassfish 2.1. No son en absoluto exhaustivos, pero pueden proporcionarnos una línea base a partir de la cual podemos ir trabajando:

1. La versión de la Máquina Virtual de Java

Obviamente es un cliché, pero hemos comprobado la diferencia que puede hacer una versión de Java. Siempre es una buena práctica instalar la última versión en los servidores donde correrá la aplicación. No es necesario contar con el root del servidor si se instala mediante un ejecutable (por ejemplo, j2sdk-1_6_0_18-solaris-sparc.sh) lo que permite instalar una nueva versión de Java sin impactar dominios de aplicaciones desplegados previamente. Primero, hay que instalar la nueva versión de la JVM y luego deberemos cambiar el path desde donde la toma Glassfish:

$GLASSFISH_HOME/config/asenv.conf
propiedad a modificar: AS_JAVA=<java_installations>/j2sdk1.6.0_18

2. Modo de ejecución de la Máquina Virtual de Java

Los servidores Glassfish corren por default en modo "cliente". Esto hace que levanten más rápido en un ambiente de desarrollo, pero esta modalidad de ejecución no es ideal en producción. Adicionalmente, por default la JVM tiene un límite de consumo de memoria de 64 Megabytes. Si nuestro servidor productivo tiene más que eso, lo mejor es incrementar los valores de acuerdo al disponible de memoria con el que contamos:

• Ingresar a http:<hostname>:4848
• Firmarse con usuario/password administrador.
• Click en nodo de application server a editar > JVM Settings > JVM Options.
• Editar las opciones de la JVM o agregar nuevas en el campo de texto correspondiente: -server -Xmx1500m -Xms1500m
• Click en Save del lado derecho.
• Reiniciar el servidor.

En el ejemplo Xms es la cantidad de memoria inicial (aquí se definieron 1500 MB o 1.5 Gigabytes) y Xmx es la cantidad de memoria máxima a usar por la JVM.

3. Número de threads de acceso

Los threads de acceso (acceptor threads) son hilos de ejecución asignados a un socket para que el servidor procese peticiones HTTP. El número por default es uno, pero es mejor designar el número de threads de acuerdo al número de cores (núcleos de procesador) que tiene la máquina donde corre el servidor, usando una proporción de 1 thread por cada 1 a 4 cores. Puede ser necesario probar la aplicación con diferentes parámetros para encontrar el óptimo. Ejemplo:

Una M3000 con 2 procesadores dual core puede tener un mínimo de (2 CPUs x 2 dual cores / 4 threads por core) = 1 thread o un máximo de (2 CPUs x 2 dual cores / 1 thread por core) = 4 threads.

Para modificar los valores en Glassfish, seguimos los siguientes pasos:

• Ingresar a http:<hostname>:4848
• Firmarse con usuario/password administrador.
• Click en nodo de application server a editar: Configuration > HTTP Service > HTTP Listeners
• Click en http-listener-1
• Editar el campo Acceptor Threads en la sección Advanced
• Click en Save del lado derecho.
• Reiniciar el servidor.

4. Número de threads de procesamiento

Los threads de procesamiento son los que ejecutan las peticiones HTTP. El default es 5, pero debe modificarse de acuerdo al número de cores que posee la máquina en proporción 1 a 1. Si la aplicación ocupa mucho uso de disco (I/O), es recomendable que el valor sea multiplicado por dos. Ejemplo:

Una M3000 con 2 dual cores puede tener 2 CPUs x 2 dual cores = 4 threads; si la aplicación usa mucho I/O, el número se multiplica por dos, quedando en 8 threads.

Nuevamente, para implementar este cambio, podemos seguir los siguientes pasos:

• Ingresar a http:<hostname>:4848
• Firmarse con usuario/password administrador.
• Click en nodo de application server a editar: Configuration > HTTP Service > Tab RequestProcessing
• Cambiar el valor Thread Count al número correspondiente.
• Click en Save del lado derecho.
• Reiniciar el servidor.

5. Subsistema Keep-alive

El keep-alive se utiliza para optimizar el uso de red. Normalmente, una petición HTTP requiere abrir la conexión – enviar datos – cerrar la conexión. Pero si una misma conexión envía información en diferentes tiempos, ¿no sería mejor dejarla siempre abierta para no pasar por el trabajoso proceso de abrirla? Siempre se recomienda cambiar el número de threads a 1 por cada 8 cores en la máquina para mejorar el uso de estos recursos. Ejemplo:

Una M3000 con 2 dual cores puede tener 2 CPUs x 2 cores = 4; por default debe tener 1 thread asignado. Si dicha M3000 tuviera 4 CPUs quad core, tendría 2 CPUs x 4 cores = 16; un default de 2 threads asignados.

• Ingresar a http:<hostname>:4848
• Firmarse con usuario/password administrador.
• Click en nodo de application server a editar: Configuration > HTTP Service > Tab KeepAlive
• Editar campo thread-count.
• Click en Save del lado derecho.
• Reiniciar el servidor.

6. Cacheo de archivos estáticos

En caso de utilizar muchos archivos estáticos en la aplicación (HTML, imágenes) es necesario cambiar los valores por default de acuerdo al tamaño promedio de estos archivos. Nota: es necesario calcular estos valores previamente a su ingreso al servidor para compararlo con el default, que en la mayoría de las veces es más que suficiente. Esto porque el incremento de cache aumenta los requerimientos de memoria llegando incluso a bloquear el servidor. Ejemplo:

La aplicación tiene 2,000 HTMLs estáticos con un tamaño promedio de 600 KB cada uno y 4,000 imágenes con un tamaño promedio de 200 KB cada una, entonces se requerirían los siguientes parámetros:

Globally = Enabled
MaxFilesCount = 6000
MediumFileSizeLimit = 614400
MediumFileSize = 1228800000 (= 614,400 bytes x 2,000 = 1.14 GigaBytes)
SmallFileSizeLimit = 204800
SmallFileSize = 819200000 (= 204,800 bytes x 4,000 = 781.25 MegaBytes)
FileCacheEnabling = On

• Ingresar a http:<hostname>:4848
• Firmarse con usuario/password administrador.
• Click en nodo de application server a editar: Configuration > HTTP Service > Tab HTTP File Cache
• Habilitar cache globalmente (primer select box)
• Cambiar tamaños de propiedades relacionadas (ver ejemplo)
• Click en Save del lado derecho.
• Reiniciar el servidor.

7. Deshabilitar log de acceso

Siempre se recomienda deshabilitar el log de acceso HTTP en un despliegue productivo, especialmente si hay un web server como front/proxy para la plataforma – pues ya tendrá su propio log – o existe mucho I/O por parte de la aplicación que estaría compitiendo por uso de disco.

• Ingresar a http:<hostname>:4848
• Firmarse con usuario/password administrador.
• Click en nodo de application server a editar: Configuration > HTTP Service
• Deshabilitar casilla Access Logging
• Click en Save del lado derecho.
• Reiniciar el servidor.

8. default-web.xml

Una vez que hayamos decidido subir el desarrollo a producción, ya no es necesario que el servidor esté corriendo un demonio que verifique si los JSP han cambiado para recompilarlos. Por otro lado, cuando se compila un JSP, siempre se requiere usar el método toCharArray para parsear cadenas estáticas, usadas normalmente en etiquetas HTML y JSP. Si desde el principio indicamos que estas cadenas deben ser arreglos de caracteres, evitamos la llamada constante a dicho método.

Para modificar este comportamiento, editamos el archivo default-web.xml, agregando las siguientes etiquetas al cuerpo del XML:

<init-param>
  <param-name>development</param-name>
  <param-value>false</param-value>
</init-param>
<init-param>
  <param-name>genStrAsCharArray</param-name>
  <param-value>true</param-value>
</init-param>

El archivo se encuentra generalmente localizado en $GLASSFISH_HOME/domains/domain1/config/default-web.xml

9. Configuración de JDBC

Siempre es deseable optimizar las conexiones de JDBC. Agregando los siguientes parámetros a la configuración de la misma en Glassfish, logramos parte del cometido:

Parámetros para Oracle:

ImplicitCachingEnabled=true
MaxStatements=200

Parámetros para MySQL:

cachePrepStmts=true
prepStmtCacheSize=512
useServerPreparedStmts=false

Cabe mencionar que estos parámetros funcionan sólo si en la aplicación se utilizan PreparedStatements como método de ejecución de queries a la base de datos.

• Ingresar a http:<hostname>:4848
• Firmarse con usuario/password administrador.
• Click en nodo de application server a editar: Resource > JDBC > Connection pools
• Click en <poolName>.
• Click en tab Additional Properties.
• Click en Add Properties.
• Agregar propiedades en forma Name=Value.
• Click en Save del lado derecho.
• Reiniciar el servidor.

10. Configuración del Garbage Collector

En términos generales, el Garbage Collector es una de las piezas de la JVM que más cambios ha sufrido desde que Java hizo su aparición. Dicho componente permite recuperar memoria una vez un objeto ha sido descartado; sin embargo el demonio del Garbage Collector requiere de cierto tuning pues en ambientes productivos puede saturarse, "deteniendo el mundo" constantemente mientras ejecuta la depuración de objetos. Libros enteros y miles de artículos han sido escritos para realizar este tipo de optimización, pero un par de pequeños cambios pueden ser más que suficientes.

Por default, se 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.

• Ingresar a http:<hostname>:4848
• Firmarse con usuario/password administrador.
• Click en nodo de application server a editar > JVM Settings > JVM Options.
• Editar las opciones de la JVM o agregar nuevas en el campo de texto correspondiente: -XX:+UseParallelGC o en su caso, -XX:+UseConcMarkSweepGC
• Click en Save del lado derecho.
• Reiniciar el servidor.

h1

El wait-based tuning: un ejemplo de la vida real

01/30/2008

Wizdoc [Icon By Buuf]

 Tips & Tricks

La semana pasada vimos qué es la Administración del Desempeño de Aplicaciones (APM) y los fundamentos del wait-based tuning (WBT). En esta parte la intención es demostrar cómo opera esta metodología con un ejemplo de la vida real.

1. Arquitectura y configuración

Para este ejemplo utilizaremos una arquitectura web estándar:


Una arquitectura web estándar.
[Click en imagen para ver versión más grande]

Podemos describir la arquitectura en función del tier donde nos encontremos:

  • Hardware. Seis servidores con arquitectura RISC de dos procesadores por socket a 1.5 GHz c/u; 4 GB en RAM y un disco duro de 73 GB de espacio. Dos servidores balanceados por cada tier.
  • Front End. Dos servidores cada uno con una instancia de un servidor web (Apache Http Server) y configuración inicial de 150 threads activos.
  • Middle End. Dos servidores con dos instancias lógicas del servidor de aplicaciones (Sun Application Server), cada una con 50 threads, 30 conexiones a base de datos, un cache asignado para 500 Entity Beans y 1 GB de memoria asignada al heap de la Máquina Virtual de Java.
  • Back End. Dos servidores con un motor de base de datos como parte de un Oracle 10g RAC configurado. En el diagrama no se incluyen el disco externo ni el balanceador de alta disponibilidad – requisitos de dicha instalación – para efectos de simplicidad.
  • Firewalls. Los equipos se encuentran separados en dos zonas de seguridad: una DMZ para el front end y una intranet segura para los equipos de middle y back-end. La única forma de acceder a los equipos del back end es mediante los firewalls y todos los puertos y servicios no esenciales han sido deshabilitados.
  • Conectividad. El balanceo de cargas es implementado por dos balanceadores dedicados específicamente para este fin; la configuración de ambas capas es activo-activo (es decir, ambos nodos están funcionando con balanceo de carga, al contrario de una configuración activo-pasivo donde uno funciona y el otro entra en acción en caso de falla).

2. Diagnóstico

De acuerdo al plan original, esta plataforma está diseñada para un ambiente de producción mediano, con la capacidad para 2,000 usuarios concurrentes. Para validar qué tanto tuning es necesario hacer, se realizan las pruebas de desempeño, carga y estrés: Se genera una prueba base con 1,000 usuarios concurrentes durante 20 minutos y a partir de dichos 20 minutos, se agregan 50 usuarios cada 5 minutos hasta llegar a 2,200. Sin ahondar mucho en cómo corre la prueba, después de los 2,200 usuarios los servidores empiezan a congelarse y a generar errores OutOfMemoryError.

El resultado de las pruebas proporciona el siguiente diagnóstico:

  • Existe un alto desperdicio de recursos, pues bajo estrés los servidores de aplicaciones y base de datos llegan a un máximo de uso de CPU del 20% y 40% respectivamente.
  • La caída del sistema se debe a falta de memoria, no CPU. Esto implica falta de tuning de pools, caches y heap del JVM.
  • La falta de tuning puede comprobarse al verificar que el pool de conexiones a base de datos y threads de procesamiento de peticiones del application server están siendo utilizados al 100%; quedando pendientes de procesar (en espera) 10 threads y 20 peticiones en promedio.
  • El Cache Hit Ratio de Entity EJBs tiene una tasa del 40%, es decir, que de 100 peticiones a la capa de persistencia, 60 tienen que ir a la base de datos y sólo 40 se extraen de memoria.

El diagrama mostrado a continuación muestra esquemáticamente los resultados de las pruebas:


Los resultados de las pruebas de diagnóstico de desempeño.
[Click en imagen para ver versión más grande]

3. Iteración 1

Enfocándonos en la última tina, cuya capacidad está siendo desperdiciada, analizamos el punto donde las peticiones esperan demasiado: Los pools y caches de recursos del Application Server. Por lo tanto, incrementamos los hilos de ejecución y conexiones a base de datos para aumentar el uso de CPU de ésta. Entonces re-configuramos y volvemos a correr el batch de pruebas de volumen para ver los resultados. Los cambios de configuración y sus efectos son los siguientes:

Cambios

Efectos

Threads de ejecución +50
Conexiones BdD +30
Entity EJB +500

CPU (Web Server) 75%
CPU (App Server) 50%
CPU (BdD) 70%
Entity EJB CHR 80%

Básicamente, al duplicar los pools y caches de recursos, incrementamos el uso de CPU de la plataforma en su conjunto en poco menos del doble. Esto se puede ver en el siguiente diagrama:


Los resultados de la primera iteración del tuning.
[Click en imagen para ver versión más grande]

Como puede verse, todavía no llegamos al óptimo pues aunque ya tenemos 80% de Cache Hit Ratio de Entity EJBs, todavía persiste un desperdicio de CPU y un excedente de peticiones en espera de ser atendidas.

4. Iteración 2

Incrementamos 20 hilos de ejecución y otras 30 conexiones a base de datos, esperando alcanzar el uso óptimo de CPU de esta última…

Cambios

Efectos

Threads de ejecución +20
Conexiones BdD +30

CPU (Web Server) 85%
CPU (App Server) 40%
CPU (BdD) 95%
Peticiones Pendientes 40

…sin embargo llegamos a un punto de saturación con 95% procesador. También mejoró el uso del Web Server, pero la saturación de la Base de Datos hace que en el Application Server se queden más peticiones en espera (¡casi el doble!). Esto puede verse en el siguiente diagrama:


Los resultados de la segunda iteración del tuning
(Una saturación de la Base de Datos)
[Click en imagen para ver versión más grande]

Por lo tanto, tendremos que disminuir un poco la capacidad de dichos recursos.

5. Iteración 3

Al disminuir los hilos de ejecución y conexiones a base de datos a 100 y 75 respectivamente, incrementamos notablemente el desempeño de nuestra plataforma:

Cambios

Efectos

Threads de ejecución -20
Conexiones BdD -15

CPU (App Server) +80%
CPU (BdD) 85%%
Threads pendientes de ejecución 5
Peticiones pendientes de ejecución 10

Como puede verse en el diagrama más abajo, prácticamente ya estamos hechos: sólo falta disminuir un poco el flujo de entrada de y hacia el application server para no tener más peticiones o hilos pendientes de ser atendidos.


Los resultados de la tercera iteración del tuning.
[Click en imagen para ver versión más grande]

6. Iteración 4

Así entonces, disminuimos los hilos de ejecución de y hacia el application server por 10 y 25 respectivamente. Volvemos a correr nuestro set de pruebas y… ¡voila! alcanzamos el tuning perfecto:


Los resultados de la tercera iteración del tuning.
[Click en imagen para ver versión más grande]

Con un uso de CPUs de aproximadamente el 85% (excelente número bajo pruebas de estrés), pools de conexiones a base de datos e hilos de ejecución a 95% y 90% de capacidad respectivamente y cero peticiones pendientes de procesar, hemos alcanzado un buen nivel de tuning. Los siguientes pasos serían la optimización sobre la base de datos y la aplicación misma, si no es que ya han sido realizados.

Conclusiones

Como hemos podido ver en los dos últimos posts, la APM nos permite generar arquitecturas siempre tomando en cuenta el desempeño final de nuestra solución; formando parte del ciclo de vida de desarrollo y puesta en producción obteniendo resultados en términos cualitativos y cuantitativos.

Por otro lado, el modelo Wait-Based Tuning nos da un marco de referencia para desarrollar pruebas de desempeño, carga y estrés, donde cualquier refinamiento debe empezar por los niveles de ejecución más altos (la aplicación) y finalizar con los más bajos (el hardware).

h1

De Performance and Tuning

01/25/2008

Wizdoc [Icon By Buuf]

 Tips & Tricks

Entre compañías con un ingreso mayor a US$1,000 millones, cerca del 85% ha experimentado incidentes de degradación de desempeño en sus aplicaciones.

— Jean-Pierre Garbani
Vicepresidente
Forrester Research

Como parte de este nuevo año me tocó, entre otras cosas, hacer un cursito de performance and tuning para el personal clave de desarrollo y operaciones de nuestro cliente. Ya tengo casi todo terminado, aunque me faltan algunos ejemplos y láminas adicionales por si alguno de mis estudiantes se pone sus moños. Si todo sale bien, tendré que darlo para dos grupos de 5 a 7 personas durante dos semanas. Por lo tanto, dejo aquí un resumen de mi presentación y que me sirve muy bien de acordeón para salir bien librado de esta tarea que me ha sido encomendada. Empezamos:

¿Por qué preocuparnos por el desempeño? Más allá de la productividad perdida para apagar el bomberazo o que las empresas le paguen a sus empleados por no hacer nada y esperen a que sus aplicaciones vuelvan a operar, o que el propio tiempo fuera de línea (downtime) pueda generar pérdidas en millones de dólares, un tema preocupante en estos tiempos de competencia destructiva es la pérdida de de credibilidad ante el cliente y la disminución de su confianza ante un evento de degradación del desempeño en las aplicaciones; sobre todo aquellas que pertenecen a la categoría del e-commerce.

Por otro lado, como es bien sabido en esta industria, el costo para corregir un problema crece exponencialmente conforme al tiempo, de acuerdo al momento durante el ciclo de vida de desarrollo en que es descubierto:

Una elección incorrecta durante la fase de análisis puede implicar sólo la modificación de documentos si es detectada a tiempo…

… pero si el mismo problema se descubre hasta la puesta en producción, puede representar un cambio radical en la arquitectura e incluso empezar desde cero.

Sin embargo, ¿Cómo podemos definir la capacidad máxima de nuestra solución? ¿Cómo podemos asegurar que el desempeño de una aplicación sea de acuerdo a lo planeado?

La Administración de Desempeño de Aplicaciones

La Administración de Desempeño de Aplicaciones (Application Performance Management – APM) es una disciplina de la que se desprenden algunas metodologías que aseguran el máximo desempeño de una plataforma a lo largo del ciclo de vida de desarrollo y despliegue. Sus objetivos principales son:

  • Anticipar requerimientos por recursos.
  • Identificar dificultades mientras todavía son problemas potenciales.
  • Implementar la solución apropiada antes de que ocurra una falla.

Nota: dos guías/metodologías de administración e ingeniería de desempeño que recomiendo ampliamente son:

La mayoría de las metodologías de administración del desempeño incluyen desde la definición de la arquitectura condiciones de éxito en función del desempeño que forman parte de los casos de uso de la solución que se está diseñando. Dichas condiciones son conocidas como Service Level Agreements – SLAs y deben incluir parámetros cuantitativos del desempeño esperado de la solución. Por otro lado – y esto es muy importante – dichos parámetros deben ser negociados entre el cliente o usuario final y el arquitecto, para poder filtrar requerimientos poco realistas (por ejemplo: quiero que mis 2 PCs soporten una carga de 5,000 usuarios concurrentes).

Por otro lado, es necesario enseñar a los desarrolladores a probar sus componentes de forma unitaria e integral, incluyendo en dichas pruebas no sólo la funcionalidad, sino también el uso de memoria y eficiencia de algoritmos. Esto se logra mediante tres simples técnicas de análisis:

Nota: estas técnicas están enfocadas a Java/J2EE, pero son aplicables a cualquier lenguaje de programación.

  • Memoria: uso del memory heap y garbage collection.
  • Perfil de código: Eficiencia de los algoritmos.
  • Análisis de cobertura: Qué porciones del código son más ejecutadas que otras.

Así mismo, durante las iteraciones del desarrollo es muy importante que el equipo de QA valide que los casos de uso y pruebas incluyan el SLA correspondiente y que los desarrollos cumplan satisfactoriamente con dichos SLAs; si un componente o módulo no cumple con todos los requerimientos debe regresarse al equipo de desarrollo para su corrección o si se ha detectado que es costoso en términos de tiempo o recursos, notificar inmediatamente al administrador de proyectos y arquitecto para su negociación con el cliente.

Finalmente, previo a la liberación a producción, es necesario comprobar que la solución cumple con los SLAs bajo la carga proyectada. Esto es logrado mediante una evaluación de capacidad y la mayoría de las metodologías proporcionan pasos generales para su implementación:

  • Desempeño bajo carga esperada.
  • Punto en que los SLAs inician su no conformancia.
  • Conforme aumenta la no conformancia con los SLAs, cuál es el patrón de degradación.
  • Cuál es el punto de saturación de la aplicación (cero conformancia con los SLAs).

Hablando en términos de ingeniería, estos puntos son los objetivos de las pruebas de desempeño, carga y estrés:


La curva ideal de volumen contra el tiempo: Conforme aumenta la carga, mejora el rendimiento debido al mayor número de hits en los caches y pools de objetos; al llegar a la capacidad máxima el desempeño permanece estático; al sobrepasarla comienza la degradación del desempeño de forma suave pero constante.

El resultado de estas pruebas debe permitir al arquitecto definir dos puntos:

  • La capacidad máxima en términos de calidad en el servicio (throughput, response time) del sistema (o lo que es lo mismo: hasta qué volumen de carga puede soportar esta plataforma sin que se desesperen los usuarios u ocurra un crashdown).
  • Planear los componentes que serán necesarios para un crecimiento vertical (esencialmente, bajo qué condiciones deberán integrarse más memoria, otro balanceador o un servidor adicional).

El APM desde el punto de vista ingenieril

Una cosa es planear el tuning de la plataforma antes de liberarla a producción y aplicar un monitoreo de ésta para asegurar que el capacity planning fue correctamente implementado; otra y muy diferente es cómo hacer dicho tuning.

Tomando las mejores prácticas de los procesos de performance and tuning – P&T ejecutadas por algunos proveedores de middleware como Sun Microsystems, IBM y Oracle es posible definir tres grandes guías que nos ayudarán a realizar un P&T de forma efectiva:

1. Cualquier aplicación empresarial emplea un modelo de ejecución por capas

Como se ve en el siguiente diagrama:


Las capas de servicios de una aplicación empresarial.

Cuando se realice el tuning de una plataforma, siempre se debe comenzar por la aplicación; una vez que la misma haya sido razonablemente optimizada se puede proseguir con la capa de servicios – el application server – y así sucesivamente hasta la capa de hardware. La razón principal es la dificultad de optimización por capa: mientras buenas técnicas de programación y adherencia a patrones de diseño deberían bastar para hacer que una aplicación en Java funcione como debería, optimizar hardware o el sistema operativo pueden ser toda una hazaña (por no decir que el recurso especializado puede costar una fortuna).

Con el modelo de capas en mente, se desprende el siguiente punto:

2. Debe existir visibilidad sobre todos los componentes tecnológicos de la plataforma

Esto incluye:

  • Cada sistema operativo donde los servicios se están ejecutando.
  • Cada elemento tecnológico de las capas del modelo de ejecución (por ejemplo, frameworks).
  • El desempeño de las tecnologías que dan soporte a la aplicación en ambientes de ejecución no J2EE (por ejemplo, un web service corriendo en .NET).
  • Dependencias externas tales como una base de datos o sistemas de información empresarial (EIS) que puedan encontrarse fuera del entorno de red local.
  • El comportamiento de la red de comunicaciones entre la aplicación y sus servicios.

Finalmente, aunque existe toda una teoría e infinidad de metodologías del tuning basadas en tiempos de respuesta, flujo de información y demás métricas de desempeño, existe un modelo alterno muy sencillo y que es posible ejecutar sin demasiados problemas sobre una plataforma distribuida:

3. El modelo wait-based tuning (WBT)

Dicho modelo fue desarrollado por Oracle para simplificar el tuning sobre sus bases de datos y es soportado nativamente por éstas desde la versión 9i. Presentado por primera vez en 1998 en el Oracle Open World (aquí está el outline de la presentación), es la metodología estándar que emplean en este tipo de servicios (Ver en este documento PDF una descripción un poco más técnica con enfoque en tuning de bases de datos). A grandes rasgos, el WBT está basado en optimizar aquellos puntos a lo largo de la plataforma donde una petición tenga que esperar para ser procesada. Es decir, cuando una petición llega del cliente hacia la plataforma, en algún punto tendrá que ser encolado para esperar a ser procesado (después de todo, un CPU no puede procesar 5,000 usuarios al mismo tiempo, ¿verdad?). En el siguiente diagrama se ve un poco más claro este concepto:


Los puntos de espera de una plataforma web.
[Click en imagen para ver en tamaño original]

Así entonces, tenemos que realizar el tuning sobre los siguientes componentes de la infraestructura:

  • Colas de procesamiento de peticiones.
  • Pools de hilos de ejecución.
  • Pools de recursos (por ejemplo, conexiones JDBC) y componentes J2EE (como el pool de EJBs)
  • Caches de objetos.

El proceso de tuning implica monitorear estos elementos y detectar su punto de saturación; luego disminuimos la capacidad de los puntos de espera precedentes para facilitar sólo la carga máxima sobre el punto de espera que estamos analizando y al final aumentamos o disminuimos la capacidad del web server para reenviar a los "usuarios excedentes" a la página de error. La mejor analogía que se me ocurre es la de las llaves de agua en cascada siendo cada contenedor una tina: si el flujo es demasiado la tina se derrama y si es insuficiente la tina queda vacía.

La semana próxima me refinaré un ejemplo del WBT para que sea más fácil de entender y no se quede como algo abstracto. No lo dejo aquí porque alargaría mucho el post y además, así como lo estoy viendo, implica generar una buena cantidad de diagramitas en Visio que sí se llevan su tiempo. También es conveniente que incluya los "Tips que todo software engineer debe saber para mejorar el performance de su application server y Java Virtual Machine", y se vería algo chispa si lo incluyo en este momento. A ver cómo me va Guiño.