Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes
Este artículo le ayudará a comprender cómo funciona el equilibrio de carga en Kubernetes, qué sucede al escalar conexiones de larga duración y por qué debería considerar el equilibrio del lado del cliente si utiliza HTTP/2, gRPC, RSockets, AMQP u otros protocolos de larga duración. . 

Un poco sobre cómo se redistribuye el tráfico en Kubernetes 

Kubernetes proporciona dos abstracciones convenientes para implementar aplicaciones: servicios e implementaciones.

Las implementaciones describen cómo y cuántas copias de su aplicación deben ejecutarse en un momento dado. Cada aplicación se implementa como un Pod y se le asigna una dirección IP.

Los servicios tienen una función similar a un equilibrador de carga. Están diseñados para distribuir el tráfico entre varios pods.

Veamos cómo se ve.

  1. En el siguiente diagrama puede ver tres instancias de la misma aplicación y un equilibrador de carga:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  2. El equilibrador de carga se denomina Servicio y se le asigna una dirección IP. Cualquier solicitud entrante se redirige a uno de los pods:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  3. El escenario de implementación determina la cantidad de instancias de la aplicación. Casi nunca tendrás que expandirte directamente debajo:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  4. A cada pod se le asigna su propia dirección IP:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

Es útil pensar en los servicios como una colección de direcciones IP. Cada vez que accede al servicio, se selecciona una de las direcciones IP de la lista y se utiliza como dirección de destino.

Se parece a esto.

  1. Se recibe una solicitud curl 10.96.45.152 en el servicio:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  2. El servicio selecciona una de las tres direcciones de pod como destino:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  3. El tráfico se redirige a un pod específico:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

Si su aplicación consta de un frontend y un backend, tendrá tanto un servicio como una implementación para cada uno.

Cuando el frontend realiza una solicitud al backend, no necesita saber exactamente cuántos pods atiende el backend: podría haber uno, diez o cien.

Además, el frontend no sabe nada sobre las direcciones de los pods que sirven al backend.

Cuando el frontend realiza una solicitud al backend, utiliza la dirección IP del servicio backend, que no cambia.

Así es como se ve.

  1. Menos de 1 solicita el componente backend interno. En lugar de seleccionar uno específico para el backend, realiza una solicitud al servicio:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  2. El servicio selecciona uno de los pods de backend como dirección de destino:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  3. El tráfico va del Pod 1 al Pod 5, seleccionado por el servicio:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  4. Under 1 no sabe exactamente cuántos pods como under 5 se esconden detrás del servicio:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

Pero, ¿cómo distribuye exactamente el servicio las solicitudes? ¿Parece que se utiliza el equilibrio por turnos? Vamos a resolverlo. 

Equilibrio en los servicios de Kubernetes

Los servicios de Kubernetes no existen. No existe ningún proceso para el servicio al que se le asigna una dirección IP y un puerto.

Puede verificar esto iniciando sesión en cualquier nodo del clúster y ejecutando el comando netstat -ntlp.

Ni siquiera podrás encontrar la dirección IP asignada al servicio.

La dirección IP del servicio se encuentra en la capa de control, en el controlador y se registra en la base de datos, etc. Otro componente utiliza la misma dirección: kube-proxy.
Kube-proxy recibe una lista de direcciones IP para todos los servicios y genera un conjunto de reglas de iptables en cada nodo del clúster.

Estas reglas dicen: "Si vemos la dirección IP del servicio, debemos modificar la dirección de destino de la solicitud y enviarla a uno de los pods".

La dirección IP del servicio se utiliza solo como punto de entrada y no es atendida por ningún proceso que escuche esa dirección IP y puerto.

Miremos esto

  1. Considere un grupo de tres nodos. Cada nodo tiene pods:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  2. Vainas atadas pintadas de color beige son parte del servicio. Debido a que el servicio no existe como proceso, se muestra en gris:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  3. El primer pod solicita un servicio y debe ir a uno de los pods asociados:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  4. Pero el servicio no existe, el proceso no existe. ¿Como funciona?

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  5. Antes de que la solicitud abandone el nodo, pasa por las reglas de iptables:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  6. Las reglas de iptables saben que el servicio no existe y reemplazan su dirección IP con una de las direcciones IP de los pods asociados con ese servicio:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  7. La solicitud recibe una dirección IP válida como dirección de destino y se procesa normalmente:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  8. Dependiendo de la topología de la red, la solicitud finalmente llega al pod:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

