Me interesé por encontrar el equilibrador de carga perfecto cuando tuvimos una serie de incidentes en el trabajo relacionados con un servicio que se comunicaba con una base de datos que se comportaba de forma errática. Aunque nuestro primer objetivo era hacer que la base de datos fuera más estable, tenía claro que el impacto en el servicio podría haberse reducido enormemente si hubiéramos sido capaces de equilibrar la carga de las solicitudes de forma más eficaz entre los distintos puntos finales de lectura de la base de datos.
Cuanto más investigaba el estado de la técnica, más me sorprendía descubrir que esto dista mucho de ser un problema resuelto. Hay muchos equilibradores de carga, pero muchos utilizan algoritmos que solo funcionan para uno o dos modos de fallo, y en estos incidentes habíamos visto una gran variedad de modos de fallo.
Este post describe lo que he aprendido sobre el estado actual del equilibrio de carga para la alta disponibilidad, mi comprensión de la dinámica problemática de las herramientas más comunes, y donde creo que debemos ir desde aquí.
(Descargo de responsabilidad: esto se basa principalmente en experimentos mentales y observaciones casuales, y no he tenido mucha suerte a la hora de encontrar literatura académica relevante. Las críticas son bienvenidas).
TL;DR
Puntos que me gustaría que sacaras de esto:
- La salud del servidor sólo puede entenderse en el contexto de la salud del cluster
- Los equilibradores de carga que utilizan comprobaciones de estado activas para expulsar servidores pueden perder tráfico innecesariamente cuando las comprobaciones de estado no son representativas del estado real del tráfico.
- La supervisión pasiva del tráfico real permite que las métricas de latencia y tasa de fallos participen en la distribución equitativa de la carga
- Si pequeñas diferencias en el estado de los servidores producen grandes diferencias en el equilibrio de la carga, el sistema puede oscilar de forma salvaje e impredecible.
- La aleatoriedad puede inhibir el mobbing y otros comportamientos correlacionados no deseados
FUNDAMENTOS
Una nota rápida sobre terminología: En este post, me referiré a clientes hablando con servidores sin referencias a "conexiones", "nodos", etc. Aunque un determinado software puede funcionar como cliente y como servidor, incluso al mismo tiempo o en el mismo flujo de peticiones, en el escenario que he descrito los servidores de aplicaciones son clientes de los servidores de bases de datos, y me centraré en esta relación cliente-servidor.
Así que en el caso general tenemos N clientes hablando con M servidores:

También voy a ignorar los detalles de las peticiones. Para simplificar, diré que la petición del cliente no es opcional y que no es posible el fallback; si la llamada falla, el cliente experimenta una degradación del servicio.
La gran pregunta, entonces, es: Cuando un cliente recibe una petición, ¿cómo debe elegir un servidor al que llamar?
(Nótese que estoy analizando solicitudes, no conexiones de larga duración que podrían transportar flujos constantes, ráfagas de tráfico o solicitudes a intervalos variables. Tampoco debería importar especialmente a las conclusiones generales si se realiza una conexión por solicitud o si se reutilizan conexiones).
Quizá te preguntes por qué hago que cada cliente hable con cada servidor, lo que comúnmente se denomina "equilibrio de carga del lado del cliente" (aunque en la terminología de este post, el equilibrador de carga también se llama cliente). ¿Por qué hacer que los clientes hagan este trabajo? Es bastante común poner todos los servidores detrás de un equilibrador de carga dedicado.

El problema es que si sólo se dispone de un nodo de balanceo de carga dedicado, se tiene un único punto de fallo. Por eso es tradicional tener al menos tres de estos nodos. Pero ahora los clientes tienen que elegir con qué equilibrador de carga hablar... ¡y cada nodo equilibrador de carga tiene que elegir a qué servidor enviar cada petición! Esto ni siquiera reubica el problema, sino que lo duplica. ("Ahora tienes dos problemas").
No estoy diciendo que los equilibradores de carga dedicados sean malos. El problema de con qué equilibrador de carga hablar se resuelve convencionalmente con el equilibrio de carga DNS, que suele estar bien, y hay mucho que decir sobre el uso de un punto más centralizado para el enrutamiento, registro, métricas, etc. Pero en realidad no permiten eludir el problema, ya que aún pueden ser presa de ciertos modos de fallo, y en general son menos flexibles que el equilibrio de carga del lado del cliente.
VALORES
Entonces, ¿qué valoramos en un equilibrador de carga? ¿Para qué lo optimizamos?
En cierto orden, según nuestras necesidades:
- Reducir el impacto de los fallos del servidor o de la red en la disponibilidad general de nuestro servicio.
- Mantener baja la latencia del servicio
- Repartir la carga uniformemente entre los servidores
- No sobrecargue un servidor si los demás tienen capacidad de sobra
- Previsibilidad: Es más fácil saber cuánto margen tiene el servicio
- Carga repartida de forma desigual si los servidores tienen capacidades variables, que pueden variar en el tiempo o por servidor (distribución equitativa, en lugar de distribución igualitaria)
- Un pico repentino, o una gran cantidad de tráfico justo después del arranque del servidor, podría no dar tiempo al servidor a calentarse. Un aumento gradual al mismo nivel de tráfico podría ser suficiente.
- Las cargas de CPU ajenas al servicio, como la instalación de actualizaciones, pueden reducir la cantidad de CPU disponible en un único servidor.
SOLUCIONES INGENUAS
Antes de intentar resolverlo todo, veamos algunas soluciones simplistas. Cómo se distribuyen uniformemente las peticiones cuando todo va bien?
- Round-robin
- El cliente recorre los servidores
- Distribución uniforme garantizada
- Selección aleatoria
- Estadísticamente se aproxima a una distribución uniforme, sin tener en cuenta el estado (compensación coordinación/CPU)
- Elección estática
- Cada cliente sólo elige un servidor para todas las peticiones
- Esto es lo que hace el equilibrio de carga DNS: Los clientes resuelven el nombre de dominio del servicio en una o más direcciones, y la pila de red del cliente elige una y la almacena en caché. Así es como se equilibra el tráfico entrante en la mayoría de los equilibradores de carga dedicados; sus clientes no necesitan saber que hay varios servidores.
- Algo así como aleatorio, funciona bien cuando 1) se respetan los TTL de DNS y 2) hay muchos más clientes que servidores (con tasas de peticiones similares).
¿Y qué pasa si uno de los servidores se cae en una configuración así? Si hay 3 servidores, 1 de cada 3 peticiones falla. Una tasa de éxito del 67% es bastante mala (¡ni siquiera un "nueve"!) La mejor tasa de éxito posible en este escenario, suponiendo un equilibrador de carga perfecto y capacidad suficiente en los dos servidores restantes, es del 100%. ¿Cómo podemos conseguirlo?

DEFINICIÓN DE SALUD
La solución habitual son los chequeos. Las comprobaciones de estado permiten a un equilibrador de carga detectar determinados fallos del servidor o de la red y evitar el envío de solicitudes a los servidores que no superen la comprobación.
En general, queremos saber cómo de "sano" está cada servidor, sea lo que sea lo que eso signifique, porque puede tener valor predictivo a la hora de responder a la pregunta principal: "¿Es probable que este servidor dé una mala respuesta si le envío esta petición?". También hay una pregunta de nivel superior: "¿Es probable que este servidor se vuelva insalubre si le envío más tráfico?". (Otra forma de decir esto es que algunos casos de insalubridad pueden depender de la carga, mientras que otros son independientes de la carga; conocer la diferencia es esencial para predecir cómo dirigir el tráfico cuando se observa insalubridad.
Así que, a grandes rasgos, la "salud" es en realidad una forma de modelar el estado externo al servicio de la predicción. Pero, ¿qué se considera no saludable? ¿Y cómo lo medimos?
ELEGIR UN PUNTO DE VISTA
Antes de entrar en detalles, es importante señalar que podemos utilizar dos puntos de vista muy diferentes:
- La salud intrínseca del servidor: Si la aplicación del servidor se está ejecutando, responde, es capaz de hablar con todas sus propias dependencias y no está sometida a una grave contención de recursos.
- La salud del servidor observada por el cliente: La salud del servidor, pero también la salud del host del servidor, la salud de la red interviniente, e incluso si el cliente está configurado con una dirección válida para el servidor.
Desde un punto de vista práctico, la salud intrínseca del servidor no importa si el cliente ni siquiera puede llegar a él. Por lo tanto, nos centraremos principalmente en la salud del servidor observada desde el cliente. Sin embargo, hay algunas sutilezas aquí: A medida que la tasa de peticiones al servidor aumenta, es probable que la aplicación del servidor sea el cuello de botella, no la red o el host. Si empezamos a ver un aumento de la latencia o de la tasa de fallos del servidor, eso podría significar que el servidor está sufriendo bajo la carga de peticiones, lo que implica que una carga adicional de peticiones podría empeorar su salud. Otra posibilidad es que el servidor tenga capacidad de sobra y el cliente sólo observe un problema de red transitorio e independiente de la carga, quizá debido a un enrutamiento no óptimo. Si ese es el caso, es poco probable que una carga de tráfico adicional cambie la situación. Dado que, en general, puede ser difícil distinguir entre estos casos, generalmente utilizaremos las observaciones del cliente como estándar de salud.
¿CUÁL ES LA MEDIDA DE LA SALUD?
Entonces, ¿qué puede saber un cliente sobre la salud de un servidor a partir de las llamadas que realiza?
- Latencia: ¿Cuánto tardan en llegar las respuestas? Puede desglosarse en Tiempo de establecimiento de la conexión, tiempo hasta el primer byte de respuesta, tiempo hasta la respuesta completa; mínimo, medio, máximo, varios percentiles. Tenga en cuenta que aquí se mezclan las condiciones de la red y la carga del servidor: fuentes independientes y dependientes de la carga, respectivamente (en la mayoría de los casos).
- Tasa de fracaso: ¿Qué fracción de las peticiones acaban en fracaso? (Más adelante veremos qué significa "fallo").
- Concurrencia: ¿Cuántas solicitudes hay en curso? Esto confunde los efectos del comportamiento del servidor y del cliente: puede haber más solicitudes en vuelo a un servidor, ya sea porque el servidor está respaldado o porque el cliente ha decidido darle una mayor proporción de solicitudes por alguna razón.
- Tamaño de la cola: Si el cliente mantiene una cola por servidor en lugar de una cola unificada, una cola más larga puede ser un indicador de mala salud o (de nuevo) de carga desigual por parte del cliente.
Con el tamaño de la cola y el número de peticiones concurrentes vemos que no todas las mediciones son de salud per se, sino que también pueden ser indicativas de la carga. No son directamente comparables, pero es de suponer que los clientes quieren hacer más peticiones a servidores más sanos y menos cargados, por lo que estas métricas pueden utilizarse junto a otras más intrínsecas, como la latencia y la tasa de fallos.
Todas estas mediciones se realizan desde la perspectiva del cliente. También es posible que el servidor informe por sí mismo de la utilización, aunque esto no se tratará en este artículo.
Todos ellos pueden medirse también en distintos intervalos de tiempo: Valor más reciente, ventana deslizante (o cubos móviles), media decreciente, o varios de estos en combinación.
DEFINICIÓN DEL FRACASO
De estos indicadores de salud, la tasa de fallos es quizá el más importante: En la mayoría de los casos de uso, un usuario prefiere obtener un éxito lento que un fallo de cualquier tipo. Pero hay diferentes tipos de fallos, y pueden implicar diferentes cosas sobre el estado del servidor.
Si una llamada se interrumpe, puede haber problemas de red o enrutamiento que provoquen una alta latencia, o puede que el servidor esté sometido a una gran carga. Pero si la llamada falla rápidamente, hay implicaciones muy diferentes: Mala configuración del DNS, servidor roto, mala ruta. Es menos probable que un fallo rápido dependa de la carga, a no ser que el servidor esté usando la reducción de carga para fallar rápido intencionadamente bajo una carga pesada, en cuyo caso es posible que no se estrese más con más carga.
Si nos fijamos en los fallos a nivel de aplicación, y no sólo en los fallos a nivel de transporte, es fundamental tener cuidado a la hora de elegir los criterios para marcar una llamada como fallida. Por ejemplo, una llamada HTTP que no se devuelve (debido al tiempo de espera, etc.) es inequívocamente un fallo, pero una respuesta bien formada con un código de estado de error (4xx o 5xx) puede no indicar un problema del servidor. Una petición individual puede estar desencadenando un Error de Servidor 500 dependiente de datos que no es representativo de la salud general del servidor. Es común ver una ráfaga de respuestas 404 o 403 debido a un llamante con solicitudes mal formadas, pero sólo ese llamante se ve afectado; juzgar el servidor poco saludable sólo sobre esa base sería imprudente. Por otro lado, es algo menos probable que un tiempo de espera de lectura sea específico de una mala petición.
¿QUÉ PASA CON LOS CHEQUES SANITARIOS?
Hasta ahora hemos hablado principalmente de formas en las que un cliente puede obtener información de forma pasiva sobre el estado del servidor a partir de las peticiones que ya está realizando. Otro enfoque es utilizar comprobaciones activas del estado.
Las comprobaciones de estado del Elastic Load Balancer (ELB) de AWS son un ejemplo de ello. Puede configurar el equilibrador de carga para que llame a algún punto final HTTP en cada servidor cada 30 segundos, y si el ELB obtiene una respuesta 5xx o un tiempo de espera 2 veces seguidas, deja de tener en cuenta el servidor para las solicitudes normales. Sin embargo, sigue realizando las llamadas de comprobación de estado y, si el servidor responde con normalidad 10 veces seguidas, vuelve a entrar en la rotación.
Esto demuestra el uso de la histéresis para garantizar que el host no entre y salga de servicio con demasiada facilidad. (Un ejemplo familiar de histéresis es la forma en que el termostato de un aire acondicionado mantiene una "ventana de tolerancia" alrededor de la temperatura deseada). Este es un enfoque común, y puede funcionar razonablemente bien para escenarios en los que un servidor está completamente sano o enfermo, y no cambia de estado con frecuencia. En la situación menos común de tasas de fallo persistentes y bajas, por debajo del 40% aproximadamente, que afectan tanto a la comprobación de estado como al tráfico normal, un ELB con la configuración por defecto no vería fallos consecutivos con la frecuencia suficiente como para mantener el host fuera de servicio.
Las comprobaciones de estado deben diseñarse con cuidado para que no tengan un efecto erróneo en el equilibrador de carga. Estos son algunos de los tipos de respuestas que una llamada de comprobación de estado puede proporcionar:
- Prueba de humo: Realiza una llamada realista y comprueba si la respuesta es la esperada.
- Comprobación de dependencia funcional: El servidor realiza llamadas a todas sus dependencias y devuelve un fallo si alguna de ellas falla
- Comprobación de disponibilidad: Sólo hay que ver si el servidor puede responder a cualquier llamar, por ejemplo
GET /ping
produce 200 OK
y un cuerpo de respuesta de pong
Es importante que el chequeo sea lo más representativo posible del tráfico real. De lo contrario, puede producir falsos positivos o falsos negativos inaceptables. Por ejemplo, si el servidor tiene un número de rutas API, y sólo una de esas rutas está rota debido a una dependencia fallida... ¿está sano ese servidor? Si la comprobación de la salud de la prueba de humo sólo afecta a esa ruta, el cliente considerará que el servidor está totalmente averiado; por el contrario, si esa ruta es la única que funciona, el cliente considerará que el servidor está perfectamente en buen estado.
Las comprobaciones funcionales pueden ser más exhaustivas, pero no necesariamente mejores, ya que pueden provocar que un servidor (¡o todos los servidores!) se marque como caído incluso si una única dependencia opcional está caída. Esto es útil para la monitorización operativa, pero peligroso para el balanceo de carga; como resultado, mucha gente se limita a configurar simples comprobaciones de disponibilidad.
Las comprobaciones activas de la salud suelen ofrecer una visión binaria de la salud de un servidor, incluso si se realiza un seguimiento a lo largo del tiempo, ya que un servidor puede encontrarse en un estado degradado en el que pueda responder sistemáticamente a algunas peticiones, pero no a otras. La monitorización pasiva de la salud del tráfico, por otro lado, proporciona una visión escalar (o incluso más matizada) de la salud, ya que como mínimo el cliente sabe qué proporción de las peticiones están recibiendo fallos - y críticamente, esta monitorización pasiva recibe una visión completa de la salud del tráfico. (Ambos tipos de comprobación pueden rastrear información de latencia, por supuesto; algunas de estas distinciones sólo son válidas para la métrica de tasa de fallos).
CONTROLES BINARIOS Y DETECCIÓN DE ANOMALÍAS
Esta visión binaria puede acarrear graves problemas, ya que no permite comparar la salud de los servidores. Simplemente se agrupan como "arriba" o "abajo", basándose en un único tipo de llamada que puede no ser representativo. Incluso si tuviera varias llamadas de comprobación de estado, no hay garantía de que sigan siendo representativas del estado de su servidor a medida que se amplía su API y cambian las necesidades de los clientes. Pero aún peor, los fallos correlacionados podrían provocar un fallo en cascada innecesario. Fíjese en estos escenarios:
- Si el 100% de sus hosts han pasado las comprobaciones de estado activas, un equilibrador de carga ideal debería enrutar a todos los hosts.
- Si el 90% pasa, diríjase sólo a ese 90%: no importa por qué falla el 10%, ya que el resto del clúster sin duda puede manejar la carga.
- Si sólo el 10% está pasando... ruta a todos los hosts-mejorapostar a que el chequeo está mal (o es irrelevante) que machacar al 10% que está pasando los chequeos.
- Si pasa el 0%, enruta a todos los hosts: fallas el 100% de las peticiones que no enrutas, como se suele decir.
Cuanto más se acerque a cero la fracción de hosts que pasan, más probable es que haya un fallo en algo externo a los hosts, o incluso algo mal en el healthcheck. Imagina que tu healthcheck depende de una cuenta de prueba, y la cuenta de prueba es eliminada. O tal vez una dependencia se cae, pero la mayoría de las peticiones todavía se pueden servir. Sin embargo, todas las comprobaciones de estado fallan; el ELB deja fuera de servicio cada uno de los hosts, aunque las peticiones entrantes se estaban atendiendo perfectamente.
Lo que queda claro es que la salud es relativa: Un servidor puede estar más sano que sus vecinos aunque todos ellos tengan un problema. Y es más fácil verlo cuando se utilizan escalares en lugar de booleanos.
Básicamente, le gustaría que su equilibrador de carga realizara algún tipo de detección de anomalías simple. Si una pequeña fracción de sus servidores se comportan de forma extraña, sólo tiene que excluirlos y enviar un aviso a Operaciones. ¿Si la mayoría o todos se comportan de forma extraña? No empeores las cosas poniendo toda la carga en un pequeño puñado de servidores, o peor aún, en ninguno de ellos.
La clave, aquí, es evaluar la salud del servidor en vista de todo el clúster, en lugar de atómicamente. Lo más cercano a esto que he visto hasta ahora es el equilibrador de carga de Envoy, que tiene un "umbral de pánico" que por defecto mantendrá todos los hosts en servicio si el 50% o más de ellos tienen comprobaciones de salud fallidas. Si está utilizando comprobaciones de salud en su equilibrador de carga, considere la posibilidad de utilizar un enfoque de este tipo.
Puede que hayas notado que me he saltado la cuestión de qué hacer cuando el 30-70% de los servidores fallan las comprobaciones. Esta situación puede indicar un verdadero fallo, y puede ser dependiente o independiente de la carga. No estoy seguro de que sea posible para un equilibrador de carga saber qué situación se aplica, incluso si está dispuesto a hacer experimentos inteligentes de carga de tráfico A/B para averiguarlo. Peor aún, poner toda la carga en un número relativamente pequeño de servidores puede hacer que esos servidores se caigan. Aparte de la reducción de carga, no hay mucho que se pueda hacer en esta situación, y no estoy seguro de poder culpar a un diseño que mantenga esos servidores en servicio, o a uno que los elimine, cuando estén dentro de ese rango medio, porque he sido uno de los humanos en el bucle durante un incidente de producción de este tipo, y tampoco estaba claro para nosotros en ese momento.
Otra diferencia entre estos enfoques activos y pasivos es que con la comprobación activa, la información sobre el estado del servidor se actualiza a un ritmo constante, independientemente de la tasa de tráfico. Esto puede ser una ventaja cuando el tráfico es lento, o una desventaja cuando es alto. (Cinco segundos de fallos pueden ser mucho tiempo cuando se tienen 10.000 peticiones por segundo). En cambio, con la comprobación pasiva, la velocidad de detección de fallos es proporcional a la tasa de peticiones.
Pero la comprobación pasiva pura tiene un inconveniente importante. Si un servidor se cae, el equilibrador de carga lo retirará rápidamente del servicio. Eso significa no más tráfico, y no más tráfico significa que la visión del cliente de la salud del servidor nunca cambia: Permanece en cero para siempre.
Por supuesto, hay formas de solucionar esto, algunas de las cuales también abordan otros casos extremos sin datos, como el inicio del cliente o la sustitución de un único servidor en la lista de servidores del cliente. Todos estos casos deben abordarse especialmente si se utiliza la comprobación pasiva.

RESUMEN SANITARIO
Resumiendo lo anterior:
- El control pasivo del tráfico ofrece necesariamente una visión más completa y matizada de la salud que los controles activos.
- Existen múltiples ejes para evaluar la salud
- La salud de un servidor sólo puede entenderse en relación con el clúster
Pero, ¿qué hacemos con esa información? ¿Cómo pueden combinarse todas estas cifras de valor real para alcanzar nuestros objetivos de menor latencia, fallos mínimos y carga distribuida uniformemente?
En primer lugar, me gustaría hacer una digresión sobre una familia de modos de fallo, luego discutir algunos enfoques comunes de equilibrio de carga consciente de la salud y, por último, enumerar algunas posibles direcciones futuras.
Una acción descoordinada puede tener consecuencias sorprendentes. Imaginemos que una gran empresa envía un correo electrónico a sus empleados: "¡Hoy ofrecemos masajes para todos los empleados en el auditorio 2! Venid cuando queráis". ¿Cuándo cree que acudirá la gente? Mi suposición es que habría grandes aglomeraciones en algunos momentos del día:
- Enseguida
- Después de comer
- A última hora de la tarde, antes de volver a casa
Con esta distribución desigual, los masajistas a veces no tienen a nadie con quien trabajar; otras veces, hay colas lo bastante largas como para que la gente desista, y quizá ni siquiera vuelva a intentarlo más tarde. Ninguna de las dos cosas es deseable. Sin ningún tipo de coordinación -porque no la hay-, la gente sigue apareciendo en grupos. El comportamiento correlacionado accidental en este escenario es fácil de prevenir utilizando una herramienta común: La hoja de inscripción. (En el terreno del software, el análogo más cercano sería un sistema de procesamiento por lotes que acepta trabajos, los programa a su conveniencia y devuelve los resultados de forma asíncrona).
Resulta que hay una serie de fenómenos similares en el tráfico de API, a menudo agrupados bajo el apodo del problema de la manada atronadora. Un ejemplo clásico es un servicio de caché consultado por cientos de nodos de aplicación. Cuando la entrada de la caché caduca, la aplicación necesita recrear el valor con datos frescos, y hacerlo requiere tanto trabajo extra como (probablemente) llamadas de red adicionales a otros servidores. Si cientos de nodos de aplicación observan simultáneamente que una entrada popular de la caché caduca (porque todos reciben constantemente peticiones de estos datos), entonces todos intentarán recrearla simultáneamente, y llamarán simultáneamente a los servicios backend responsables de producir datos frescos. Esto no sólo es un despilfarro (en el mejor de los casos, sólo un nodo de aplicación debería realizar esta tarea, una vez por vida útil de la caché), sino que incluso podría aplastar a los servidores backend, que normalmente están protegidos detrás de la caché.
La solución clásica para los problemas de "rebaño atronador" en la caducidad de la caché es caducar probabilísticamente la entrada de la caché antes de tiempo en función de cada llamada, en lugar de que caduque en el mismo instante en todas partes. El enfoque más sencillo es añadir jitter, un pequeño número aleatorio que se resta de la fecha de caducidad cada vez que el cliente consulta la caché. Un perfeccionamiento de esta técnica, XFetch, sesga el jitter para retrasar la actualización hasta el último momento posible.
Otro problema familiar ocurre cuando un gran número de usuarios de un servicio instalan una tarea periódica para llamar a una API. Tal vez cada usuario de un servicio de copia de seguridad instala una tarea cron para cargar una copia de seguridad a medianoche (ya sea en su zona horaria local, o más probablemente en UTC.) El servidor de copia de seguridad se sobrecarga entonces a medianoche UTC y queda en gran medida sin utilizar durante el día.
De nuevo, hay una solución estándar: Al dar de alta a un nuevo usuario, genera un archivo crontab sugerido para que lo instale, utilizando una hora seleccionada aleatoriamente para cada usuario. Esto puede funcionar incluso sin un punto central de coordinación si el propio software de copia de seguridad escribe el archivo crontab, seleccionando una hora aleatoria cuando se instala por primera vez. (Puede que te des cuenta de que un enfoque similar podría funcionar para el caso de los masajes si, por alguna razón, no se pudiera utilizar una hoja de inscripción central: Cada empleado elige al azar una hora del día en la que está libre, y va a esa hora, aunque no sea necesariamente la hora óptima para su propio horario).
Estas dos soluciones -expiración aleatoria y programación aleatoria- hacen uso de la aleatoriedad para contrarrestar el comportamiento descoordinado pero correlacionado. Este es un principio importante: La aleatoriedad inhibe la correlación. Lo veremos de nuevo al abordar algunos retos relacionados con el equilibrio de carga.
También vemos, en el escenario del masaje, un enfoque alternativo consistente en confiar en un punto central de coordinación. Esta es una de las ventajas de utilizar un pequeño grupo de servidores potentes para un equilibrador de carga dedicado: cada servidor tiene una visión de más alto nivel del flujo de tráfico que la que tendría cada uno de un número mayor de clientes. Otra forma de aumentar la coordinación es hacer que los servidores autoinformen de la utilización como metadatos parásitos en sus respuestas. Esto no siempre es posible, pero la utilización comunicada por el servidor proporciona a los clientes información agregada a la que no tendrían acceso de otro modo. Esto podría dar a los equilibradores de carga del lado del cliente una visión más global del tipo que podría tener un equilibrador de carga dedicado. Además, a veces puede ayudar a distinguir entre fallos del servidor y de la red, con implicaciones para las interpretaciones dependientes de la carga frente a las independientes de la carga.
Con este aspecto de la dinámica del sistema en mente, volvamos a ver cómo los equilibradores de carga utilizan la información sanitaria.
USO DE LA SALUD EN EL EQUILIBRIO DE CARGA
Los equilibradores de carga suelen separar el uso de la información sanitaria en dos preocupaciones:
- Decidir qué servidores son candidatos a recibir solicitudes y, a continuación
- Decidir qué candidato seleccionar para cada solicitud
El enfoque clásico los trata como dos niveles totalmente separados. Los ELB, ALB y NLB de AWS, por ejemplo, utilizan diversos algoritmos para distribuir la carga (aleatorio, round-robin, aleatorio determinista y least-outstanding), pero existe un mecanismo independiente, basado en gran medida en comprobaciones de estado activas, para determinar qué servidores pueden participar en ese proceso de selección. (Según la documentación, parece que los NLB también utilizarán algún tipo de supervisión pasiva para decidir si expulsar a un servidor, pero los detalles son escasos).
Aleatorio, round-robin y aleatorio determinista (como flow-hash) ignoran completamente la salud: Un servidor está dentro o fuera. Por otro lado, el algoritmo de "least-outstanding" utiliza una métrica de salud pasiva. (Nótese que incluso este algoritmo para la selección de servidores se mantiene totalmente separado de las comprobaciones activas utilizadas para sacar servidores del clúster). Least-outstanding ("elige el servidor con menor concurrencia de peticiones") es uno de los varios enfoques para usar métricas de salud pasivas para asignar peticiones, cada uno basado en la optimización de una de las métricas mencionadas anteriormente: Latencia, tasa de fallos, concurrencia, tamaño de la cola.
ALGORITMOS DE SELECCIÓN
Algunos algoritmos de selección de equilibrio de carga eligen el servidor con el mejor valor para una métrica. A primera vista, esto tiene sentido: Esto da a la petición actual la mejor oportunidad de tener éxito, y rápidamente. Sin embargo, puede llevar a lo que yo llamo mobbing: Si la latencia es la métrica de salud elegida y un servidor muestra una latencia ligeramente inferior a los demás (vista desde todos los clientes), entonces todos los clientes enviarán todo su tráfico a ese servidor, al menos hasta que empiece a sufrir la carga y posiblemente incluso empiece a fallar. A medida que el servidor empieza a sufrir, su latencia efectiva aumenta, y posiblemente un servidor diferente gana el título de globalmente más saludable. Esto puede repetirse cíclicamente, y ser instigado por nada más que una muy ligera diferencia en la salud inicial.

El comportamiento de mobbing implica la confluencia de varios defectos en el sistema:
- La latencia es una métrica de salud retardada. Si en su lugar se utilizara la concurrencia (recuento de solicitudes en vuelo), los clientes no se molestarían, ya que la métrica de concurrencia se actualiza instantáneamente en el lado del cliente en cuanto se asignan más solicitudes a un servidor. Las mediciones retardadas, incluso con amortiguación, pueden provocar oscilaciones o resonancias indeseables.
- Los clientes no tienen una visión global de la situación y, por lo tanto, actúan de forma descoordinada para producir comportamientos correlacionados no deseados.
- Una pequeña diferencia en la salud del servidor produce una gran diferencia en el comportamiento del equilibrio de carga. Como hay retroalimentaciones de lo segundo a lo primero, esto se ajusta a una descripción de los sistemas caóticos, que son muy sensibles a las condiciones iniciales.
Los remedios, tal y como yo los veo:
- Utilice métricas de salud rápidas siempre que sea posible. De hecho, un algoritmo de selección de equilibrio de carga muy común es enviar todas las peticiones al servidor con menos peticiones en curso. (A veces se denomina menos conexiones o menos solicitudes en curso, dependiendo de si está orientado a la conexión o a la solicitud -algunas conexiones son de larga duración y llevan muchas solicitudes a lo largo de su vida). Por el contrario, no creo haber visto un algoritmo de selección de menor latencia, probablemente por esta misma razón.
- O bien intentar aproximarse a una visión global de la situación (utilizando un equilibrador de carga dedicado con un pequeño número de servidores, o incorporando la utilización notificada por el servidor) o utilizar la aleatoriedad para inhibir comportamientos correlacionados no deseados.
- Utiliza algoritmos que tengan aproximadamente el mismo comportamiento para aproximadamente las mismas entradas. No tienen por qué tener un comportamiento de variación continua, pero pueden utilizar la aleatoriedad para conseguir algo parecido.
Existe una alternativa popular a "elegir al mejor" denominada "dos opciones", descrita en el artículo "El poder de dos opciones aleatorias", en el que se analiza un enfoque general para la asignación de recursos (no específico ni centrado en los equilibradores de carga, pero ciertamente relevante). En este enfoque, se seleccionan dos candidatos y se utiliza el que tiene mejor salud. Esto se aproxima a una distribución uniforme cuando la salud a largo plazo de todos los servidores se aproxima a un valor idéntico, pero incluso una pequeña diferencia persistente en la salud puede desequilibrar enormemente la distribución de la carga. Una simulación simplista sin retroalimentación lo ilustra:
;; Select the index of one of N servers with health ranging ;; from 1000 to 1000-N, +/-N (defn selecttc [n] (let [spread n ;; top and bottom health ranges overlap by ~half ;; Compute health of a server, by index health (fn [i] (+ (- 1000 i spread) (* 2 spread (rand)))) ;; Randomly choose two servers, without replacement [i1 i2] (take 2 (shuffle (range n)))] ;; Pick the index of the healthier server (if (< (health i1) (health i2)) i2 i1)))
;; Ejecuta 10.000.000 de pruebas con 5 hosts e informa del número de veces ;; que se seleccionó cada índice de host (sort-by key (frequencies (repeatedly 10000000 #(selecttc 5)))) ;;= ([0 2849521] [1 2435167] [2 2001078] [3 1566792] [4 1147442])
Suponiendo que el aumento de la carga no afectara a la métrica de salud, esto produciría una diferencia de 2,5 veces en la carga de peticiones entre el más sano y el menos sano cuando los hosts tienen incluso una clasificación aproximada de salud. Nótese que el rango de salud del host 0 es de 995-1005 y el del host 4 es de 991-1001; a pesar de estar sólo 1-2% separados en términos absolutos, este ligero sesgo se magnifica en un gran desequilibrio en la carga.
Aunque la doble elección reduce el mobbing (y funciona bastante bien cuando no hay sesgo, que bien podría ser el caso si se producen retroalimentaciones), está claro que no es un mecanismo de selección apropiado para utilizar con métricas de salud retardada. Además, el artículo parece centrarse en la reducción máxima de la carga dado un conjunto idéntico de opciones, lo que no es el caso de los equilibradores de carga conscientes de la salud.
Por otra parte, la doble opción funciona bien con la menos destacada porque la retroalimentación es instantánea y autocorrectiva. Por su parte, la opción de menor resistencia supone un reto, ya que puede tener valores pequeños y cuantitativos. ¿Es un servidor con una conexión abierta el doble de saludable que un servidor con dos? ¿Qué tal cero y uno? Es más fácil trabajar con el menos destacado si hay relativamente pocos clientes (como en un equilibrador de carga dedicado) en relación con la carga de peticiones, lo que da lugar a comparaciones más sencillas (por ejemplo, 17 frente a 20.) Con valores medios pequeños, la aleatoriedad como criterio de desempate se vuelve muy importante, no sea que el primer servidor de la lista siempre reciba peticiones por defecto: si cada cliente sólo tiene una conexión abierta, pero hay 300 clientes, puede que colectivamente acribillen a ese único servidor. La doble elección, con su aleatoriedad, se presenta como un antídoto natural contra el mobbing resultante de los pequeños valores discretos de least-outstanding.
Una opción muy prometedora, aunque todavía académica, es la selección aleatoria ponderada. A cada servidor se le asigna un peso derivado de sus métricas de salud, y se elige un servidor según ese peso. Por ejemplo, si los servidores tuvieran un peso de 7, 3 y 1, tendrían un 70%, 30% y 10% de posibilidades de ser seleccionados cada vez, respectivamente. El uso de este algoritmo requiere cuidado para evitar la trampa de la inanición, y la derivación del peso debe utilizar una función no lineal bien elegida para que un servidor con el 90% de la salud de los demás reciba un peso muy reducido, tal vez sólo el 20% relativo. En el trabajo, estoy experimentando con este enfoque, y tengo grandes esperanzas en él después de algunos experimentos de integración local, pero todavía no lo he visto probado con tráfico del mundo real. Si resulta, probablemente entraré en más detalles en un futuro post sobre un nuevo algoritmo de equilibrio de carga.
COMBINAR LAS MÉTRICAS SANITARIAS
He estado posponiendo la cuestión de cómo utilizar múltiples métricas de salud. En mi opinión, esta es la parte más difícil, y afecta al meollo de la cuestión: ¿Cómo definir la salud para su aplicación?
Digamos que estás haciendo un seguimiento de la latencia, la tasa de fallos y la concurrencia, porque todo esto te importa. ¿Cómo se combinan? ¿Es tan malo un porcentaje de fallos del 5% como una latencia 10 veces mayor? (¿100 veces?) ¿En qué momento prefieres arriesgarte con un servidor disponible al 90% cuando el otro está mostrando picos masivos de latencia? Se me ocurren dos estrategias generales.
Podrías adoptar un enfoque escalonado, definiendo umbrales de aceptabilidad para cada métrica, y eligiendo sólo entre los servidores con tasas de fallo aceptables; si no hay ninguno, elige entre los que tengan una latencia aceptable, etc. Tal vez se defina un umbral de desbordamiento para que, si el grupo aceptable es demasiado pequeño, también se tengan en cuenta los servidores del nivel inmediatamente inferior. (Esta idea tiene cierto parecido con los niveles de prioridad de Envoy).
Otra posibilidad es utilizar métricas combinadas, en las que las métricas se combinan según alguna función continua. Quizá se dé más peso a algunas. Actualmente estoy experimentando con la derivación de un factor de peso [0,1] para cada métrica de salud, y multiplicándolos juntos, con algunos elevados a potencias más altas (al cuadrado o al cubo) para darles más peso. (Sospecho que se podrían utilizar potencias muy grandes para implementar algo parecido al enfoque por niveles incluso utilizando un combinador de métricas fusionadas).
También merece la pena considerar cómo pueden covar estas métricas, lo que sugiere posibles beneficios de un modelado más avanzado de la salud del servidor y de la conexión. Consideremos un servidor que ha entrado en un mal estado y está emitiendo respuestas de fallo muy rápidamente. Si la única métrica de salud es la latencia, este servidor ahora parece el más sano del clúster, y por lo tanto recibe más tráfico. rachelbythebay llama a esto el efecto de captura de carga equilibrada. ¡Rápido no siempre es sano! Dependiendo de tu configuración, un enfoque combinado puede o no suprimir suficientemente el tráfico a este servidor rebelde, mientras que un enfoque por niveles que priorice la baja tasa de fallos lo excluiría por completo.
La latencia y la tasa de fallos, en general, están relacionadas entre sí de maneras no evidentes. Además de la hipótesis de que "se produzcan fallos rápidamente", también está la cuestión de los fallos por tiempo de espera frente a los fallos sin tiempo de espera. En condiciones de alta latencia, el cliente producirá una serie de errores de tiempo de espera. ¿Se trata de "fallos" propiamente dichos, o sólo de respuestas de latencia excesivamente alta? ¿Deberían afectar a la métrica de latencia, a la métrica de tasa de fallos o a ambas? Compárelo con los fallos debidos a registros DNS erróneos y otros fallos de conexión rápida. Mi recomendación es registrar sólo los números de latencia de los éxitos, o de los fracasos que usted sabe que indican un tiempo de espera, como SocketTimeoutException y similares en Java. (Un compañero de trabajo sugiere la alternativa de registrar sólo los valores de latencia de los fallos cuando empeora la media de latencia).
CICLO DE VIDA
En la mayoría de los casos, se supone que el cliente se comunica con un conjunto estático de servidores. Pero los servidores se sustituyen, de uno en uno o en grandes grupos. Cuando se añade un nuevo servidor al clúster, el equilibrador de carga no debe enviarle todo el tráfico de inmediato, sino que debe aumentar el tráfico lentamente durante un tiempo. Este periodo de calentamiento permite que el servidor se optimice por completo: Calentamiento de la caché de disco y de instrucciones, optimización de hotspots en Java, etc. HAProxy implementa un arranque lento con este fin. Más allá del calentamiento, también es un momento de incertidumbre: El cliente no tiene historial con el servidor, por lo que limitar la dependencia de éste puede limitar el riesgo.
Si estás utilizando un enfoque de combinación de métricas, puede ser conveniente utilizar la edad del servidor como una métrica de salud pesudo, comenzando desde cerca de cero y aumentando hasta la salud completa en el transcurso de un minuto más o menos. (Empezar precisamente desde cero puede ser peligroso, dependiendo de tu algoritmo; el cliente puede enterarse de la sustitución completa de un conjunto de servidores a la vez, o ser reconfigurado para apuntar a un cluster diferente, y considerar brevemente que todos los servidores tienen salud cero). Es probable que cualquier mecanismo para manejar el reemplazo total de la lista de servidores también sea suficiente para manejar el arranque del cliente.
CARGA Y DESCARGA
Sólo he tocado ligeramente el tema del load shedding, en el que un servicio sometido a una gran carga de peticiones intenta responder a algunas o a todas las peticiones con fallos, muy rápidamente, en un esfuerzo por reducir la carga de la CPU y la contención de otros recursos. A veces no basta con hacer todo lo posible, o simplemente hay que mantener el servicio con vida el tiempo suficiente para poder escalarlo. La reducción de la carga es una apuesta basada en la idea de que devolver los fallos del 50% del tráfico ahora puede permitir responder con éxito al 100% del tráfico más adelante, y que intentar gestionar todo el tráfico ahora mismo puede acabar con el servicio por completo. Pero, ¿cómo saber cuándo hacerlo y en qué medida?
Sospecho que esto es en gran medida una preocupación separable: Si el equilibrador de carga es lo suficientemente bueno en la distribución de la carga, simplemente poniendo algo como Hystrix o concurrency-limits delante podría ser suficiente. El único beneficio que podría ver sería la gestión de la carga adicional en los servidores sanos cuando algunos servidores no lo son. Si sólo el 20% de los servidores están sanos, ¿es razonable que deban soportar 5 veces su cuota normal de carga? Un equilibrador de carga podría decidir razonablemente limitar el exceso a 10 veces más o menos, y nunca pedir a un servidor que asuma la "cuota de carga" de 9 servidores que han sido marcados como no sanos. Aunque esto es factible, ¿es deseable? No estoy seguro. No es totalmente adaptable, en el sentido de que todavía hay que configurar un límite de sobrecarga, y esa configuración puede quedar desfasada fácilmente (o ser irrelevante, por ejemplo, en un periodo de poco tráfico).
CONCLUSIONES
Basándome en lo anterior, creo que aunque muchas de las opciones existentes para el equilibrio de carga en entornos genéricos de alta disponibilidad tienden a funcionar bien para distribuir la carga en condiciones normales y en un conjunto selecto de condiciones de error, se quedan cortas en otras condiciones debido al mobbing, a la insuficiente capacidad de respuesta ante fallos y a la reacción exagerada ante estados degradados correlacionados.
Un equilibrador de carga de alta disponibilidad ideal evitaría las comprobaciones de estado activas para su funcionamiento normal y, en su lugar, realizaría un seguimiento pasivo de una serie de métricas de estado, incluidas las solicitudes actuales en vuelo y las métricas decrecientes (o continuas) de latencia y tasa de fallos. Un cliente que rastrea estas métricas está en una posición mucho mejor para realizar la detección de anomalías que uno que sólo observa periódicamente los resultados de las comprobaciones de estado activas.
Por supuesto, un equilibrador de carga realmente ideal encarnaría la eficiencia perfecta, en la que incluso bajo una carga creciente de peticiones todas las peticiones se gestionan tan satisfactoria y rápidamente como es posible... justo hasta que el sistema alcanza su límite teórico, momento en el que falla repentinamente (o empieza a perder carga), en lugar de mostrar gradualmente un estrés creciente. Aunque archivaría esto bajo "problemas que me encantaría tener", pone de relieve la necesidad de revisar las herramientas de monitorización si el equilibrador de carga es particularmente bueno ocultando los fallos del servidor al mundo exterior.
La principal cuestión abierta, en mi opinión, es cómo combinar estas métricas de salud y utilizarlas en la selección de servidores de forma que se minimice el comportamiento caótico y los demás problemas mencionados en este post, sin dejar de ser de aplicación general. Aunque actualmente apuesto por la selección aleatoria ponderada multifactorial, aún está por ver cómo se comporta en el mundo real.