J'ai commencé à m'intéresser à la recherche du parfait équilibreur de charge lorsque nous avons eu une série d'incidents au travail impliquant un service communiquant avec une base de données qui se comportait de manière erratique. Bien que notre première préoccupation ait été de rendre la base de données plus stable, il m'est apparu clairement que l'impact sur le service aurait pu être considérablement réduit si nous avions été en mesure d'équilibrer la charge des requêtes de manière plus efficace entre les différents points d'extrémité de lecture de la base de données.
Plus je me suis penché sur l'état de l'art, plus j'ai été surpris de découvrir que ce problème était loin d'être résolu. Il existe de nombreux équilibreurs de charge, mais beaucoup utilisent des algorithmes qui ne fonctionnent que pour un ou deux modes de défaillance - et dans ces incidents, nous avons vu une variété de modes de défaillance.
Ce billet décrit ce que j'ai appris sur l'état actuel de l'équilibrage de charge pour la haute disponibilité, ma compréhension de la dynamique problématique des outils les plus courants, et ce que je pense que nous devrions faire à partir de maintenant.
(Clause de non-responsabilité : ce document est principalement basé sur des expériences de pensée et des observations occasionnelles, et je n'ai pas eu beaucoup de chance de trouver de la documentation académique pertinente. Les critiques sont les bienvenues).
TL;DR
Les points que j'aimerais que vous reteniez :
- La santé du serveur ne peut être comprise que dans le contexte de la santé de la grappe.
- Les équilibreurs de charge qui utilisent des contrôles de santé actifs pour exclure des serveurs peuvent perdre inutilement du trafic lorsque les contrôles de santé ne sont pas représentatifs de l'état réel du trafic.
- La surveillance passive du trafic réel permet aux mesures de latence et de taux d'échec de participer à la répartition équitable de la charge.
- Si de petites différences dans l'état des serveurs entraînent de grandes différences dans l'équilibrage de la charge, le système peut osciller de façon sauvage et imprévisible.
- Le caractère aléatoire peut inhiber le mobbing et d'autres comportements corrélés non désirés.
FONDATIONS
Une petite remarque sur la terminologie : Dans ce billet, je parlerai de clients qui parlent à des serveurs sans faire référence à des "connexions", des "nœuds", etc. Bien qu'un logiciel donné puisse fonctionner à la fois comme client et comme serveur, même en même temps ou dans le même flux de requêtes, dans le scénario que j'ai décrit, les serveurs d'applications sont des clients des serveurs de base de données, et je me concentrerai sur cette relation client-serveur.
Dans le cas général, nous avons donc N clients qui s'adressent à M serveurs :

Je vais également ignorer les spécificités des demandes. Pour simplifier, je dirai que la demande du client n'est pas facultative et que le repli n'est pas possible ; si l'appel échoue, le client subit une dégradation du service.
La grande question est donc la suivante : Lorsqu'un client reçoit une requête, comment doit-il choisir le serveur à appeler ?
(Il convient de noter que je m'intéresse aux demandes, et non aux connexions de longue durée qui peuvent acheminer des flux réguliers, des rafales de trafic ou des demandes à intervalles variables. Le fait qu'une connexion soit établie pour chaque demande ou qu'elle soit réutilisée ne devrait pas avoir d'importance particulière pour les conclusions générales).
Vous vous demandez peut-être pourquoi je fais en sorte que chaque client parle à chaque serveur, ce qui est communément appelé "équilibrage de charge côté client" (bien que dans la terminologie de ce billet, l'équilibreur de charge soit également appelé un client). Pourquoi faire faire ce travail aux clients ? Il est assez courant de placer tous les serveurs derrière un équilibreur de charge dédié.

Le problème est que si vous n'avez qu'un seul nœud d'équilibrage de charge dédié, vous avez un seul point de défaillance. C'est pourquoi il est d'usage d'installer au moins trois nœuds de ce type. Mais remarquez maintenant que les clients doivent choisir à quel équilibreur de charge s'adresser... et que chaque nœud d'équilibreur de charge doit encore choisir à quel serveur envoyer chaque demande ! Le problème n'est même pas déplacé, il est simplement doublé. ("Maintenant, vous avez deux problèmes.")
Je ne dis pas que les équilibreurs de charge dédiés sont mauvais. Le problème de savoir à quel équilibreur de charge s'adresser est traditionnellement résolu par l'équilibrage de charge DNS, ce qui est généralement bien, et il y a beaucoup à dire sur l'utilisation d'un point plus centralisé pour le routage, la journalisation, les métriques, etc. Mais ils ne permettent pas vraiment de contourner le problème, car ils peuvent toujours être la proie de certains modes de défaillance, et ils sont généralement moins flexibles que l'équilibrage de charge côté client.
VALEURS
Quelle est donc la valeur d'un équilibreur de charge ? Pour quoi optimisons-nous ?
Dans un certain ordre, en fonction de nos besoins :
- Réduire l'impact des pannes de serveur ou de réseau sur la disponibilité globale de nos services
- Maintenir le temps de latence des services à un niveau bas
- Répartir uniformément la charge entre les serveurs
- Ne pas trop solliciter un serveur si les autres ont une capacité de réserve.
- Prévisibilité : Il est plus facile de voir quelle est la marge de manœuvre du service.
- Répartition de la charge inégalement si les serveurs ont des capacités variables, qui peuvent varier dans le temps ou selon le serveur (distribution équitable, plutôt qu'égale)
- Un pic soudain ou un volume important de trafic juste après le démarrage du serveur risque de ne pas laisser au serveur le temps de se réchauffer. Une augmentation progressive jusqu'au même niveau de trafic peut suffire.
- Les charges de CPU hors service, telles que l'installation de mises à jour, peuvent réduire la quantité de CPU disponible sur un seul serveur.
SOLUTIONS NAÏVES
Avant d'essayer de tout résoudre, examinons quelques solutions simplistes. Comment répartir équitablement les demandes quand tout va bien ?
- Round-robin
- Le client passe d'un serveur à l'autre
- Garantie d'une distribution uniforme
- Sélection aléatoire
- Approche statistique d'une distribution uniforme, sans tenir compte de l'état (compromis coordination/CPU)
- Choix statique
- Chaque client choisit un seul serveur pour toutes ses demandes.
- C'est ce que fait l'équilibrage de charge DNS : Les clients résolvent le nom de domaine du service en une ou plusieurs adresses, et la pile réseau du client en choisit une et la met en cache. C'est ainsi que le trafic entrant est équilibré pour la plupart des équilibreurs de charge dédiés ; leurs clients n'ont pas besoin de savoir qu'il existe plusieurs serveurs.
- Un peu comme le hasard, il fonctionne bien lorsque 1) les TTL DNS sont respectés et 2) il y a beaucoup plus de clients que de serveurs (avec des taux de requêtes similaires).
Et que se passe-t-il si l'un des serveurs tombe en panne dans une telle configuration ? S'il y a trois serveurs, une demande sur trois échoue. Le meilleur taux de réussite possible dans ce scénario, en supposant un équilibreur de charge parfait et une capacité suffisante sur les deux serveurs restants, est de 100 %. Comment y parvenir ?

DÉFINIR LA SANTÉ
La solution habituelle est celle des contrôles de santé. Les contrôles de santé permettent à un équilibreur de charge de détecter certaines défaillances du serveur ou du réseau et d'éviter d'envoyer des requêtes aux serveurs qui ne satisfont pas au contrôle.
En général, nous souhaitons connaître la "santé" de chaque serveur, quelle qu'en soit la signification, car cela peut avoir une valeur prédictive pour répondre à la question principale : "Ce serveur est-il susceptible de donner une mauvaise réponse si je lui envoie cette requête ? "Ce serveur est-il susceptible de donner une mauvaise réponse si je lui envoie cette requête ?" Il existe également une question de plus haut niveau : "Ce serveur risque-t-il de devenir malsain si je lui envoie plus de trafic ?" (Ou redevenir sain si je lui envoie moins de trafic). Une autre façon de dire cela est que certains cas de mauvaise santé peuvent dépendre de la charge, tandis que d'autres sont indépendants de la charge ; il est essentiel de connaître la différence pour prédire comment acheminer le trafic lorsqu'une mauvaise santé est observée.
D'une manière générale, la "santé" est donc un moyen de modéliser l'état extérieur au service de la prédiction. Mais qu'est-ce qui est considéré comme malsain ? Et comment le mesurer ?
CHOISIR UN POINT DE VUE
Avant d'entrer dans les détails, il est important de noter qu'il existe deux points de vue très différents que nous pouvons utiliser :
- La santé intrinsèque du serveur : L'application serveur est en cours d'exécution, elle répond, elle est capable de communiquer avec toutes ses dépendances et elle n'est pas soumise à de fortes pressions sur les ressources.
- La santé du serveur observée par le client : La santé du serveur, mais aussi la santé de l'hôte du serveur, la santé du réseau intermédiaire, et même si le client est configuré avec une adresse valide pour le serveur.
D'un point de vue pratique, la santé intrinsèque du serveur n'a pas d'importance si le client ne peut même pas l'atteindre. C'est pourquoi nous nous intéresserons principalement à la santé du serveur telle qu'elle est observée par le client. Il y a cependant une certaine subtilité : À mesure que le nombre de requêtes adressées au serveur augmente, c'est l'application serveur qui risque d'être le goulot d'étranglement, et non le réseau ou l'hôte. Si nous commençons à observer une augmentation de la latence ou du taux d'échec du serveur, cela peut signifier que le serveur souffre de la charge de requêtes, ce qui implique qu'une charge de requêtes supplémentaire pourrait aggraver son état de santé. Il se peut aussi que le serveur ait une capacité suffisante et que le client n'observe qu'un problème de réseau transitoire, indépendant de la charge, peut-être dû à un routage non optimal. Dans ce cas, il est peu probable qu'une charge de trafic supplémentaire change la situation. Étant donné que, dans le cas général, il peut être difficile de faire la distinction entre ces deux cas, nous utiliserons généralement les observations du client comme norme de santé.
QUELLE EST LA MESURE DE LA SANTÉ ?
Que peut donc apprendre un client sur l'état de santé d'un serveur à partir des appels qu'il émet ?
- Le temps de latence: Combien de temps faut-il pour que les réponses reviennent ? Ce délai peut être décomposé en plusieurs étapes : Temps d'établissement de la connexion, temps jusqu'au premier octet de la réponse, temps jusqu'à la réponse complète ; minimum, moyenne, maximum, divers percentiles. Il convient de noter qu'il y a un amalgame entre les conditions du réseau et la charge du serveur - sources indépendantes de la charge et sources dépendantes de la charge, respectivement (dans la majorité des cas).
- Taux d'échec: Quelle fraction des demandes aboutit à un échec ? (Plus d'informations sur la signification de l'échec dans un instant).
- Concurrence: Combien de demandes sont actuellement en cours de traitement ? Cette question confond les effets du comportement du serveur et du client : il peut y avoir plus de demandes en vol vers un serveur, soit parce que le serveur est sauvegardé, soit parce que le client a décidé de lui confier une plus grande proportion de demandes pour une raison ou pour une autre.
- Taille de la file d'attente: Si le client maintient une file d'attente par serveur plutôt qu'une file d'attente unifiée, une file d'attente plus longue peut être un indicateur de mauvaise santé ou (à nouveau) d'une charge inégale de la part du client.
Avec la taille de la file d'attente et le nombre de requêtes simultanées, nous constatons que toutes les mesures ne sont pas liées à la santé en soi, mais qu'elles peuvent également être indicatives de la charge. Ces mesures ne sont pas directement comparables, mais les clients souhaitent vraisemblablement confier davantage de demandes à des serveurs en meilleure santé et moins chargés, de sorte qu'elles peuvent être utilisées parallèlement à des mesures plus intrinsèques telles que la latence et le taux de défaillance.
Toutes ces mesures sont effectuées du point de vue du client. Il est également possible de faire en sorte que le serveur rapporte lui-même son utilisation, bien que ce point ne soit pas abordé dans ce billet.
Tous ces éléments peuvent également être mesurés sur différents intervalles de temps : Valeur la plus récente, fenêtre glissante (ou paliers roulants), moyenne décroissante, ou plusieurs de ces éléments combinés.
DÉFINITION DE L'ÉCHEC
Parmi ces indicateurs de santé, le taux d'échec est peut-être le plus important : Dans la plupart des cas d'utilisation, un appelant préférerait obtenir un succès lent plutôt qu'un échec quelconque. Mais il existe différents types d'échec, et ils peuvent impliquer différentes choses sur l'état du serveur.
Si un appel n'aboutit pas, il peut y avoir des problèmes de réseau ou de routage entraînant une latence élevée, ou le serveur peut être très sollicité. Mais si l'appel échoue rapidement, les implications sont très différentes : Mauvaise configuration du DNS, serveur en panne, mauvaise route. Une défaillance rapide est moins susceptible de dépendre de la charge, à moins que le serveur n'utilise le délestage de charge pour tomber intentionnellement en panne rapide en cas de forte charge, auquel cas il est possible qu'il ne soit pas davantage sollicité par une charge supplémentaire.
Si l'on considère les échecs au niveau de l'application, et pas seulement les échecs au niveau du transport, il est essentiel de choisir avec soin les critères permettant de marquer un appel comme ayant échoué. Par exemple, un appel HTTP qui n'aboutit pas (en raison d'un dépassement de délai, etc.) est sans ambiguïté un échec, mais une réponse bien formée avec un code d'état d'erreur (4xx ou 5xx) peut ne pas indiquer un problème de serveur. Une requête individuelle peut déclencher une erreur de serveur 500 dépendante des données, qui n'est pas représentative de l'état général du serveur. Il est courant de voir une explosion de réponses 404 ou 403 dues à un appelant dont les requêtes sont mal formées, mais seul cet appelant est affecté ; juger le serveur malsain uniquement sur cette base ne serait pas judicieux. D'un autre côté, il est moins probable qu'un délai de lecture soit spécifique à une mauvaise requête.
QU'EN EST-IL DES CHÈQUES SANTÉ ?
Jusqu'à présent, nous avons surtout parlé des moyens par lesquels un client peut glaner passivement des informations sur la santé du serveur à partir des requêtes qu'il est déjà en train d'effectuer. Une autre approche consiste à utiliser des contrôles de santé actifs.
Les contrôles de santé de l'ELB (Elastic Load Balancer) d'AWS en sont un exemple. Vous pouvez configurer l'équilibreur de charge pour qu'il appelle un point d'extrémité HTTP sur chaque serveur toutes les 30 secondes. Si l'ELB obtient une réponse 5xx ou un dépassement de délai deux fois de suite, il ne prend plus le serveur en considération pour les demandes normales. Il continue cependant à effectuer les appels de contrôle de santé, et si le serveur répond normalement 10 fois de suite, il est réintégré dans la rotation.
Cela démontre l'utilisation de l'hystérésis pour s'assurer que l'hôte n'entre pas en service et ne sort pas du service trop rapidement. (Un exemple familier d'hystérésis est la façon dont le thermostat d'un climatiseur maintient une "fenêtre de tolérance" autour de la température désirée). Il s'agit d'une approche courante, qui peut fonctionner raisonnablement bien dans les scénarios où un serveur est soit en bonne santé, soit en mauvaise santé, et ne change pas fréquemment d'état. Dans la situation moins courante de taux de défaillance faibles et persistants, inférieurs à 40 %, qui affectent à la fois le contrôle de santé et le trafic normal, un ELB, dans sa configuration par défaut, ne verrait pas de défaillances consécutives suffisamment fréquentes pour mettre l'hôte hors service.
Les contrôles de santé doivent être conçus avec soin pour éviter qu'ils n'aient un effet erroné sur l'équilibreur de charge. Voici quelques-uns des types de réponses qu'un appel à un contrôle de santé peut être censé fournir :
- Test de fumée : Passez un appel réaliste et voyez si la réponse attendue se produit.
- Contrôle fonctionnel des dépendances : Le serveur fait appel à toutes ses dépendances et renvoie un message d'échec si l'une d'entre elles échoue.
- Vérification de la disponibilité : Il suffit de voir si le serveur peut répondre à tous appeler, par exemple
GET /ping
rendements 200 OK
et un corps de réponse de pong
Il est important que le bilan de santé soit aussi représentatif que possible du trafic réel. Dans le cas contraire, il peut produire des faux positifs ou des faux négatifs inacceptables. Par exemple, si le serveur possède un certain nombre de routes API et qu'une seule de ces routes est cassée à cause d'une dépendance défaillante... ce serveur est-il sain ? Si votre test de santé n'atteint que cette route, votre client considérera que le serveur est entièrement cassé ; en revanche, si cette route est la seule qui fonctionne, votre client peut considérer que le serveur est parfaitement sain.
Les contrôles fonctionnels peuvent être plus complets, mais ce n'est pas nécessairement mieux, car cela peut facilement aboutir à ce qu'un serveur (ou tous les serveurs !) soit marqué comme étant hors service si même une seule dépendance optionnelle est hors service. C'est utile pour la surveillance opérationnelle, mais dangereux pour l'équilibrage de la charge ; c'est pourquoi de nombreuses personnes se contentent de configurer de simples contrôles de disponibilité.
Les contrôles de santé actifs fournissent généralement une vue binaire de la santé d'un serveur, même s'ils sont suivis dans le temps, puisqu'un serveur peut être dans un état dégradé où il peut répondre de manière cohérente à certaines requêtes mais pas à d'autres. La surveillance passive de l'état du trafic, en revanche, donne une vision scalaire (ou même plus nuancée) de l'état, puisque le client sait au moins quelle proportion des requêtes reçoit des échecs - et surtout, cette surveillance passive permet d'obtenir une vision complète de l'état du trafic. (Les deux types de contrôle peuvent bien sûr suivre les informations relatives à la latence ; certaines de ces distinctions ne valent que pour la mesure du taux d'échec).
LES CONTRÔLES DE SANTÉ BINAIRES ET LA DÉTECTION DES ANOMALIES
Cette vision binaire peut entraîner de graves problèmes, car elle ne permet pas de comparer la santé des différents serveurs. Ceux-ci sont simplement regroupés en deux catégories, "en hausse" ou "en baisse", sur la base d'un seul type d'appel qui peut ne pas être représentatif. Même si vous aviez plusieurs appels de contrôle de santé, rien ne garantit qu'ils restent représentatifs de la santé de votre serveur au fur et à mesure que son API s'étend et que les besoins des clients évoluent. Pire encore, des défaillances corrélées peuvent entraîner des défaillances en cascade inutiles. Examinez les scénarios suivants :
- Si 100% de vos hôtes ont des contrôles de santé actifs et réussis, un équilibreur de charge idéal devrait router vers tous les hôtes.
- Si 90 % réussissent, il convient d'acheminer les données vers ces 90 % uniquement - la raison pour laquelle les 10 % échouent n'a pas d'importance, puisque le reste de la grappe peut sans aucun doute gérer la charge.
- Si seulement 10% passent... route vers tous les hôtes - il vaut mieuxparier sur le fait que le contrôle de santé est erroné (ou non pertinent) plutôt que d'écraser les 10% qui passent les contrôles.
- Si 0 % des demandes passent, acheminez-les vers tous les hôtes - vous échouez à 100 % des demandes que vous n'acheminez pas, comme on dit.
Plus la fraction d'hôtes qui passe est proche de zéro, plus il est probable qu'il y ait une défaillance dans quelque chose d'externe aux hôtes, ou même quelque chose qui ne va pas avec le contrôle de santé. Imaginez que votre contrôle de santé dépende d'un compte de test, et que ce compte soit supprimé. Ou peut-être qu'une dépendance tombe en panne, mais que la plupart des requêtes peuvent encore être traitées. Néanmoins, tous les contrôles de santé échouent ; l'ELB met hors service chacun de vos hôtes, même si les requêtes entrantes étaient parfaitement traitées.
Il en ressort que la santé est relative: Un serveur peut être en meilleure santé que ses voisins même s'ils ont tous un problème. Il est d'ailleurs plus facile de s'en rendre compte en utilisant des scalaires plutôt que des booléens.
Essentiellement, vous aimeriez que votre équilibreur de charge effectue une sorte de détection d'anomalie simple. Si une petite fraction de vos serveurs se comporte bizarrement, il suffit de les exclure et d'envoyer un message aux services d'exploitation. Si la plupart ou la totalité des serveurs se comportent bizarrement ? N'aggravez pas la situation en faisant porter toute la charge sur une petite poignée de serveurs, ou pire, sur aucun d'entre eux.
La clé, ici, est d'évaluer la santé du serveur en vue de l'ensemble du cluster, plutôt que de manière atomique. Ce qui s'en rapproche le plus jusqu'à présent est l'équilibreur de charge d'Envoy, qui dispose d'un "seuil de panique" qui, par défaut, maintient tous les hôtes en service si 50 % ou plus d'entre eux ont des bilans de santé défaillants. Si vous utilisez des contrôles de santé dans votre équilibreur de charge, envisagez d'utiliser une telle approche.
Vous remarquerez peut-être que je n'ai pas abordé la question de savoir ce qu'il faut faire lorsque 30 à 70 % des serveurs échouent aux contrôles. Cette situation peut indiquer une véritable défaillance et peut être dépendante ou indépendante de la charge. Je ne suis pas sûr qu'il soit possible pour un équilibreur de charge de savoir quelle situation s'applique, même s'il est prêt à faire des expériences astucieuses de charge de trafic A/B pour le découvrir. Pire encore, le fait de faire peser toute la charge sur un nombre relativement restreint de serveurs peut entraîner l'arrêt de ces derniers. Outre le délestage, il n'y a pas grand-chose à faire dans cette situation, et je ne suis pas sûr de pouvoir critiquer une conception qui maintient ces serveurs en service, ou une conception qui les met hors service, lorsque l'on se trouve dans cette fourchette intermédiaire - parce que j'ai été l'un des humains dans la boucle lors d'un tel incident de production, et ce n'était pas clair pour nous à ce moment-là non plus.
Une autre différence entre ces approches actives et passives est qu'avec la vérification active, les informations sur la santé du serveur sont mises à jour à un rythme régulier, quel que soit le taux de trafic. Cela peut être un avantage lorsque le trafic est faible, ou un inconvénient lorsqu'il est élevé. (Cinq secondes d'échecs peuvent représenter une longue période lorsque vous avez 10 000 requêtes par seconde). En revanche, avec la vérification passive, la vitesse de détection des défaillances est proportionnelle au taux de requêtes.
Mais il y a un inconvénient majeur à la vérification passive de la santé. Si un serveur tombe en panne, l'équilibreur de charge le met rapidement hors service. Cela signifie qu'il n'y a plus de trafic, et l'absence de trafic signifie que la vision qu'a le client de l'état de santé du serveur ne change jamais : Elle reste à zéro pour toujours.
Il existe bien sûr des solutions à ce problème, dont certaines concernent également d'autres cas d'absence de données, tels que le démarrage du client ou le remplacement d'un seul serveur dans la liste des serveurs du client. Tous ces cas doivent être traités de manière spécifique si l'on utilise la vérification passive.

BILAN DE SANTÉ
En résumé :
- La surveillance passive du trafic donne nécessairement une vision plus complète et plus nuancée de l'état de santé que les contrôles actifs.
- Il existe plusieurs axes d'évaluation de la santé
- L'état de santé d'un serveur ne peut être compris que par rapport à la grappe.
Mais que faisons-nous de ces informations ? Comment combiner tous ces chiffres à valeur réelle pour atteindre nos objectifs de réduction de la latence, de minimisation des défaillances et de répartition uniforme de la charge ?
J'aimerais d'abord faire une digression sur une famille de modes de défaillance, puis discuter de quelques approches courantes d'équilibrage de charge tenant compte de l'état de santé, et enfin énumérer quelques orientations possibles pour l'avenir.
Une action non coordonnée peut avoir des conséquences surprenantes. Imaginons qu'une grande entreprise envoie un courriel à ses employés : "Nous offrons des massages à tous les employés dans l'auditorium 2 aujourd'hui ! Venez quand vous voulez." Quand pensez-vous que les gens viendront ? Je pense qu'il y aura une grande affluence à certains moments de la journée :
- Tout de suite
- Après le déjeuner
- Fin d'après-midi avant de rentrer à la maison
Avec cette répartition inégale, les massothérapeutes n'ont parfois personne sur qui travailler ; à d'autres moments, les files d'attente sont suffisamment longues pour que les gens abandonnent, voire ne réessayent pas plus tard. Aucune de ces situations n'est souhaitable. Sans aucune coordination - parce qu 'il n'y a pas de coordination - les gens se présentent quand même en groupes ! Le comportement corrélé accidentel dans ce scénario est facile à prévenir à l'aide d'un outil courant : La feuille d'inscription. (Dans le domaine des logiciels, l'analogue le plus proche serait un système de traitement par lots qui accepte des tâches, les planifie à sa convenance et renvoie les résultats de manière asynchrone).
Il s'avère qu'il existe un certain nombre de phénomènes similaires dans le trafic des API, souvent regroupés sous le nom de " problème du troupeau tonitruant". Un exemple classique est celui d'un service de cache consulté par des centaines de nœuds d'application. Lorsque l'entrée du cache expire, l'application doit recréer la valeur avec des données fraîches, ce qui nécessite à la fois du travail supplémentaire et (probablement) des appels réseau supplémentaires à d'autres serveurs. Si des centaines de nœuds d'application observent simultanément l'expiration d'une entrée de cache populaire (parce qu'ils reçoivent tous constamment des demandes pour ces données), ils tenteront tous simultanément de la recréer et d'appeler simultanément les services dorsaux responsables de la production de données fraîches. Ce n'est pas seulement du gaspillage (dans le meilleur des cas, un seul nœud d'application devrait effectuer cette tâche, une fois par durée de vie du cache), mais cela pourrait même écraser les serveurs dorsaux, qui sont normalement à l'abri derrière le cache.
La solution classique aux problèmes de troupeau tonnant dans l'expiration du cache est d'expirer de manière probabiliste l'entrée du cache de manière anticipée pour chaque appelant, plutôt que de la faire expirer au même moment partout. L'approche la plus simple consiste à ajouter de la gigue, un petit nombre aléatoire soustrait de la date d'expiration chaque fois que le client consulte le cache. Un raffinement de cette technique, XFetch, biaise la gigue pour retarder le rafraîchissement jusqu'au dernier moment possible.
Un autre problème bien connu survient lorsqu'un grand nombre d'utilisateurs d'un service mettent en place une tâche périodique pour appeler une API. Il se peut que chaque utilisateur d'un service de sauvegarde installe une tâche cron pour télécharger une sauvegarde à minuit (soit dans leur fuseau horaire local, soit plus probablement en UTC).
Là encore, il existe une solution standard : Lors de l'intégration d'un nouvel utilisateur, générez un fichier crontab suggéré à installer, en utilisant une heure choisie au hasard pour chaque utilisateur. Cela peut même fonctionner sans point central de coordination si le logiciel de sauvegarde écrit lui-même le fichier crontab, en choisissant une heure aléatoire lors de la première installation. (Vous remarquerez peut-être qu'une approche similaire pourrait fonctionner pour le scénario du massage si une feuille d'inscription centrale ne pouvait pas être utilisée pour une raison quelconque : Les employés choisissent au hasard un moment de la journée où ils sont libres et s'y rendent à ce moment-là, même si ce n'est pas nécessairement le moment optimal pour leur propre emploi du temps).
Ces deux solutions - expiration par intermittence et programmation aléatoire - font toutes deux appel au hasard pour contrer les comportements non coordonnés mais corrélés. Il s'agit là d'un principe important : L'aléatoire inhibe la corrélation. Nous le verrons à nouveau lorsque nous aborderons certains défis liés à l'équilibrage de la charge.
Le scénario du massage montre également une autre approche consistant à s'appuyer sur un point central de coordination. C'est l'un des avantages de l'utilisation d'une petite grappe de serveurs puissants pour un équilibreur de charge dédié : chaque serveur a une vue d'ensemble du flux de trafic que n'aurait pas un grand nombre de clients. Un autre moyen d'améliorer la coordination consiste à demander aux serveurs de signaler eux-mêmes leur utilisation sous forme de métadonnées parasites dans leurs réponses. Ce n'est pas toujours possible, mais l'utilisation rapportée par le serveur donne aux clients des informations agrégées auxquelles ils n'auraient pas accès autrement. Cela pourrait donner aux équilibreurs de charge côté client une vue plus globale du type de celle qu'un équilibreur de charge dédié pourrait avoir. En prime, cela peut parfois aider à faire la distinction entre les pannes de serveur et de réseau, avec des implications pour les interprétations dépendantes ou non de la charge.
En gardant à l'esprit cet aspect de la dynamique du système, revenons à la manière dont les répartiteurs de charge utilisent les informations relatives à la santé.
L'UTILISATION DE L'ÉTAT DE SANTÉ DANS L'ÉQUILIBRAGE DE LA CHARGE
Les répartiteurs de charge séparent généralement l'utilisation des informations sur la santé en deux catégories :
- Décider quels sont les serveurs candidats aux demandes, puis
- Décider quel candidat sélectionner pour chaque demande
L'approche classique les traite comme deux niveaux totalement distincts. Les systèmes ELB, ALB et NLB d'AWS, par exemple, utilisent divers algorithmes pour répartir la charge (aléatoire, round-robin, aléatoire déterministe, et le plus faible), mais il existe un mécanisme distinct, largement basé sur des contrôles de santé actifs, pour déterminer quels serveurs peuvent participer à ce processus de sélection. (D'après la documentation, il semble que les NLB utiliseront également une surveillance passive pour décider de l'exclusion d'un serveur, mais les détails sont rares).
Les méthodes aléatoires, round-robin et aléatoires déterministes (telles que flow-hash) ignorent complètement la santé : Un serveur est soit in, soit out. L'algorithme du moins-disant, quant à lui, utilise une mesure passive de l'état de santé. (Notez que même cet algorithme de sélection des serveurs est totalement séparé des vérifications actives utilisées pour retirer les serveurs de la grappe). L'algorithme Least-outstanding ("choisir le serveur dont la concurrence est la plus faible") est l'une des nombreuses approches permettant d'utiliser des mesures de santé passives pour l'allocation des requêtes, chacune étant basée sur l'optimisation de l'une des mesures mentionnées plus haut : Latence, taux d'échec, simultanéité, taille de la file d'attente.
ALGORITHMES DE SÉLECTION
Certains algorithmes de sélection de l'équilibrage de charge choisissent le serveur ayant la meilleure valeur pour une métrique. À première vue, c'est logique : Cela donne à la requête en cours la meilleure chance d'aboutir, et rapidement. Cependant, cela peut conduire à ce que j'appelle le mobbing: Si la latence est l'indicateur de santé choisi et qu'un serveur présente une latence légèrement inférieure à celle des autres (vue par tous les clients), tous les clients enverront l'ensemble de leur trafic vers ce serveur, du moins jusqu'à ce qu'il commence à souffrir de la charge, voire à tomber en panne. Au fur et à mesure que le serveur commence à souffrir, sa latence effective augmente et il est possible qu'un autre serveur obtienne le titre de serveur globalement le plus sain. Ce phénomène peut se répéter de manière cyclique et n'être déclenché que par une très légère différence de santé initiale.

Le comportement de harcèlement moral est le résultat d'une confluence de plusieurs défauts dans le système :
- La latence est une mesure de santé retardée. Si la simultanéité (nombre de requêtes en vol) était utilisée à la place, les clients ne se plaindraient pas, puisque la mesure de la simultanéité est instantanément mise à jour du côté du client dès qu'un plus grand nombre de requêtes est alloué à un serveur. Les mesures retardées, même avec amortissement, peuvent entraîner une oscillation ou une résonance indésirable.
- Les clients n'ont pas une vision globale de la situation et agissent donc de manière non coordonnée pour produire un comportement corrélé non désiré.
- Une petite différence dans l'état de santé du serveur produit une grande différence dans le comportement d'équilibrage de la charge. Étant donné qu'il y a des rétroactions de ce dernier vers le premier, cela correspond à une description des systèmes chaotiques, qui sont très sensibles aux conditions initiales.
Les remèdes, tels que je les vois :
- Utilisez des mesures de santé rapides dans la mesure du possible. En effet, un algorithme très courant de sélection de l'équilibrage de la charge consiste à envoyer toutes les demandes au serveur ayant le moins de demandes en cours. (Parfois appelé "moins de connexions" ou "moins de demandes en attente", selon qu'il s'agit d'une connexion ou d'une demande - certaines connexions ont une longue durée de vie et transmettent de nombreuses demandes au cours de leur existence). En revanche, je ne crois pas avoir vu d'algorithme de sélection de la moindre latence, probablement pour cette même raison.
- Il faut soit tenter d'obtenir une vision globale de la situation (en utilisant un équilibreur de charge dédié avec un petit nombre de serveurs, ou en incorporant l'utilisation signalée par les serveurs), soit utiliser le hasard pour inhiber les comportements corrélés non désirés.
- Utilisez des algorithmes qui ont approximativement le même comportement pour approximativement les mêmes entrées. Ils n'ont pas besoin d'avoir un comportement continuellement variable, mais peuvent utiliser le hasard pour obtenir quelque chose qui s'en rapproche.
Il existe une alternative populaire au "pick-the-best" appelée " two-choice", décrite dans l'article The Power of Two Random Choices, qui traite d'une approche générale de l'allocation des ressources (qui n'est pas spécifique ni même centrée sur les équilibreurs de charge, mais qui est certainement pertinente). Dans cette approche, deux candidats sont sélectionnés et celui qui est en meilleure santé est utilisé. Cette méthode permet d'obtenir une répartition uniforme lorsque l'état de santé à long terme de tous les serveurs approche une valeur identique, mais même une petite différence persistante dans l'état de santé peut déséquilibrer considérablement la répartition de la charge. Une simulation simpliste sans rétroaction illustre ce phénomène :
;; Select the index of one of N servers with health ranging ;; from 1000 to 1000-N, +/-N (defn selecttc [n] (let [spread n ;; top and bottom health ranges overlap by ~half ;; Compute health of a server, by index health (fn [i] (+ (- 1000 i spread) (* 2 spread (rand)))) ;; Randomly choose two servers, without replacement [i1 i2] (take 2 (shuffle (range n)))] ;; Pick the index of the healthier server (if (< (health i1) (health i2)) i2 i1)))
; ; Exécutez 10 000 000 d'essais avec 5 hôtes et indiquez le nombre de fois ; ; que chaque index d'hôte a été sélectionné (tri par clé (fréquences (répétitivement 10000000 #(selecttc 5)))) ;;= ([0 2849521] [1 2435167] [2 2001078] [3 1566792] [4 1147442]))
En supposant que l'augmentation de la charge n'affecte pas la métrique de santé, cela produirait une différence de 2,5 fois dans la charge de requête entre les hôtes les plus sains et les plus malsains lorsque les hôtes ont même un classement approximatif de la santé. Notez que l'état de santé de l'hôte 0 est compris entre 995 et 1005 et celui de l'hôte 4 entre 991 et 1001 ; bien qu'il n'y ait que 1 à 2% d'écart en termes absolus, ce léger biais est amplifié par un important déséquilibre de la charge.
Bien que le double choix réduise le mobbing (et fonctionne très bien en l'absence de biais, ce qui peut être le cas s'il y a des rétroactions), il est clair qu'il ne s'agit pas d'un mécanisme de sélection approprié à utiliser avec des mesures de santé différées. En outre, l'article semble se concentrer sur la réduction maximale de la charge pour un ensemble identique d'options, ce qui n'est pas le cas pour les équilibreurs de charge tenant compte de l'état de santé.
D'autre part, la méthode des deux choix fonctionne bien avec la moins bonne note, car le retour d'information est à la fois instantané et autocorrectif. Le principe de la moindre résistance est en soi un défi, car il peut avoir des valeurs quantifiées de petite taille. Un serveur avec une connexion ouverte est-il deux fois plus sain qu'un serveur avec deux connexions ouvertes ? Qu'en est-il de zéro et de un ? Avec de petites valeurs moyennes, la randomisation comme moyen de départage devient très importante, de peur que le premier serveur de la liste ne reçoive toujours des demandes par défaut - si chaque client n'a qu'une connexion ouverte, mais qu'il y a 300 clients, ils peuvent collectivement se liguer contre ce seul serveur. Le double choix, avec son caractère aléatoire, se présente comme un antidote naturel au mobbing résultant des petites valeurs discrètes de least-outstanding.
Une option très prometteuse, bien qu'encore théorique, est la sélection aléatoire pondérée. Chaque serveur se voit attribuer un poids dérivé de ses mesures de santé, et un serveur est choisi en fonction de ce poids. Par exemple, si les serveurs ont des poids de 7, 3 et 1, ils auront respectivement 70 %, 30 % et 10 % de chances d'être sélectionnés à chaque fois. L'utilisation de cet algorithme nécessite des précautions pour éviter le piège de la famine, et la dérivation des poids doit utiliser une fonction non linéaire bien choisie de sorte qu'un serveur dont l'état de santé correspond à 90 % de celui des autres reçoive un poids considérablement réduit, peut-être seulement 20 % en termes relatifs. Au travail, j'expérimente cette approche et j'ai de grands espoirs en elle après quelques expériences d'intégration locale, mais je ne l'ai pas encore vue testée avec un trafic réel. Si c'est le cas, j'entrerai probablement dans les détails dans un prochain article sur un nouvel algorithme d'équilibrage de charge.
COMBINER LES MESURES DE SANTÉ
J'ai remis à plus tard la question de l'utilisation de plusieurs indicateurs de santé. À mon avis, c'est la partie la plus difficile, et elle touche au cœur de la question : Comment définir la santé pour votre application ?
Supposons que vous suiviez la latence, le taux d'échec et la simultanéité, car tous ces éléments sont importants pour vous. Comment les combiner ? Un taux d'échec de 5 % est-il aussi mauvais qu'une latence multipliée par 10 (100x ?)? (100x ?) À quel moment préférez-vous tenter votre chance avec un serveur disponible à 90 % alors que l'autre affiche des pics de latence massifs ? Deux stratégies générales viennent à l'esprit.
Vous pourriez adopter une approche par paliers, en définissant des seuils d'acceptabilité pour chaque mesure et en ne sélectionnant que les serveurs présentant des taux de défaillance acceptables ; s'il n'y en a pas, choisissez ceux dont la latence est acceptable, etc. Peut-être avez-vous défini un seuil de débordement de sorte que si le groupe acceptable est trop petit, les serveurs du niveau immédiatement inférieur sont également pris en considération. (Cette idée ressemble un peu aux niveaux de priorité d'Envoy).
Vous pouvez également utiliser des mesures fusionnées, dans lesquelles les mesures sont combinées en fonction d'une fonction continue. Il se peut que vous accordiez plus d'importance à certaines mesures. J'expérimente actuellement la dérivation d'un facteur de pondération [0,1] pour chaque mesure de santé, et leur multiplication, avec certains élevés à des puissances supérieures (au carré ou au cube) pour leur donner plus de poids. (Je soupçonne que de très grandes puissances pourraient être utilisées pour mettre en œuvre quelque chose comme l'approche par paliers même en utilisant un combinateur de métriques fusionnées).
Il convient également d'examiner la manière dont ces mesures peuvent varier conjointement, ce qui laisse entrevoir les avantages possibles d'une modélisation plus avancée de l'état du serveur et de la connexion. Prenons l'exemple d'un serveur qui est entré dans un mauvais état et qui crache des réponses d'échec très rapidement. Si la seule mesure de santé est la latence, ce serveur apparaît désormais comme le plus sain de la grappe et reçoit donc une plus grande partie du trafic. rachelbythebay appelle cela l'effet de capture équilibrée de la charge. Rapide n'est pas toujours sain ! En fonction de votre configuration, une approche fusionnée peut ou ne peut pas supprimer suffisamment le trafic vers ce serveur rebelle, tandis qu'une approche par niveaux qui donne la priorité à un faible taux d'échec l'exclurait complètement.
La latence et le taux d'échec, en général, sont liés l'un à l'autre de manière peu évidente. Outre le scénario de la "production rapide d'échecs", il y a aussi la question des échecs avec ou sans dépassement de délai. Dans des conditions de latence élevée, le client produira un certain nombre d'erreurs de dépassement de délai. S'agit-il d'"échecs" à proprement parler, ou simplement de réponses à latence excessivement élevée ? Doivent-elles affecter la métrique de latence, la métrique de taux d'échec, ou les deux ? Comparez avec les échecs dus à de mauvais enregistrements DNS et à d'autres échecs de connexion rapide. Je recommande de n'enregistrer que les chiffres de latence des succès, ou des échecs dont vous savez qu' ils indiquent un dépassement de délai, comme les exceptions SocketTimeoutException et similaires en Java. (Un collègue propose une autre solution consistant à n'enregistrer que les valeurs de latence pour les échecs lorsque cela aggrave la moyenne de la latence).
CYCLE DE VIE
Ce qui précède suppose principalement que le client s'adresse à un ensemble statique de serveurs. Or, les serveurs sont remplacés, soit un à la fois, soit par grands groupes. Lorsqu'un nouveau serveur est ajouté à la grappe, l'équilibreur de charge ne doit pas lui imposer d'emblée une part complète du trafic, mais plutôt l'augmenter lentement sur une certaine période. Cette période d'échauffement permet au serveur d'être pleinement optimisé : réchauffement du disque et du cache d'instructions, optimisation des hotspots en Java, etc. HAProxy met en œuvre un démarrage lent à cette fin. Au-delà de la période de chauffe, il s'agit également d'une période d'incertitude : Le client n'a pas d'historique avec le serveur, donc limiter la dépendance à son égard peut limiter les risques.
Si vous utilisez une approche de combinaison de métriques, il peut être pratique d'utiliser l'âge du serveur comme une métrique de pesudo-santé, en partant de presque zéro et en augmentant jusqu'à la pleine santé au cours d'une minute ou deux. (Le client peut apprendre le remplacement complet d'un ensemble de serveurs en une seule fois, ou être reconfiguré pour pointer vers un cluster différent, et considérer brièvement que tous les serveurs sont à zéro santé). Il est probable que tout mécanisme permettant de gérer le remplacement total de la liste des serveurs suffira également à gérer le démarrage du client.
CHARGEMENT DU BERCEAU
Je n'ai fait qu'effleurer le délestage, qui consiste pour un service soumis à une forte charge de requêtes à tenter de répondre à certaines ou à toutes les requêtes par des échecs, très rapidement, dans le but de réduire la charge du processeur et la contention d'autres ressources. Parfois, vos meilleurs efforts ne suffisent pas, ou vous devez simplement maintenir le service en vie suffisamment longtemps pour qu'il puisse être mis à l'échelle. Le délestage de charge est un pari fondé sur l'idée que le fait de renvoyer les échecs pour 50 % du trafic maintenant peut vous permettre de répondre avec succès à 100 % du trafic plus tard, et que le fait d'essayer de gérer tout le trafic maintenant peut entraîner l'arrêt complet du service. Mais comment savoir quand le faire, et dans quelle mesure ?
Je pense qu'il s'agit d'une préoccupation largement séparable : si l'équilibreur de charge est suffisamment bon pour distribuer la charge, le simple fait de mettre quelque chose comme Hystrix ou des limites de concurrence en avant pourrait être suffisant. Le seul endroit où je verrais un avantage serait la gestion de la charge supplémentaire sur les serveurs sains lorsque certains serveurs ne sont pas sains. Si seulement 20 % des serveurs sont sains, est-il raisonnable qu'ils prennent 5 fois leur part normale de la charge ? Un équilibreur de charge pourrait raisonnablement décider de plafonner le dépassement à 10 fois environ, et ne jamais demander à un serveur de prendre la "part de charge" de 9 serveurs qui ont été marqués comme malsains. Si cette solution est réalisable, est-elle pour autant souhaitable ? Je n'en suis pas certain. Ce n'est pas totalement adaptatif, dans le sens où un plafond de surcharge doit toujours être configuré, et que cette configuration peut facilement devenir obsolète (ou ne pas être pertinente, par exemple, dans une période de faible trafic).
CONCLUSIONS
Sur la base de ce qui précède, je pense que si bon nombre des options existantes pour l'équilibrage de la charge dans des environnements génériques à haute disponibilité tendent à bien fonctionner pour répartir la charge dans des conditions normales et dans un ensemble sélectionné de conditions d'erreur, elles sont insuffisantes dans d'autres conditions en raison du mobbing, d'une réactivité insuffisante à la défaillance et d'une réaction excessive à des états dégradés corrélés.
Un équilibreur de charge à haute disponibilité idéal éviterait les contrôles de santé actifs pour son fonctionnement normal, et suivrait plutôt passivement une variété de mesures de santé, y compris les demandes en cours et les mesures décroissantes (ou glissantes) de latence et de taux d'échec. Un client qui suit ces mesures est bien mieux placé pour détecter les anomalies qu'un client qui ne fait qu'observer les résultats des contrôles de santé actifs périodiques.
Bien sûr, un équilibreur de charge vraiment idéal incarnerait l'efficacité parfaite, dans lequel, même sous une charge de demande croissante, toutes les demandes sont traitées avec autant de succès et de rapidité que possible... jusqu'à ce que le système atteigne sa limite théorique, moment où il tombe soudainement en panne (ou commence à se délester de la charge), plutôt que de montrer progressivement une augmentation du stress. Bien que je classe cette situation dans la catégorie des "problèmes que j'aimerais avoir", elle met en évidence la nécessité de revoir les outils de surveillance si l'équilibreur de charge est particulièrement doué pour dissimuler les défaillances des serveurs au monde extérieur.
La principale question en suspens, à mon avis, est de savoir comment combiner ces mesures de santé et les utiliser dans la sélection des serveurs d'une manière qui minimise le comportement chaotique et les autres problèmes mentionnés dans ce billet, tout en restant applicable de manière générale. Bien que je parie actuellement sur la sélection aléatoire pondérée multifactorielle, il reste à voir comment elle se comporte dans le monde réel.