¿Puede iptables equilibrar la carga?

No, los iptables se utilizan para filtrar y no fueron diseñados para equilibrar.

Sin embargo, es posible escribir un conjunto de reglas que funcionen como pseudoequilibrador.

Y esto es exactamente lo que se implementa en Kubernetes.

Si tiene tres pods, kube-proxy escribirá las siguientes reglas:

  1. Seleccione el primer sub con una probabilidad del 33%; de lo contrario, pase a la siguiente regla.
  2. Elija la segunda con una probabilidad del 50%; de lo contrario, pase a la siguiente regla.
  3. Seleccione el tercero debajo.

Este sistema da como resultado que cada grupo se seleccione con una probabilidad del 33%.

Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

Y no hay garantía de que el Pod 2 sea el siguiente después del Pod 1.

Nota: iptables utiliza un módulo estadístico con distribución aleatoria. Por tanto, el algoritmo de equilibrio se basa en una selección aleatoria.

Ahora que comprende cómo funcionan los servicios, veamos escenarios de servicios más interesantes.

Las conexiones de larga duración en Kubernetes no escalan de forma predeterminada

Cada solicitud HTTP desde el frontend hasta el backend es atendida por una conexión TCP separada, que se abre y se cierra.

Si el frontend envía 100 solicitudes por segundo al backend, se abren y cierran 100 conexiones TCP diferentes.

Puede reducir el tiempo de procesamiento de solicitudes y la carga abriendo una conexión TCP y utilizándola para todas las solicitudes HTTP posteriores.

El protocolo HTTP tiene una función llamada HTTP keep-alive o reutilización de la conexión. En este caso, se utiliza una única conexión TCP para enviar y recibir múltiples solicitudes y respuestas HTTP:

Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

Esta característica no está habilitada de forma predeterminada: tanto el servidor como el cliente deben configurarse en consecuencia.

La configuración en sí es sencilla y accesible para la mayoría de entornos y lenguajes de programación.

Aquí hay algunos enlaces a ejemplos en diferentes idiomas:

¿Qué pasa si usamos keep-alive en un servicio de Kubernetes?
Supongamos que tanto el frontend como el backend admiten el mantenimiento.

Tenemos una copia del frontend y tres copias del backend. El frontend realiza la primera solicitud y abre una conexión TCP con el backend. La solicitud llega al servicio y uno de los pods de backend se selecciona como dirección de destino. El backend envía una respuesta y el frontend la recibe.

A diferencia de la situación habitual en la que la conexión TCP se cierra después de recibir una respuesta, ahora se mantiene abierta para futuras solicitudes HTTP.

¿Qué sucede si el frontend envía más solicitudes al backend?

Para reenviar estas solicitudes, se utilizará una conexión TCP abierta, todas las solicitudes irán al mismo backend donde fue la primera solicitud.

¿No debería iptables redistribuir el tráfico?

No en este caso.

Cuando se crea una conexión TCP, pasa por reglas de iptables, que seleccionan un backend específico al que irá el tráfico.

Dado que todas las solicitudes posteriores se realizan en una conexión TCP ya abierta, ya no se llaman las reglas de iptables.

Veamos cómo se ve.

  1. El primer pod envía una solicitud al servicio:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  2. Ya sabes lo que pasará a continuación. El servicio no existe, pero existen reglas de iptables que procesarán la solicitud:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  3. Se seleccionará uno de los pods de backend como dirección de destino:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  4. La solicitud llega al pod. En este punto, se establecerá una conexión TCP persistente entre los dos pods:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  5. Cualquier solicitud posterior del primer pod pasará por la conexión ya establecida:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

El resultado es un tiempo de respuesta más rápido y un mayor rendimiento, pero se pierde la capacidad de escalar el backend.

Incluso si tienes dos pods en el backend, con una conexión constante, el tráfico siempre irá a uno de ellos.

¿Se puede arreglar esto?

