Resiliency Testing con Toxiproxy

0
330

Índice de contenidos

  1. Introducción
  2. Entorno
  3. Resiliencia y patrones de resiliencia
    3.1 ¿Qué es resiliencia?
    3.2 Patrones de resiliencia
  4. Ejemplo
    4.1 Infraestructura
    4.2 Microservicio de transacciones
    4.3 Microservicio de usuarios
    4.4 Implementación de los patrones de resiliciencia
    4.4.1 Circuit Breaker
    4.4.2 Fallback
    4.4.3 Compensating transaction
    4.5 Test de resiliencia
  5. Conclusiones
  6. Referencias

1. Introducción

En este tutorial vamos a ver cómo podemos testear la resiliencia de nuestra aplicación usando Toxiproxy.
En el ejemplo usaremos Micronaut pero se podría haber usado cualquier framework que dé soporte a resiliencia como Spring Cloud o Eclipse Microprofile. Puede verse un ejemplo de qué es o cómo se usa Micronaut aquí.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,3 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Mojave 10.14.1
  • IntelliJ IDEA 2018.3.4
  • Docker version 18.09.1
  • Docker-compose version 1.23.2, build 1110ad01
  • Java version OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.2+7, mixed mode)
  • Micronaut 1.0.4

3. Resiliencia y patrones de resiliencia

3.1 ¿Qué es resiliencia?

Si buscamos la definición de la RAE para el término resiliencia encontramos lo siguiente:

Capacidad de adaptación de un ser vivo frente a un agente perturbador o un estado o situación adversos.

Extrapolando esta definición al software, la resiliencia es la capacidad que tiene nuestro sistema de recuperarse ante diferentes fallos.

3.2 Patrones de resiliencia

Para poder lidiar con estos fallos (latencia y caídas del sistema, entre otros) existen varios patrones que pueden ayudarnos. Aunque la lista es larga (aquí hay algunos) para el ejemplo nos vamos a centrar en los siguientes:

  • Circuit Breaker: Previene contra continuas llamadas a un servicio que está fallando o que tiene problemas de rendimiento.
  • Fallback: Proporciona un mecanismo a través del cual ofrecer una alternativa ante un servicio que está fallando.
  • Compensating Transaction: Se encarga de deshacer una operación previa para poder dejar el sistema en un estado consistente.

4. Ejemplo

Como ejemplo se expone el caso de una operativa común que podemos encontrar en nuestro día a día:

  • Solicitar información de otro dominio
  • Publicar información hacia otro dominio cambiando su estado
  • Guardar información en el sistema de persistencia local

4.1 Infraestructura

Contaremos con dos microservicios uno que actuará como cliente (el SUT) que será el microservicio de usuarios y otro que actuará como sistema de terceros (el DoC) siendo éste el microservicio de transacciones y estando alojado dentro de un contenedor Docker. Además levantaremos una base de datos Postgres que usaremos para persistir datos, también como un contenedor Docker. El SUT no accederá directamente a ningún recurso sino que lo hará a través de un proxy, concretamente a través de un servidor de Toxiproxy también levantado como un contenedor docker.

El fichero docker-compose.yml define los servicios así:

El servicio toxiproxy levantará los siguientes puertos:

  • 8474: Necesario para poder conectar el cliente de Toxiproxy con el servidor
  • 9090: Puerto por el que escucha el microservicio de transacciones
  • 5432: Puerto por el que escucha la base de datos postgres

 

Destacar que el servicio db y ms2 exponen los puertos 5432 y 9090 solo de forma interna, siendo el servicio toxiproxy el que se encarga de exponer estos puertos externamente.

4.2 Microservicio de transacciones

El microservicio de transacciones contiene varios endpoints a través de los cuales poder realizar operaciones:

  • GET /transactions/{userId}
  • POST /transactions/{userId}
  • DELETE /transactions/{userId}/{txId}

4.3 Microservicio de usuarios

Añadir la dependencia del cliente de Toxiproxy en el proyecto:

El código del microservicio de usuarios contiene las acciones necesarias para interacturar con el microservicio de transacciones. La primera acción será poder comunicarse con él para poder recuperar las transacciones por usuario.

La segunda acción será poder crear nuevas transacciones para un usuario, comunicándolo al microservicio dependiente y persistiendo los datos en una base de datos. Operativa común.

4.4 Implementación de los patrones de resiliciencia

 4.4.1 Circuit Breaker

Micronaut se integra con las librerías de Hystrix para implementar este patrón habilitando el uso de la anotación @HystrixCommand.

4.4.2 Fallback

Micronaut nos permite implementar este patrón creando una implementación para la interfaz del cliente y se integra con Hystrix para invocar a estos métodos cuando el circuito se abre.

4.4.3 Compensating transaction

La implementación de este patrón es manual ya que dependerá de cómo queramos compensar cada acción de forma específica:

4.5 Test de resiliencia

Los test se ejecutan con JUnit y se integra con Micronaut para levantar el contexto y ejecutar los test contra la aplicación real. Se crea el cliente de Toxiproxy y se crean los proxies en runtime ya que cuando levantamos el contenedor el servidor no tiene ningún proxy creado y nuestros servicios estarán totalmente aislados. Los proxies se pueden crear a través del cliente de Java o, manualmente, haciendo uso del api REST de Toxiproxy:

Lo más importante aquí es tener en cuenta que el servidor de Toxiproxy se está ejecutando dentro de docker por lo que no podemos acceder a los servicios como localhost, sino que tendremos que referirnos a ellos con el nombre de servicio que hayamos indicado en el fichero docker-compose.yml.

Para poder probar cómo se comporta la aplicación ante distintos fallos de red vamos a realizar las siguientes pruebas.

Simular caída de red al intentar comunicarnos con el microservicio dependiente:

Vemos cómo abre el circuito:

Simular alta latencia de red al intentar comunicarnos con el servicio dependiente:

Vemos cómo abre el circuito también ante latencia:

Simular caída de red contra la base de datos:

 

Aquí vemos cómo se compensa la transacción al no haber conexión a base de datos para poder dejar el sistema de terceros en un estado consistente.

Simular latencia añadiendo “nervio”

También existe una opción bastante interesante que nos permite simular “nervio” en la latencia de forma que ésta varía a lo largo del tiempo. Podemos ver cómo el circuito se abre y se cierra en función de la latencia de red en ese momento:

Aquí la salida por consola de los diez intentos de obtener las transacciones del usuario:

5. Conclusiones

Aunque este tipo de test son más costosos que los test unitarios a los que estamos acostumbrados, sin duda tienen un gran potencial aportándonos una visión más cercana de cómo se comportará nuestra aplicación ante fallos reales que pueden ocurrir en nuestro sistema y que, como dice la ley de Murphy:

Si algo malo puede pasar, pasará.

6. Referencias

Dejar respuesta

Please enter your comment!
Please enter your name here