Kubernetes equilibra la carga y escala las conexiones de larga duración


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 en el 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 la implementación de aplicaciones: servicios e implementaciones.

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

Los servicios de características son similares a un equilibrador de carga. Están diseñados para distribuir el tráfico a través de múltiples hogares.

Veamos como se ve .

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

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

  3. El script de implementación determina el número de instancias de la aplicación. Casi nunca tendrá que implementar directamente en:

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



Es útil considerar los servicios como un conjunto 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.

Esto es lo siguiente .

  1. Hay una solicitud curl 10.96.45.152 al servicio:

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

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



Si su aplicación consta de un front-end y un back-end, tendrá un servicio y una implementación para cada uno.

Cuando el frontend cumple con la solicitud al backend, no necesita saber exactamente cuántos montones sirve el backend: puede haber uno, diez o cien.

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

Cuando el frontend hace una solicitud al backend, usa la dirección IP del servicio de backend, que no cambia.

Así es como se ve .

  1. Bajo 1 solicita el componente interno del backend. En lugar de elegir uno específico para el backend, realiza una solicitud de servicio:

  2. El servicio selecciona uno de los pods de back-end como la dirección de destino:

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

  4. Menos de 1, no sabe exactamente cuántos hogares de menos de 5 están ocultos detrás del servicio:



Pero, ¿cómo distribuye exactamente el servicio las solicitudes? ¿Parece que se usa el balanceo round-robin? Vamos a hacerlo bien. 

Equilibrio en los servicios de Kubernetes


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

Puede verificar esto yendo a cualquier nodo en el clúster y ejecutando el comando netstat -ntlp.

Ni siquiera puede 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 forma 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 usa solo como un punto de entrada y no es servida por ningún proceso que escuche esta dirección IP y puerto.

Miremos eso

  1. Considere un grupo de tres nodos. Hay pods en cada nodo:

  2. Los hogares de punto pintados en beige son parte del servicio. Como el servicio no existe como proceso, está atenuado:

  3. El primero solicita el servicio y debe recaer en uno de los hogares relacionados:

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

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

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

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

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



¿Son iptables capaces de equilibrar la carga?


No, las iptables se usan para filtrar y no se diseñaron para equilibrar.

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

Y eso es exactamente lo que hace Kubernetes.

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

  1. Elija el primero con una probabilidad del 33%, de lo contrario, vaya a la siguiente regla.
  2. Elija el segundo con una probabilidad del 50%, de lo contrario pase a la siguiente regla.
  3. Elige el tercero debajo.

Tal sistema lleva al hecho de que cada sub se selecciona con una probabilidad del 33%.



Y no hay garantía de que bajo 2 se seleccionará después del archivo 1.

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

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

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


Cada solicitud HTTP desde el front-end hasta el back-end 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 la solicitud y reducir la carga si abre una conexión TCP y la usa para todas las solicitudes HTTP posteriores.

El protocolo HTTP contiene una característica llamada HTTP keep-alive o reutilización de la conexión. En este caso, se usa una conexión TCP para enviar y recibir muchas solicitudes y respuestas HTTP:



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

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

Aquí hay algunos enlaces a ejemplos en diferentes idiomas:


¿Qué pasa si usamos keep-alive en Kubernetes?
Supongamos que tanto el frontend como el backend admiten mantener vivo.

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

A diferencia de la situación habitual, cuando la conexión TCP se cierra después de recibir la respuesta, ahora se mantiene abierta para las siguientes solicitudes HTTP.

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

Para reenviar estas solicitudes, se utilizará una conexión TCP abierta, todas las solicitudes se enviarán a la misma debajo del backend, donde se obtuvo la primera solicitud.

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

No en este caso

Cuando se crea una conexión TCP, sigue las reglas de iptables, que seleccionan una específica para el backend donde irá el tráfico.

Como todas las siguientes solicitudes pasan por una conexión TCP ya abierta, las reglas de iptables ya no se invocan.

Veamos como se ve .

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

  2. Ya sabes lo que sucederá después. El servicio no existe, pero hay reglas de iptables que manejarán la solicitud:

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

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

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



Como resultado, obtuvo una respuesta más rápida y un mayor ancho de banda, pero perdió la capacidad de escalar el back-end.

Incluso si tiene dos módulos en el back-end, con una conexión constante, el tráfico siempre irá a uno de ellos.

¿Se puede arreglar esto?

Como Kubernetes no sabe cómo equilibrar las conexiones persistentes, esta tarea es su responsabilidad.

Los servicios son un conjunto 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 a cada hogar y equilibrar las solicitudes entre estas conexiones utilizando round-robin.

O aplique algoritmos de equilibrio más sofisticados .

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

  1. Obtenga la lista de puntos finales del servicio.
  2. Para cada punto final, abra una conexión persistente.
  3. Cuando necesite hacer una solicitud, use una de las conexiones abiertas.
  4. Actualice regularmente 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 enviar la primera solicitud al servicio, puede equilibrar las solicitudes en el lado del cliente:

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

  3. Tan pronto como reciba la lista, guárdela en el lado del cliente y úsela para conectarse a los pods:

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



Ahora la pregunta es: ¿este problema solo se aplica a HTTP keep-alive?

Balanceo de carga del lado del cliente


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

Si su aplicación usa una base de datos, la conexión TCP no se abre cada vez que necesita ejecutar una solicitud u obtener un documento de la base de datos. 

En cambio, se abre y utiliza una conexión TCP permanente a la base de datos.

Si su base de datos se implementa en Kubernetes y el acceso se proporciona como un servicio, entonces se encontrará con los mismos problemas que se describen en la sección anterior.

Una réplica de la base de datos se cargará más que el resto. Kube-proxy y Kubernetes no ayudarán a equilibrar las conexiones. Debe encargarse de equilibrar las consultas a su base de datos.

Según la biblioteca que use para conectarse a la base de datos, puede tener varias opciones para resolver este problema.

El siguiente es un ejemplo de acceso 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

Hay muchos otros protocolos que usan conexiones TCP persistentes:

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

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

Pero si estos protocolos son tan populares, ¿por qué no hay una solución de equilibrio estandarizada? ¿Por qué se requiere un cambio en la lógica del cliente? ¿Existe una solución nativa de Kubernetes?

Kube-proxy e iptables están diseñados para cerrar la mayoría de los escenarios de implementación estándar para Kubernetes. Esto es por conveniencia.

Si utiliza un servicio web que proporciona una API REST, tiene suerte: en este caso, no se utilizan conexiones TCP permanentes, puede utilizar cualquier servicio de Kubernetes.

Pero tan pronto como comience a usar conexiones TCP persistentes, tendrá que descubrir cómo distribuir uniformemente la carga en los backends. Kubernetes no contiene soluciones preparadas para este caso.

Sin embargo, por supuesto, hay opciones que pueden ayudar.

Equilibrando conexiones duraderas en Kubernetes


Kubernetes tiene cuatro tipos de servicios:

  1. Clusterip
  2. NodePort
  3. Loadbalancer
  4. Sin cabeza

Los primeros tres servicios se basan en la dirección IP virtual, que es utilizada por kube-proxy para construir reglas de iptables. Pero la base fundamental de todos los servicios es un servicio de tipo sin cabeza.

Ninguna dirección IP está asociada con el servicio sin cabeza, y solo proporciona un mecanismo para obtener una lista de direcciones IP y puertos de hogares asociados (puntos finales).

Todos los servicios se basan en el servicio sin cabeza.

El servicio ClusterIP es un servicio sin cabeza con algunas adiciones: 

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

Por lo tanto, puede ignorar kube-proxy y usar directamente la lista de puntos finales recibidos del servicio sin cabeza para equilibrar la carga en su aplicación.

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

Si su aplicación ya está implementada, tal tarea puede parecer imposible. No obstante, hay una alternativa.

Service Mesh te ayudará


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

Cuando se inicia la aplicación, esta:

  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.

Tan pronto como la aplicación quiera hacer una solicitud, esta:

  1. Selecciona una conexión disponible utilizando algún tipo de lógica (por ejemplo, round-robin).
  2. Cumple la solicitud

Estos pasos funcionan para WebSockets, gRPC y AMQP.

Puede separar esta lógica en una biblioteca separada y usarla en sus aplicaciones.

Sin embargo, se pueden utilizar cuadrículas de servicio como Istio o Linkerd.

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

  1. Busca automáticamente las direcciones IP de los servicios.
  2. Comprueba conexiones como WebSockets y gRPC.
  3. Balancea las solicitudes utilizando el protocolo correcto.

Service Mesh ayuda a administrar el tráfico dentro del clúster, pero requiere bastante recursos. Otras opciones son utilizar bibliotecas de terceros, como Netflix Ribbon, o servidores proxy programables, como Envoy.

¿Qué sucede si ignoras los problemas de equilibrio?


No puede usar el equilibrio de carga y no notar ningún cambio. Veamos algunos escenarios de trabajo.

Si tiene más clientes que servidores, este no es un gran problema.

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



Las conexiones se pueden distribuir de manera desigual: tal vez cuatro clientes conectados al mismo servidor, pero hay una buena posibilidad 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, es posible que sus recursos no se utilicen lo suficiente y aparezca un posible cuello de botella.

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

Otros servidores estarán inactivos:



Si estos dos servidores no pueden manejar el procesamiento de solicitudes del cliente, la escala 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, tan pronto como comience a trabajar con protocolos de aplicación que usan 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 con la posibilidad de equilibrar en el lado del cliente.

Traducción preparado por un equipo Kubernetes aaS a partir de Mail.ru .

Qué más leer sobre el tema :

  1. Tres niveles de autoescalado en Kubernetes y cómo usarlos de manera efectiva
  2. Kubernetes en el espíritu de la piratería con una plantilla de implementación .
  3. Kubernetes .

All Articles