Para comenzar, es necesario introducir algunos términos y conceptos básicos; para ello, recurriremos a un ejemplo de la vida real: un supermercado.
En un supermercado hay clientes caminando entre los pasillos, que compran productos, y pagan en las cajas. Los supermercados generalmente tienen más clientes que empleados, ya que, a cada cliente, le toma tiempo seleccionar los productos, y por tal razón no necesitamos un cajero por cada cliente.
En el mundo del software, las peticiones a la aplicación representan a esos clientes, y ésta utiliza recursos (hardware y software) para manejar esas peticiones.
Carga es el término que representa a todos los usuarios de la aplicación en un determinado momento sin importar su actividad. Nos importa el número total de usuarios, no sólo los que están haciendo peticiones, ya que todos los usuarios consumen recursos. En un supermercado, los clientes ocupan un espacio físico, mientras que en una aplicación ocupan memoria u otro tipo de mecanismo para almacenar el estado de un cliente.
Después de que el supermercado abre, el tráfico de clientes no permanece constante durante el día, sino que tiene horas pico. Una carga de picos se refiere al número máximo de clientes dentro de un período de tiempo. Este número no es un promedio. Al planear una prueba de desempeño, encontrar la carga pico y probar la aplicación contra esta carga es lo más importante.
El rendimiento de procesamiento, o throughput, mide la cantidad de peticiones servidas por unidad de tiempo. En el caso del supermercado, suponiendo que cobrarle a cada cliente toma un minuto, y que hay cinco cajeros, el rendimiento será de cinco clientes por minuto. Esta métrica se caracteriza por tener un límite superior, es decir, no importa cuantos clientes visiten el supermercado, el número máximo de clientes atendidos en un intervalo de tiempo permanecerá constante. Así mismo, si una aplicación recibe 50 peticiones por segundo, pero su rendimiento máximo es de 30, algunas peticiones tendrán que encolarse. Este límite típicamente representa un cuello de botella en la aplicación.
El tiempo de respuesta de una aplicación se refiere al tiempo desde que el usuario inicia una petición hasta que el resultado es visualizado. Hay veces en que en el supermercado hay que esperar en la cola hasta que una caja este disponible. Este tiempo de espera se suma al tiempo requerido para realizar la compra. Por lo tanto, si el cliente espero cuatro minutos, más el minuto que requirió la transacción, el tiempo de respuesta sería de cinco minutos. Entender el tiempo de respuesta de una aplicación requiere entender la relación entre carga, rendimiento de procesamiento, y el mismo tiempo de respuesta. La aplicación alcanza su rendimiento máximo con cierta cantidad de usuarios. Más allá de ésta (punto de saturación), el rendimiento permanece constante.
Sin embargo, el tiempo de respuesta empieza a aumentar. La carga adicional espera a que los recursos saturados se liberen antes de que los pueda utilizar. Entender cuantos usuarios simultáneos soporta la aplicación y cuanto aumentan los tiempos de respuesta en los momentos de mayor tráfico son las principales motivaciones para realizar una prueba de desempeño.
Así que, ¿qué podemos hacer para mejorar el desempeño de la aplicación? Básicamente, se pueden hacer dos cosas: optimizar o escalar la aplicación. La optimización se enfoca en modificar la aplicación para que consuma menos recursos, o para minimizar los cuellos de botella. Las técnicas de escalamiento, por otra parte, se enfocan en añadir recursos, por ejemplo, aumentando el número de servidores que hospedan la aplicación. En el escalamiento vertical se añaden más recursos al servidor, como CPU o memoria (el equivalente a añadir más cajeros en el supermercado), mientras que en el escalamiento horizontal, se añaden servidores para formar un cluster (el equivalente a añadir sucursales del supermercado).
Perfiles de aplicaciones
Una pregunta importante antes de planear una prueba de desempeño es: ¿Qué es lo que la aplicación necesita hacer bien? Obviamente, los requerimientos de varios tipos de aplicaciones son diferentes, y en esa misma forma deben ser sus pruebas de desempeño. Por ejemplo, una aplicación financiera por lo general maneja grandes volúmenes de usuarios e información confidencial y actualizada en tiempo real. Así que en este tipo de aplicaciones no se puede sacar gran provecho de caches de datos. Otra consideración importante es la seguridad. Los mecanismos de cifrado de datos, y otras medidas de seguridad como firewalls tienen un gran impacto en el desempeño. También es necesario registrar la actividad de los usuarios en bitácoras para auditoría o por regulaciones gubernamentales, lo cual impacta el procesamiento en la aplicación.
Por otro lado, las aplicaciones tipo e-Commerce son usadas por los clientes para ver y comprar productos, y usar servicios como estado del envío o cancelaciones. Aunque puede haber una gran cantidad de usuarios concurrentes, solo un pequeño porcentaje realiza una compra. Sin embargo, se debe manejar muy bien la memoria para evitar un consumo excesivo.En este caso, hay que asegurar las partes de la aplicación donde se realicen transacciones (particularmente con SSL), aunque éstas normalmente representan una pequeña fracción del tráfico total.
Por último, en el caso de aplicaciones de administración de información interna en una empresa, un día típico para el usuario puede incluir revisar el estatus del inventario, registrar nuevos productos, o consultar reportes generados en el momento. Por lo general, se conoce la cantidad de usuarios. Las páginas no contienen muchos gráficos pesados, ya que son ayudas de navegación, más que contenido. Los usuarios de estas aplicaciones están dispuestos a aceptar un tiempo de respuesta más lento que los usuarios de otro tipo de aplicaciones, sin embargo, no por eso el desempeño pierde importancia.
Scripts de prueba
Los scripts dirigen la prueba de rendimiento ya que simulan una serie de peticiones que el usuario realiza cuando utiliza la aplicación. Para desarrollarlos se necesita entender el comportamiento de los usuarios. Representar de manera precisa a los usuarios en los scripts de la prueba tiene gran influencia en el rendimiento y carga de la misma prueba y sus resultados.
Durante la prueba, el mismo script es ejecutado varias veces simulando múltiples usuarios. Para simular las diferentes opciones presentadas por la aplicación, se utiliza un conjunto de scripts. Éstos forman un escenario de prueba.
Típicamente, se utiliza una herramienta para grabar las actividades de un usuario y generar el script automáticamente (incluyendo URLs de visita, tiempos de espera entre peticiones, cookies, etcétera).
Las siguientes recomendaciones se deben tomar en cuenta para desarrollar scripts:
•Desarrollar scripts pequeños. Esto resulta en mayor flexibilidad para definir escenarios de pruebas (en vez de escribir un script grande que abarque varias actividades) ya que los scripts pequeños permiten asignar diferente importancia a diferentes actividades y facilitan el mantenimiento.
•Escribir scripts atómicos. Por ejemplo, no hay que incluir en cada script las funciones de login y logout, sino separarlos y hacer referencia a ellos desde los demás scripts. Esto permite elaborar escenarios complejos para los usuarios virtuales, como el siguiente: Login – Consulta – Ver Detalle – Imprimir Reporte – Modificar – Logout.
•Permitir datos dinámicos. Durante la prueba, varios usuarios virtuales ejecutan el mismo script. Ya que no podemos construir un script diferente por cada usuario que se simule, tenemos que paremetrizar el script para que represente varios usuarios. Por ejemplo, en un script de Login se puede variar el nombre de usuario. Los parámetros requieren suficiente variación para reproducir exitosamente la actividad de los usuarios.
Ejemplos de datos dinámicos comunes son los siguientes:
• Identificador (de usuarios, registros, productos, etc).
• Datos ingresados por el usuario.
• Parámetros de búsqueda.
• Cantidades.
• Fechas y horas.
Lo importante es generar una cantidad razonable de datos o selecciones dinámicas y no caer en la trampa de que los usuarios interactuarán con la aplicación justo como los diseñadores lo planearon. Un ejemplo claro, es la función de logout. Los usuarios no saben como salir (logout) de la aplicación, no importa que tan grande sea el botón para hacerlo. Por esto, no se puede depender de que los usuarios salgan correctamente del sistema, y se deben desarrollar los scripts tomándolo en cuenta.
Planeación de la prueba
Antes de empezar las pruebas de desempeño, es necesario realizar las pruebas de funcionalidad e integración para asegurase de que la aplicación funciona correctamente. El comportamiento errático puede exhibir mejor o peor desempeño que la versión correcta.
Comenzamos definiendo objetivos claros de desempeño para la aplicación. Ésta puede contener una infinidad de cuellos de botella, y si no hay objetivos claros, nunca sabremos cuando hemos llegado a un nivel aceptable de desempeño.
Decir que un caso de uso debe completarse en “alrededor” de cinco segundos no es muy útil. Se debe enunciar un valor exacto que permita verificar el desempeño de la aplicación. Los objetivos deben ser flexibles también, en el sentido en que puede haber variación entre ejecuciones. Por ejemplo, ¿es aceptable que el caso de uso se ejecute en tiempo el 90% de las veces? ¿Es aceptable que el tiempo de respuesta sea de 5 segundos (cuando se espera sea de 3) una de cada 10 veces?
A continuación procedemos a estimar los datos necesarios para empezar la prueba. Asumiendo que la aplicación recibirá 1,000 peticiones por día (un período de ocho horas), tenemos que:
1,000 peticiones por día / 8 horas por día =
125 peticiones/hora
Esto nos da una distribución uniforme sin considerar los picos, así que, asumiendo un pico de cinco veces el promedio tenemos:
125 peticiones/hora * 5 = 625 peticiones/hora
Convertido a segundos:
625 peticiones/hora / 60 min. por hora /
60 seg. por min. = 0.17 peticiones/seg.
A continuación, asumiendo:
•1,000 usuarios por día y 30% de ellos utiliza la aplicación durante la hora pico.
•La visita promedio de un usuario dura 10 minutos.
•El usuario utiliza en promedio 5 páginas por visita.
•Cada sesión por usuario requiere 20 KB de memoria.
•El tiempo de inactividad para eliminar la sesión es de 15 minutos.
Podemos sacar los siguientes cálculos:
1,000 usuarios * 30% de usuarios pico = 300 usuarios pico/hora (0.0833 por seg)
La cantidad de usuarios concurrentes la obtenemos multiplicando los usuarios por segundo, por la duración promedio de cada sesión (10 minutos).
0.0833 * 10 * 60 seg. por min. = 50
Dado que cada usuario visita en promedio 5 páginas en un periodo de 10 minutos, y tenemos 50 usuarios concurrentes, podemos estimar cuantas páginas debemos procesar por segundo:
(50 usuarios * 5 pág.) / (10 min. * 60 seg) = 0.416 pág. por seg. (redondeamos a .42).
Si cada página contiene 10 elementos, las peticiones al servidor (por cada elemento y sin tomar en cuenta caches) son:
((1 petición inicial) + (10 peticiones de elementos)) * 0.42 pág./seg. = 4.62 peticiones/seg.
En cuanto a los requerimientos de memoria para soportar a los usuarios:
0.0833 usuarios/seg. * (10 min. por usuario activo + 15 de inactividad) * 60 seg. por min. = 124.95 usuarios (aprox. 125)
125 usuarios * 20KB/usuario = 2.4 MB
Proceso y análisis
El éxito de la prueba depende de la recolección y análisis de datos y los cambios para mejorar el desempeño de la aplicación. El primer paso es ejecutar una prueba de desempeño simulando algunos usuarios y tal vez en un solo servidor. Durante la prueba hay que recolectar datos acerca de los clientes, los servidores y otros componentes usados.
Después, hay que análizar esos datos antes de seguir con la siguiente prueba. Sin embargo, se recomienda ejecutar la misma prueba dos o tres veces sin hacer cambios y comparar los resultados. ¿Las pruebas produjeron más o menos los mismos resultados? Si la respuesta es afirmativa, se puede proceder a las optimizaciones. En caso contrario (si la variación es mas del 10%), hay que corregir esto antes de continuar. Algunas posibles causas pueden ser:
•Probar en un ambiente compartido.
•Ejecuciones cortas. Hay que darle suficiente tiempo a la prueba, es decir, “calentar motores”.
•Crecimiento en la base de datos. La aplicación probablemente creo y/o actualizo muchos registros durante la prueba y esto hace que se degrade el desempeño. En este caso, hay que considerar scripts que limpien la base entre ejecuciones, o averiguar si este comportamiento será normal en producción.
Después de analizar los datos y posiblemente identificar cuellos de botella, es tiempo de optimizar. Idealmente, se debe hacer solo un cambio, ejecutar la prueba de nuevo y analizar los datos antes de hacer otro ajuste o seguir con otra prueba. Esto ayuda a medir de forma aislada, el impacto del cambio en la aplicación.
Teniendo en cuenta todo lo anterior, las pruebas de desempeño se deben conducir en los siguientes puntos del ciclo de desarrollo:
•Pruebas unitarias. Estas deben ser realizadas por cada desarrollador antes de liberar sus componentes y significa que éstos deben ser analizados en cuanto a memoria y código, por lo general con al menos 10 usuarios, solo para validar los tiempos de respuesta objetivo.
•Pruebas de integración de la aplicación. Esta prueba genera una carga con el número de usuarios proyectados que se espera que la aplicación soporte en producción.
•Pruebas de producción. Aquí se prueba en un ambiente lo más similar al que va a estar trabajando la aplicación en producción.
Datos
La prueba de desempeño es tan buena como los datos que produce. Los siguientes datos son aquellos que siempre se deben recolectar en la prueba de desempeño:
•Utilización del CPU. Este dato se debe medir en todos los sistemas involucrados en la prueba para distinguir posibles cuellos de botella.
•Datos de la Red. Aquí se puede utilizar el comando netstat. La medición se debe tomar antes y después de la prueba y la diferencia es la cantidad de tráfico que ésta generó.
•Monitoreo de las bitácoras. Éstos reflejan los problemas ocasionados por la carga o sistemas de soporte. El nivel de registro, debe estar en niveles aceptables. Demasiada información es difícil de manejar y además impacta negativamente
al desempeño.
•Monitoreo de Servidores. Algunos servidores cuentan con herramientas que permiten monitorear sus recursos internos, pero también hay que tomar en cuenta que algunas impactan el desempeño, por lo que hay que utilizarlas con moderación.
Problemas comunes
Entre los problemas más comunes que se descubren en las pruebas de desempeño están:
•Baja utilización de los recursos. Los clientes generan baja utilización del CPU y el tiempo de respuesta aumenta al tiempo que se agregan usuarios, mientras que el rendimiento permanece igual. La causa principal es un recurso saturado en el sistema (la red, la base de datos, un sistema de soporte, etc.)
•Insuficiente capacidad en la red. Este problema es frecuente en aplicaciones que regresan una respuesta más grande que la anticipada en el diseño. Hay dos soluciones, aumentar la capacidad de la red o reducir la cantidad de datos que se transfiere por la red.
•Recursos insuficientes. La mayoría de las ocasiones esto se debe a la base de datos o una aplicación de soporte. Una solución podría ser solucionar los problemas de desempeño de ese recurso (agregando CPUs, índices, etc.). Otra, es incrementar el número de conexiones o accesos a ese recurso.
•Alta utilización del CPU. Un CPU saturado no necesariamente significa que el servidor no puede soportar carga adicional. Los usuarios sólo conocen el tiempo de respuesta. Un servidor saturado puede continuar soportando una intensa carga mientras el tiempo de respuesta permanezca aceptable (si la cola permanece lo suficientemente corta). Además, es útil distinguir entre la utilización del CPU y el tiempo en que el CPU espera a algún recurso saturado.
•Balanceo de carga desigual. Esto sucede en algunos clusters donde uno o más servidores reciben una cantidad desproporcionada de tráfico con respecto a los demás. Aquí se requiere configurar el balanceador de carga a un algoritmo óptimo.
Planeación de capacidad
La planeación de capacidad nos dice que tanto hardware y software se necesitan para el correcto desempeño de la aplicación basándose en los resultados de las pruebas de desempeño. También nos proporciona un plan de crecimiento y actualización sobre un periodo definido
de tiempo.
Esta planeación siempre se debe basar en los picos, para que se puedan soportar. Este ejercicio combina los objetivos de la prueba con los resultados para determinar la combinación y número apropiado de servidores. Enfocándose en los datos de escalabilidad y utilización de CPU, se construye los planes de capacidad inicial. Después, ya que la aplicación este funcionando, se compara el desempeño real con el de los planes. Si se encuentran diferencias, hay que ajustar los planes
de capacidad.
También se tiene que decidir cuanto se quiere de colchón, en caso de que haya una equivocación al proyectar la carga o para permitir un crecimiento en el futuro.Idealmente, hay que tratar de clasificar la aplicación en una de las siguientes categorías:
•Extremadamente debajo de su capacidad. La aplicación puede soportar más de 50% de capacidad adicional. En este caso, se puede considerar reducir recursos de hardware o servidores y/o para evitar costos de licencias.
•Debajo de su capacidad. La aplicación puede soportar fácilmente más de 25% de capacidad adicional. En este caso, se puede estar tranquilo en cuanto a que la aplicación cumplirá sus objetivos, y no hay razón para quitar recursos a la aplicación.
•Cerca de su capacidad. La aplicación cumple los objetivos de desempeño, pero menos de 25% arriba de su capacidad adicional. En este caso, se necesita platicar con el cliente para determinar posibles cambios futuros que puedan impactar en el desempeño y en base a esto, ver si se justifica añadir recursos.
•Sobre su capacidad. La aplicación no cumple con los objetivos de desempeño, y hay que evaluar si vale la pena optimizarla o escalarla.
•Extremadamente sobre su capacidad. La aplicación se satura con la carga esperada. Se necesita optimizar seriamente la aplicación y posiblemente agregar más hardware para satisfacer la demanda.
Conclusiones
Las pruebas de desempeño son una actividad importante del desarrollo de sistemas. Desafortunadamente, su uso no es muy extendido. Por medio de una analogía, vimos que básicamente los mismos problemas encontrados en el mundo real, también evitan que las aplicaciones de software alcance su máximo potencial en cuanto a desempeño. Finalmente, cabe recalcar que una prueba de desempeño es un proceso iterativo. Se prueba, recolectan datos, se verifican, analizan y se optimiza. Este proceso ocurre repetidamente hasta que se alcancen los objetivos de todos los escenarios de prueba.
- Log in to post comments