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.

Consumer-driven contract testing con Spring Cloud Contract

Introducción

Como hemos comentado en entradas anteriores, la arquitectura en microservicos cada vez está ganando más fuerza, y esto supone un reto tanto para el desarrollo como para el testing. En el post vamos a hablar sobre Consumer-driven contract (CDC) testing y cómo podemos verificar que los microservicios trabajan correctamente y según lo esperado entre ellos.

Para ello, como siempre vamos a trabajar con un proyecto de ejemplo usando Spring Cloud Contract, que podréis descargar del repositorio de GitHub.

Consumer-driven contract (CDC) testing

Conociendo la arquitectura en microservicios sabemos que el mayor reto consiste en comprobar y validar la comunicación entre los distintos servicios y para ello las estrategias más comunes son ejecutar pruebas end-to-end o crear mocks de los servicios y lanzar pruebas de integración sobre ellos.

MicroservicesCon las pruebas end-to-end vamos a comprobar la comunicación entre los servicios simulando una experiencia más cercana a el entorno de producción. Primero debemos tener levantado todo el entorno (servicios, BBDD) lo que es costoso y dificulta la trazabilidad. Por otro lado, es fácil que los problemas de comunicación hagan que nuestras pruebas se ralenticen más de lo normal o que fallen. Las dependencia de tantos componentes debilitan nuestras pruebas.

Como hemos visto en un post anterior, mockear un servicio nos elimina los problemas anteriores. Por el contrario, puede que las pruebas contra el mock sean satisfactorias, pero en el entorno de producción la aplicación falle debido a un mal diseño de los mocks.

 

 

cdc testingCon Spring Cloud Contract diseñaremos los contratos de los servicios. El contracto sencillamente es la definición del comportamiento de nuestro servicio en base a una request. Con esa definición se van a generar teses a nivel de proyecto para validar los contratos, así como, se publicará el Stub en nuestro repositorio maven contra el que lanzaremos las pruebas de los clientes del servicio (como vemos en la imagen). No obstante, lo vamos a ver ahora y quedará mucho más claro.

Entre los beneficios que hemos visto, utilizar CDC testing con Spring Cloud Contract nos aporta:

  • Poder desarrollar nuestro servicio mediante metodología ATDD.
  • Una documentación actualizada y “honesta” de las APIs que conforman nuestro sistema.
  • Disponer de un mayor control de los clientes que consumen nuestros servicios. Debido a que sabremos quien realiza las pruebas contra los Stubs publicados.
  • Los contratos publicados siempre actualizados, debido que en el proceso de publicación se encuentra integrado en el proceso de “build”.

Entendiendo el proyecto de pruebas

 

project_services

El proyecto con el que vamos a trabajar consiste en un sistema de dos servicios. Inventory-service y Account-service.

Account-service expone una API que devuelve los datos de los usuarios e inventory-service expone una API que devuelve el inventario de un usuario vendedor.

Inventory-service consume account-service para obtener los datos del usuario de los items que va a devolver cuando le llamen, por lo que vamos a lanzar las pruebas sobre el Stub publicado de account-service.

Inventory-service publica su Stub para que cualquier cliente futuro pueda ejecutar las pruebas contra él. Así que vuestra imaginación puede volar y ayudaros a construir un nuevo servicio siguiendo los pasos del CDC 😛

 

Preparando el proyecto

Antes de ponernos a definir los primeros contratos vamos a ver que dependencias necesitamos en nuestro proyecto. Los ejemplos los vamos a ver del proyecto de account-service.

Dentro del build.gradle vamos a definir también en que directorio vamos a alojar los contratos, así como donde se guardarán las clases base de tests que utilizará para generar los teses de verificación de los contratos.

Definiendo los contratos

Siguiendo con account-service vamos a ver el contracto que hemos definido para devolver los datos de un usuario en base a su id, en /src/test/resources/contracts/ShouldReturnAccountById.groovy

Los contratos tienen una semantica muy clara y diferenciada. Vemos como definimos tanto la request que esperamos como la response que devolvemos para esa request.

Con este contracto estamos definiendo que para cuando llamen al endpoint: /account/[0-9]+ vamos a responder con un estado 200 y con un json que contien esos campos y datos.

Vemos que la url tiene un regex, por lo que un ejemplo de una llamada que la cumpla sería: /account/12345.

Es un contrato sencillo, y con esto tanto desarrollador (para comenzar el desarrollo) como el cliente (para consumirlo) ya sabrían cual sería el endpoint, así como los datos que tienen que mandar y los que van a recibir.

Podéis ver toda la información del DSL de los contratos en el siguiente enlace.

Si seguimos la metodología ATDD, sabemos que deberíamos hacer un desarrollo mínimo para poder lanzar los teses y que fallen e ir refactorizando hasta que los teses no fallen.

Veamos el paso de generar los contratos y publicar el Stub. Lanzando el comando generateContractTests va a autogenerar los teses de validación del contracto que hemos hablado anteriormente.


➜ account-service git:(master) ./gradlew generateContractTests
Starting a Gradle Daemon (subsequent builds will be faster)
:copyContracts
:generateContractTests

Y cuando lanzamos el comando install publicará en nuestro maven local (.m2) el Stub. Veámoslo:


➜ account-service git:(master) ./gradlew clean install
:clean
:compileJava
:compileGroovy NO-SOURCE
:processResources NO-SOURCE
:classes
:jar
:copyContracts
:generateClientStubs
:verifierStubsJar
:install

Si accedéis a .m2/repository/com/qajungle/account-service/0.0.1-SNAPSHOT podréis ver el Stub:

account-service-0.0.1-SNAPSHOT-stubs.jar

Desarrollando y ejecutando los teses

El cliente (inventory-service) consume account-service para obtener los datos del usuario del inventario. Veamos la clase de servicio y el gateway sobre el que tendremos que desarrollar el test:

/src/main/java/com/qajungle/services/InventoryService.java

/src/main/java/com/qajungle/gateway/AccountGateway.java

Como vemos AccountGateway.java es el encargado de realizar la llamada al API de account-service. Por lo que en este punto es donde tendremos que atacar contra el Stub publicado por el servicio. Veamos el test:

/src/test/java/com/qajungle/services/InventoryServiceTest.java

Lo más importante en este punto es ver que estamos atacando al Stub del servicio mediante el tag:


@AutoConfigureStubRunner(ids = "com.qajungle:account-service:+:stubs:8082", workOffline = true)

Si lanzamos el test del proyecto veremos que todo ha ido correctamente:


➜ inventory-service git:(master) ./gradlew -Dtest.single=InventoryService test
:compileJava
:compileGroovy NO-SOURCE
:processResources
:classes
:copyContracts
:generateContractTests
:compileTestJava
:compileTestGroovy
:processTestResources
:testClasses
:test

BUILD SUCCESSFUL

Cambiemos el contracto en account-service para comprobar que lanza contra el Stub. Por ejemplo cambiendo el valor de un campo.


➜ inventory-service git:(master) ✗ ./gradlew -Dtest.single=InventoryService test
:compileJava UP-TO-DATE
:compileGroovy NO-SOURCE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:copyContracts
:generateContractTests
:compileTestJava UP-TO-DATE
:compileTestGroovy
:processTestResources UP-TO-DATE
:testClasses
:test


com.qajungle.services.InventoryServiceTest > should_return_seller_information FAILED
org.junit.ComparisonFailure at InventoryServiceTest.java:28

org.junit.ComparisonFailure: expected:<“[aritz]”> but was:<“[fail]”>

El resultado es que el test fallará con un ComparisonFailure en este caso.

Conclusiones

El hacer uso de contract testing nos proporciona unos beneficios que no podemos encontrar en otras estrategias para el testing de servicios. Es muy importante sacar los valores que nos aporta ya no solo en no depender del entorno, de los teses E2E, si no de poder desarrollar mediante ATDD, disponer de una documentación fidedigna y llevar un mayor control de quien consume nuestros servicios.

A día de hoy solo he probado Spring Cloud Contract, pero me consta que existen otras herramientas como Pact, que espero probar en un futuro cercano. No obstante como habéis podido comprobar Spring Cloud Contract no es complejo de utilizar.

Testing de microservicios – Estrategias

Introducción

Cada vez va cogiendo más fuerza el desarrollo usando arquitectura de microservicios por lo que el mundo de la calidad de software ha necesitado de nuevas estrategias. En este post hablaremos diferentes estrategias que se utilizan para garantizar la calidad de los microservicios.

Breve introducción a los microservicios

La arquitectura en microservicios busca desarrollar una aplicación dividiéndola en pequeños servicios (cada uno con su lógica de negocio) que corren de forma autónoma y que se comunican entre sí. Pero, ¿Que beneficios e inconvenientes nos aporta frente a la arquitectura monolítica?

microservicios

Como se puede apreciar en la imagen, una arquitectura monolítica es divida en varios servicios creando así una arquitectura de microservicios, donde cada servicio se comunica entre sí aportando la funcionalidad que ofrecía en su arquitectura monolítica.

 

BENEFICIOS

  • Cada microservicio es independiente por lo que su despliegue y desarrollo también, un cambio en uno no afecta en los demás.
  • La comunicación se  realiza mediante su interfaz pública (API) por lo que el lenguaje de desarrollo de los microservicios es independiente.
  • Mayor gestión de la escalabilidad, podemos escalar solo los microservicios que reciban más carga.

INCONVENIENTES

No todo son beneficios y es que la arquitectura en microservicios también nos aporta ciertos inconvenientes y retos, muchos de ellos debido a la introducción del concepto de sistemas distribuidos.

  • En el caso que una aplicación se componga de muchos microservicios es necesaria una mayor coordinación en los despliegues.
  • Será necesario una buena monitorización debido a que nos basamos en una arquitectura con un flujo muy dependiente de la comunicación. Un error en un punto del flujo tiene que ser rápidamente reconocible.
  • Garantizar la consistencia de los datos es un punto a tener en cuenta.
  • La saturación de la red puede afectar gravemente en el rendimiento de nuestra aplicación.
  • Las estrategias de calidad son mucho más complejas que en sistemas monolíticos debido a que no solo tendremos que testear los microservicios de forma independiente si no la funcionalidad en el flujo de los datos.

Estrategias de testing 

Como hemos dicho en la introducción, el mundo de la calidad del software ha tenido que aplicar nuevas estrategias de calidad para el reto que supone garantizar la calidad de las aplicaciones con arquitectura de microservicios.

En mi opinión el reto no se encuentra en el testing individual de cada microservicio, si no, en la integración y en la garantía de la consistencia de los datos. En comparación con los sistemas monolíticos, en una arquitectura por microservicios o distribuida el QA/Tester va a necesitar de un mayor conocimiento de la arquitectura y del flujo existente, debido a que es necesario controlar y verificar la información en todos los puntos de comunicación así como la funcionalidad de todos los microservicios.

La estrategia principal o las pruebas que se realizan son las siguientes (teniendo en cuenta que la estrategia es muy dependiente del proyecto que nos encontremos):

  • Tests unitarios: Como en todo proyecto ya sea monolítico o no, son necesarias pruebas unitarias. Mediante las pruebas unitarias comprobaremos la funcionalidad de los métodos o módulos de código que creamos necesarios.
  • Tests de integración: Los teses unitarios prueban los componentes de forma aislada por lo que también necesitamos probar el comportamiento entre los métodos. Debemos tener en cuenta que solo probaremos el comportamiento de los métodos de cada aplicación de forma aislada (de cada microservicio), por lo que las llamadas entre microservicios las mockearemos.
  • Tests de contratos o de API: La arquitectura de los microservicios depende de la comunicación entre los diferentes servicios. Cada microservicio presenta una API que consumen los demás microservicios, cuando la diseñamos estamos definiendo un “contrato”. Debemos realizar los diferentes tests a las APIs de los microservicios para garantizar que se mantiene el “contrato”.
  • Tests E2E: E2E (end-to-end) o teses funcionales tratan de garantizar la calidad de nuestra aplicación sin la necesidad de mockear  ningún método o llamada. Un test recorrerá la funcionalidad entre todos los microservicios que requiera. Debemos tener en cuenta que son susceptibles a fallar por motivos agenos a la funcionalidad (problemas de red, perdida de datos etc.). En el caso de los microservicios se recomiendan seguir unas reglas que nos ayudarán a reducir el esfuerzo de nuestros teses:
    • Dado que las pruebas E2E son difíciles de mantener, debemos intentar centrarnos en probar solo las funcionalidades más importantes, probando las demás en teses unitarios o de integración.
    • El uso de WebDrivers como selenium para probar las funcionalidades de usuario tiene un coste grande en cuanto a mantenimiento por lo que la mayoría de las acciones del usuario podemos hacerlas simulando las llamadas a la API.
    • Debemos intentar mantener un entorno limpio para cada ejecución dado que las pruebas son muy dependientes de los datos y pruebas anteriores pueden desestimar el resultado de los teses.

Referencias

  1.  http://testdetective.com/microservices-testing/
  2. http://www.sczyh30.com/vertx-blueprint-microservice/index.html