Ya hemos visto otros post sobre como aumentar la resiliencia de los servicios web a través de Hystrix. Y hoy vamos a profundizar un poco más en ello con ayuda de un balanceador de carga como es Netflix Ribbon. El cual nos permitirá enviar las llamadas a uno u otro servicio web y proveerá de mecanismos en caso de error.
Tradicionalmente en una estructura monolítica se ha tenido siempre un balanceador de carga con IP/DNS conocido que a su vez reparte las invocaciones que se le realizan entre los distintos nodos que tiene configurado. El cual por motivos de resiliencia y consumo se suele mantener en un servidor aparte.
Esta solución nos llevan a diversos problemas, sobre todo si lo orientamos a microservicios desplegados en la nube:
- Es más difícil gestionarlo si los nodos destinos aumentan o disminuyen dinámicamente.
- El mantenerlos en un servidor aparte aumenta el coste de la solución.
- Unifica el punto de entrada en una sola entidad de nuestra arquitectura, pudiendo perjudicar a nuestro sistema si esta sufre problemas de conexión o rendimiento.
- Aumento del tráfico de red hacia un único punto, generando un cuello de botella.
- Tener más de un punto de entrada.
- Asociar nodos al balanceador de forma dinámica.
- Reducir el coste en infraestructura.
- Reducir el coste de trafico de red tanto a la entrada, evitando el cuello de botella. Como a la salida, al realizar llamadas entre iguales y reduciendo la latencia entre el balanceador y el cliente.
Por tanto, aunque a través de la configuración de Spring podremos configurar cuales son los nodos a los cuales vamos a atacar. Debido a las mejoras que ofrece un balanceador de carga de cliente y dado que Ribbon también pertenece al framework de Spring Cloud Netflix. Vamos a utilizar el patrón Service Discovery, con lo cual, llamaremos a los servicios en base a su nombre y con ayuda del servidor Netflix Eureka. También vimos ya algo del tema aquí.
Ahora veremos brevemente, como montar el servidor y los clientes. Pero si quieres más detalle puedes mirar el código aquí. El servidor Eureka lo montaremos con una aplicación puramente Spring, con la dependencia spring-cloud-starter-netflix-eureka-server y la anotación @EnableEurekaServer.
@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(final String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
Eso sí, al ser una prueba de concepto en local y con un único Service Registry, tendremos que modificar el fichero de configuración de la aplicación para evitar errores de esta configuración y trazas excesivas. Y es debido a que el Service Registry está preparado para que funcione en un cluster y por tanto al iniciarse intenta buscar otras instancias y registrarse en las mismas. Por ello el fichero de configuración quedará de la siguiente forma:
spring.application.name= ${springboot.app.name:eureka-serviceregistry} server.port = ${server-port:8761} eureka.instance.hostname= ${springboot.app.name:eureka-serviceregistry} eureka.client.registerWithEureka= false eureka.client.fetchRegistry= false eureka.client.serviceUrl.defaultZone: http://${registry.host:localhost}:${server.port}/eureka/ logging.level.com.netflix.eureka=OFF logging.level.com.netflix.discovery=OFF
Ahora crearemos los clientes. Van a ser dos mocks, lo único que cambiará será un pequeño valor en la respuesta (para que podamos identificar claramente a quien estamos llamando) y el puerto de arranque. Estos clientes si los realizaremos con Apache Camel:
@Component public class BookMockRouter extends RouteBuilder { private static Map<Integer, Book> books = new HashMap<>(); static { books.put(1, new Book(1, "Dune", "Frank Herbert")); books.put(2, new Book(2, "The stars my destination", "Alfred Bester")); books.put(3, new Book(3, "Ender's game", "Orson S. Card")); books.put(4, new Book(4, "Configure Ribbon", "Server 1")); } @Override public void configure() throws Exception { restConfiguration().component("servlet").bindingMode(RestBindingMode.json_xml); rest().get("book").produces(MediaType.APPLICATION_JSON_VALUE).route().bean(BookMockRouter.class, "getAll(})") .marshal().json(); } public Collection<Book> getAll() { return books.values(); } }
En el fichero de propiedades configuraremos el servidor de Eureka (eureka.client.serviceUrl.defaultZone), con que nombre daremos a conocer al cliente (spring.application.nam) y el puerto donde vamos a desplegar el servicio (server.port).
#tomcat port server.port=9090 # the name of Camel camel.springboot.name=Service1 # to reconfigure the camel servlet context-path mapping to use /api/* instead of /camel/* camel.component.servlet.mapping.context-path=/api/* # expose actuator endpoint via HTTP management.endpoints.web.exposure.include=info,health # turn on actuator health check management.endpoint.health.enabled = true #Eureka configuration spring.application.name=app-camel-mock-client eureka.client.serviceUrl.defaultZone = http://localhost:8761/eureka/ eureka.client.healthcheck.enabled= true eureka.instance.leaseRenewalIntervalInSeconds= 1 eureka.instance.leaseExpirationDurationInSeconds= 2
Si pensamos en un entorno real y que fueran desplegados con un docker-compose por ejemplo. Lo único que diferenciaría estas instancias sería el puerto de despliegue. Y si el despliegue lo realizásemos en distintas máquinas, la configuración de cada microservicio podría ser exactamente igual. De esta forma, y con la ayuda de Netflix Eureka y Netflix Ribbon, podemos crear un cluster de microservicios.
Por último nos queda la creación del balanceador de cliente. Aquí lo mismo, que hemos dicho con los microservicios. Como veremos ahora será muy sencillo. Tenemos que indicar al endpoint que vamos a llamar, que al utilizar Netflix Eureka será el nombre del cliente que hemos configurado. Y que deseamos utilizar el balanceador Netflix Ribbon.
// .removeHeader(Exchange.HTTP_URI) -> avoid to include bridgeEndpoint rest().get("book").route().removeHeader(Exchange.HTTP_URI).serviceCall() .name("app-camel-mock-client/api/book").ribbonLoadBalancer().end();
Si queremos una configuración básica, el fichero de propiedades no se diferenciará mucho de que hemos usado en los clientes. Simplemente añadiremos la propiedad server.ribbon.eureka.enabled=true.
Con esto y una vez que tengamos todos los servidores arrancados podemos probar como llamando a http://localhost:9092/book vamos recibiendo alternativamente la respuesta de un servidor u otro.
Pero que pasa si se cae uno de los clientes. Hemos hablado que Ribbon proveerá mecanismos para evitar fallos y aumentar la resiliencia. Pero si tiramos uno de los dos clientes, vemos como falla 1 de cada 2 llamadas. Y esto es normal puesto que hace planificación por round-robin, por lo que una vez intentará llamar a un servidor y luego al otro.
¿Pero porque sucede esto? Básicamente es porque el cliente se ha caído y el Service Registry aún no se ha enterado. Por lo que Ribbon intentará seguir llamando alternativamente a ambos clientes. ¿Pero Ribbon no tiene algún mecanismo para detectar que no se encuentra activo el nodo? Si, lo tiene pero por defecto utiliza un mecanismo Dummy que dará por bueno siempre al servidor.
La solución a este problema es implementar una clase que herede de la interfaz IPing. Y que en la construcción del balanceador se la pasemos. De esta forma Ribbon podrá verificar por si mismo si el servidor al que va a llamar se encuentra habilitado o no y evitar llamarlo en caso negativo.
Para el ejemplo vamos a crear una implementación muy rústica que se basará en si el endpoint actuator/health de los clientes se encuentra activo y devolviendo UP.
@Log4j2 public class ActuatorHealthPing implements IPing { @Override public boolean isAlive(final Server server) { server.setAlive(false); String urlStr = String.format("http://%s/actuator/health", server.getId()); boolean isAlive = false; try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { HttpResponse response = httpClient.execute(new HttpGet(urlStr)); String content = EntityUtils.toString(response.getEntity()); if (content != null && "UP".equals(new ObjectMapper().readTree(content).path("status").asText())) { isAlive = true; } } catch (IOException e) { log.warn("Error while get server status: " + e.getMessage()); } server.setAlive(isAlive); return isAlive; } }
Por otro lado, programáticamente crearemos la configuración de Ribbon. A la cual le pasaremos una instancia de la clase que hemos creado en el paso anterior. Y se la pasaremos a nuestro balanceador de la siguiente forma.
RibbonConfiguration configuration = new RibbonConfiguration(); configuration.setPing(new ActuatorHealthPing()); RibbonServiceLoadBalancer loadBalancer = new RibbonServiceLoadBalancer(configuration); rest().get("book").route().removeHeader(Exchange.HTTP_URI).serviceCall() .name("app-camel-mock-client/api/book").loadBalancer(loadBalancer).end();
Ya solo nos quedaría arrancarlo con la nueva configuración. Y comprobar como Ribbon nos sigue devolviendo una respuesta correcta a pesar de que el servidor se haya caido justo momentos antes.
Como siempre todo el código puedes verlo aquí.
Por último. Aunque hemos hablado de las bondades de usar un Service Registry como Eureka o un balanceador como Ribbon. Hay que saber también que Apache Camel cuenta con su propia implementación de un Service Discovery, aunque en este caso tendremos que indicar manualmente las IPs de los servicios. Y también con su propia implementación de un balanceadores de carga del lado del cliente.
No hay comentarios:
Publicar un comentario