Équilibrage de charge et mise à l'échelle de connexions à longue durée de vie Kubernetes


Cet article vous aidera à comprendre le fonctionnement de l'équilibrage de charge dans Kubernetes, ce qui se passe lors de la mise à l'échelle des connexions à longue durée de vie et pourquoi vous devriez envisager un équilibrage côté client si vous utilisez HTTP / 2, gRPC, RSockets, AMQP ou d'autres protocoles à longue durée de vie. 

Un peu sur la façon dont le trafic est redistribué dans Kubernetes 


Kubernetes fournit deux abstractions pratiques pour déployer des applications: Services et Déploiements.

Les déploiements décrivent comment et combien de copies de votre application doivent être exécutées à un moment donné. Chaque application est déployée comme sous (Pod) et se voit attribuer une adresse IP.

Les services d'entités sont similaires à un équilibreur de charge. Ils sont conçus pour répartir le trafic sur plusieurs foyers.

Voyons à quoi ça ressemble .

  1. Dans le diagramme ci-dessous, vous voyez trois instances de la même application et un équilibreur de charge:

  2. L'équilibreur de charge est appelé le service, une adresse IP lui est attribuée. Toute demande entrante est redirigée vers l'un des pods:

  3. Le script de déploiement détermine le nombre d'instances d'application. Vous n'aurez presque jamais à déployer directement sous:

  4. Chaque pod se voit attribuer sa propre adresse IP:



Il est utile de considérer les services comme un ensemble d'adresses IP. Chaque fois que vous accédez au service, l'une des adresses IP est sélectionnée dans la liste et utilisée comme adresse de destination.

C'est comme suit .

  1. Il y a une demande de curl 10.96.45.152 au service:

  2. Le service sélectionne l'une des trois adresses de pod comme destination:

  3. Le trafic est redirigé vers un pod spécifique:



Si votre application se compose d'un frontend et d'un backend, vous aurez à la fois un service et un déploiement pour chacun.

Lorsque le frontend répond à la demande du backend, il n'a pas besoin de savoir exactement combien de foyers le backend sert: il peut y en avoir un, dix ou cent.

De plus, le frontend ne sait rien des adresses des foyers desservant le backend.

Lorsque le frontend fait une demande au backend, il utilise l'adresse IP du service backend, qui ne change pas.

Voici à quoi ça ressemble .

  1. Moins de 1 demande le composant interne backend. Au lieu d'en choisir un spécifique pour le backend, il effectue une demande de service:

  2. Le service sélectionne l'un des modules backend comme adresse de destination:

  3. Le trafic passe du foyer 1 au foyer 5 sélectionné par le service:

  4. En dessous de 1, il ne sait pas exactement combien de foyers de moins de 5 ans sont cachés derrière le service:



Mais comment le service distribue-t-il exactement les demandes? L'équilibrage à tour de rôle semble-t-il être utilisé? Faisons les choses correctement. 

Équilibrage dans les services Kubernetes


Les services Kubernetes n'existent pas. Il n'existe aucun processus pour le service auquel une adresse IP et un port sont attribués.

Vous pouvez le vérifier en accédant à n'importe quel nœud du cluster et en exécutant la commande netstat -ntlp.

Vous ne pouvez même pas trouver l'adresse IP attribuée au service.

L'adresse IP du service est située dans la couche de contrôle, dans le contrôleur et enregistrée dans la base de données - etcd. La même adresse est utilisée par un autre composant - kube-proxy.
Kube-proxy reçoit une liste d'adresses IP pour tous les services et forme un ensemble de règles iptables sur chaque nœud du cluster.

Ces règles disent: "Si nous voyons l'adresse IP du service, nous devons modifier l'adresse de destination de la demande et l'envoyer à l'un des pods."

L'adresse IP du service est utilisée uniquement comme point d'entrée et n'est desservie par aucun processus écoutant cette adresse IP et ce port.

Regardons ça

  1. Prenons un cluster de trois nœuds. Il y a des pods sur chaque nœud:

  2. Les foyers tricotés peints en beige font partie du service. Étant donné que le service n'existe pas en tant que processus, il est grisé:

  3. Le premier demande le service et devrait tomber sur l'un des foyers associés:

  4. Mais le service n'existe pas, il n'y a pas de processus. Comment ça marche?

  5. Avant que la requête ne quitte le nœud, elle passe par les règles iptables:

  6. Les règles iptables savent qu'il n'y a pas de service, et remplacez son adresse IP par l'une des adresses IP des pods associés à ce service:

  7. La demande reçoit une adresse IP valide comme adresse de destination et est normalement traitée:

  8. Selon la topologie du réseau, la demande parvient finalement au foyer:



Les iptables sont-ils capables d'équilibrer la charge?


Non, les iptables sont utilisés pour le filtrage et n'ont pas été conçus pour l'équilibrage.

Cependant, il est possible d'écrire un ensemble de règles qui fonctionnent comme un pseudo-équilibreur .

Et c'est exactement ce que fait Kubernetes.

Si vous avez trois pods, kube-proxy écrira les règles suivantes:

  1. Choisissez le premier avec une probabilité de 33%, sinon passez à la règle suivante.
  2. Choisissez le second avec une probabilité de 50%, sinon passez à la règle suivante.
  3. Choisissez le troisième sous.

Un tel système conduit au fait que chaque sous est sélectionné avec une probabilité de 33%.



Et il n'y a aucune garantie que sous 2, il sera sélectionné après le fichier 1.

Remarque : iptables utilise un module statistique de distribution aléatoire. Ainsi, l'algorithme d'équilibrage est basé sur une sélection aléatoire.

Maintenant que vous comprenez comment fonctionnent les services, examinons des scénarios de travail plus intéressants.

Les connexions à longue durée de vie dans Kubernetes ne sont pas mises à l'échelle par défaut


Chaque requête HTTP du front-end au back-end est servie par une connexion TCP distincte, qui s'ouvre et se ferme.

Si le frontend envoie 100 requêtes par seconde au backend, alors 100 connexions TCP différentes s'ouvrent et se ferment.

Vous pouvez réduire le temps de traitement de la demande et réduire la charge si vous ouvrez une connexion TCP et l'utilisez pour toutes les demandes HTTP suivantes.

Le protocole HTTP contient une fonctionnalité appelée HTTP keep-alive, ou réutilisation de la connexion. Dans ce cas, une connexion TCP est utilisée pour envoyer et recevoir de nombreuses demandes et réponses HTTP:



Cette fonctionnalité n'est pas activée par défaut: le serveur et le client doivent être configurés en conséquence.

La configuration elle-même est simple et accessible pour la plupart des langages de programmation et des environnements.

Voici quelques liens vers des exemples dans différentes langues:


Que se passe-t-il si nous utilisons Keep-Alive dans Kubernetes?
Supposons que le front-end et le back-end prennent en charge.

Nous avons une copie du frontend et trois copies du backend. Le frontend fait la première demande et ouvre une connexion TCP au backend. La demande atteint le service, l'un des modules backend est sélectionné comme adresse de destination. Il envoie une réponse au backend et le frontend la reçoit.

Contrairement à la situation habituelle, lorsque la connexion TCP est fermée après réception de la réponse, elle est désormais maintenue ouverte pour les requêtes HTTP suivantes.

Que se passe-t-il si le frontend envoie plus de demandes backend?

Pour transmettre ces demandes, une connexion TCP ouverte sera utilisée, toutes les demandes seront envoyées à la même sous le backend, où la première demande a été reçue.

Iptables ne devrait-il pas redistribuer le trafic?

Pas dans ce cas.

Lorsqu'une connexion TCP est créée, elle passe par les règles iptables, qui en sélectionnent une spécifique pour le backend où le trafic ira.

Comme toutes les requêtes suivantes passent par une connexion TCP déjà ouverte, les règles iptables ne sont plus appelées.

Voyons à quoi ça ressemble .

  1. Le premier sous envoie une demande au service:

  2. Vous savez déjà ce qui va se passer ensuite. Le service n'existe pas, mais il existe des règles iptables qui traiteront la demande:

  3. L'un des modules backend sera sélectionné comme adresse de destination:

  4. La demande parvient au foyer. À ce stade, une connexion TCP permanente entre les deux pods sera établie:

  5. Toute prochaine demande du premier pod passera par une connexion déjà établie:



En conséquence, vous avez obtenu une réponse plus rapide et une bande passante plus élevée, mais vous avez perdu la possibilité de faire évoluer le backend.

Même si vous avez deux pods dans le backend, avec une connexion constante, le trafic ira toujours vers l'un d'eux.

Cela peut-il être corrigé?

Étant donné que Kubernetes ne sait pas comment équilibrer les connexions persistantes, cette tâche est de votre responsabilité.

Les services sont un ensemble d'adresses IP et de ports appelés points de terminaison.

Votre application peut obtenir une liste de points de terminaison du service et décider comment répartir les demandes entre eux. Vous pouvez ouvrir une connexion persistante à chaque foyer et équilibrer les demandes entre ces connexions à l'aide du round-robin.

Ou appliquez des algorithmes d'équilibrage plus sophistiqués .

Le code côté client qui est responsable de l'équilibrage doit suivre cette logique:

  1. Obtenez la liste des points de terminaison du service.
  2. Pour chaque point de terminaison, ouvrez une connexion persistante.
  3. Lorsque vous devez faire une demande, utilisez l'une des connexions ouvertes.
  4. Mettez régulièrement à jour la liste des points de terminaison, créez-en de nouveaux ou fermez les anciennes connexions persistantes si la liste change.

Voici à quoi cela ressemblera .

  1. Au lieu d'envoyer la première demande au service, vous pouvez équilibrer les demandes côté client:

  2. Vous devez écrire du code qui demande quels pods font partie du service:

  3. Dès que vous recevez la liste, enregistrez-la côté client et utilisez-la pour vous connecter aux pods:

  4. Vous êtes vous-même responsable de l'algorithme d'équilibrage de charge:



Maintenant, la question est: ce problème ne s'applique-t-il qu'à HTTP Keep-Alive?

Équilibrage de charge côté client


HTTP n'est pas le seul protocole qui peut utiliser des connexions TCP persistantes.

Si votre application utilise une base de données, la connexion TCP ne s'ouvre pas à chaque fois que vous devez exécuter une demande ou obtenir un document de la base de données. 

Au lieu de cela, une connexion TCP permanente à la base de données est ouverte et utilisée.

Si votre base de données est déployée dans Kubernetes et que l'accès est fourni en tant que service, vous rencontrerez les mêmes problèmes que ceux décrits dans la section précédente.

Une réplique de base de données sera chargée plus que les autres. Kube-proxy et Kubernetes n'aideront pas à équilibrer les connexions. Vous devez veiller à équilibrer les requêtes dans votre base de données.

Selon la bibliothèque que vous utilisez pour vous connecter à la base de données, vous pouvez avoir différentes options pour résoudre ce problème.

Voici un exemple d'accès à un cluster de base de données MySQL à partir de 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

Il existe des tonnes d'autres protocoles qui utilisent des connexions TCP persistantes:

  • WebSockets et WebSockets sécurisés
  • HTTP / 2
  • gRPC
  • RSockets
  • AMQP

Vous devez déjà être familiarisé avec la plupart de ces protocoles.

Mais si ces protocoles sont si populaires, pourquoi n'y a-t-il pas de solution d'équilibrage standardisée? Pourquoi un changement de logique client est-il nécessaire? Existe-t-il une solution native Kubernetes?

Kube-proxy et iptables sont conçus pour fermer la plupart des scénarios de déploiement standard pour Kubernetes. C'est pour plus de commodité.

Si vous utilisez un service Web qui fournit une API REST, vous avez de la chance - dans ce cas, les connexions TCP permanentes ne sont pas utilisées, vous pouvez utiliser n'importe quel service Kubernetes.

Mais dès que vous commencez à utiliser des connexions TCP persistantes, vous devrez trouver comment répartir uniformément la charge sur les backends. Kubernetes ne contient pas de solutions toutes faites pour ce cas.

Cependant, bien sûr, il existe des options qui peuvent aider.

Équilibrer les connexions de longue durée dans Kubernetes


Kubernetes propose quatre types de services:

  1. Clusterip
  2. NodePort
  3. Équilibreur de charge
  4. Sans tête

