Balanceo de carga y resiliencia en un microservicio con Spring Cloud Netflix y Ribbon

Escrito por el , actualizado el .
java planeta-codigo programacion spring
Enlace permanente Comentarios

Spring

Java

Un servicio que recibe numerosas peticiones o es crítico para el funcionamiento de una aplicación es necesario escalarlo o crear múltiples instancias de él para atender toda la carga que se demanda o para que en caso de que una instancia falle haya otras disponibles que funcionen correctamente para atender las peticiones.

En este entorno de múltiples instancias se hace necesario un servicio de registro y descubrimiento que usando Spring, Spring Boot y Spring Cloud Netflix una implementación es Eureka. Una vez descubiertas las instancias que hay del servicio es necesario hacer balanceo de carga para conseguir escalabilidad y tolerancia a fallos, en el mismo proyecto de Spring Cloud Netflix para realizar balanceo de carga en el cliente se ofrece Ribbon.

Hay varias formas de usar Ribbon una de ellas es con lo que denominan feign client, con Spring RestTemplate o directamente con LoadBalancerClient que es la que muestro en este artículo. Este cliente con Ribbon obtiene del servicio de registro y descubrimiento la lista inicial de instancias de un servicios registrado con sus ubicaciones siendo el host en el que se encuentran y el puerto en el que ofrecen su servicio. Con esta lista y el estado de los servicios se realiza el balanceo de carga. Sin embargo, dada la naturaleza de los microservicios se pueden añadir con posterioridad más instancias de un servicio o algunas pueden empezar fallar, Ribbon se encarga de mantener actualizada la lista de instancias de un servicio.

Combinado con Hystrix un ejemplo de cliente que hace peticiones a un servicio es el siguiente. Para demostrar su funcionamiento el cliente realiza varias llamadas a un servicio cada unos pocos milisegundos balanceando la carga entre las instancias que existan. Si con posterioridad se añade una nueva instancia del servicio Ribbon al cabo de un tiempo de que haya sido iniciada lo añadirá a la lista y empieza a seleccionarla para enviarle peticiones. Si una instancia falla hasta que Eureka no marca la instancia como fuera de servicio y el cliente no actualiza su lista de instancias en el ejemplo de cliente seguirá enviando peticiones a la instancia fuera de servicio y con Hystrix utilizando el método de fallback como respuesta.

Ante el fallo de una instancia para evitar que temporalmente el cliente empiece a fallar cuando le redirige una petición este puede reintentar las peticiones en otra instancia, esta funcionalidad se proporciona con Spring Retry o utilizando Zuul como proxy.

El cliente usa la clase LoadBalancerClient que en cada invocación del método choose() devuelve una instancia diferente de servicio realizando balanceo de carga utilizando el método round-robin. La clase ServiceInstance proporciona la URL de la instancia del servicio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package io.github.picodotdev.blogbitix.springcloud.client;

...

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
@EnableHystrixDashboard
public class Main implements CommandLineRunner {

	@Autowired
	private DefaultConfiguration configuration;

	@Autowired
	private ClientService service;
    
	@Override
	public void run(String... args) throws Exception {
		System.out.printf("Valor de propiedad de configuración (%s): %s%n", "config.key", configuration.getKey());
		System.out.printf("Valor de propiedad de configuración (%s): %s%n", "config.password", configuration.getPassword());

		for (int i = 0; i < 20000; ++i) {
			String response = service.get();
			System.out.printf("Service response: %s%n", response);
			Thread.sleep(100);
		}
	}

	public static void main(String[] args) throws Exception {
		SpringApplication application = new SpringApplication(Main.class);
		application.setApplicationContextClass(AnnotationConfigApplicationContext.class);
		SpringApplication.run(Main.class, args);
	}
}
Main.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package io.github.picodotdev.blogbitix.springcloud.client;

...

import java.net.URI;

@Component
public class ClientService {

    @Autowired
    private LoadBalancerClient loadBalancer;

    @HystrixCommand(fallbackMethod = "getFallback", commandProperties = {
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "4"),
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
    })
    public String get() {
        ServiceInstance instance = loadBalancer.choose("service");
        URI uri = instance.getUri();
        return Client.create().resource(uri).get(String.class);
    }

    private String getFallback() {
        return "Fallback";
    }
}
ClientService.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
plugins {
    id 'application'
    id 'org.springframework.boot' version '2.1.12.RELEASE'
}

application {
    mainClassName = 'io.github.picodotdev.blogbitix.springcloud.client.Main'
}

dependencies {
    implementation(platform('org.springframework.boot:spring-boot-dependencies:2.1.12.RELEASE'))
    implementation(platform('org.springframework.cloud:spring-cloud-dependencies:Greenwich.SR2'))

    // Spring
    def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
    compile('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-web', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-log4j2', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-actuator', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-config', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-netflix-ribbon', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix-dashboard', excludeSpringBootStarterLogging)

    compile('org.glassfish.jersey.core:jersey-client:2.27')
    compile('org.glassfish.jersey.inject:jersey-hk2:2.27')

    runtime('com.google.code.gson:gson:2.8.5')
    runtime('com.fasterxml.jackson.core:jackson-databind:2.9.6')
    runtime('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.6')
}
build.gradle

La clase del servicio y los comandos para iniciar el servicio de registro y descubrimiento, el servicio de configuración, las instancias del servicio en diferentes puertos y el cliente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package io.github.picodotdev.blogbitix.springcloud.service;

...

@RestController
public class DefaultController {

	@Autowired
	private DefaultConfiguration configuration;

	private Random random;

	public DefaultController() {
		this.random = new Random();
	}

	@RequestMapping("/")
	public String home(HttpServletRequest request) throws Exception {
		// Timeout simulation
		//Thread.sleep(random.nextInt(2000));

		return String.format("Hello world (%s, %s)", request.getRequestURL(), configuration.getKey());
	}
}
DefaultController.java
1
2
3
4
5
6
$ ./gradlew discoveryserver:run --args="--port=8761"
$ ./gradlew configserver:run --args="--port=8090"
$ ./gradlew service:run --args="--port=8080"
$ ./gradlew service:run --args="--port=8081"
$ ./gradlew service:run --args="--port=8082"
$ ./gradlew client:run
gradle-run.sh

Esta es la salida y funcionamiento del cliente realizando balanceado la carga entre las múltiples instancias y que ocurre cuando se añade una nueva o una empieza a fallar y se elimina de la lista.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ ./gradlew client:run
... # initially two service instances (8080, 8081)
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8081/, value)
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8081/, value)
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8081/, value)
... # new service instance, ./gradlew service:run --args="--port=8082"
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8081/, value)
Service response: Hello world (http://192.168.1.4:8082/, value)
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8081/, value)
Service response: Hello world (http://192.168.1.4:8082/, value)
... # kill service instance (8082), Ctrl+C
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8081/, value)
Service response: Fallback
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8081/, value)
Service response: Fallback
... # service instances list is updated
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8081/, value)
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8081/, value)
Service response: Hello world (http://192.168.1.4:8080/, value)
Service response: Hello world (http://192.168.1.4:8081/, value)
System.out

Ribbon posee numerosas propiedades de configuración a nivel global para todos los clientes de servicios o de forma específica para cada servicio ya sea con la anotación @RibbonClient o mediante la configuración en archivos externos de Spring Boot. Algunas propiedades de configuración interesantes son las de timeout que permiten que un cliente no agote sus recursos esperando a que a los servicios que llama si tardan en responder y a su vez el cliente actuando de servidor recibe muchas llamadas de sus clientes. En un comando de Hystrix también se puede especificar un timeout de modo que si se realizan reintentos el tiempo total para Hystrix deberá ser superior que el tiempo total de todos los posibles reintentos teniendo en cuenta el timeout del cliente con Ribbon. Usando el cliente HTTP Jersey como en este caso también pueden establecerse timeouts para una petición.

El balanceo de carga que con Ribbon se realiza en el cliente es más sencillo que realizar el balanceo de carga en el servidor ya que no requiere una pieza más en la infraestructura pero requiere que el cliente tenga algo de lógica para hacer el balanceo de carga.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradle-run.sh


Este artículo forma parte de la serie spring-cloud:

  1. Datos de sesión externalizados con Spring Session
  2. Aplicación Java autocontenida con Spring Boot
  3. Configuración de una aplicación en diferentes entornos con Spring Cloud Config
  4. Información y métricas de la aplicación con Spring Boot Actuator
  5. Registro y descubrimiento de servicios con Spring Cloud y Consul
  6. Aplicaciones basadas en microservicios
  7. Registro y descubrimiento de servicios con Spring Cloud Netflix
  8. Recargar sin reiniciar la configuración de una aplicación Spring Boot con Spring Cloud Config
  9. Almacenar cifrados los valores de configuración sensibles en Spring Cloud Config
  10. Tolerancia a fallos en un cliente de microservicio con Spring Cloud Netflix y Hystrix
  11. Balanceo de carga y resiliencia en un microservicio con Spring Cloud Netflix y Ribbon
  12. Proxy para microservicios con Spring Cloud Netflix y Zuul
  13. Monitorizar una aplicación Java de Spring Boot con Micrometer, Prometheus y Grafana
  14. Exponer las métricas de Hystrix en Grafana con Prometheus de una aplicación Spring Boot
  15. Servidor OAuth, gateway y servicio REST utilizando tokens JWT con Spring
  16. Trazabilidad en microservicios con Spring Cloud Sleuth
  17. Implementar tolerancia a fallos con Resilience4j
  18. Iniciar una aplicación de Spring Boot en un puerto aleatorio
  19. Utilizar credenciales de conexión a la base de datos generadas por Vault en una aplicación de Spring
  20. Microservicios con Spring Cloud, Consul, Nomad y Traefik
  21. Trazabilidad en servicios distribuidos con Sleuth y Zipkin
  22. Configuración de una aplicación con Spring Boot y configuración centralizada con Spring Cloud Config
Comparte el artículo: