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.
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