Trucos para añadir pruebas de performance con Taurus a tu pipeline de Integración Continua

Hace tiempo que trabajo en testing de performance, y ya varios años que me ha tocado participar en proyectos donde se implementa Continuous Integration y Continuous Delivery. Tuve la suerte de poder participar en un proyecto para un cliente con una realidad bastante exigente en cuanto a requerimientos de performance, y donde implementaban CI/CD incluyendo pruebas de performance en su pipeline. En particular algo bien interesante en esto, es que uno no está ejecutando pruebas de carga en cada nuevo build, no estamos simulando la carga esperada del sistema, sino que a las pruebas de performance en el pipeline le ponemos otro foco. El foco en este caso está en detectar degradaciones de performance lo antes posible. Apuntamos a feedback inmediato y a bajo costo (ejecutar una prueba de carga que simule la realidad esperada es cara ya que se requiere contar con infraestructura similar a la de producción y de acceso exclusivo, lo cual no sería fácil tenerlo para cada momento en que se quiera hacer un build en un enfoque de CI/CD). ¿Cómo logramos esto entonces? pues ejecutando pruebas más puntuales, fijas, de menor carga, pero que siempre hagan una misma validación, revisando que los resultados no tengan desviaciones que hagan que se “pierda” performance.

 

Para protocolos abiertos como HTTP/S existen cientos de herramientas. Una de las más utilizadas es JMeter, ya que cuenta con una gran popularidad, la comunidad Apache le da respaldo, y además de HTTP soporta una gran variedad de protocolos. El hecho de que herramientas como BlazeMeter, Visual Studio Team Services, Soasta, y tantas más, de las que ejecutan las pruebas de performance desde el Cloud, la utilicen como formato de prueba, convierte a JMeter en el estándar de facto para las pruebas de performance. Desde mi punto de vista el punto flojo de esta herramienta es que el lenguaje que brinda para automatizar es un lenguaje gráfico, lo cual no es lo más deseado para los programadores. Además, como este lenguaje gráfico se guarda en un formato xml, esto no lo hace CI/CD-friendly, ya que si quiero ver los cambios entre una versión y otra, mirar las diferencias del xml no va a resultar nada práctico.

 

Es por esto que herramientas como Gatling son de mi preferencia para este tipo de pruebas integradas a nuestro motor de integración continua, ya que las pruebas se programan en código (en este caso, en el lenguaje Scala). Otra opción, y creo que más alineada al desarrollador moderno que le gusta programar en un editor de texto minimalista y de fondo negro, es utilizar Taurus. Taurus es una herramienta opensource desarrollada por Blazemeter, que permite especificar las pruebas de performance en formato yml, en archivos de textos bien simples (¡en este mismo blog Aritz ya ha hablado de Taurus!). Esta herramienta solo sirve para especificar el script, y luego para ejecutarlo lo que hace es convertirlo a un script JMeter (o a alguna de las 8 o 9 herramientas con las que es compatible), logrando recolectar los resultados y generando una interfaz gráfica liviana (a diferencia de JMeter) para ver los resultados en vivo, así como también enviando los datos a BlazeMeter para poder contar con un reporte HTML en vivo, el cual realmente queda muy bien (y para usarlo no necesitamos pagar nada, es más, ni siquiera necesitamos tener una cuenta en BlazeMeter).

 

Al trabajar con Taurus, en este enfoque de CI/CD, hay un “truco” que a mi ver es bien fácil de implementar y que da muy buen resultado. Básicamente, la idea es dividir todo el archivo yml de Taurus en tres archivos:

  • Uno con el flujo que estamos simulando, o sea, el caso de prueba, la secuencia de requests.
  • Otro para la especificación del escenario, o sea, qué carga queremos simular, cuántos threads, etc.
  • Y por último, uno donde indiquemos cuáles son los criterios de aceptación.

 

Aquí puedes ver un ejemplo:

Así tendremos tres archivos que nos permitirán ejecutar la prueba con el siguiente comando

bzt runner.yml criteria.yml load.yml

En este proyecto en Github tengo un ejemplo súper básico, pero que muestra esta idea de dividir los scripts, y puedes ver cómo organizarlos: https://github.com/fltoledo/taurus_simplest_example

 

Lo que nos permite esto es lo siguiente:

  • Facilitar la lectura, entendimiento y mantenibilidad de las pruebas.
  • Utilizar el mismo script (flujo, caso de prueba) en distintos ambientes, donde quizá lo que cambia es la carga que le quiero ejecutar, y así también los criterios de aceptación.

