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

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

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

Kubernetes fournit deux abstractions pratiques pour le déploiement d'applications : les services et les 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 en tant que Pod et se voit attribuer une adresse IP.

Les services ont une fonction similaire à celle d'un équilibreur de charge. Ils sont conçus pour répartir le trafic sur plusieurs pods.

Voyons à quoi ça ressemble.

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

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

  2. L'équilibreur de charge est appelé un service et se voit attribuer une adresse IP. Toute requête entrante est redirigée vers l'un des pods :

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

  3. Le scénario de déploiement détermine le nombre d'instances de l'application. Vous n’aurez presque jamais besoin de développer directement sous :

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

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

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

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.

Ça ressemble à ça.

  1. Une requête curl 10.96.45.152 est reçue au service :

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

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

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

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

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

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

Lorsque le frontend fait une requête au backend, il n'a pas besoin de savoir exactement combien de pods le backend dessert : il peut y en avoir un, dix ou une centaine.

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

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

Voici à quoi il ressemble.

  1. Moins de 1 demande le composant backend interne. Au lieu d'en sélectionner un spécifique pour le backend, il fait une requête au service :

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

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

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

  3. Le trafic va du Pod 1 au Pod 5, sélectionné par le service :

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

  4. Under 1 ne sait pas exactement combien de pods comme under 5 sont cachés derrière le service :

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

Mais comment exactement le service distribue-t-il les requêtes ? Il semble que l'équilibrage à tour de rôle soit utilisé ? Voyons cela. 

É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 vous connectant à n'importe quel nœud du cluster et en exécutant la commande netstat -ntlp.

Vous ne pourrez 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 génère 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 requête et l'envoyer à l'un des pods. »

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

Regardons ça

  1. Considérons un cluster de trois nœuds. Chaque nœud possède des pods :

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

  2. Des cosses attachées peintes en beige font partie de la prestation. Le service n'existant pas en tant que processus, il est affiché en gris :

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

  3. Le premier pod demande un service et doit se rendre dans l'un des pods associés :

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

  4. Mais le service n’existe pas, le processus n’existe pas. Comment ça marche?

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

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

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

  6. Les règles iptables savent que le service n'existe pas et remplacent son adresse IP par l'une des adresses IP des pods associés à ce service :

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

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

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

  8. En fonction de la topologie du réseau, la requête finit par atteindre le pod :

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

iptables peut-il é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 pseudo-équilibreur.

Et c’est exactement ce qui est implémenté dans Kubernetes.

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

  1. Sélectionnez le premier sous avec une probabilité de 33%, sinon passez à la règle suivante.
  2. Choisissez la seconde avec une probabilité de 50%, sinon passez à la règle suivante.
  3. Sélectionnez le troisième ci-dessous.

Ce système aboutit à ce que chaque pod soit sélectionné avec une probabilité de 33 %.

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

Et rien ne garantit que le Pod 2 sera choisi ensuite après le Pod 1.

Noter: iptables utilise un module statistique à distribution aléatoire. Ainsi, l’algorithme d’équilibrage est basé sur une sélection aléatoire.

Maintenant que vous comprenez le fonctionnement des services, examinons des scénarios de service plus intéressants.

Les connexions de longue durée dans Kubernetes ne sont pas évolutives par défaut

Chaque requête HTTP du frontend au backend est servie par une connexion TCP distincte, qui est ouverte et fermée.

Si le frontend envoie 100 requêtes par seconde au backend, alors 100 connexions TCP différentes sont ouvertes et fermées.

Vous pouvez réduire le temps et la charge de traitement des requêtes en ouvrant une connexion TCP et en l'utilisant pour toutes les requêtes HTTP ultérieures.

Le protocole HTTP possède une fonctionnalité appelée HTTP keep-alive, ou réutilisation de connexion. Dans ce cas, une seule connexion TCP est utilisée pour envoyer et recevoir plusieurs requêtes et réponses HTTP :

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

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 et environnements de programmation.

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

Que se passe-t-il si nous utilisons keep-alive dans un service Kubernetes ?
Supposons que le frontend et le backend prennent en charge le maintien en vie.

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

Contrairement à la situation habituelle dans laquelle la connexion TCP est fermée après réception d'une réponse, elle reste désormais ouverte pour d'autres requêtes HTTP.

Que se passe-t-il si le frontend envoie plus de requêtes au backend ?

Pour transmettre ces requêtes, une connexion TCP ouverte sera utilisée, toutes les requêtes seront dirigées vers le même backend où la première requête est allée.

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 sélectionnent un backend spécifique vers lequel ira le trafic.

Puisque toutes les requêtes ultérieures se font sur une connexion TCP déjà ouverte, les règles iptables ne sont plus appelées.

Voyons à quoi ça ressemble.

  1. Le premier pod envoie une requête au service :

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

  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 requête :

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

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

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

  4. La requête atteint le pod. À ce stade, une connexion TCP persistante entre les deux pods sera établie :

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

  5. Toute requête ultérieure du premier pod passera par la connexion déjà établie :

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

Le résultat est un temps de réponse plus rapide et un débit plus élevé, mais vous perdez la possibilité de faire évoluer le backend.

Même si vous disposez de deux pods dans le backend, avec une connexion constante, le trafic sera toujours dirigé vers l'un d'eux.

Cela peut-il être corrigé?

Puisque Kubernetes ne sait pas équilibrer les connexions persistantes, cette tâche vous incombe.

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 requêtes entre eux. Vous pouvez ouvrir une connexion persistante à chaque pod et équilibrer les demandes entre ces connexions à l'aide du round robin.

Ou postulez davantage algorithmes d'équilibrage complexes.

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

  1. Obtenez une liste des points de terminaison du service.
  2. Ouvrez une connexion persistante pour chaque point de terminaison.
  3. Lorsqu'une demande doit être faite, 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.

Voilà à quoi cela ressemblera.

  1. Au lieu que le premier pod envoie la requête au service, vous pouvez équilibrer les requêtes côté client :

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

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

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

  3. Une fois que vous avez la liste, enregistrez-la côté client et utilisez-la pour vous connecter aux pods :

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

  4. Vous êtes responsable de l'algorithme d'équilibrage de charge :

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

Maintenant, la question se pose : ce problème s’applique-t-il uniquement au HTTP keep-alive ?

Équilibrage de charge côté client

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

Si votre application utilise une base de données, une connexion TCP n'est pas ouverte à chaque fois que vous devez faire une requête ou récupérer un document de la base de données. 

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

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

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

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

Vous trouverez ci-dessous un exemple d'accès à un cluster de bases 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 de nombreux autres protocoles qui utilisent des connexions TCP persistantes :

  • WebSockets et WebSockets sécurisés
  • HTTP / 2
  • gRPC
  • Prises RS
  • AMQP

Vous devriez déjà connaître la plupart de ces protocoles.

Mais si ces protocoles sont si populaires, pourquoi n’existe-t-il pas de solution d’équilibrage standardisée ? Pourquoi la logique client doit-elle changer ? Existe-t-il une solution Kubernetes native ?

Kube-proxy et iptables sont conçus pour couvrir les cas d'utilisation les plus courants lors du déploiement sur Kubernetes. C'est pour plus de commodité.

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

Mais une fois que vous aurez commencé à 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, il existe certainement des options qui peuvent aider.

Équilibrer les connexions de longue durée dans Kubernetes

Il existe quatre types de services dans Kubernetes :

  1. ClusterIP
  2. Port de nœud
  3. Équilibreur de charge
  4. Sans tête

Les trois premiers services fonctionnent sur la base d'une adresse IP virtuelle, qui est utilisée par Kube-proxy pour créer des règles iptables. Mais la base fondamentale de tous les services est un service sans tête.

Le service sans tête n'a aucune adresse IP associée et fournit uniquement un mécanisme pour récupérer une liste d'adresses IP et de ports des pods (points de terminaison) qui lui sont associés.

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 génère les règles iptables nécessaires.

De cette façon, vous pouvez ignorer kube-proxy et utiliser directement la liste des points de terminaison obtenue à partir du service sans tête pour équilibrer la charge de votre application.

Mais comment pouvons-nous ajouter une logique similaire à toutes les applications déployées dans le cluster ?

Si votre application est déjà déployée, cette tâche peut paraître impossible. Il existe cependant 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 maintient un pool de connexions.
  3. Met régulièrement à jour le pool en ajoutant ou en supprimant des points de terminaison.

Une fois que l’application souhaite faire une demande, elle :

  1. Sélectionne une connexion disponible en utilisant une certaine logique (par exemple, round-robin).
  2. Exécute la requête.

Ces étapes fonctionnent pour les connexions WebSockets, gRPC et AMQP.

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

Cependant, vous pouvez utiliser des maillages de services comme Istio ou Linkerd à la place.

Service Mesh augmente votre application avec un processus qui :

  1. Recherche automatiquement les adresses IP de service.
  2. Teste les connexions telles que WebSockets et gRPC.
  3. Équilibre les demandes en utilisant le bon protocole.

Service Mesh aide à gérer le trafic au sein du cluster, mais il est assez gourmand en ressources. D'autres options utilisent des bibliothèques tierces comme Netflix Ribbon ou des proxys programmables comme Envoy.

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

Vous pouvez choisir de ne pas utiliser l’équilibrage de charge sans remarquer aucun changement. Examinons quelques scénarios de travail.

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

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

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

Les connexions peuvent ne pas être réparties de manière é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 d'être sous-utilisées et un goulot d'étranglement potentiel apparaîtra.

Disons qu'il y a deux clients et cinq serveurs. Dans le meilleur des cas, il y aura deux connexions permanentes vers deux serveurs sur cinq.

Les serveurs restants seront inactifs :

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

Si ces deux serveurs ne peuvent pas gérer les demandes des clients, la mise à l'échelle horizontale ne sera d'aucune utilité.

Conclusion

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

Cependant, une fois 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 sont plus adaptés. Kubernetes ne fournit pas de mécanismes internes pour équilibrer les connexions TCP persistantes.

Cela signifie que vous devez écrire des applications en gardant à l'esprit l'équilibrage côté client.

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

Quoi d'autre à lire sur le sujet:

  1. Trois niveaux d'autoscaling dans Kubernetes et comment les utiliser efficacement
  2. Kubernetes dans l'esprit du piratage avec un modèle de mise en œuvre.
  3. Notre chaîne Telegram sur la transformation numérique.

Source: habr.com

Ajouter un commentaire