Dado que Kubernetes no sabe cómo equilibrar las conexiones persistentes, esta tarea recae en usted.

Los servicios son una colección de direcciones IP y puertos llamados puntos finales.

Su aplicación puede obtener una lista de puntos finales del servicio y decidir cómo distribuir las solicitudes entre ellos. Puede abrir una conexión persistente para cada pod y equilibrar las solicitudes entre estas conexiones mediante round-robin.

O aplicar más algoritmos de equilibrio complejos.

El código del lado del cliente responsable del equilibrio debe seguir esta lógica:

  1. Obtenga una lista de puntos finales del servicio.
  2. Abra una conexión persistente para cada punto final.
  3. Cuando sea necesario realizar una solicitud, utilice una de las conexiones abiertas.
  4. Actualice periódicamente la lista de puntos finales, cree otros nuevos o cierre conexiones persistentes antiguas si la lista cambia.

Así es como se verá.

  1. En lugar de que el primer pod envíe la solicitud al servicio, puede equilibrar las solicitudes en el lado del cliente:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  2. Debe escribir un código que pregunte qué pods forman parte del servicio:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  3. Una vez que tenga la lista, guárdela en el lado del cliente y úsela para conectarse a los pods:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

  4. Usted es responsable del algoritmo de equilibrio de carga:

    Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

Ahora surge la pregunta: ¿este problema sólo se aplica al mantenimiento de conexión HTTP?

Equilibrio de carga del lado del cliente

HTTP no es el único protocolo que puede utilizar conexiones TCP persistentes.

Si su aplicación utiliza una base de datos, entonces no se abre una conexión TCP cada vez que necesita realizar una solicitud o recuperar un documento de la base de datos. 

En su lugar, se abre y utiliza una conexión TCP persistente a la base de datos.

Si su base de datos está implementada en Kubernetes y el acceso se proporciona como un servicio, encontrará los mismos problemas descritos en la sección anterior.

Una réplica de la base de datos estará más cargada que las demás. Kube-proxy y Kubernetes no ayudarán a equilibrar las conexiones. Debes tener cuidado de equilibrar las consultas a tu base de datos.

Dependiendo de la biblioteca que utilice para conectarse a la base de datos, es posible que tenga diferentes opciones para resolver este problema.

A continuación se muestra un ejemplo de cómo acceder a un clúster de base de datos MySQL desde Node.js:

var mysql = require('mysql');
var poolCluster = mysql.createPoolCluster();

var endpoints = /* retrieve endpoints from the Service */

for (var [index, endpoint] of endpoints) {
  poolCluster.add(`mysql-replica-${index}`, endpoint);
}

// Make queries to the clustered MySQL database

Existen muchos otros protocolos que utilizan conexiones TCP persistentes:

  • WebSockets y WebSockets seguros
  • HTTP / 2
  • gRPC
  • RSockets
  • AMQP

Ya debería estar familiarizado con la mayoría de estos protocolos.

Pero si estos protocolos son tan populares, ¿por qué no existe una solución de equilibrio estandarizada? ¿Por qué es necesario cambiar la lógica del cliente? ¿Existe una solución nativa de Kubernetes?

Kube-proxy e iptables están diseñados para cubrir los casos de uso más comunes al implementar en Kubernetes. Esto es por conveniencia.

Si está utilizando un servicio web que expone una API REST, está de suerte; en este caso, no se utilizan conexiones TCP persistentes; puede utilizar cualquier servicio de Kubernetes.

Pero una vez que comiences a usar conexiones TCP persistentes, tendrás que descubrir cómo distribuir uniformemente la carga entre los servidores. Kubernetes no contiene soluciones preparadas para este caso.

Sin embargo, ciertamente existen opciones que pueden ayudar.

Equilibrando conexiones de larga duración en Kubernetes

Hay cuatro tipos de servicios en Kubernetes:

  1. IP de clúster
  2. Puerto de nodo
  3. equilibrador de carga
  4. Sin cabeza

Los primeros tres servicios funcionan en función de una dirección IP virtual, que kube-proxy utiliza para crear reglas de iptables. Pero la base fundamental de todos los servicios es un servicio sin cabeza.

El servicio sin cabeza no tiene ninguna dirección IP asociada y solo proporciona un mecanismo para recuperar una lista de direcciones IP y puertos de los pods (puntos finales) asociados con él.

Todos los servicios se basan en el servicio sin cabeza.

El servicio ClusterIP es un servicio headless con algunas adiciones: 

  1. La capa de gestión le asigna una dirección IP.
  2. Kube-proxy genera las reglas de iptables necesarias.

De esta manera, puede ignorar kube-proxy y usar directamente la lista de puntos finales obtenida del servicio sin cabeza para equilibrar la carga de su aplicación.

Pero, ¿cómo podemos agregar una lógica similar a todas las aplicaciones implementadas en el clúster?

Si su aplicación ya está implementada, esta tarea puede parecer imposible. Sin embargo, existe una opción alternativa.

Service Mesh te ayudará

Probablemente ya hayas notado que la estrategia de equilibrio de carga del lado del cliente es bastante estándar.

Cuando se inicia la aplicación,:

  1. Obtiene una lista de direcciones IP del servicio.
  2. Abre y mantiene un grupo de conexiones.
  3. Actualiza periódicamente el grupo agregando o eliminando puntos finales.

Una vez que la aplicación quiere realizar una solicitud,:

  1. Selecciona una conexión disponible usando alguna lógica (por ejemplo, operación por turnos).
  2. Ejecuta la solicitud.

Estos pasos funcionan para conexiones WebSockets, gRPC y AMQP.

Puede separar esta lógica en una biblioteca independiente y utilizarla en sus aplicaciones.

Sin embargo, puedes utilizar mallas de servicios como Istio o Linkerd.

Service Mesh aumenta su aplicación con un proceso que:

  1. Busca automáticamente direcciones IP de servicio.
  2. Prueba conexiones como WebSockets y gRPC.
  3. Equilibra las solicitudes utilizando el protocolo correcto.

Service Mesh ayuda a gestionar el tráfico dentro del clúster, pero consume bastantes recursos. Otras opciones son utilizar bibliotecas de terceros como Netflix Ribbon o proxies programables como Envoy.

¿Qué sucede si ignoras los problemas de equilibrio?

Puede optar por no utilizar el equilibrio de carga y aún así no notar ningún cambio. Veamos algunos escenarios de trabajo.

Si tienes más clientes que servidores, esto no es un problema tan grande.

Digamos que hay cinco clientes que se conectan a dos servidores. Incluso si no hay equilibrio, se utilizarán ambos servidores:

Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

Es posible que las conexiones no estén distribuidas uniformemente: tal vez cuatro clientes conectados al mismo servidor, pero hay muchas posibilidades de que se utilicen ambos servidores.

Lo que es más problemático es el escenario opuesto.

Si tiene menos clientes y más servidores, sus recursos pueden estar infrautilizados y aparecerá un posible cuello de botella.

Digamos que hay dos clientes y cinco servidores. En el mejor de los casos, habrá dos conexiones permanentes a dos de cada cinco servidores.

Los servidores restantes estarán inactivos:

Equilibrio de carga y escalado de conexiones de larga duración en Kubernetes

Si estos dos servidores no pueden manejar las solicitudes de los clientes, el escalado horizontal no ayudará.

Conclusión

Los servicios de Kubernetes están diseñados para funcionar en la mayoría de los escenarios de aplicaciones web estándar.

Sin embargo, una vez que comienza a trabajar con protocolos de aplicaciones que utilizan conexiones TCP persistentes, como bases de datos, gRPC o WebSockets, los servicios ya no son adecuados. Kubernetes no proporciona mecanismos internos para equilibrar las conexiones TCP persistentes.

Esto significa que debe escribir aplicaciones teniendo en cuenta el equilibrio del lado del cliente.

Traducción preparada por el equipo. Kubernetes aaS de Mail.ru.

¿Qué más leer sobre el tema?:

  1. Tres niveles de autoescalado en Kubernetes y cómo utilizarlos de forma eficaz
  2. Kubernetes con espíritu de piratería con una plantilla de implementación.
  3. Nuestro canal de Telegram sobre transformación digital.

Fuente: habr.com

Añadir un comentario