Con respecto a este segundo punto, creo que es fundamental poder utilizar el mismo script de pruebas, y tener distintos archivos que especifique distintas cargas y criterios de aceptación. Por ejemplo, en el ambiente de test quiero ejecutar una carga bien chica y puntual, pero en staging me interesa ejecutar una carga mayor, ya que el hardware es más similar a producción, y así los criterios de aceptación serán más exigentes también.

Otro aspecto que considero que es importante, está relacionado a qué métricas definirán los criterios de aceptación. Acá lo que tengo para aportar es lo siguiente:

También será importante definir la carga a ejecutar, y esto lo dejé explicado en este post que te invito a leer aquí.

Por último, considero beneficioso tener el código de pruebas en el mismo repositorio de código que el sistema, en un folder específico, pero lo importante es que me facilite la trazabilidad entre las pruebas y lo probado.

 

Resumiendo (TL;DR), creo que Taurus es una excelente herramienta para integrar pruebas de performance a nuestro pipeline, obteniendo así feedback temprano sobre la calidad de los cambios que se van introduciendo al sistema, específicamente en cuanto a velocidad. Para lograrlo en forma efectiva, te compartí por aquí algunos trucos que he ido recolectando en estos últimos años en diversos proyectos, y me gustaría también que me compartas tus experiencias al respecto. Puedes hacerlo en este post, en mi blog en esta entrada, o me encuentras en Twitter acá. Si quieres saber más sobre mí puedes hacerlo leyendo aquí.

Pruebas de rendimiento y rastreo mediante Spring Cloud Sleuth y Zipkin

Introducción

Siguiendo por el hilo del post anterior en este artículo me gustaría tratar sobre el tracing de los servicios. Cuando hablamos de “tracear” los microservicios, estamos hablando de rastrear todas las llamadas y respuestas que se hacen entre los diferentes servicios de nuestro sistema.

Tener un control sobre cada llamada es muy importante ya que nos aporta una visibilidad en tiempo real del estado de cada petición y respuesta que se da, de tal forma que no perdemos ningún detalle de lo que ocurre (tipos de peticiones y respuestas, tiempos y errores) entre nuestros servicios.

En nuestro caso vamos a hacer las pruebas con zipkin como sistema de rastreo y para nuestros servicios vamos a hacer uso de Spring Cloud Sleuth que es la herramienta que nos aporta Spring Cloud para instrumentar sus servicios.

Podéis descargar el proyecto de pruebas como siempre desde mi GitHub.

He de decir que cuando desarrollé el proyecto de pruebas lo hice sobre la versión 1.3 de Sleuth, ahora se encuentran en su versión 2.0. Si queréis migrarlo os dejo el siguiente enlace.

No solo me gustaría centrarme en el uso de Zipkin o Sleuth, si no, que este post viene dado por un debate que tuve con mis compañeros de equipo (y que luego trasladé a Federico Toledo), sobre la mejor estrategia para probar el rendimiento de los servicios públicos e internos.

Entendamos públicos como aquellos que están abiertas al consumo exterior e internas como aquellas que solo son consumidos por otros servicios.

Mi opinión es que el mejor approach era realizar las pruebas de rendimiento sobre los servicios públicos, ya que estos internamente iban a llamar a los servicios internos, siempre y cuando tendríamos alguna forma clara de rastrear y obtener los datos de los tiempos y el rendimiento de los servicios internos.

Bien, ¿Y qué tal si usamos zipkin y sleuth y así vemos un ejemplo de esto mismo? Vamos a ver qué conclusiones sacamos.

De forma sencilla, como funciona Sleuth y Zipkin

Veamos un ejemplo del rastreo de una petición para ver y entender la información recogida. En la imagen de abajo vemos dos trazas. Las trazas están compuestas por spans (lo veremos ahora) y muestran la información del “recorrido” de la petición, es decir, lanzando una petición, porque servicios hemos pasado.

Traza zipkin
En este caso hemos lanzado una petición a purchase-service que internamente ha llamado a inventory y account-service. Si accedemos a la información de la traza, lo vemos con más detalle.

Lo que vamos a ver son los spans, es decir, la unidad de información que contiene los datos de una request/response, los tiempos etc. Vamos a tener un span por cada petición y respuesta que se haya dado dentro de la traza. Cada traza va a tener asociados una serie de spans, por lo que en nuestro caso los spans A, B y C estarán asociados a la traza T.

Zipkin spans

span info
Vamos a acceder al span de inventory-service para ver su información con más detalle:

Las anotaciones son los registros de los eventos de la petición. En este caso se puede ver los tiempos en los que el cliente ha realizado la petición (CS), esta se ha recibido (SR), se ha respondido (SS) y cuando el cliente ha obtenido la respuesta (CR).

Por otro lado, en la tabla inferior vemos información más detallada sobre la petición en cuestión. La key get-inventory-item y su valor se ha definido en la instrumentación del código, para aportar mayor información en el span.

Si hubiera algún error, veríamos los detalles del error en este punto:

zipkin error

Dado que creo que es un tema muy interesante, os recomiendo el siguiente enlace enlace a la documentación de sleuth, donde se profundiza mucho más en la terminología y en cómo trabaja con zipkin.

Entendiendo el proyecto de pruebas

 

microservice

El proyecto con el que vamos a trabajar consiste en tres servicios: Purchase, Inventory y Account.

Cuando llamemos a la API de purchase-service para comprar un item esta llamará a inventory-service y account-service para obtener los datos necesarios para la compra.

Vamos a instrumentar las APIs de los tres servicios y atacaremos a purchase-service, dado que internamente llamará a los otros servicios.

Si queréis investigar sobre el proyecto de pruebas, veréis que Inventory-service también expone una API que llama internamente a account-service para obtener información.

Preparando el proyecto

Lo primero que vamos a necesitar es instalar zipkin en nuestro equipo. Yo recomiendo hacer uso de docker e instalar la imagen de zipkin. Os dejo un enlace para hacerlo.

Ahora tenemos que configurar nuestro proyecto e instrumentar las APIs para rastrear las llamadas. Lo primero es añadir las dependencias necesarias en nuestro build.gradle (en nuestro caso haremos uso de gradle).

Vamos a añadir la configuración para conectar a nuestro zipkin local. Para ello en el fichero application.properties de src/main/resources añadimos las siguientes propiedades:

spring.zipkin.baseUrl: Url de nuestro zipkin.

spring.sleuth.sampler.percentage: La cantidad de peticiones que se rastrean. Por defecto es el 10% (0.1), como en nuestro caso no vamos a saturar el sistema ya que es un proyecto de pruebas vamos a rastrear el 100% de las peticiones.

sample.zipkin.enabled: Cuando lo tenemos a true, las trazas se envían a zipkin, en nuestro caso es lo que queremos. En caso contrario se enviarán a la consola de logs.

Vamos a ver un ejemplo básico de como instrumentamos la API de purchase. Vamos al controlador y al método purchase que será donde accedamos cuando llamemos a /purchase. Creamos un nuevo tag, añadiendo información que nos resulte interesante de rastrear.

Haremos lo mismo para inventory-service y para account-service:
https://github.com/aaguila/spring-cloud-sleuth-zipkin-example/blob/master/inventory-service/src/main/java/com/qajungle/controllers/InventoryController.java

https://github.com/aaguila/spring-cloud-sleuth-zipkin-example/blob/master/account-service/src/main/java/com/qajungle/controllers/AccountController.java

En este caso la información que metemos en el tag tiene menos sentido debido a que en la petición ya tenemos el id. Para la versión 2.0  la forma de gestionar los spans cambia, ya que ahora en vez de usar Sleuth hace uso de Brave. Podéis verlo en la guía de migración.

Con la configuración básica que hemos añadido al proyecto ya podríamos rastrear las peticiones. Vamos a ver qué resultado obtenemos.

Rastreando las pruebas de rendimiento

Lo primero que vamos a hacer es arrancar zipkin para que comience a rastrear las peticiones de nuestros servicios. Para ello basta con arrancar el docker y ejecutar el comando:


docker run -d -p 9411:9411 openzipkin/zipkin

Para nuestro ejemplo vamos a configurar nuestras pruebas en taurus ya que creo que es una herramienta que hemos usado con anterioridad en el blog. Vamos a crear un script sencillo para generar una carga durante cinco minutos de diez peticiones por segundo con cinco usuarios concurrentes que aumentaran cada minuto. Podéis verlo en el directorio de purchase-service src/test/resources/taurus-tests

Tras lanzar las pruebas vemos que las peticiones han ido correctamente, sin ningún error y con un tiempo de respuesta estable, tiene sentido debido a que no hemos generado mucha carga.

Purchase load test

Claro… esto como hemos comentado en la introducción, nos da el reporte de las llamadas a purchase-service, pero no podemos diferenciar que parte del tiempo de respuesta corresponde a que las llamadas a las APIs internas, o en caso de que hubiera un error en una API interna cual sería etc. Perdemos cierta visibilidad, y aquí es donde entra el rastreo de las peticiones, que nos dará mucha más información.

En las siguientes imágenes vemos dos peticiones a la API de purchase:

  • La primera se ha realizado al comienzo de la carga, cuando la carga era un usuario y una petición por segundo.
  • La segunda se ha realizado al final de la carga, cuando la carga eran cinco usuarios concurrentes y diez peticiones por segundo.

1s trace

9s traceMediante zipkin vemos de forma desglosada cuanto tiempo se lleva cada API, por lo que en caso de que el tiempo de respuesta sería muy alto o estaría fuera de los límites que hayamos previsto, podríamos ver si el problema está en alguna API interna. O en el caso de que haya algún error en la llamada podríamos tener más información.

Conclusiones

Instrumentar nuestros servicios webs y así poder rastrear las peticiones nos aporta información de gran interés. Ya no solo para controlar el estado de cada petición y respuesta, si no, que puede ser una gran herramienta para realizar pruebas de rendimiento si nuestro sistema se compone de servicios externos e internos.

En el caso de nuestro proyecto nos hemos ahorrado el realizar pruebas de rendimiento para dos servicios internos de forma independiente, pero en el caso de un sistema complejo basado en microservicios, el valor puede ser mucho mayor, ya que las pruebas son más realistas, nos ahorramos pruebas, tiempo etc.

Rest API performance testing con Taurus

Introducción

En el post vamos a tratar paso a paso como comenzar a lanzar pruebas de rendimiento sobre Rest API con Taurus, ejecutar una prueba y analizar los resultados. Por si queréis realizar las pruebas vosotros también os proporciono el código que podréis descargar de GitHub.

Durante los años que he trabajado con pruebas de rendimiento he podido probar varias herramientas de pago y libres. Desde hace unos pocos meses suelo hablar con Federico Toledo, sobre el mundo del testing en general, pero sobre todo le suelo hacer consultas sobre pruebas de rendimiento.

Entre varias recomendaciones, salió Taurus. Taurus es una herramienta open source que nos permite automatizar de forma sencilla nuestras pruebas. Lo interesante de Taurus es que las pruebas se definen mediante YAML, lo que hace que los teses sean más legibles y fáciles de escribir.

Otra característica interesante de Taurus es que nos permite ejecutar pruebas de otras herramientas como JMeter, Gatling, Selenium, JUnit etc. Podéis encontrar la lista en el siguiente enlace.

Entorno de pruebas

Vamos a levantar un servidor Rest API sobre el que vamos a lanzar las pruebas de rendimiento, Json-server.

Lo ideal sería tener una máquina con el servidor y otra con la ejecución de la prueba, si esto no os es posible, o si quereís hacer todo más rápido con fines de aprender, está bien instalar todo junto. No perdamos de vista que al momento de ejecutar pruebas de verdad esto lo debeis separar, para que el sistema y la herramienta de generación de carga no compitan por recursos.

Por lo que si queréis experimentar conmigo, podéis ver como instalarlo en el enlace anterior. 🙂

Objetivo de las pruebas

  1. Aplicación
    Vamos a imaginarnos que tenemos una aplicación en la que almacenamos datos de los posts publicados. Disponemos de una API Rest para que los consumidores puedan listar los posts o buscar un posts concreto. Por otro lado internamente usamos la API para publicar nueva información.
  2. Carga estimada
    Tras un análisis inicial para saber el número de peticiones por minuto (througput) que esperamos en nuestra API se estima que en el peor de los casos puede ser de:
    – 1200 rpm (peticiones por minuto) y una concurrencia de 20 usuarios en el caso del listado de posts (GET: /posts)
    – 1 rpm y una concurrencia de 1 usuario en el caso de la publicación de posts (POST: /posts)
    Lo ideal sería que el tiempo de respuesta fuera inferior a 40 ms.
  3. Que probar
    Tras el análisis inicial debemos saber cual es nuestro objetivo real de las pruebas. Podemos realizar pruebas de rendimiento para ver el porqué la aplicación no responde en un tiempo aceptable, realizar pruebas periódicas para ver que el tiempo de respuesta no se degrada, hasta pruebas para ver el número de máquinas que hacen falta para soportar una carga esperada.
    En este caso vamos a comprobar que para la carga esperada nuestra aplicación responde en un tiempo de respuesta aceptable y sin errores, es decir load testing o pruebas de carga.

Definiendo los casos de prueba

Vamos a configurar la prueba con dos escenarios diferentes:

1.- Escenario de peticiones al listado de posts. 20 usuarios concurrentes con un throughput de 20 rps (en Taurus el throughput se define por segundos), de tal forma que 20 * 60 = 1200 rpm.

2.- Escenario de peticiones al listado de posts. 1 usuario concurrente con un throughput de 1 rps.

En ambos casos para poder ver si el tiempo de respuesta se degrada con el tiempo vamos a ejecutar la prueba durante 1 hora, con una rampa de usuarios de 5 minutos, es decir, cada minuto se añadirán 4 usuarios, de tal forma que cuando pasen los 5 minutos tendremos la concurrencia de 20 usuarios.

La configuración de la prueba es bastante legible, no obstante os dejo el siguiente enlace donde podéis ver la sintaxis de configuración.

Ejecutando las pruebas

En la configuración de las pruebas de carga los dos escenarios se encuentran en la misma configuración “load-api-testing.yml”. Vamos a ver como ejecutarlos.

Para ejecutar las pruebas de carga tenemos que ejecutar el siguiente comando dentro del directorio de las pruebas:

bzt load-api-testing.yml 

taurus execution

Taurus nos proporciona un visor por consola aportando la información básica del estado de la prueba en vivo.

El panel izquierdo de la imagen nos muestra las peticiones que se hacen sobre el servidor api rest que hemos levantado para realizar las pruebas.

Taurus mediante su configuración nos permite subir los resultados al visor de resultados de nuestra cuenta de Blazemeter. Os dejo el siguiente enlace para que veáis como se configura.

 

Para conseguir unos datos objetivos sería deseable ejecutar cada prueba un mínimo de tres veces, siempre y cuando dispongamos de tiempo para poder realizarlas. Esto es normal ya que en una única ejecución hemos podido encontrarnos con problemas externos que pueden desprestigiar los resultados.

Analizando los resultados

Como hemos comentado anteriormente Taurus nos permite subir los resultados al visor de nuestra cuenta de Blazemeter. Vamos a analizar los resultados de nuestra prueba centrandonos en los datos más importantes para el objetivo del post.

Reporte temporal

grafico resultados

A simple vista se ve que el tiempo de respuesta va aumentando con el tiempo, superando incluso el límite que habíamos definido como ideal, RT < 40ms. Esto puede ser debido a dos motivos:

  1. Con el tiempo el sistema se degrada debido a la concurrencia de 50 usuarios.
  2. Debido que metemos un nuevo dato cada segundo, la base de datos crece,  y por lo tanto el tiempo de respuesta aumenta.

Para descartar la primera opción se debería hacer una prueba sin añadir datos cada segundo a la base de datos, de esa forma veríamos si el tiempo de respuesta se mantiene en un rango aceptable.

No obstante si ese fuera el caso, a futuro ibamos a tener el mismo problema ya que en algún momento ibamos a tener ese número de datos en la base de datos, y llevaría a picos en el tiempo de respuesta, como podemos ver entre las 17:05 y 17:15.

Estado de las peticiones

grafico resultados

Hemos visto el gráfico de datos, esto nos ayuda a ver la estabilidad del entorno en las pruebas, ahora vamos a analizar los datos del tiempo de respuesta.

En muchas ocasiones valoramos los datos por la media (avg), en este caso la media del tiempo de respuesta (27ms). Si únicamente nos fijaramos en la media nuestra prueba sería exitosa, y es ese el caso?

En la pestaña de Request Stats nos dan los resultados de los percentiles. Los percentiles nos muestran una medida bajo la cual se encuentra un porcentaje de la muestra, es decir, en nuestro caso el p95 (95%) de la muestra se encuentra por debajo de 73 ms.

Como vemos los resultados no son lo que esperábamos, si lo que deseamos es que la mayoría de lo usuarios tengan un tiempo de respuesta menor que 40 ms. Dista bastante de la media de 27ms.

Os dejo un enlace muy interesante sobre las métricas para performance testing:

https://www.federico-toledo.com/promedio-desviacion-estandar-y-percentiles-metricas-para-testing-de-performance/

Errores

errores

Si volvemos a ver la gráfica del reporte temporal podemos ver que sobre las 16:45, cuando el entorno comienza a desestabilizarse, empezamos a tener picos de errores.

Si vamos a la pestaña de errores podemos ver el número de errores. En el caso de nuestras pruebas la mayoría de errores vienen debido a problemas de conexión o respuesta con el servidor, muestra de que no tolera la carga.

Conclusiones

A pesar de que disponemos de una gran cantidad de herramientas, Taurus nos permite definir las pruebas de forma muy sencilla y reutilizable. Nos aporta los datos necesarios para obtener unos resultados concretos.

No lo he comentado anteriormente pero Taurus se puede integrar de forma muy fácil a nuestro CI, os dejo el siguiente enlace.

Por otro lado, teniendo claro el objetivo de nuestras pruebas y con un buen análisis de los resultados podemos llegar a conclusiones acertadas.