Les trois premiers services sont basés sur l'adresse IP virtuelle, qui est utilisée par kube-proxy pour construire des règles iptables. Mais la base fondamentale de tous les services est un service de type sans tête.

Aucune adresse IP n'est associée au service sans tête, et elle ne fournit qu'un mécanisme pour obtenir une liste d'adresses IP et de ports de foyers associés (points de terminaison).

Tous les services sont basés sur le service sans tête.

Le service ClusterIP est un service sans tête avec quelques ajouts: 

  1. La couche de gestion lui attribue une adresse IP.
  2. Kube-proxy forme les règles iptables nécessaires.

Ainsi, vous pouvez ignorer kube-proxy et utiliser directement la liste des points de terminaison reçus du service sans tête pour équilibrer la charge dans votre application.

Mais comment ajouter une logique similaire à toutes les applications déployées dans un cluster?

Si votre application est déjà déployée, une telle tâche peut sembler impossible. Cependant, il y a une alternative.

Service Mesh vous aidera


Vous avez probablement déjà remarqué que la stratégie d'équilibrage de charge côté client est assez standard.

Lorsque l'application démarre, elle:

  1. Obtient une liste d'adresses IP du service.
  2. Ouvre et gère un pool de connexions.
  3. Met à jour périodiquement le pool, en ajoutant ou supprimant des points de terminaison.

Dès que l'application souhaite faire une demande, elle:

  1. Sélectionne une connexion disponible en utilisant une sorte de logique (par exemple round-robin).
  2. Répond à la demande.

Ces étapes fonctionnent pour WebSockets, gRPC et AMQP.

Vous pouvez séparer cette logique dans une bibliothèque distincte et l'utiliser dans vos applications.

Cependant, des grilles de service telles que Istio ou Linkerd peuvent être utilisées à la place.

Service Mesh complète votre application avec un processus qui:

  1. Recherche automatiquement les adresses IP des services.
  2. Vérifie les connexions telles que WebSockets et gRPC.
  3. Équilibre les demandes en utilisant le bon protocole.

Service Mesh permet de gérer le trafic au sein du cluster, mais il est assez gourmand en ressources. D'autres options utilisent des bibliothèques tierces, telles que le ruban Netflix, ou des proxys programmables, tels que Envoy.

Que se passe-t-il si vous ignorez les problèmes d'équilibrage?


Vous ne pouvez pas utiliser l'équilibrage de charge et ne remarquer aucun changement. Regardons quelques scénarios de travail.

Si vous avez plus de clients que de serveurs, ce n'est pas un gros problème.

Supposons qu'il y ait cinq clients qui se connectent à deux serveurs. Même s'il n'y a pas d'équilibrage, les deux serveurs seront utilisés:



Les connexions peuvent être réparties de manière inégale: peut-être quatre clients connectés au même serveur, mais il y a de fortes chances que les deux serveurs soient utilisés.

Ce qui est plus problématique, c'est le scénario inverse.

Si vous avez moins de clients et plus de serveurs, vos ressources risquent de ne pas être suffisamment utilisées et un goulot d'étranglement potentiel apparaîtra.

Supposons qu'il y ait deux clients et cinq serveurs. Au mieux, il y aura deux connexions permanentes à deux serveurs sur cinq.

Les autres serveurs seront inactifs:



Si ces deux serveurs ne peuvent pas gérer le traitement des demandes des clients, la mise à l'échelle horizontale n'aidera pas.

Conclusion


Les services Kubernetes sont conçus pour fonctionner dans la plupart des scénarios d'application Web standard.

Cependant, dès que vous commencez à travailler avec des protocoles d'application qui utilisent des connexions TCP persistantes, telles que des bases de données, gRPC ou WebSockets, les services ne conviennent plus. Kubernetes ne fournit pas de mécanismes internes pour équilibrer les connexions TCP persistantes.

Cela signifie que vous devez écrire des applications avec possibilité d'équilibrage côté client.

Traduction préparée par une équipe Kubernetes de de Mail.ru aaS .

Quoi d'autre à lire sur le sujet :

  1. Trois niveaux de mise à l'échelle automatique dans Kubernetes et comment les utiliser efficacement
  2. Kubernetes dans l'esprit du piratage avec un modèle d'implémentation .
  3. Kubernetes .

All Articles