Resiliency Testing con Toxiproxy

0
1203

Í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í:

version: "3"
services:
  toxiproxy:
    image: shopify/toxiproxy
    ports:
      - 8474:8474
      - 9090:9090
      - 5432:5432
  db:
      image: postgres:9.5
      environment:
        - POSTGRES_DB=ms1-db
        - POSTGRES_USER=postgres
        - POSTGRES_PASSWORD=postgres
  ms2:
    build: .

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:

<dependency>
    <groupId>eu.rekawek.toxiproxy</groupId>
    <artifactId>toxiproxy-java</artifactId>
    <version>2.1.3</version>
</dependency>
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.
public List<Transaction> getTransactions(UUID someUserId) {
    LOG.info("getting new transactions...");
    // 1º point of failure
    return transactionsClient.getNewTransactions(someUserId); 
}

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.

public void createUserTransaction(UUID someUserId, String concept) {
    // 2º point of failure
    final var newTx = transactionsClient.createTransaction(someUserId, new ConceptRequest(concept));
    LOG.info("transaction created = {}, updating user transactions...", newTx);
    final var user = new User(someUserId, newTx);
    compensatingTransaction(
            // 3º point of failure
            () -> userRepository.save(user), 
            () -> transactionsClient.removeTransaction(someUserId, newTx.getId())
    );
}

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.

@HystrixCommand
@Client("http://localhost:9090/transactions")
public interface TransactionClient {

    @Get("/{userId}")
    List getNewTransactions(@PathVariable UUID userId);

    @Post("/{userId}")
    Transaction createTransaction(@PathVariable UUID userId, @Body ConceptRequest conceptRequest);

    @Delete("/{userId}/{txId}")
    void removeTransaction(@PathVariable UUID userId, @PathVariable UUID txId);

}

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.

@Fallback
public class TransactionClientFallback implements TransactionClient {

    private static final Logger LOG = LoggerFactory.getLogger(SomeService.class);

    @Override
    public List getNewTransactions(UUID userId) {
        LOG.warn("executing fallback method when getting new transactions");
        return Collections.emptyList();
    }

    @Override
    public Transaction createTransaction(UUID userId, ConceptRequest concept) {
        LOG.warn("executing fallback method when create transaction");
        return Transaction.errorTransaction();
    }

    @Override
    public void removeTransaction(UUID userId, UUID txId) {
        throw new IllegalStateException("compensation transaction has not been performed. Error occurred.");
    }

}

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:

private static void compensatingTransaction(Runnable action, Runnable compensating) {
     try {
         action.run();
     } catch (Exception e) {
         LOG.warn("Error occurred. Compensating transaction...");
         try { compensating.run(); } catch (Exception ignore) { }
     }
}

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:

@Before
public void init() {
    // initialize toxiproxy
    this.toxiproxyClient = new ToxiproxyClient("localhost", 8474);
    this.thirdPartyProxy = initProxy(toxiproxyClient, "ms2-proxy", "toxiproxy:9090", "ms2:9090");
    this.postgresProxy = initProxy(toxiproxyClient, "postgres-proxy", "toxiproxy:5432", "db:5432");
}

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:

@Test
public void shouldOpenCircuitOpenedOnThirdPartyWhenNetworkFailure() throws IOException {
    // GIVEN
    thirdPartyProxy.delete(); // simulate network failure
    // WHEN
    List transactions = someService.getTransactions(userId);
    // THEN
    assertEquals(Collections.emptyList(), transactions);
}

Vemos cómo abre el circuito:

...
08:22:01.090 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:22:02.096 [HystrixTimer-1] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
...

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

@Test
public void shouldOpenCircuitOnThirdPartyWhenNetworkHighLatency() throws IOException {
    // GIVEN
    thirdPartyProxy.toxics().latency("high-latency", ToxicDirection.DOWNSTREAM, 10_000);
    // WHEN
    final var transactions = someService.getTransactions(userId);
    // THEN
    assertEquals(Collections.emptyList(), transactions);
}

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

