Proxy para microservicios con Spring Cloud Netflix y Zuul

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

Spring

Java

Teniendo una buen número de microservicios con múltiples instancias ofreciendo cada uno una API y en una ubicación diferente para simplificar la visión de los que actúen clientes de los microservicios se puede utilizar un proxy. Con un proxy es posible centralizar todas las peticiones, que sea éste el encargado de conocer la ubicación de todas las instancias de los microservicios y de hacer la llamada allí donde se encuentre cada una de ellas.

Entre las varias funcionalidades que proporcionar el proyecto Spring Cloud Netflix es esta de proxy mediante Zuul. Para hacer de proxy Zuul necesita tener una correspondencia entre URLs y servicios que realmente proporcionan la funcionalidad, una forma que tiene Zuul de conocer la ubicación de las instancias es utilizando el servicio de registro y descubrimiento Eureka. Además, Zuul como cliente de los microservicios posee la funcionalidad de Hystrix que implementa el patrón circuit breaker para tolerancia a fallos, Ribbon para hacer balanceo de carga entre varias instancias de los microservicios a nivel de servidor además de reintentos cuando una instancia falla.

En el ejemplo que he utilizado para esta serie de artículos sobre Spring Cloud hay un servicio que por defecto se inicia en el puerto 8080 y ofrece un endpoint / que devuelve un mensaje. Para crear un microservicio proxy con Zuul hay que crear una aplicación Spring Boot anotar la clase principal con la anotación @EnableZuulProxy y proporcionar la configuración para la correspondencia de rutas y microservicios, además de las propiedades para hacer reintentos en caso de que un microservicio falle y de timeouts en caso de que se comporte no como se espera en cuanto tiempos de respuesta.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package io.github.picodotdev.blogbitix.springcloud.proxy;

...

@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class Main {

	public static void main(String[] args) throws Exception {
		SpringApplication application = new SpringApplication(Main.class);
		application.setApplicationContextClass(AnnotationConfigApplicationContext.class);
		SpringApplication.run(Main.class, args);
	}
}
Main (zuul).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
plugins {
    id 'application'
    id 'org.springframework.boot' version '2.1.12.RELEASE'
}

application {
    mainClassName = 'io.github.picodotdev.blogbitix.springcloud.proxy.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') }
    implementation('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
    implementation('org.springframework.boot:spring-boot-starter-web', excludeSpringBootStarterLogging)
    implementation('org.springframework.boot:spring-boot-starter-log4j2', excludeSpringBootStarterLogging)
    implementation('org.springframework.boot:spring-boot-starter-actuator', excludeSpringBootStarterLogging)
    implementation('org.springframework.cloud:spring-cloud-starter-config', excludeSpringBootStarterLogging)
    //implementation('org.springframework.cloud:spring-cloud-starter-bus-amqp', excludeSpringBootStarterLogging)
    implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client', excludeSpringBootStarterLogging)
    implementation('org.springframework.cloud:spring-cloud-starter-netflix-zuul', excludeSpringBootStarterLogging)
    implementation('org.springframework.retry:spring-retry:1.2.2.RELEASE', excludeSpringBootStarterLogging)

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

Se puede establecer un tiempo máximo para establecer la conexión, de tiempo de petición, el número de reintentos en la misma instancia si falla o en otro número de instancias, el número máximo de conexiones y el número máximo de conexiones al mismo host. Todas ellas definibles en cada servicio de forma individual bajo las propiedades hystrix.command.service y service.ribbon donde service es el identificativo del servicio. Las rutas se indican bajo la propiedad zuul.routes con la relación identificativo del servicio y path.

 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
server:
  port: ${port:8095}

zuul:
  routes:
    service: '/service/**'
  retryable: true

hystrix:
  command:
    service:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 20000
    circuitBreaker:
      requestVolumeThreshold: 4
      errorThresholdPercentage: 50

service:
  ribbon:
    ConnectTimeout: 1000
    ReadTimeout: 2000
    MaxTotalHttpConnections: 100
    MaxConnectionsPerHost: 100
    MaxAutoRetries: 0
    MaxAutoRetriesNextServer: 2
    ServerListRefreshInterval: 2000
    OkToRetryOnAllOperations: true
    retryableStatusCodes: 500,404
proxy.yml

Dado que Zuul es un proxy para múltiples instancias de microservicios a cada microservicio hay que darle una ruta, cuando Zuul realiza la llamada a una instancia del microservicio se encarga de omitirla. En el ejemplo, la ruta en Zuul /service/** está asociada al microservicio service pero el servicio service ofrece su endpoint en /, Zuul se encarga de omitir la parte de la ruta para el proxy y hace la llamada a la ruta / como espera el microservicio.

Lógicamente los clientes deben contactar con el proxy en vez de con el microservicio directamente. Arrancado el servicio de descubrimiento y registro Eureka, el servidor de configuración de Spring Cloud, dos instancias del servicio y el proxy con Zuul haciendo las llamadas al proxy se observa que se obtiene el resultado del microservicio. Como en el ejemplo hay varias instancias del servicio Zuul realiza balanceo de carga entre ellas con Ribbon utilizando la política round-robin y el mensaje es diferente en cada una de las respuestas según la instancia invocada. Con Zuul además se consigue balanceo de carga a nivel de servidor que Ribbon solo ofrece a nivel de 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package io.github.picodotdev.blogbitix.springcloud.client;

...

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

	@Autowired
	private DefaultConfiguration configuration;

	@Autowired
	private ClientService service;

	@Autowired
	private ProxyService proxy;
    
	@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());
		System.out.printf("Valor de propiedad de configuración (%s): %s%n", "config.service", configuration.getService());

		for (int i = 0; i < 20000; ++i) {
			String response = (configuration.getService().equals("service")) ? service.get() : proxy.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 (client).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
package io.github.picodotdev.blogbitix.springcloud.client;

...

@Component
public class ProxyService {

    @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 = "25000")
    })
    public String get() {
        ServiceInstance instance = loadBalancer.choose("proxy");
        URI uri = instance.getUri();
        String resource = String.format("%s%s", uri.toString(), "/service");
        return Client.create().resource(resource).get(String.class);
    }

    private String getFallback() {
        return "Fallback";
    }
}
ProxyService.java
1
2
3
4
5
6
7
$ ./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 proxy:run --args="--port=8085"
$ ./gradlew client:run --args="--service=proxy"
gradle-run.sh

Las URLs del servicio en el microservicio y en el proxy son.

1
2
3
4
5
# Microservicio
$ curl http://192.168.1.4:8080/

# Microservicio en el proxy
$ curl http://192.168.1.4:8085/service
curl.sh

El cliente de ejemplo realiza peticiones al proxy, en la salida se muestra el resultado del balanceo de carga cuando hay varias instancias, cuando se añade una nueva instancia entra a formar parte del balanceo de carga. Otro beneficio de Zuul es que ofrece la funcionalidad de reintentos de modo que si una instancia de un servicio falla la petición se reintenta en otra. En el artículo Balanceo de carga y resilencia en un microservicio con Spring Cloud Netflix y Ribbon usando solo Ribbon se observaba que cuando una instancia falla se le siguen haciendo peticiones hasta que la lista de instancias del servicio en Eureka se actualiza quitando la fallida, con Hystrix se obtiene la respuesta fallback pero no se evita completamente el error. Zuul puede ocultar el error provocado por una instancia que falla reintentado la petición en la misma nuevamente, en otra u otras instancias según se configure. El comportamiento con Zuul cuando una instancia falla se puede comparar con el comportamiento incluido en el artículo anterior usando en el cliente los microservicios directamente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ./gradlew client:run --args="--service=proxy"
... # 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: 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

Zuul además es capaz de proporciona otras muchas funcionalidades como:

  • Autenticación
  • Seguridad
  • Recolección de métricas y monitorización
  • Pruebas de carga
  • Pruebas de verificación o canary testing
  • Enrutado dinámico
  • Migración de servicio
  • Abandono de carga o load shedding
  • Manejo de respuesta estática
  • Gestión de tráfico active/active
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:
./gradlew-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. Servicio de configuración para microservicios con Spring Cloud Config
  9. Recargar sin reiniciar la configuración de una aplicación Spring Boot con Spring Cloud Config
  10. Almacenar cifrados los valores de configuración sensibles en Spring Cloud Config
  11. Tolerancia a fallos en un cliente de microservicio con Spring Cloud Netflix y Hystrix
  12. Balanceo de carga y resilencia en un microservicio con Spring Cloud Netflix y Ribbon
  13. Proxy para microservicios con Spring Cloud Netflix y Zuul
  14. Monitorizar una aplicación Java de Spring Boot con Micrometer, Prometheus y Grafana
  15. Exponer las métricas de Hystrix en Grafana con Prometheus de una aplicación Spring Boot
  16. Servidor OAuth, gateway y servicio REST utilizando tokens JWT con Spring
  17. Trazabilidad en microservicios con Spring Cloud Sleuth
  18. Implementar tolerancia a fallos con Resilience4j
  19. Iniciar una aplicación de Spring Boot en un puerto aleatorio
  20. Utilizar credenciales de conexión a la base de datos generadas por Vault en una aplicación de Spring
  21. Microservicios con Spring Cloud, Consul, Nomad y Traefik
Comparte el artículo: