Ribbon: el balanceador de carga de spring-cloud-netflix (2/3)

Configuración avanzada de Ribbon

En esta sección explicaremos qué compone y cómo cambiar la configuración estándar proporcionada por Ribbon.

La configuración por defecto de Ribbon (integrado con Eureka) se pueden ver en la clase RibbonClientConfiguration. Esta clase define los siguientes beans (entre paréntesis se incluye la interfaz que implementan):

  • DefaultClientConfigImpl (IClientConfig): esta clase será la encargada de las propiedades de configuración. Aquí se pueden ver todas las posibles propiedades a definir así como los valores que toman por defecto.
  • ZoneAvoidanceRule (IRule): ésta será la regla que se utilice para elegir entre las instancias restantes después del proceso de filtrado.
  • NoOpPing (IPing): ésta es la clase encargada de realizar el ping contra las instancias para determinar si están disponibles en caso de utilizar otra configuración. En este caso al estar integrado con Eureka, será la clase NIWSDiscoveryPing, la que en realidad no realiza ningún ping, sino que delega a la InstanceInfo recuperada de Eureka donde se comprobará que el estado de la instancia sea UP.
  • ConfigurationBasedServerList (ServerList): esta clase es la encargada de leer la propiedad [ribbonClientName].ribbon.listOfServers que nos indicará las instancias existentes para un microservicio. En nuestro caso este listado lo proporcionará Eureka por lo que esta clase no se utilizará.
  • ZonePreferenceServerListFilter (ServerListFilter): éste es el filtro que se aplicará a las instancias recuperadas por el balanceador. ZonePreferenceServerListFilter da prioridad a las instancias que se encuentran en la misma zona que la que realiza la petición.
  • ZoneAwareLoadBalancer (ILoadBalancer): es el balanceador que se utilizará. Este balanceador es capaz de descartar zonas en base a métricas de peticiones pendientes e instancias disponibles.

En nuestro caso queremos cambiar dicha configuración para poder probar diferentes reglas de balanceo, para ello deberemos crear nuestra propia clase de configuración para sobreescribir los beans deseados. A continuación se muestra un ejemplo de configuración en la que se indica que la regla de balanceo esté compuesta por RetryRule y WeightedResponseTimeRule que serán explicadas en apartados posteriores:

MicroS4 5

Esta clase de configuración se puede utilizar para sobreescribir los beans que queramos de los previamente listados.

Para poder utilizar esta configuración deberemos indicarlo en nuestra clase main por medio de la anotación @RibbonClient como se muestra en la siguiente imagen:

MicroS4 6

Reglas de balanceo  

Como hemos explicado previamente las reglas de balanceo son las reglas que se aplican a la lista de instancias devueltas por el balanceador una vez que éste haya comprobado cuales están disponibles y descartado las que correspondan de acuerdo a sus filtros. Muchas de las reglas y balanceadores utilizan lógica que distingue zonas, así que para poder explicarlas y probarlas hemos definido la estructura de zonas que se puede ver en la siguiente imagen:MicroS4 7 390Como se puede ver existen dos zonas: defaultZone en la que se encuentran eureka, customer-backend y una instancia de security, e ireland compuesto por dos instancias de security. Así el cluster de instancias de security cuenta con una instancia en defaultZone y dos en ireland.

Como se ha mencionado en un post anterior sobre Eureka, la zona de un microservicio se define con la propiedad eureka.instance.metadataMap.zone.

A continuación se explican las diferentes reglas disponibles y su funcionamiento:

La lógica de esta regla se basa en descartar zonas según la disponibilidad de las instancias de microservicio en dicha zona. Los dos criterios de descarte implementados son el porcentaje de instancias no disponibles y el número de peticiones activas por servidor.

En esta situación dicha zona será descartada y las instancias disponibles serán las que existan en las restantes zonas. Para escoger cual de esas instancias será la invocada se utilizará el algoritmo RoundRobin.

Así por ejemplo, en una situación como la de la anterior imagen suponiendo que la instancia de defaultRegion sea descartada, se aplicará un RoundRobin sobre las dos instancias de la zona ireland.

En una situación en la que tuviésemos una zona adicional japan y ésta fuese descartada por el motivo que fuera, el algoritmo RoundRobin se aplicaría sobre las tres instancias disponibles en las zonas defaultZone e ireland.

IMPORTANTE: hay un detalle a tener en cuenta a la hora de utilizar este filtro. Si partimos de una configuración por defecto no podremos hacerlo funcionar debido a que la lógica inicialmente diseñada por Netflix es ligeramente diferente a la utilizada por el OSS (sistemas de soporte a las operaciones) Spring Cloud-Netflix. Esto se explica posteriormente en el apartado de ZonePreferenceServerListFilter de la sección ‘Balanceador y filtros de balanceado’.

Esta regla es la más sencilla de todas. Aplicará el conocido algoritmo RoundRobin que alternará las peticiones entre las diferentes instancias disponibles.

Esta regla está basada en el tiempo de respuesta medio de la instancia. A cada instancia se le asignará un peso en función de su tiempo medio de respuesta. Cuanto mayor sea el tiempo, menor peso tendrá la instancia. La instancia se elige de forma aleatoria entre las posibles con sus valores ajustados de forma acorde al peso.

El ajuste de los pesos se realiza cada 30 segundos en base a las estadísticas de peticiones, por lo que este proceso no se realiza para cada petición.

El proceso de cálculo de pesos consiste en sumar los tiempos medios de todas las instancias y a partir de ese valor el peso se calcula como:

(suma del tiempo medio de todas las instancias) – (tiempo medio de la instancia)

Por lo que a tiempo medio más bajo, se tendrá más peso. Una vez asignados los pesos se sumará el valor de los mismos. A cada instancia se le asignará un rango entre cero y la suma de los pesos igual al valor de su peso. Como paso final se generará un número aleatorio y según en qué rango se sitúe, la instancia que corresponda será la elegida.

Durante las primeras peticiones al no haber estadísticas de tiempos medios de respuesta y por tanto no haber pesos disponibles se utiliza el algoritmo RoundRobin.

Ejemplo:
Supongamos que tenemos tres servidores con los siguientes tiempos medios A = 0.25s, B = 0.35s, C = 0.75s
Se calcula la suma total de los tiempos siendo un total de 1.35s
Se adjudican los pesos A = 1.1, B = 1.0, C = 0.6 y la suma de los mismos será 2.7
El cálculo de los rangos adjudicados a cada instancia será: A = [0, 1.1], B = (1.1, 2.1] y C (2.1, 2.7]

Como se puede ver en el ejemplo cuanto menor sea el tiempo medio de respuesta de una instancia, mayor será su peso y por tanto su rango. De esta forma la instancia con mejor tiempo medio de respuesta será la que tenga más opciones de ser elegida, pero el factor aleatorio permite que también la instancia más lenta sea la elegida; en la proporción en que se diferencia su tiempo medio del de las demás.

Hay también que resaltar que como consecuencia de este algoritmo dos instancias con un tiempo medio muy similar tendrán unas posibilidades muy similares de ser elegidas, así como el hecho de que si una instancia tiene un tiempo medio muy alto será muy difícil que salga elegida (para el ejemplo anterior si la instancia C tuviese un tiempo medio de 5s nos encontraríamos con unos pesos A = 5.35, B = 5.25, C = 0.6 de forma que cualquiera de las otras dos instancias tiene casi diez veces más posibilidades de ser escogida).

Esta regla añade lógica de reintentos a cualquiera de las otras reglas existentes. Se puede utilizar conjuntamente con cualquiera de las otras reglas disponibles como se muestra en la siguiente configuración:

MicroS4 8

En caso de no indicar ninguna otra regla, RetryRule utiliza por defecto la RoundRobinRule. Otro parámetro que nos permite configurar esta regla es un timeout para reintentos de forma que se intentará encontrar una instancia válida durante el periodo determinado por ese timeout. En caso de no definirse, el valor por defecto es de 500 milisegundos.

Hay que tener en cuenta que esta política de reintento es para localizar una instancia, por lo que aunque nuestra petición retorne un error si el estado de la instancia en Eureka es correcto no se aplicará política de reintento, ya que dicha política es para localizar una instancia válida, no para reintento de peticiones.

Como ejemplo, si quisiéramos invocar al microservicio security y tuviésemos una situación como la de la imagen donde la instancia está registrada pero está caída:

MicroS4 9

La política de reintento intentaría recuperar una instancia con estado UP durante el timeout que hayamos definido. En caso de no encontrarlo lógicamente la petición fallaría.

Esta situación la hemos provocado con la siguiente implementación en nuestro microservicio security y configurando la propiedad eureka.client.healthcheck.enabled=true tal y como se explicó en un post anterior sobre Eureka:

MicroS4 10

Implementar nuestra propia regla

Otra posibilidad que siempre está presente es, en caso de que ninguno de las reglas de balanceo nos satisfaga, implementar la nuestra propia. A modo de ejemplo hemos implementado una regla basada en el concepto de la WeightedResponseTimeRule, solo que en este caso en vez de asignar pesos en base al tiempo medio de respuesta y escoger el servidor aleatoriamente en base a esos pesos, escogeremos directamente el servidor que tenga el mejor tiempo de respuesta. A continuación se puede ver el código fuente de dicha regla:

MicroS4 11

Esta regla se ha implementado con finalidades didácticas pero no sería válida para un entorno productivo real, ya que podría provocar que ciertas instancias no fuesen elegibles en un largo periodo de tiempo. Supongamos por ejemplo que tenemos una instancia A y estamos arrancando la segunda, B. Si a la instancia B se le deriva una petición y se produce un pequeño retardo de red, o la instancia no ha tenido una petición de warmup y por tanto tarda más en responder o cualquier otro tipo de situación, su tiempo medio de respuesta estaría compuesto únicamente por el tiempo de esa petición, que en este caso sería muy alto.

Esto provocaría que la instancia A, al tener mejor tiempo medio de respuesta, fuese siempre la elegida y se le enviasen todas las peticiones hasta que por sobrecarga de las mismas llegase a tener un tiempo medio de respuesta mayor al de B. En ese momento se empezarían a derivar peticiones a la instancia B.

Obviamente esto nos supone una situación no deseada, ya que en vez de realizar un balanceo lógico entre las dos instancias de forma que la carga de peticiones sea compartida entre las dos, lo que ocurre es que una instancia es ‘descartada’ hasta que la otra tiene un comportamiento tan malo como ésta, momento en el cual se cambian y la nueva empiece a recibir todas las peticiones hasta que tenga un comportamiento peor que la primera.

 

 

 

 

Analizamos Ribbon a fondo en 3 entregas:

Abraham Rodríguez actualmente desarrolla funciones de ingeniero backend J2EE en Paradigma donde ya ha realizado diversos proyectos enfocados a arquitecturas de microservicios. Especializado en sistemas Cloud, ha trabajado con AWS y Openshift y es Certified Google Cloud Platform Developer. Cuenta con experiencia en diversos sectores como banca, telefonía, puntocom... Y es un gran defensor de las metodologías ágiles y el software libre."

Ver toda la actividad de José Abraham Rodríguez López

Recibe más artículos como este

Recibirás un email por cada nuevo artículo.. Acepto los términos legales

Posts relacionados

Comentarios

  1. […] [Puedes acceder directamente a la segunda parte del artículo aquí] […]

Escribe un comentario