...
08:22:01.090 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:22:02.096 [HystrixTimer-1] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
...

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

@Test
public void shouldCompensateTransactionWhenDatabaseNetworkFail() throws IOException {
    // GIVEN
    postgresProxy.delete();
    // WHEN
    someService.createUserTransaction(userId, "new tx concept");
    // THEN
    final var transactions = transactionsClient.getNewTransactions(userId);
    assertEquals(0, transactions.size());
}

 

08:21:53.589 [main] WARN  com.zaxxer.hikari.pool.PoolBase - HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@32f14274 (This connection has been closed.)
08:21:53.590 [main] WARN  com.zaxxer.hikari.pool.PoolBase - HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@7af56b26 (This connection has been closed.)
08:21:53.591 [main] WARN  com.zaxxer.hikari.pool.PoolBase - HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@c86c486 (This connection has been closed.)
...
08:21:58.586 [main] ERROR o.h.e.jdbc.spi.SqlExceptionHelper - HikariPool-1 - Connection is not available, request timed out after 5007ms.
08:21:58.586 [main] ERROR o.h.e.jdbc.spi.SqlExceptionHelper - El intento de conexión falló.
08:21:58.589 [main] WARN  c.s.s.c.s.sample.domain.SomeService - Error occurred. Compensating transaction...

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:

@Test
public void shouldOpenCircuitOnThirdPartyWhenJitter() throws IOException {
    someService.createUserTransaction(userId, "jitter concept");
    // GIVEN
    thirdPartyProxy.toxics().latency("latency-with-jitter", ToxicDirection.DOWNSTREAM, 10_000).setJitter(50_000);
    // WHEN
    for (int i = 0; i < 10; i++) {
        var transactions = someService.getTransactions(userId);
        System.out.println("transactions = " + transactions);
    }
    // some assertions here
}

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

08:45:47.998 [main] INFO  c.s.s.c.s.sample.domain.SomeService - transaction created = {"Transaction":{"id":ef04cab2-f132-4792-9eee-351594b9c79c, "date":2019-02-20T07:45:47.964227, "concept":"jitter concept", "transactionStatus":"CREATED"}}, updating user transactions...
feb. 20, 2019 8:45:48 A. M.
08:45:48.171 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:45:49.207 [HystrixTimer-1] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
transactions = []
08:45:49.208 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
transactions = [{"Transaction":{"id":ef04cab2-f132-4792-9eee-351594b9c79c, "date":2019-02-20T07:45:47.964227, "concept":"jitter concept", "transactionStatus":"CREATED"}}]
08:45:49.231 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:45:50.238 [HystrixTimer-2] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
transactions = []
08:45:50.240 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
transactions = [{"Transaction":{"id":ef04cab2-f132-4792-9eee-351594b9c79c, "date":2019-02-20T07:45:47.964227, "concept":"jitter concept", "transactionStatus":"CREATED"}}]
08:45:50.263 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:45:51.270 [HystrixTimer-3] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
transactions = []
08:45:51.271 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
transactions = [{"Transaction":{"id":ef04cab2-f132-4792-9eee-351594b9c79c, "date":2019-02-20T07:45:47.964227, "concept":"jitter concept", "transactionStatus":"CREATED"}}]
08:45:51.292 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
transactions = [{"Transaction":{"id":ef04cab2-f132-4792-9eee-351594b9c79c, "date":2019-02-20T07:45:47.964227, "concept":"jitter concept", "transactionStatus":"CREATED"}}]
08:45:51.309 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
transactions = [{"Transaction":{"id":ef04cab2-f132-4792-9eee-351594b9c79c, "date":2019-02-20T07:45:47.964227, "concept":"jitter concept", "transactionStatus":"CREATED"}}]
08:45:51.319 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:45:52.324 [HystrixTimer-6] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
transactions = []
08:45:52.324 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:45:53.331 [HystrixTimer-4] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
transactions = []

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

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad