Architecture d'équilibrage de charge réseau dans Yandex.Cloud

Architecture d'équilibrage de charge réseau dans Yandex.Cloud
Bonjour, je m'appelle Sergey Elantsev, je développe équilibreur de charge réseau dans Yandex.Cloud. Auparavant, j'ai dirigé le développement de l'équilibreur L7 pour le portail Yandex - mes collègues plaisantent en disant que quoi que je fasse, il s'avère que c'est un équilibreur. Je dirai aux lecteurs de Habr comment gérer la charge dans une plate-forme cloud, ce que nous considérons comme l'outil idéal pour atteindre cet objectif et comment nous progressons vers la construction de cet outil.

Tout d'abord, introduisons quelques termes :

  • VIP (Virtual IP) - adresse IP de l'équilibreur
  • Serveur, backend, instance : une machine virtuelle exécutant une application
  • RIP (Real IP) - adresse IP du serveur
  • Healthcheck - vérifier l'état de préparation du serveur
  • Zone de disponibilité, AZ : infrastructure isolée dans un centre de données
  • Région - une union de différentes AZ

Les équilibreurs de charge résolvent trois tâches principales : ils effectuent l'équilibrage eux-mêmes, améliorent la tolérance aux pannes du service et simplifient sa mise à l'échelle. La tolérance aux pannes est assurée grâce à la gestion automatique du trafic : l'équilibreur surveille l'état de l'application et exclut de l'équilibrage les instances qui ne réussissent pas le contrôle d'activité. La mise à l'échelle est assurée en répartissant uniformément la charge entre les instances, ainsi qu'en mettant à jour la liste des instances à la volée. Si l’équilibrage n’est pas suffisamment uniforme, certaines instances recevront une charge dépassant leur limite de capacité et le service deviendra moins fiable.

Un équilibreur de charge est souvent classé selon la couche de protocole du modèle OSI sur lequel il s'exécute. Le Cloud Balancer fonctionne au niveau TCP, qui correspond à la quatrième couche, L4.

Passons à un aperçu de l'architecture du Cloud Balancer. Nous augmenterons progressivement le niveau de détail. Nous divisons les composants de l'équilibreur en trois classes. La classe du plan de configuration est responsable de l'interaction de l'utilisateur et stocke l'état cible du système. Le plan de contrôle stocke l'état actuel du système et gère les systèmes de la classe du plan de données, qui sont directement responsables de la transmission du trafic des clients vers vos instances.

Plan de données

Le trafic aboutit sur des appareils coûteux appelés routeurs frontaliers. Pour augmenter la tolérance aux pannes, plusieurs de ces appareils fonctionnent simultanément dans un même centre de données. Ensuite, le trafic est dirigé vers des équilibreurs, qui annoncent les adresses IP anycast à toutes les zones de disponibilité via BGP pour les clients. 

Architecture d'équilibrage de charge réseau dans Yandex.Cloud

Le trafic est transmis via ECMP - il s'agit d'une stratégie de routage selon laquelle il peut y avoir plusieurs routes de même qualité vers la cible (dans notre cas, la cible sera l'adresse IP de destination) et les paquets peuvent être envoyés le long de n'importe laquelle d'entre elles. Nous prenons également en charge le travail dans plusieurs zones de disponibilité selon le schéma suivant : nous annonçons une adresse dans chaque zone, le trafic se dirige vers la plus proche et ne dépasse pas ses limites. Plus loin dans cet article, nous examinerons plus en détail ce qui arrive au trafic.

Plan de configuration

 
Le composant clé du plan de configuration est l'API, à travers laquelle les opérations de base avec les équilibreurs sont effectuées : créer, supprimer, modifier la composition des instances, obtenir les résultats des contrôles de santé, etc. D'une part, il s'agit d'une API REST, et d'autre part D'autre part, nous dans le Cloud utilisons très souvent le framework gRPC, nous « traduisons » donc REST en gRPC et utilisons ensuite uniquement gRPC. Toute demande conduit à la création d'une série de tâches idempotentes asynchrones exécutées sur un pool commun de travailleurs Yandex.Cloud. Les tâches sont écrites de telle manière qu'elles peuvent être suspendues à tout moment puis redémarrées. Cela garantit l’évolutivité, la répétabilité et la journalisation des opérations.

Architecture d'équilibrage de charge réseau dans Yandex.Cloud

En conséquence, la tâche de l'API fera une requête au contrôleur de service de l'équilibreur, qui est écrite en Go. Il peut ajouter et supprimer des équilibreurs, modifier la composition des backends et des paramètres. 

Architecture d'équilibrage de charge réseau dans Yandex.Cloud

Le service stocke son état dans Yandex Database, une base de données gérée distribuée que vous pourrez bientôt utiliser. Dans Yandex.Cloud, comme nous l'avons déjà fait dit, le concept de nourriture pour chiens s'applique : si nous utilisons nous-mêmes nos services, nos clients seront également heureux de les utiliser. La base de données Yandex est un exemple de mise en œuvre d'un tel concept. Nous stockons toutes nos données dans YDB, et nous n'avons pas à penser à la maintenance et à la mise à l'échelle de la base de données : ces problèmes sont résolus pour nous, nous utilisons la base de données comme un service.

Revenons au contrôleur d'équilibreur. Sa tâche est de sauvegarder les informations sur l'équilibreur et d'envoyer une tâche pour vérifier l'état de préparation de la machine virtuelle au contrôleur de contrôle de santé.

Contrôleur de contrôle de santé

Il reçoit les demandes de modification des règles de vérification, les enregistre dans YDB, répartit les tâches entre les nœuds de contrôle de santé et regroupe les résultats, qui sont ensuite enregistrés dans la base de données et envoyés au contrôleur d'équilibrage de charge. Il envoie à son tour une demande de modification de la composition du cluster dans le plan de données au nœud d'équilibrage de charge, dont je parlerai ci-dessous.

Architecture d'équilibrage de charge réseau dans Yandex.Cloud

Parlons davantage des contrôles de santé. Ils peuvent être divisés en plusieurs classes. Les audits ont différents critères de réussite. Les vérifications TCP doivent établir avec succès une connexion dans un délai déterminé. Les vérifications HTTP nécessitent à la fois une connexion réussie et une réponse avec un code d'état 200.

De plus, les contrôles diffèrent selon la classe d'action - ils sont actifs et passifs. Les contrôles passifs surveillent simplement ce qui se passe dans le trafic sans prendre aucune mesure particulière. Cela ne fonctionne pas très bien sur L4 car cela dépend de la logique des protocoles de niveau supérieur : sur L4, il n'y a aucune information sur la durée de l'opération ou si la connexion a été bonne ou mauvaise. Les contrôles actifs nécessitent que l'équilibreur envoie des requêtes à chaque instance de serveur.

La plupart des équilibreurs de charge effectuent eux-mêmes des contrôles d'activité. Chez Cloud, nous avons décidé de séparer ces parties du système pour augmenter l'évolutivité. Cette approche nous permettra d'augmenter le nombre d'équilibreurs tout en maintenant le nombre de demandes de bilan de santé au service. Les vérifications sont effectuées par des nœuds de vérification de l'état distincts, sur lesquels les cibles de vérification sont fragmentées et répliquées. Vous ne pouvez pas effectuer de vérifications à partir d'un seul hôte, car cela pourrait échouer. Nous n’aurons alors pas l’état des instances qu’il a vérifiées. Nous effectuons des vérifications sur n'importe laquelle des instances à partir d'au moins trois nœuds de contrôle de santé. Nous partageons les objectifs des vérifications entre les nœuds à l'aide d'algorithmes de hachage cohérents.

Architecture d'équilibrage de charge réseau dans Yandex.Cloud

Séparer l’équilibrage et le bilan de santé peut entraîner des problèmes. Si le nœud de contrôle de santé envoie des requêtes à l'instance, en contournant l'équilibreur (qui ne dessert pas actuellement le trafic), alors une situation étrange se produit : la ressource semble être active, mais le trafic ne l'atteindra pas. Nous résolvons ce problème de cette façon : nous sommes assurés de lancer le trafic de contrôle de santé via des équilibreurs. En d'autres termes, le schéma de déplacement des paquets avec le trafic des clients et des contrôles de santé diffère peu : dans les deux cas, les paquets atteindront les équilibreurs, qui les livreront aux ressources cibles.

La différence est que les clients adressent des demandes à VIP, tandis que les contrôles de santé adressent des demandes à chaque RIP individuel. Un problème intéressant se pose ici : nous donnons à nos utilisateurs la possibilité de créer des ressources dans des réseaux IP gris. Imaginons que deux propriétaires de cloud différents aient caché leurs services derrière des équilibreurs. Chacun d'eux dispose de ressources dans le sous-réseau 10.0.0.1/24, avec les mêmes adresses. Vous devez être capable de les distinguer d'une manière ou d'une autre, et ici vous devez plonger dans la structure du réseau virtuel Yandex.Cloud. Il est préférable de trouver plus de détails dans vidéo de about:événement cloud, il est important pour nous maintenant que le réseau soit multicouche et comporte des tunnels qui peuvent être distingués par un identifiant de sous-réseau.

Les nœuds de contrôle de santé contactent les équilibreurs en utilisant des adresses dites quasi-IPv6. Une quasi-adresse est une adresse IPv6 avec une adresse IPv4 et un identifiant de sous-réseau utilisateur intégrés à l'intérieur. Le trafic atteint l'équilibreur, qui en extrait l'adresse de la ressource IPv4, remplace IPv6 par IPv4 et envoie le paquet au réseau de l'utilisateur.

Le trafic inverse se déroule de la même manière : l'équilibreur voit que la destination est un réseau gris provenant des contrôleurs de santé et convertit IPv4 en IPv6.

VPP - le cœur du plan de données

L'équilibreur est implémenté à l'aide de la technologie Vector Packet Processing (VPP), un framework de Cisco pour le traitement par lots du trafic réseau. Dans notre cas, le framework fonctionne au-dessus de la bibliothèque de gestion des périphériques réseau dans l'espace utilisateur - Data Plane Development Kit (DPDK). Cela garantit des performances élevées de traitement des paquets : beaucoup moins d'interruptions se produisent dans le noyau et il n'y a aucun changement de contexte entre l'espace noyau et l'espace utilisateur. 

VPP va encore plus loin et extrait encore plus de performances du système en combinant des packages en lots. Les gains de performances proviennent de l'utilisation agressive des caches sur les processeurs modernes. On utilise à la fois des caches de données (les paquets sont traités en « vecteurs », les données sont proches les unes des autres) et des caches d'instructions : dans VPP, le traitement des paquets suit un graphe dont les nœuds contiennent des fonctions qui effectuent la même tâche.

Par exemple, le traitement des paquets IP dans VPP s'effectue dans l'ordre suivant : d'abord, les en-têtes des paquets sont analysés dans le nœud d'analyse, puis ils sont envoyés au nœud, qui transmet ensuite les paquets selon les tables de routage.

Un peu hardcore. Les auteurs de VPP ne tolèrent pas de compromis dans l'utilisation des caches de processeur, donc le code typique pour traiter un vecteur de paquets contient une vectorisation manuelle : il existe une boucle de traitement dans laquelle une situation comme « nous avons quatre paquets dans la file d'attente » est traitée, puis pareil pour deux, puis - pour un. Les instructions de prélecture sont souvent utilisées pour charger des données dans des caches afin d'en accélérer l'accès lors des itérations suivantes.

n_left_from = frame->n_vectors;
while (n_left_from > 0)
{
    vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next);
    // ...
    while (n_left_from >= 4 && n_left_to_next >= 2)
    {
        // processing multiple packets at once
        u32 next0 = SAMPLE_NEXT_INTERFACE_OUTPUT;
        u32 next1 = SAMPLE_NEXT_INTERFACE_OUTPUT;
        // ...
        /* Prefetch next iteration. */
        {
            vlib_buffer_t *p2, *p3;

            p2 = vlib_get_buffer (vm, from[2]);
            p3 = vlib_get_buffer (vm, from[3]);

            vlib_prefetch_buffer_header (p2, LOAD);
            vlib_prefetch_buffer_header (p3, LOAD);

            CLIB_PREFETCH (p2->data, CLIB_CACHE_LINE_BYTES, STORE);
            CLIB_PREFETCH (p3->data, CLIB_CACHE_LINE_BYTES, STORE);
        }
        // actually process data
        /* verify speculative enqueues, maybe switch current next frame */
        vlib_validate_buffer_enqueue_x2 (vm, node, next_index,
                to_next, n_left_to_next,
                bi0, bi1, next0, next1);
    }

    while (n_left_from > 0 && n_left_to_next > 0)
    {
        // processing packets by one
    }

    // processed batch
    vlib_put_next_frame (vm, node, next_index, n_left_to_next);
}

Ainsi, les Healthchecks communiquent via IPv6 avec le VPP, ce qui les transforme en IPv4. Ceci est réalisé par un nœud du graphe, que nous appelons NAT algorithmique. Pour le trafic inverse (et la conversion d'IPv6 vers IPv4), il existe le même nœud NAT algorithmique.

Architecture d'équilibrage de charge réseau dans Yandex.Cloud

Le trafic direct provenant des clients de l'équilibreur passe par les nœuds graphiques, qui effectuent eux-mêmes l'équilibrage. 

Architecture d'équilibrage de charge réseau dans Yandex.Cloud

Le premier nœud est celui des sessions persistantes. Il stocke le hachage de 5 tuples pour les séances établies. Le 5-tuple comprend l'adresse et le port du client à partir duquel les informations sont transmises, l'adresse et les ports des ressources disponibles pour recevoir le trafic, ainsi que le protocole réseau. 

Le hachage à 5 tuples nous aide à effectuer moins de calculs dans le nœud de hachage cohérent suivant, ainsi qu'à mieux gérer les modifications de la liste de ressources derrière l'équilibreur. Lorsqu'un paquet pour lequel il n'y a pas de session arrive à l'équilibreur, il est envoyé au nœud de hachage cohérent. C'est là que s'effectue l'équilibrage à l'aide d'un hachage cohérent : nous sélectionnons une ressource dans la liste des ressources « live » disponibles. Ensuite, les paquets sont envoyés au nœud NAT, qui remplace l'adresse de destination et recalcule les sommes de contrôle. Comme vous pouvez le constater, nous suivons les règles du VPP - like to like, regroupant des calculs similaires pour augmenter l'efficacité des caches du processeur.

Hachage cohérent

Pourquoi l’avons-nous choisi et qu’est-ce que c’est ? Tout d'abord, considérons la tâche précédente : sélectionner une ressource dans la liste. 

Architecture d'équilibrage de charge réseau dans Yandex.Cloud

En cas de hachage incohérent, le hachage du paquet entrant est calculé et une ressource est sélectionnée dans la liste par le reste de la division de ce hachage par le nombre de ressources. Tant que la liste reste inchangée, ce schéma fonctionne bien : nous envoyons toujours des paquets avec le même 5-tuple à la même instance. Si, par exemple, une ressource ne répond plus aux contrôles de santé, le choix changera pour une partie importante des hachages. Les connexions TCP du client seront interrompues : un paquet qui a précédemment atteint l'instance A peut commencer à atteindre l'instance B, qui ne connaît pas la session de ce paquet.

Un hachage cohérent résout le problème décrit. La façon la plus simple d'expliquer ce concept est la suivante : imaginez que vous disposez d'un anneau sur lequel vous distribuez des ressources par hachage (par exemple, par IP:port). Sélectionner une ressource revient à faire tourner la roue d'un angle déterminé par le hachage du paquet.

Architecture d'équilibrage de charge réseau dans Yandex.Cloud

Cela minimise la redistribution du trafic lorsque la composition des ressources change. La suppression d'une ressource n'affectera que la partie de l'anneau de hachage cohérent dans laquelle se trouvait la ressource. L'ajout d'une ressource modifie également la distribution, mais nous avons un nœud de sessions persistantes, qui nous permet de ne pas basculer les sessions déjà établies vers de nouvelles ressources.

Nous avons examiné ce qui arrive au trafic direct entre l'équilibreur et les ressources. Examinons maintenant le trafic de retour. Il suit le même modèle que le trafic de vérification : via le NAT algorithmique, c'est-à-dire via le NAT inversé 44 pour le trafic client et via le NAT 46 pour le trafic des contrôles de santé. Nous adhérons à notre propre schéma : nous unifions le trafic des contrôles de santé et le trafic réel des utilisateurs.

Nœud d'équilibrage de charge et composants assemblés

La composition des équilibreurs et des ressources dans VPP est signalée par le service local - loadbalancer-node. Il s'abonne au flux d'événements du contrôleur d'équilibrage de charge et est capable de tracer la différence entre l'état actuel du VPP et l'état cible reçu du contrôleur. Nous obtenons un système fermé : les événements de l'API arrivent au contrôleur d'équilibrage, qui assigne des tâches au contrôleur de contrôle de santé pour vérifier la « vivacité » des ressources. Celui-ci, à son tour, attribue des tâches au nœud de contrôle de santé et regroupe les résultats, après quoi il les renvoie au contrôleur de l'équilibreur. Le nœud Loadbalancer s'abonne aux événements du contrôleur et modifie l'état du VPP. Dans un tel système, chaque service connaît uniquement ce qui est nécessaire concernant les services voisins. Le nombre de connexions est limité et nous avons la capacité d’exploiter et de faire évoluer différents segments de manière indépendante.

Architecture d'équilibrage de charge réseau dans Yandex.Cloud

Quels problèmes ont été évités ?

Tous nos services dans le plan de contrôle sont écrits en Go et présentent de bonnes caractéristiques d'évolutivité et de fiabilité. Go possède de nombreuses bibliothèques open source pour créer des systèmes distribués. Nous utilisons activement GRPC, tous les composants contiennent une implémentation open source de découverte de services - nos services surveillent les performances de chacun, peuvent modifier leur composition de manière dynamique, et nous avons lié cela à l'équilibrage GRPC. Pour les métriques, nous utilisons également une solution open source. Dans le plan des données, nous avons obtenu des performances décentes et une réserve de ressources importante : il s'est avéré très difficile d'assembler un support sur lequel nous pourrions compter sur les performances d'un VPP, plutôt que d'une carte réseau en fer.

Problèmes et solutions

Qu'est-ce qui n'a pas si bien fonctionné ? Go dispose d'une gestion automatique de la mémoire, mais des fuites de mémoire se produisent toujours. Le moyen le plus simple de les gérer est d'exécuter des goroutines et de penser à les terminer. À retenir : surveillez la consommation de mémoire de vos programmes Go. Le nombre de goroutines est souvent un bon indicateur. Il y a un plus dans cette histoire : dans Go, il est facile d'obtenir des données d'exécution - la consommation de mémoire, le nombre de goroutines en cours d'exécution et de nombreux autres paramètres.

De plus, Go n’est peut-être pas le meilleur choix pour les tests fonctionnels. Ils sont assez verbeux et l'approche standard consistant à « tout exécuter dans CI par lots » ne leur convient pas très bien. Le fait est que les tests fonctionnels sont plus gourmands en ressources et provoquent de réels délais d'attente. Pour cette raison, les tests peuvent échouer car le processeur est occupé par des tests unitaires. Conclusion : si possible, effectuez les tests « lourds » séparément des tests unitaires. 

L'architecture des événements de microservices est plus complexe qu'un monolithe : collecter des journaux sur des dizaines de machines différentes n'est pas très pratique. Conclusion : si vous réalisez des microservices, pensez immédiatement au traçage.

Nos plans

Nous lancerons un équilibreur interne, un équilibreur IPv6, ajouterons la prise en charge des scripts Kubernetes, continuerons à fragmenter nos services (actuellement, seuls healthcheck-node et healthcheck-ctrl sont fragmentés), ajouterons de nouveaux contrôles de santé et implémenterons également une agrégation intelligente des contrôles. Nous envisageons la possibilité de rendre nos services encore plus indépendants - afin qu'ils ne communiquent pas directement entre eux, mais via une file d'attente de messages. Un service compatible SQS fait récemment son apparition dans le Cloud File d'attente des messages Yandex.

Récemment, la sortie publique de Yandex Load Balancer a eu lieu. Explorer documentation au service, gérez les équilibreurs d'une manière qui vous convient et augmentez la tolérance aux pannes de vos projets !

Source: habr.com

Ajouter un commentaire